From 0e55b585435dd15883f992d9ee9a582e5778d054 Mon Sep 17 00:00:00 2001 From: liumengyi Date: Tue, 21 Nov 2023 08:59:09 +0800 Subject: [PATCH 01/92] * Set `data-app` to close link in module menu. --- lib/zin/wg/modulemenu/v1.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/zin/wg/modulemenu/v1.php b/lib/zin/wg/modulemenu/v1.php index da63534a51..2c833f59bd 100644 --- a/lib/zin/wg/modulemenu/v1.php +++ b/lib/zin/wg/modulemenu/v1.php @@ -146,7 +146,11 @@ class moduleMenu extends wg private function buildCloseBtn(): wg|null { - $closeLink = $this->prop('closeLink'); + $closeLink = $this->prop('closeLink'); + $tab = $this->prop('app'); + $titleAttrs = array(); + if($tab) $titleAttrs['app'] = $tab; + if(isInModal()) $titleAttrs['load'] = 'modal'; if(!$closeLink) return null; $activeKey = $this->prop('activeKey'); @@ -155,7 +159,7 @@ class moduleMenu extends wg return a ( set('href', $closeLink), - isInModal() ? set('data-load', 'modal') : null, + setData($titleAttrs), icon('close', setStyle('color', 'var(--color-slate-600)')) ); } From ff42439a7c9d601542c83899fdc55e8388ca351c Mon Sep 17 00:00:00 2001 From: liumengyi Date: Tue, 21 Nov 2023 08:59:35 +0800 Subject: [PATCH 02/92] * Modify module url in testtask::cases. --- module/tree/model.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/tree/model.php b/module/tree/model.php index ed30609749..c9de2a250c 100644 --- a/module/tree/model.php +++ b/module/tree/model.php @@ -1332,7 +1332,7 @@ class treeModel extends model $data->id = $parent ? uniqid() : $module->id; $data->parent = $parent ? $parent : $module->parent; $data->name = $module->name; - $data->url = html::a(helper::createLink('testtask', 'cases', "taskID=$extra&type=byModule&module={$module->id}"), $module->name, '_self', "id='module{$module->id}' title='{$module->name}'"); + $data->url = helper::createLink('testtask', 'cases', "taskID=$extra&type=byModule&module={$module->id}"); return $data; } From 61fe0db3af306bf2c2585d1b8f90664bb4befbfc Mon Sep 17 00:00:00 2001 From: liumengyi Date: Tue, 21 Nov 2023 09:01:14 +0800 Subject: [PATCH 03/92] * Fix bug of module menu. --- lib/zin/wg/modulemenu/v1.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/zin/wg/modulemenu/v1.php b/lib/zin/wg/modulemenu/v1.php index 2c833f59bd..68fc914dbf 100644 --- a/lib/zin/wg/modulemenu/v1.php +++ b/lib/zin/wg/modulemenu/v1.php @@ -159,7 +159,7 @@ class moduleMenu extends wg return a ( set('href', $closeLink), - setData($titleAttrs), + $titleAttrs ? setData($titleAttrs) : null, icon('close', setStyle('color', 'var(--color-slate-600)')) ); } From 9f64b3bc6c791413aea4ce3ec739171df1dc95ae Mon Sep 17 00:00:00 2001 From: liumengyi Date: Tue, 21 Nov 2023 09:02:28 +0800 Subject: [PATCH 04/92] * Fix bug. --- module/testsuite/control.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/testsuite/control.php b/module/testsuite/control.php index f03d845906..649febfe11 100644 --- a/module/testsuite/control.php +++ b/module/testsuite/control.php @@ -40,7 +40,7 @@ class testsuite extends control parent::__construct($moduleName, $methodName); $this->view->products = $this->products = $this->loadModel('product')->getPairs('', 0, '', 'all'); - if(empty($products) && (helper::isAjaxRequest('zin') || helper::isAjaxRequest('fetch'))) + if(empty($this->products) && (helper::isAjaxRequest('zin') || helper::isAjaxRequest('fetch'))) { $tab = $this->app->tab == 'project' || $this->app->tab == 'execution' ? $this->app->tab : 'qa'; $objectID = $tab == 'project' || $tab == 'execution' ? $this->session->{$tab} : 0; From 039a3b6d4748e0530f26c9a2a3046b92906a7d7d Mon Sep 17 00:00:00 2001 From: liumengyi Date: Tue, 21 Nov 2023 09:22:13 +0800 Subject: [PATCH 05/92] * Modify style of 2.5 level drop menu. --- lib/zin/wg/mainnavbar/css/v1.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/zin/wg/mainnavbar/css/v1.css b/lib/zin/wg/mainnavbar/css/v1.css index 5fa612218d..c5091dc85c 100644 --- a/lib/zin/wg/mainnavbar/css/v1.css +++ b/lib/zin/wg/mainnavbar/css/v1.css @@ -1,4 +1,4 @@ -#mainNavbar .main-navbar-left {position: absolute; z-index: 2;} +#mainNavbar .main-navbar-left {position: absolute; z-index: 2; left: 16px;} #mainNavbar .main-navbar-left #switcher {position: relative; top: 5px; border: 1px solid rgb(var(--color-primary-500-rgb));} #mainNavbar .main-navbar-left #switcher > .dropmenu-btn {background: #FFF;} #mainNavbar .main-navbar-left #switcher > .dropmenu-btn:before {background: unset;} From 0454daba25051cb4b21d80fe9ba97a67d8dd58b1 Mon Sep 17 00:00:00 2001 From: caoyanyi Date: Tue, 21 Nov 2023 09:28:01 +0800 Subject: [PATCH 06/92] * Adjust store type for selected. --- module/store/js/browse.ui.js | 4 ++-- module/store/ui/browse.html.php | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/module/store/js/browse.ui.js b/module/store/js/browse.ui.js index a713901149..50881a2e6c 100644 --- a/module/store/js/browse.ui.js +++ b/module/store/js/browse.ui.js @@ -9,12 +9,12 @@ window.treeClick = function(info) if (info.item.items && info.item.items.length > 0) return; if(checkedCategories[info.item.id] != undefined) { - $('#' + info.item.id).parent().removeClass('selected'); + $('.store-tree-' + info.item.id + ' > .listitem').removeClass('active'); delete checkedCategories[info.item.id]; } else { - $('#' + info.item.id).parent().addClass('selected'); + $('.store-tree-' + info.item.id + ' > .listitem').addClass('active'); checkedCategories[info.item.id] = true; } diff --git a/module/store/ui/browse.html.php b/module/store/ui/browse.html.php index 17619268d2..d8ea877810 100644 --- a/module/store/ui/browse.html.php +++ b/module/store/ui/browse.html.php @@ -16,10 +16,11 @@ $tree = array(); foreach($categories as $id => $category) { $tree[] = array( - 'id' => $id, - 'parent' => 0, - 'name' => $category, - 'key' => 'storeTree-' . $id + 'id' => $id, + 'parent' => 0, + 'name' => $category, + 'key' => "storeTree-{$id}", + 'className' => "store-tree-{$id}" ); } jsVar('tree', $tree); From 44d1292fc406148b9beb0e55696ff3a5e45a314d Mon Sep 17 00:00:00 2001 From: caoyanyi Date: Tue, 21 Nov 2023 09:33:19 +0800 Subject: [PATCH 07/92] * Filter zentao, zdoo, xuanxuan app. --- module/space/control.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module/space/control.php b/module/space/control.php index 19307c74fb..8aa0ebc374 100644 --- a/module/space/control.php +++ b/module/space/control.php @@ -128,8 +128,8 @@ class space extends control $defaultApp = ''; foreach($pagedApps->apps as $app) { - if(in_array($app->id, array(29,51,52,53,54,142))) continue; - if(!$appID and $app->alias == 'GitLab') $defaultApp = $app->id; + if(strpos($app->name, 'zentao') === 0 || strpos($app->name, 'zdoo') === 0 || strpos($app->name, 'xuanxuan') === 0) continue; + if(!$appID && $app->alias == 'GitLab') $defaultApp = $app->id; $apps[$app->id] = $app->alias; } From ebe58152494cfe4fa577bf5da80ea56f89278907 Mon Sep 17 00:00:00 2001 From: liumengyi Date: Tue, 21 Nov 2023 09:45:07 +0800 Subject: [PATCH 08/92] * Modify style of 2.5 level drop menu. --- lib/zin/wg/mainnavbar/css/v1.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/zin/wg/mainnavbar/css/v1.css b/lib/zin/wg/mainnavbar/css/v1.css index c5091dc85c..b1435ff623 100644 --- a/lib/zin/wg/mainnavbar/css/v1.css +++ b/lib/zin/wg/mainnavbar/css/v1.css @@ -1,4 +1,5 @@ -#mainNavbar .main-navbar-left {position: absolute; z-index: 2; left: 16px;} +#mainNavbar {padding-left: 1rem; padding-right: 1rem;} +#mainNavbar .main-navbar-left {position: absolute; z-index: 2;} #mainNavbar .main-navbar-left #switcher {position: relative; top: 5px; border: 1px solid rgb(var(--color-primary-500-rgb));} #mainNavbar .main-navbar-left #switcher > .dropmenu-btn {background: #FFF;} #mainNavbar .main-navbar-left #switcher > .dropmenu-btn:before {background: unset;} From 4d3fbbd758eb3cbe31912ecfb2da2075ab44f8ab Mon Sep 17 00:00:00 2001 From: liumengyi Date: Tue, 21 Nov 2023 10:07:31 +0800 Subject: [PATCH 09/92] * Replace `array()` with `[]` in stepseditor js file. --- lib/zin/wg/stepseditor/js/v1.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/zin/wg/stepseditor/js/v1.js b/lib/zin/wg/stepseditor/js/v1.js index 127ca3324b..e61ed72f2e 100644 --- a/lib/zin/wg/stepseditor/js/v1.js +++ b/lib/zin/wg/stepseditor/js/v1.js @@ -194,7 +194,7 @@ class StepsEditor extends zui.Component reset(data, skipRender) { - this._items = array(); + this._items = []; this.update(data, skipRender); } From ce17db9404ee9140514b8f49444de558637c13f4 Mon Sep 17 00:00:00 2001 From: liumengyi Date: Tue, 21 Nov 2023 10:24:38 +0800 Subject: [PATCH 10/92] * Replace `select` with `picker` of `$config->execution->importBug->dtable->fieldList['taskPri']['control']`. --- module/execution/config/dtable.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/execution/config/dtable.php b/module/execution/config/dtable.php index 7d6907f480..449f363aa2 100644 --- a/module/execution/config/dtable.php +++ b/module/execution/config/dtable.php @@ -209,7 +209,7 @@ $config->execution->importBug->dtable->fieldList['taskPri']['type'] = 'h $config->execution->importBug->dtable->fieldList['taskPri']['width'] = 68; $config->execution->importBug->dtable->fieldList['taskPri']['sortType'] = false; $config->execution->importBug->dtable->fieldList['taskPri']['type'] = 'control'; -$config->execution->importBug->dtable->fieldList['taskPri']['control'] = 'select'; +$config->execution->importBug->dtable->fieldList['taskPri']['control'] = 'picker'; $config->execution->importBug->dtable->fieldList['taskPri']['controlItems'] = $lang->task->priList; $config->execution->importBug->dtable->fieldList['assignedTo']['title'] = $lang->bug->assignedTo; From bcebe95d179dd28cb0183a5542b80b293b4e13bb Mon Sep 17 00:00:00 2001 From: wangxuepeng Date: Tue, 21 Nov 2023 10:29:36 +0800 Subject: [PATCH 11/92] * Change to new writing method. --- module/upgrade/ui/confirm.html.php | 8 +-- module/upgrade/ui/consistency.html.php | 14 ++--- module/upgrade/ui/createprogram.html.php | 66 +++++++++++------------ module/upgrade/ui/execute.html.php | 8 +-- module/upgrade/ui/mergebyproduct.html.php | 36 ++++++------- module/upgrade/ui/mergebysprint.html.php | 22 ++++---- module/upgrade/ui/moveextfiles.html.php | 8 +-- module/upgrade/ui/renameobject.html.php | 6 +-- 8 files changed, 84 insertions(+), 84 deletions(-) diff --git a/module/upgrade/ui/confirm.html.php b/module/upgrade/ui/confirm.html.php index ab414aa6ae..9dd2f42981 100644 --- a/module/upgrade/ui/confirm.html.php +++ b/module/upgrade/ui/confirm.html.php @@ -15,10 +15,10 @@ jsVar('writable', $writable); div ( - set::id('main'), + setID('main'), div ( - set::id('mainContent'), + setID('mainContent'), formPanel ( setClass('upgrade-confirm'), @@ -28,7 +28,7 @@ div set::title($lang->upgrade->confirm), div ( - set::className('border p-4 mb-4'), + setClass('border p-4 mb-4'), set::style(array('background-color' => 'var(--color-gray-100)')), div(html(nl2br($confirm))) ), @@ -46,7 +46,7 @@ div modal ( - set::id('progress'), + setID('progress'), set::title('1%'), div ( diff --git a/module/upgrade/ui/consistency.html.php b/module/upgrade/ui/consistency.html.php index 50385d172f..5c40b55f49 100644 --- a/module/upgrade/ui/consistency.html.php +++ b/module/upgrade/ui/consistency.html.php @@ -14,10 +14,10 @@ set::zui(true); div ( - set::id('main'), + setID('main'), div ( - set::id('mainContent'), + setID('mainContent'), panel ( set::style(array('margin' => '0 auto')), @@ -25,28 +25,28 @@ div set::title($lang->upgrade->consistency), div ( - set::className('border p-4 mb-4'), + setClass('border p-4 mb-4'), set::style(array('background-color' => 'var(--color-gray-100)')), div ( - set::className('article-h3 mb-2'), + setClass('article-h3 mb-2'), $lang->upgrade->noticeSQL ), div ( - set::className('text-danger leading-loose'), + setClass('text-danger leading-loose'), html("SET @@sql_mode= '';
"), html(nl2br($alterSQL)) ) ), div ( - set::className('text-center'), + setClass('text-center'), btn ( on::click('loadCurrentPage'), set::type('primary'), - set::className('px-10'), + setClass('px-10'), $lang->refresh ) ) diff --git a/module/upgrade/ui/createprogram.html.php b/module/upgrade/ui/createprogram.html.php index eadda72b43..1df09e8b09 100644 --- a/module/upgrade/ui/createprogram.html.php +++ b/module/upgrade/ui/createprogram.html.php @@ -17,113 +17,113 @@ $createProgram = function($data) return div ( - formRowGroup(set::className('programParams hidden'), set::title($lang->upgrade->dataMethod), set::items(array())), + formRowGroup(setClass('programParams hidden'), set::title($lang->upgrade->dataMethod), set::items(array())), div ( - set::className('programParams hidden py-4'), - radioList(set::className('my-2'), set::name('projectType'), set::inline('true'), set::items($this->lang->upgrade->projectType), set::value($data->projectType), set('data-on', 'change'), set('data-call', 'changeProjectType')), - div(set::className('createProjectTip text-gray ' . ($data->projectType == 'project' ? '' : 'hidden')), html($lang->upgrade->createProjectTip)), - div(set::className('createExecutionTip text-gray ' . ($data->projectType == 'execution' ? '' : 'hidden')), html($lang->upgrade->createExecutionTip)) + setClass('programParams hidden py-4'), + radioList(setClass('my-2'), set::name('projectType'), set::inline('true'), set::items($this->lang->upgrade->projectType), set::value($data->projectType), setData(array('on' => 'change', 'call' => 'changeProjectType'))), + div(setClass('createProjectTip text-gray ' . ($data->projectType == 'project' ? '' : 'hidden')), html($lang->upgrade->createProjectTip)), + div(setClass('createExecutionTip text-gray ' . ($data->projectType == 'execution' ? '' : 'hidden')), html($lang->upgrade->createExecutionTip)) ), formRowGroup(set::title($data->systemMode == 'light' ? $lang->upgrade->setProject : $lang->upgrade->setProgram), set::items(array())), div ( - set::className('programForm mt-4 form-grid'), + setClass('programForm mt-4 form-grid'), formGroup ( - set::className('programName'), + setClass('programName'), set::label($lang->upgrade->programName), set::required(true), inputGroup ( - picker(set::id('programs'), set::name('programs'), set::items($data->programs), set::value($data->programID), set('data-on', 'change'), set('data-call', 'changePrograms'), set::className('hidden')), + picker(setID('programs'), set::name('programs'), set::items($data->programs), set::value($data->programID), setData(array('on' => 'change', 'call' => 'changePrograms')), setClass('hidden')), input(set::name('programName')), span ( - set('class', 'input-group-addon'), - checkbox(set::name('newProgram'), set::text($lang->upgrade->newProgram), set::value('0'), set::checked(true), set('data-on', 'change'), set('data-call', 'changeNewProgram')) + setClass('input-group-addon'), + checkbox(set::name('newProgram'), set::text($lang->upgrade->newProgram), set::value('0'), set::checked(true), setData(array('on' => 'change', 'call' => 'changeNewProgram'))) ), - input(set::className('hidden'), set::name('programID')) + input(setClass('hidden'), set::name('programID')) ) ), formGroup ( - set::className($data->systemMode == 'light' ? 'programStatus hidden' : 'programStatus'), + setClass($data->systemMode == 'light' ? 'programStatus hidden' : 'programStatus'), set::label($lang->program->common . $lang->program->status), inputGroup ( - picker(set::id('programStatus'), set::name('programStatus'), set::items($lang->program->statusList), set::value('wait')) + picker(setID('programStatus'), set::name('programStatus'), set::items($lang->program->statusList), set::value('wait')) ) ), formGroup ( - set::className('projectName hidden'), + setClass('projectName hidden'), set::label($lang->upgrade->projectName), inputGroup ( input(set::name('projectName'), set::value(isset($data->sprintName) ? $data->sprintName : '')), - picker(set::id('projects'), set::name('projects'), set::items($data->projects), set::className('picker-field hidden')), + picker(setID('projects'), set::name('projects'), set::items($data->projects), setClass('picker-field hidden')), span ( - set('class', 'input-group-addon ' . (count($data->projects) ? '' : 'hidden')), - checkbox(set::name('newProject'), set::text($lang->upgrade->newProgram), set::value('0'), set::checked(true), set('data-on', 'change'), set('data-call', 'changeNewProject')) + setClass('input-group-addon ' . (count($data->projects) ? '' : 'hidden')), + checkbox(set::name('newProject'), set::text($lang->upgrade->newProgram), set::value('0'), set::checked(true), setData(array('on' => 'change', 'call' => 'changeNewProject'))) ) ) ), formGroup ( - set::className('programParams hidden projectStatus'), + setClass('programParams hidden projectStatus'), set::label($lang->project->status), inputGroup ( - picker(set::id('projectStatus'), set::name('projectStatus'), set::items($lang->project->statusList), set::className('picker-field')) + picker(setID('projectStatus'), set::name('projectStatus'), set::items($lang->project->statusList), setClass('picker-field')) ) ), formGroup ( - set::className('lineName'), + setClass('lineName'), set::label($lang->upgrade->line), inputGroup ( - picker(set::id('lines'), set::name('lines'), set::items($data->lines), set::className('hidden')), + picker(setID('lines'), set::name('lines'), set::items($data->lines), setClass('hidden')), input(set::name('lineName'), set::value(isset($data->lineName) ? $data->lineName : '')), span ( - set('class', 'input-group-addon ' . (count($data->lines) ? '' : 'hidden')), - checkbox(set::name('newLine'), set::text($lang->upgrade->newProgram), set::value('0'), set::checked(true), set('data-on', 'change'), set('data-call', 'changeNewLine')) + setClass('input-group-addon ' . (count($data->lines) ? '' : 'hidden')), + checkbox(set::name('newLine'), set::text($lang->upgrade->newProgram), set::value('0'), set::checked(true), setData(array('on' => 'change', 'call' => 'changeNewLine'))) ) ) ), formGroup ( - set::className('programParams hidden'), + setClass('programParams hidden'), set::label($lang->project->PM), inputGroup ( - picker(set::id('PM'), set::name('PM'), set::items($data->users), set::className('picker-field')) + picker(setID('PM'), set::name('PM'), set::items($data->users), setClass('picker-field')) ) ), formGroup ( - set::className('programParams hidden'), + setClass('programParams hidden'), set::label($lang->project->dateRange), set::required(true), inputGroup ( - datePicker(set::id('begin'), set::name('begin'), set::value(date('Y-m-d'))), - span(set::className('input-group-addon'), $lang->project->to), - datePicker(set::id('end'), set::name('end')), - span(set::className('input-group-addon'), checkbox(set::name('longTime'), set::value('1'), set::text($lang->project->longTime), set('data-on', 'change'), set('data-call', 'changeLongTime'))) + datePicker(setID('begin'), set::name('begin'), set::value(date('Y-m-d'))), + span(setClass('input-group-addon'), $lang->project->to), + datePicker(setID('end'), set::name('end')), + span(setClass('input-group-addon'), checkbox(set::name('longTime'), set::value('1'), set::text($lang->project->longTime), setData(array('on' => 'change', 'call' => 'changeLongTime')))) ) ), formGroup ( - set::className('programParams hidden'), + setClass('programParams hidden'), set::label($lang->project->acl), radioList(set::name('programAcl'), set::items($lang->program->aclList), set::value('open')), - radioList(set::name('projectAcl'), set::className('hidden'), set::items($lang->project->subAclList), set::value('open')) + radioList(set::name('projectAcl'), setClass('hidden'), set::items($lang->project->subAclList), set::value('open')) ) ), - center(set::className('form-actions mt-4'), btn(set::btnType('submit'), set::className('primary'), $lang->save)) + center(setClass('form-actions mt-4'), btn(set::btnType('submit'), setClass('primary'), $lang->save)) ); }; diff --git a/module/upgrade/ui/execute.html.php b/module/upgrade/ui/execute.html.php index caade28dd3..9e4b95eb00 100644 --- a/module/upgrade/ui/execute.html.php +++ b/module/upgrade/ui/execute.html.php @@ -14,17 +14,17 @@ set::zui(true); div ( - set::id('main'), + setID('main'), div ( - set::id('mainContent'), + setID('mainContent'), panel ( set::style(array('margin' => '0 auto')), zui::width('800px'), div ( - set::className('article-h1 mb-4'), + setClass('article-h1 mb-4'), icon ( 'close', @@ -45,7 +45,7 @@ div ) : null, in_array($result, array('fail', 'sqlFail')) ? div ( - set::className('mt-4'), + setClass('mt-4'), $result == 'sqlFail' ? $lang->upgrade->afterExec : $lang->upgrade->afterDeleted, btn ( diff --git a/module/upgrade/ui/mergebyproduct.html.php b/module/upgrade/ui/mergebyproduct.html.php index ec96928246..82ff7dcf85 100644 --- a/module/upgrade/ui/mergebyproduct.html.php +++ b/module/upgrade/ui/mergebyproduct.html.php @@ -25,29 +25,29 @@ $getMergeData = function($data) { $productGroups[] = div ( - set::className('sprintItem mb-2'), - checkbox(set::id("sprints-{$productID}-{$sprint->id}"), set::name("sprints[{$productID}][]"), set::text($sprint->name), set::value($sprint->id), set('data-on', 'change'), set('data-call', 'changeSprints'), set('data-params', 'event'), set('data-product', $productID), set('data-begin', $product->createdDate), set('data-end', $sprint->end), set('data-status', $sprint->status), set('data-pm', $sprint->PM)), - input(set::className('hidden'), set::name("sprintIdList[{$productID}][{$sprint->id}]"), set::value($sprint->id)) + setClass('sprintItem mb-2'), + checkbox(setID("sprints-{$productID}-{$sprint->id}"), set::name("sprints[{$productID}][]"), set::text($sprint->name), set::value($sprint->id), setData(array('on' => 'change', 'call' => 'changeSprints', 'params' => 'event', 'product' => $productID, 'begin' => $product->createdDate, 'end' => $sprint->end, 'status' => $sprint->status, 'pm' => $sprint->PM))), + input(setClass('hidden'), set::name("sprintIdList[{$productID}][{$sprint->id}]"), set::value($sprint->id)) ); } } $checkBoxGroup[] = div ( - set::className('mt-4 flex py-4'), + setClass('mt-4 flex py-4'), set::style(array('background-color' => 'var(--color-gray-50)')), cell ( set::width('1/2'), - set::className('productList px-4 flex items-center overflow-hidden'), - checkbox(set::id("products{$productID}"), set::name('products[]'), set::text($product->name), set::value($product->id), set('data-on', 'change'), set('data-call', 'changeProducts'), set('data-params', 'event'), set('data-begin', $product->createdDate), set('data-programid', $product->program), set('data-productid', $productID)) + setClass('productList px-4 flex items-center overflow-hidden'), + checkbox(setID("products{$productID}"), set::name('products[]'), set::text($product->name), set::value($product->id), setData(array('on' => 'change', 'call' => 'changeProducts', 'params' => 'event', 'begin' => $product->createdDate, 'programid' => $product->program, 'productid' => $productID))) ), cell ( set::width('1/2'), - set::className('productList px-4'), + setClass('productList px-4'), div ( - set::className('scroll-handle'), + setClass('scroll-handle'), $productGroups ) ) @@ -56,26 +56,26 @@ $getMergeData = function($data) return div ( - set::className('flex mt-4'), + setClass('flex mt-4'), cell ( - set::id('source'), + setID('source'), set::width('1/2'), - set::className('border p-4 overflow-hidden'), + setClass('border p-4 overflow-hidden'), div ( - set::className('flex'), + setClass('flex'), cell ( set::width('1/2'), - set::className('item checkbox-primary px-4 overflow-hidden'), - checkbox(set::id('checkAllProducts'), set::text($lang->productCommon), set('data-on', 'change'), set('data-call', 'changeAllProducts')) + setClass('item checkbox-primary px-4 overflow-hidden'), + checkbox(setID('checkAllProducts'), set::text($lang->productCommon), setData(array('on' => 'change', 'call' => 'changeAllProducts'))) ), cell ( set::width('1/2'), - set::className('item checkbox-primary px-4'), - checkbox(set::id('checkAllSprints'), set::text($lang->projectCommon), set('data-on', 'change'), set('data-call', 'changeAllSprints')) + setClass('item checkbox-primary px-4'), + checkbox(setID('checkAllSprints'), set::text($lang->projectCommon), setData(array('on' => 'change', 'call' => 'changeAllSprints'))) ) ), div @@ -86,8 +86,8 @@ $getMergeData = function($data) cell ( set::width('1/2'), - set::className('border ml-4 p-4'), - set::id('programBox'), + setClass('border ml-4 p-4'), + setID('programBox'), $createProgram($data) ) ); diff --git a/module/upgrade/ui/mergebysprint.html.php b/module/upgrade/ui/mergebysprint.html.php index e768ae2a2f..93ef3ab729 100644 --- a/module/upgrade/ui/mergebysprint.html.php +++ b/module/upgrade/ui/mergebysprint.html.php @@ -20,10 +20,10 @@ $getMergeData = function($data) { $checkBoxGroup[] = div ( - set::className('py-2'), + setClass('py-2'), div ( - set::className('sprintItem px-4'), + setClass('sprintItem px-4'), checkbox(set::name("sprints[]"), set::text($sprint->name), set::value($sprint->id), set('data-on', 'change'), set('data-call', 'changeSprints'), set('data-params', 'event')) ) ); @@ -31,33 +31,33 @@ $getMergeData = function($data) return div ( - set::className('flex mt-4'), + setClass('flex mt-4'), cell ( - set::id('source'), + setID('source'), set::width('1/2'), - set::className('border p-4 overflow-hidden'), + setClass('border p-4 overflow-hidden'), div ( - set::className('flex'), + setClass('flex'), cell ( set::flex('1'), - set::className('item checkbox-primary px-4 overflow-hidden'), - checkbox(set::id('checkAllSprints'), set::text($lang->projectCommon), set('data-on', 'change'), set('data-call', 'changeAllSprints')) + setClass('item checkbox-primary px-4 overflow-hidden'), + checkbox(setID('checkAllSprints'), set::text($lang->projectCommon), set('data-on', 'change'), set('data-call', 'changeAllSprints')) ) ), div ( - set::className('mt-4'), + setClass('mt-4'), $checkBoxGroup ) ), cell ( set::width('1/2'), - set::className('border ml-4 p-4'), - set::id('programBox'), $createProgram($data) + setClass('border ml-4 p-4'), + setID('programBox'), $createProgram($data) ) ); }; diff --git a/module/upgrade/ui/moveextfiles.html.php b/module/upgrade/ui/moveextfiles.html.php index f62c7512e4..ade3a7ea73 100644 --- a/module/upgrade/ui/moveextfiles.html.php +++ b/module/upgrade/ui/moveextfiles.html.php @@ -22,7 +22,7 @@ if($result == 'success') $checkList[] = checkbox ( on::click('checkFileClick'), - set::id($file), + setID($file), set::name("files[$file]"), set::text($file), set::checked(true) @@ -32,10 +32,10 @@ if($result == 'success') div ( - set::id('main'), + setID('main'), div ( - set::id('mainContent'), + setID('mainContent'), formPanel ( setClass('bg-canvas'), @@ -53,7 +53,7 @@ div checkbox ( on::click('checkAllClick'), - set::id('checkAll'), + setID('checkAll'), set::name('checkAll'), set::text($lang->upgrade->fileName), set::checked(true) diff --git a/module/upgrade/ui/renameobject.html.php b/module/upgrade/ui/renameobject.html.php index cbff4adfaf..77c31a2d0b 100644 --- a/module/upgrade/ui/renameobject.html.php +++ b/module/upgrade/ui/renameobject.html.php @@ -37,11 +37,11 @@ $items[] = array div ( - set::id('main'), + setID('main'), div ( - set::id('mainContent'), - set::className('bg-white'), + setID('mainContent'), + setClass('bg-white'), set::style(array('margin' => '50px auto 0', 'width' => '1200px')), formBatchPanel ( From 8ebcd035b626acb662519dfc67ece3fb26d8a1f2 Mon Sep 17 00:00:00 2001 From: xieqiyu Date: Tue, 21 Nov 2023 10:32:37 +0800 Subject: [PATCH 12/92] * Modify task required item. --- module/task/model.php | 13 +++++++++-- module/task/tao.php | 18 ++------------- module/task/ui/activate.html.php | 11 +++++---- module/task/ui/edit.html.php | 38 ++++++++++++++++++-------------- module/task/zen.php | 1 + 5 files changed, 41 insertions(+), 40 deletions(-) diff --git a/module/task/model.php b/module/task/model.php index 774342f29d..e369ad110c 100755 --- a/module/task/model.php +++ b/module/task/model.php @@ -28,7 +28,7 @@ class taskModel extends model if(strpos($this->config->task->activate->requiredFields, 'comment') !== false && !$comment) { - dao::$errors[] = sprintf($this->lang->error->notempty, $this->lang->comment); + dao::$errors['comment'] = sprintf($this->lang->error->notempty, $this->lang->comment); return false; } @@ -331,6 +331,14 @@ class taskModel extends model */ public function afterUpdate(object $oldTask, object $task): void { + /* Update children task. */ + if(isset($task->execution) && $task->execution != $oldTask->execution) + { + $newExecution = $this->loadModel('execution')->getByID((int)$task->execution); + $task->project = $newExecution->project; + $this->dao->update(TABLE_TASK)->set('execution')->eq($task->execution)->set('module')->eq($task->module)->set('project')->eq($task->project)->where('parent')->eq($task->id)->exec(); + } + /* Multi-task change to normal task. */ if($task->mode == 'single') $this->dao->delete()->from(TABLE_TASKTEAM)->where('task')->eq($task->id)->exec(); @@ -2720,8 +2728,9 @@ class taskModel extends model } $requiredFields = $this->taskTao->getRequiredFields4Edit($task); - $this->taskTao->doUpdate($task, $oldTask, $requiredFields); + if(dao::isError()) return false; + $this->taskTao->doUpdate($task, $oldTask, $requiredFields); if(dao::isError()) return false; $this->afterUpdate($oldTask, $task); diff --git a/module/task/tao.php b/module/task/tao.php index 1e473802b2..18edb728a4 100644 --- a/module/task/tao.php +++ b/module/task/tao.php @@ -548,14 +548,6 @@ class taskTao extends taskModel $execution = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($task->execution)->fetch(); if($this->isNoStoryExecution($execution)) $task->story = 0; - /* Update children task. */ - if(isset($task->execution) && $task->execution != $oldTask->execution) - { - $newExecution = $this->loadModel('execution')->getByID((int)$task->execution); - $task->project = $newExecution->project; - $this->dao->update(TABLE_TASK)->set('execution')->eq($task->execution)->set('module')->eq($task->module)->set('project')->eq($task->project)->where('parent')->eq($task->id)->exec(); - } - /* Set the datetime and operator when the task is modified. */ if(empty($task->lastEditedDate) || empty($task->lastEditedBy)) { @@ -867,22 +859,16 @@ class taskTao extends taskModel * @access protected * @return string */ - protected function getRequiredFields4Edit(object $task): string + protected function getRequiredFields4Edit(object $task): string|bool { $execution = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($task->execution)->fetch(); $requiredFields = ',' . $this->config->task->edit->requiredFields . ','; if($this->isNoStoryExecution($execution)) $requiredFields = str_replace(',story,', ',', $requiredFields); - if($task->status != 'cancel' && strpos($requiredFields, ',estimate,') !== false) - { - if(empty($task->estimate)) dao::$errors['estimate'] = sprintf($this->lang->error->notempty, $this->lang->task->estimate); - $requiredFields = str_replace(',estimate,', ',', $requiredFields); - } - if(strpos(',doing,pause,', $task->status) && empty($task->left)) { - dao::$errors[] = sprintf($this->lang->task->error->leftEmptyAB, $this->lang->task->statusList[$task->status]); + dao::$errors['left'] = sprintf($this->lang->task->error->leftEmptyAB, $this->lang->task->statusList[$task->status]); return false; } diff --git a/module/task/ui/activate.html.php b/module/task/ui/activate.html.php index 4e2f5da569..d5537a4b91 100644 --- a/module/task/ui/activate.html.php +++ b/module/task/ui/activate.html.php @@ -330,7 +330,7 @@ formPanel ( set::width('1/2'), set::label($lang->task->assignedTo), - set::required($isMultiple), + set::required($isMultiple || strpos(",{$this->config->task->activate->requiredFields},", ',assignedTo,')), inputGroup ( picker @@ -339,6 +339,7 @@ formPanel set::name('assignedTo'), set::items($isMultiple ? $teamMembers : $members), set::value($isMultiple ? '' : $task->finishedBy), + set::required(strpos(",{$this->config->task->activate->requiredFields},", ',assignedTo,') !== false), on::change('setTeamUser') ), $modalTeamBtn @@ -350,11 +351,9 @@ formPanel formGroup ( set::label($lang->comment), - editor - ( - set::name('comment'), - set::rows('5') - ) + set::control('editor'), + set::name('comment'), + set::rows('5') ), modalTrigger ( diff --git a/module/task/ui/edit.html.php b/module/task/ui/edit.html.php index 387a2156b0..6855719356 100644 --- a/module/task/ui/edit.html.php +++ b/module/task/ui/edit.html.php @@ -411,16 +411,19 @@ detailBody ( set::name($lang->task->estimate), set::required(strpos(",{$this->config->task->edit->requiredFields},", ",estimate,") !== false), - inputControl + formGroup ( - input + inputControl ( - set::name('estimate'), - set::value($task->estimate), - !empty($task->team) ? set::readonly(true) : null - ), - to::suffix($lang->task->suffixHour), - set::suffixWidth(20) + input + ( + set::name('estimate'), + set::value($task->estimate), + !empty($task->team) ? set::readonly(true) : null + ), + to::suffix($lang->task->suffixHour), + set::suffixWidth(20) + ) ) ), item @@ -448,16 +451,19 @@ detailBody item ( set::name($lang->task->left), - inputControl + formGroup ( - input + inputControl ( - set::name('left'), - set::value($task->left), - !empty($task->team) ? set::readonly(true) : null - ), - to::suffix($lang->task->suffixHour), - set::suffixWidth(20) + input + ( + set::name('left'), + set::value($task->left), + !empty($task->team) ? set::readonly(true) : null + ), + to::suffix($lang->task->suffixHour), + set::suffixWidth(20) + ) ) ) ), diff --git a/module/task/zen.php b/module/task/zen.php index 33e9e334e8..4b677815c0 100644 --- a/module/task/zen.php +++ b/module/task/zen.php @@ -765,6 +765,7 @@ class taskZen extends task ->setDefault('assignedTo', $oldTask->openedBy) ->get(); + if(strpos(",{$this->config->task->finish->requiredFields},", ',comment,') !== false && empty($_POST['comment'])) dao::$errors['comment'] = sprintf($this->lang->error->notempty, $this->lang->comment); if(!$this->post->currentConsumed && $oldTask->consumed == '0') dao::$errors['currentConsumed'][] = $this->lang->task->error->consumedEmpty; if($task->realStarted > $task->finishedDate) dao::$errors['finishedDate'][] = $this->lang->task->error->finishedDateSmall; From 20f8c6099d19c9608e7f0c294b28229383413b29 Mon Sep 17 00:00:00 2001 From: wangxuepeng Date: Tue, 21 Nov 2023 10:33:41 +0800 Subject: [PATCH 13/92] * Change to new writing method. --- module/upgrade/ui/checkextension.html.php | 4 ++-- module/upgrade/ui/mergerepo.html.php | 8 ++++---- module/upgrade/ui/selectversion.html.php | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/module/upgrade/ui/checkextension.html.php b/module/upgrade/ui/checkextension.html.php index 92b6e03ae1..1c7da61066 100644 --- a/module/upgrade/ui/checkextension.html.php +++ b/module/upgrade/ui/checkextension.html.php @@ -37,10 +37,10 @@ foreach($extensionsName as $extension => $extensionName) div ( - set::id('main'), + setID('main'), div ( - set::id('mainContent'), + setID('mainContent'), set::className('bg-white p-4'), set::style(array('margin' => '50px auto 0', 'width' => '800px')), div diff --git a/module/upgrade/ui/mergerepo.html.php b/module/upgrade/ui/mergerepo.html.php index cc2b68dd43..1a34fadfbd 100644 --- a/module/upgrade/ui/mergerepo.html.php +++ b/module/upgrade/ui/mergerepo.html.php @@ -18,7 +18,7 @@ foreach($repoes as $repoID => $repo) $repoItems[] = checkbox ( on::change('checkRepo'), - set::id("repoes[{$repoID}]"), + setID("repoes[{$repoID}]"), set::name("repoes[{$repoID}]"), set::text($repo) ); @@ -26,10 +26,10 @@ foreach($repoes as $repoID => $repo) div ( - set::id('main'), + setID('main'), div ( - set::id('mainContent'), + setID('mainContent'), setClass('px-1 mt-2 mx-auto'), width('1200px'), panel @@ -59,7 +59,7 @@ div checkbox ( on::change('checkAllRepoes'), - set::id('checkAllRepoes'), + setID('checkAllRepoes'), set::name('checkAllRepoes'), set::text($lang->upgrade->repo) ) diff --git a/module/upgrade/ui/selectversion.html.php b/module/upgrade/ui/selectversion.html.php index f9e5a15188..1b9a5161d5 100644 --- a/module/upgrade/ui/selectversion.html.php +++ b/module/upgrade/ui/selectversion.html.php @@ -14,10 +14,10 @@ set::zui(true); div ( - set::id('main'), + setID('main'), div ( - set::id('mainContent'), + setID('mainContent'), formPanel ( zui::width('800px'), From 8e487888dee0893593886680ed2ef535ade97b69 Mon Sep 17 00:00:00 2001 From: wangxuepeng Date: Tue, 21 Nov 2023 10:57:45 +0800 Subject: [PATCH 14/92] * Change to new writing method. --- module/productplan/ui/browsebylist.html.php | 4 ++-- module/productplan/ui/close.html.php | 2 +- module/productplan/ui/linkbug.html.php | 2 +- module/productplan/ui/linkstory.html.php | 2 +- module/productplan/ui/view.html.php | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/module/productplan/ui/browsebylist.html.php b/module/productplan/ui/browsebylist.html.php index fa62e053fb..65e92b6190 100644 --- a/module/productplan/ui/browsebylist.html.php +++ b/module/productplan/ui/browsebylist.html.php @@ -66,8 +66,8 @@ if($canBatchAction) menu ( - set::id('navStatus'), - set::className('dropdown-menu'), + setID('navStatus'), + setClass('dropdown-menu'), set::items(array_values($items)) ); diff --git a/module/productplan/ui/close.html.php b/module/productplan/ui/close.html.php index 02b2ae6a68..926a56715c 100644 --- a/module/productplan/ui/close.html.php +++ b/module/productplan/ui/close.html.php @@ -24,7 +24,7 @@ formPanel set::required(true), select ( - set::id('closedReasonBox'), + setID('closedReasonBox'), set::name('closedReason'), set::strong(false), set::items($lang->productplan->closedReasonList), diff --git a/module/productplan/ui/linkbug.html.php b/module/productplan/ui/linkbug.html.php index 9f66f8ff69..39959a7a8c 100644 --- a/module/productplan/ui/linkbug.html.php +++ b/module/productplan/ui/linkbug.html.php @@ -25,7 +25,7 @@ searchForm dtable ( - set::id('unlinkBugList'), + setID('unlinkBugList'), set::userMap($users), set::checkable(true), set::cols($cols), diff --git a/module/productplan/ui/linkstory.html.php b/module/productplan/ui/linkstory.html.php index d7f028f2f9..dc431e1175 100644 --- a/module/productplan/ui/linkstory.html.php +++ b/module/productplan/ui/linkstory.html.php @@ -32,7 +32,7 @@ searchForm dtable ( - set::id('unlinkStoryList'), + setID('unlinkStoryList'), set::userMap($users), set::cols($cols), set::data(array_values($allStories)), diff --git a/module/productplan/ui/view.html.php b/module/productplan/ui/view.html.php index 71e1771c55..b6fc141c5d 100644 --- a/module/productplan/ui/view.html.php +++ b/module/productplan/ui/view.html.php @@ -203,7 +203,7 @@ detailBody ), dtable ( - set::id('storyDTable'), + setID('storyDTable'), set::userMap($users), set::bordered(true), set::cols($storyCols), @@ -245,7 +245,7 @@ detailBody ), dtable ( - set::id('bugDTable'), + setID('bugDTable'), set::userMap($users), set::bordered(true), set::cols($bugCols), From 66522370f1d8ca314f91534485bdbad169a76bdc Mon Sep 17 00:00:00 2001 From: xieqiyu Date: Tue, 21 Nov 2023 10:59:58 +0800 Subject: [PATCH 15/92] * Modify bug and testcase requited items. --- module/bug/ui/create.html.php | 1 + module/testcase/css/edit.ui.css | 2 +- module/testcase/ui/edit.html.php | 8 ++++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/module/bug/ui/create.html.php b/module/bug/ui/create.html.php index 12ee32c9bc..bce0b48609 100644 --- a/module/bug/ui/create.html.php +++ b/module/bug/ui/create.html.php @@ -335,6 +335,7 @@ formPanel formGroup ( set::label($lang->bug->steps), + set::required(strpos(",{$this->config->bug->create->requiredFields},", ",steps,") !== false), editor ( set::name('steps'), diff --git a/module/testcase/css/edit.ui.css b/module/testcase/css/edit.ui.css index 3d532f5123..d59d65f629 100644 --- a/module/testcase/css/edit.ui.css +++ b/module/testcase/css/edit.ui.css @@ -1 +1 @@ -.table-data th {width: 78px !important;} +.table-data th {width: 80px !important;} diff --git a/module/testcase/ui/edit.html.php b/module/testcase/ui/edit.html.php index f8eaea5d3f..041cc5ab2e 100644 --- a/module/testcase/ui/edit.html.php +++ b/module/testcase/ui/edit.html.php @@ -117,6 +117,7 @@ detailBody section ( set::title($lang->testcase->precondition), + set::required(strpos(",{$this->config->testcase->edit->requiredFields},", ",precondition,") !== false), formGroup ( textarea @@ -195,6 +196,7 @@ detailBody item ( set::name($lang->testcase->module), + set::required(strpos(",{$this->config->testcase->edit->requiredFields},", ",module,") !== false), formGroup ( inputGroup @@ -232,6 +234,7 @@ detailBody !$isLibCase ? item ( set::name($lang->testcase->story), + set::required(strpos(",{$this->config->testcase->edit->requiredFields},", ",story,") !== false), formGroup ( setID('storyIdBox'), @@ -290,6 +293,7 @@ detailBody item ( set::name($lang->testcase->stage), + set::required(strpos(",{$this->config->testcase->edit->requiredFields},", ",stage,") !== false), formGroup ( picker @@ -304,6 +308,7 @@ detailBody item ( set::name($lang->testcase->pri), + set::required(strpos(",{$this->config->testcase->edit->requiredFields},", ",pri,") !== false), formGroup ( priPicker @@ -317,6 +322,7 @@ detailBody item ( set::name($lang->testcase->status), + set::required(strpos(",{$this->config->testcase->edit->requiredFields},", ",status,") !== false), !$forceNotReview && $case->status == 'wait' ? $lang->testcase->statusList[$case->status] : formGroup ( @@ -324,6 +330,7 @@ detailBody ( set::name('status'), set::items($lang->testcase->statusList), + set::required(strpos(",{$this->config->testcase->edit->requiredFields},", ",status,") !== false), set::value($case->status) ) ) @@ -331,6 +338,7 @@ detailBody item ( set::name($lang->testcase->keywords), + set::required(strpos(",{$this->config->testcase->edit->requiredFields},", ",keywords,") !== false), formGroup ( input From d7b50df2a4d17216350a756657c6a007a17ce207 Mon Sep 17 00:00:00 2001 From: tianshujie Date: Tue, 21 Nov 2023 11:00:22 +0800 Subject: [PATCH 16/92] * Show '0' of the label. --- lib/zin/wg/formgroup/v1.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/zin/wg/formgroup/v1.php b/lib/zin/wg/formgroup/v1.php index 699c7c9f88..d85673f7f3 100644 --- a/lib/zin/wg/formgroup/v1.php +++ b/lib/zin/wg/formgroup/v1.php @@ -54,7 +54,7 @@ class formGroup extends wg zui::width($width), set($this->getRestProps()), setCssVar('form-grid-label-width', $labelWidth), - empty($label) ? null : new formLabel + empty($label) && $label !== '0' ? null : new formLabel ( set::className($labelClass, $strong ? 'font-bold' : null), set::required($required), From 425413a58e357c1c6f8a53581cb996a145e459e9 Mon Sep 17 00:00:00 2001 From: tianshujie Date: Tue, 21 Nov 2023 11:01:09 +0800 Subject: [PATCH 17/92] * Fixed the error of getting array without key value. --- module/kanban/model.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/kanban/model.php b/module/kanban/model.php index ba632da60e..81621f7dc7 100755 --- a/module/kanban/model.php +++ b/module/kanban/model.php @@ -1743,7 +1743,7 @@ class kanbanModel extends model if($cardIdList) { $cardsData = $this->buildExecutionCards($cardsData, $column, $lane->type, $cardIdList, $objectGroup, $searchValue, $menus); - $columnsData[$column->id]['cards'] = count($cardsData[$column->lane][$column->id]); + $columnsData[$column->id]['cards'] = empty($cardsData[$column->lane][$column->id]) ? 0 : count($cardsData[$column->lane][$column->id]); } } From b914553655205377be7afe1055dd85b11786b037 Mon Sep 17 00:00:00 2001 From: xieqiyu Date: Tue, 21 Nov 2023 11:09:46 +0800 Subject: [PATCH 18/92] * Modify testtask required items. --- module/testtask/ui/create.html.php | 8 +++----- module/testtask/ui/edit.html.php | 1 + 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/module/testtask/ui/create.html.php b/module/testtask/ui/create.html.php index b6c44d14f1..05bdaab4a1 100644 --- a/module/testtask/ui/create.html.php +++ b/module/testtask/ui/create.html.php @@ -167,11 +167,9 @@ formPanel formGroup ( set::label($lang->testtask->desc), - editor - ( - set::name('desc'), - set::rows(10) - ) + set::control('editor'), + set::name('desc'), + set::rows(10) ), formGroup ( diff --git a/module/testtask/ui/edit.html.php b/module/testtask/ui/edit.html.php index 36c2174128..3a6ba1aecc 100644 --- a/module/testtask/ui/edit.html.php +++ b/module/testtask/ui/edit.html.php @@ -128,6 +128,7 @@ formPanel formGroup ( set::label($lang->testtask->desc), + set::required(strpos(",{$this->config->testtask->edit->requiredFields},", ",desc,") !== false), editor ( set::name('desc'), From ab31a8efde9a5c0f722e85922b9cb99957966cf1 Mon Sep 17 00:00:00 2001 From: liumengyi Date: Tue, 21 Nov 2023 11:07:54 +0800 Subject: [PATCH 19/92] * Replace `select` with `picker` of version field in testsuite::linkCase. --- module/testsuite/config/table.php | 18 ++++++++++++++---- module/testsuite/js/linkcase.ui.js | 12 ++++-------- module/testsuite/ui/linkcase.html.php | 1 + 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/module/testsuite/config/table.php b/module/testsuite/config/table.php index 0649dcf93c..2121eba42a 100644 --- a/module/testsuite/config/table.php +++ b/module/testsuite/config/table.php @@ -162,10 +162,20 @@ $config->testsuite->linkcase->dtable->fieldList['status'] = $config->testcase->d $config->testsuite->linkcase->dtable->fieldList['id']['name'] = 'id'; -$config->testsuite->linkcase->dtable->fieldList['version']['name'] = 'version'; -$config->testsuite->linkcase->dtable->fieldList['version']['title'] = $lang->testsuite->linkVersion; -$config->testsuite->linkcase->dtable->fieldList['version']['type'] = 'text'; -$config->testsuite->linkcase->dtable->fieldList['version']['group'] = 'version'; +$config->testsuite->linkcase->dtable->fieldList['title']['link']['params'] = 'caseID={id}'; +$config->testsuite->linkcase->dtable->fieldList['title']['data-toggle'] = 'modal'; +$config->testsuite->linkcase->dtable->fieldList['title']['data-size'] = 'lg'; + +$config->testsuite->linkcase->dtable->fieldList['pri']['type'] = 'control'; +$config->testsuite->linkcase->dtable->fieldList['pri']['control'] = 'picker'; +$config->testsuite->linkcase->dtable->fieldList['pri']['controlItems'] = $lang->testcase->priList; + +$config->testsuite->linkcase->dtable->fieldList['version']['name'] = 'version'; +$config->testsuite->linkcase->dtable->fieldList['version']['title'] = $lang->testsuite->linkVersion; +$config->testsuite->linkcase->dtable->fieldList['version']['type'] = 'control'; +$config->testsuite->linkcase->dtable->fieldList['version']['control'] = 'picker'; +$config->testsuite->linkcase->dtable->fieldList['version']['group'] = 'version'; +$config->testsuite->linkcase->dtable->fieldList['version']['controlItems'] = array(1 => 1); $config->testsuite->linkcase->dtable->fieldList['openedBy'] = $config->testcase->dtable->fieldList['openedBy']; unset($config->testsuite->linkcase->dtable->fieldList['title']['nestedToggle']); diff --git a/module/testsuite/js/linkcase.ui.js b/module/testsuite/js/linkcase.ui.js index 71e484365a..fc28cbbf0c 100644 --- a/module/testsuite/js/linkcase.ui.js +++ b/module/testsuite/js/linkcase.ui.js @@ -47,14 +47,10 @@ window.renderCell = function(result, info) } if(info.col.name == 'version' && result[0]) { - const testcase = info.row.data; - let html = ""; - result[0] = {html}; + if(info.row.data.version == 1) return result; + let versions = []; + for(i = 1; i <= info.row.data.version; i++) versions.push({'text': i, 'value': i}); + result[0].children.props.items = versions; } return result; } diff --git a/module/testsuite/ui/linkcase.html.php b/module/testsuite/ui/linkcase.html.php index 25b0b68f75..7dea89f4b8 100644 --- a/module/testsuite/ui/linkcase.html.php +++ b/module/testsuite/ui/linkcase.html.php @@ -48,6 +48,7 @@ dtable set::cols($config->testsuite->linkcase->dtable->fieldList), set::fixedLeftWidth('33%'), set::checkable(true), + set::plugins(array('form')), set::footToolbar($footToolbar), set::onRenderCell(jsRaw('window.renderCell')), set::footPager(usePager()) From 050e2e086123668e077c1a45ec524b46ac8d5483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E9=87=91=E5=8B=87?= Date: Tue, 21 Nov 2023 03:14:50 +0000 Subject: [PATCH 20/92] * Performance for stats. --- framework/router.class.php | 20 ++ module/block/ui/scrumlistblock.html.php | 8 +- module/block/zen.php | 16 +- module/cron/control.php | 58 ------ module/editor/control.php | 2 +- module/execution/control.php | 2 + module/execution/model.php | 7 - module/product/config.php | 4 +- module/product/config/dtable.php | 42 ++-- module/product/control.php | 3 + module/product/model.php | 85 +------- module/product/tao.php | 33 +-- module/product/zen.php | 8 - module/program/control.php | 21 +- module/program/model.php | 259 +++++------------------- module/program/ui/browse.html.php | 7 +- module/project/control.php | 4 +- module/project/js/browse.ui.js | 2 +- module/project/model.php | 29 +-- module/project/ui/browsebylist.html.php | 4 + 20 files changed, 149 insertions(+), 465 deletions(-) diff --git a/framework/router.class.php b/framework/router.class.php index a7bd03d858..b6814b2e97 100755 --- a/framework/router.class.php +++ b/framework/router.class.php @@ -716,4 +716,24 @@ class router extends baseRouter return parent::mergeParams($defaultParams, $passedParams); } + + /** + * 加载一个模块: + * + * Load a module. + * + * @access public + * @return bool|object if the module object of die. + */ + public function loadModule() + { + /* 不能直接请求基类的方法 Cannot call methods of base control class. */ + if(method_exists('Control', $this->methodName)) + { + echo 'Cannot call methods of base control class.'; + return false; + } + + return parent::loadModule(); + } } diff --git a/module/block/ui/scrumlistblock.html.php b/module/block/ui/scrumlistblock.html.php index 1bb6033a6f..f017fc2bd3 100644 --- a/module/block/ui/scrumlistblock.html.php +++ b/module/block/ui/scrumlistblock.html.php @@ -15,10 +15,10 @@ jsVar('delayInfo', $lang->project->delayInfo); foreach($executionStats as $scrum) { - $scrum->totalEstimate = zget($scrum->hours, 'totalEstimate', 0) . $lang->execution->workHourUnit; - $scrum->totalConsumed = zget($scrum->hours, 'totalConsumed', 0) . $lang->execution->workHourUnit; - $scrum->totalLeft = zget($scrum->hours, 'totalLeft', 0) . $lang->execution->workHourUnit; - $scrum->progress = zget($scrum->hours, 'progress', 0); + $scrum->totalEstimate = zget($scrum, 'estimate', 0) . $lang->execution->workHourUnit; + $scrum->totalConsumed = zget($scrum, 'consumed', 0) . $lang->execution->workHourUnit; + $scrum->totalLeft = zget($scrum, 'left', 0) . $lang->execution->workHourUnit; + $scrum->progress = zget($scrum, 'progress', 0); } if(!$longBlock) diff --git a/module/block/zen.php b/module/block/zen.php index 701c448be0..4fcbcd9173 100644 --- a/module/block/zen.php +++ b/module/block/zen.php @@ -755,21 +755,7 @@ class blockZen extends block $type = isset($block->params->type) ? $block->params->type : 'all'; $orderBy = isset($block->params->orderBy) ? $block->params->orderBy : 'id_desc'; - $projects = $this->loadModel('project')->getOverviewList($type, 0, $orderBy, $count); - - /* Get all tasks and compute totalEstimate, totalConsumed, totalLeft, progress according to them. */ - $tasks = $this->dao->select('id, project, estimate, consumed, `left`, status, closedReason, execution') - ->from(TABLE_TASK) - ->where('project')->in(array_keys($projects)) - ->andWhere('parent')->lt(1) - ->andWhere('deleted')->eq(0) - ->fetchGroup('project', 'id'); - $hours = $this->loadModel('program')->computeProjectHours($tasks); - - $projects = $this->program->appendStatToProjects($projects, 'hours', array('hours' => $hours)); - foreach($projects as $project) $project->progress = $project->hours->progress; - - $this->view->projects = $projects; + $this->view->projects = $this->loadModel('project')->getOverviewList($type, 0, $orderBy, $count); $this->view->users = $this->loadModel('user')->getPairs('noletter', '', 0, array_unique(array_column($this->view->projects, 'PM'))); } diff --git a/module/cron/control.php b/module/cron/control.php index da2a63ed35..2aa3dda32f 100644 --- a/module/cron/control.php +++ b/module/cron/control.php @@ -115,64 +115,6 @@ class cron extends control return $this->sendSuccess(array('load' => true)); } - /** - * Schedule cron task by RoadRunner. - * - * @access public - * @return void - */ - public function rrSchedule() - { - if('cli' !== PHP_SAPI) return; - - set_time_limit(0); - session_write_close(); - - $this->loadModel('common'); - - $execId = mt_rand(); - while(true) - { - dao::$cache = array(); - if(empty($this->config->global->cron) || !$this->canSchedule($execId)) - { - sleep(60); - continue; - } - - $this->schedule($execId); - sleep(30); - } - } - - /** - * Consume cron task by RoadRunner. - * - * @access public - * @return void - */ - public function rrConsume() - { - if('cli' !== PHP_SAPI) return; - - set_time_limit(0); - session_write_close(); - - $this->loadModel('common'); - - while(true) - { - if(empty($this->config->global->cron)) - { - sleep(60); - continue; - } - - $this->execTasks(mt_rand()); - sleep(10); - } - } - /** * 使用Ajax请求执行定时任务. * Ajax execute cron. diff --git a/module/editor/control.php b/module/editor/control.php index 472162befb..a762c99d54 100644 --- a/module/editor/control.php +++ b/module/editor/control.php @@ -73,7 +73,7 @@ class editor extends control $extension = 'php'; if($filePath) { - $filePath = helper::safe64Decode($filePath); + $filePath = realpath($filePath); if(strpos(strtolower($filePath), strtolower($this->app->getBasePath())) !== 0) return print($this->lang->editor->editFileError); if($action == 'extendOther' and file_exists($filePath)) $this->view->showContent = file_get_contents($filePath); diff --git a/module/execution/control.php b/module/execution/control.php index 3bf371e8c1..2afc5ff4e2 100644 --- a/module/execution/control.php +++ b/module/execution/control.php @@ -2691,6 +2691,8 @@ class execution extends control $this->app->loadClass('pager', true); $pager = new pager($recTotal, $recPerPage, $pageID); + $this->loadModel('program')->refreshStats(); // Refresh stats fields of projects. + $queryID = ($status == 'bySearch') ? (int)$param : 0; $actionURL = $this->createLink('execution', 'all', "status=bySearch&orderBy=$orderBy&productID=$productID¶m=myQueryID"); $this->execution->buildSearchForm($queryID, $actionURL); diff --git a/module/execution/model.php b/module/execution/model.php index ea2d81d0b7..8c335d0b73 100755 --- a/module/execution/model.php +++ b/module/execution/model.php @@ -1610,14 +1610,11 @@ class executionModel extends model $parentList = array(); $emptyHour = array('totalEstimate' => 0, 'totalConsumed' => 0, 'totalLeft' => 0, 'progress' => 0); $today = helper::today(); - $hours = $this->loadModel('project')->computeProgress($executions); $burns = $this->getBurnData($executions); foreach($executions as $execution) { $execution->productName = isset($productList[$execution->id]) ? trim($productList[$execution->id]->productName, ',') : ''; $execution->end = date(DT_DATE1, strtotime($execution->end)); - $execution->hours = isset($hours[$execution->id]) ? $hours[$execution->id] : (object)$emptyHour; - $execution->teamCount = isset($memberGroup[$execution->id]) ? $memberGroup[$execution->id]->teams : 0; if(isset($executions[$execution->parent])) $executions[$execution->parent]->isParent = 1; if(empty($productID) && !empty($productList[$execution->id])) $execution->product = trim($productList[$execution->id]->product, ','); @@ -4658,10 +4655,6 @@ class executionModel extends model $execution->project = $execution->projectName; $execution->parent = ($execution->parent && $execution->grade > 1) ? 'pid' . (string)$execution->parent : ''; $execution->isParent = !empty($execution->isParent) or !empty($execution->tasks); - $execution->progress = $execution->hours->progress; - $execution->totalEstimate = $execution->hours->totalEstimate; - $execution->totalConsumed = $execution->hours->totalConsumed; - $execution->totalLeft = $execution->hours->totalLeft; $execution->actions = array(); foreach($this->config->projectExecution->dtable->fieldList['actions'][$execution->projectModel] as $actionKey) { diff --git a/module/product/config.php b/module/product/config.php index 848e0d94f8..546b8bf50c 100644 --- a/module/product/config.php +++ b/module/product/config.php @@ -47,8 +47,8 @@ $config->product->report->planLabels[] = ''; $config->product->statisticFields = array(); $config->product->statisticFields['requirements'] = array('draftRequirements', 'activeRequirements', 'changingRequirements', 'reviewingRequirements', 'closedRequirements'); -$config->product->statisticFields['stories'] = array('draftStories', 'activeStories', 'changingStories', 'reviewingStories', 'closedStories', 'finishClosedStories', 'unclosedStories'); -$config->product->statisticFields['bugs'] = array('unResolvedBugs', 'closedBugs', 'fixedBugs'); +$config->product->statisticFields['stories'] = array('draftStories', 'activeStories', 'changingStories', 'reviewingStories', 'closedStories', 'finishedStories', 'totalStories'); +$config->product->statisticFields['bugs'] = array('unresolvedBugs', 'closedBugs', 'fixedBugs'); $config->product->statisticFields['plans'] = array('plans'); $config->product->statisticFields['releases'] = array('releases'); diff --git a/module/product/config/dtable.php b/module/product/config/dtable.php index 9e2e630128..72c0ba5893 100644 --- a/module/product/config/dtable.php +++ b/module/product/config/dtable.php @@ -98,12 +98,12 @@ $config->product->dtable->fieldList['testCaseCoverage']['type'] = 'progress' $config->product->dtable->fieldList['testCaseCoverage']['sortType'] = false; $config->product->dtable->fieldList['testCaseCoverage']['border'] = 'right'; -$config->product->dtable->fieldList['unResolvedBugs']['name'] = 'unResolvedBugs'; -$config->product->dtable->fieldList['unResolvedBugs']['title'] = $lang->product->activatedBug; -$config->product->dtable->fieldList['unResolvedBugs']['minWidth'] = 64; -$config->product->dtable->fieldList['unResolvedBugs']['type'] = 'number'; -$config->product->dtable->fieldList['unResolvedBugs']['sortType'] = false; -$config->product->dtable->fieldList['unResolvedBugs']['align'] = 'center'; +$config->product->dtable->fieldList['unresolvedBugs']['name'] = 'unresolvedBugs'; +$config->product->dtable->fieldList['unresolvedBugs']['title'] = $lang->product->activatedBug; +$config->product->dtable->fieldList['unresolvedBugs']['minWidth'] = 64; +$config->product->dtable->fieldList['unresolvedBugs']['type'] = 'number'; +$config->product->dtable->fieldList['unresolvedBugs']['sortType'] = false; +$config->product->dtable->fieldList['unresolvedBugs']['align'] = 'center'; $config->product->dtable->fieldList['bugFixedRate']['name'] = 'bugFixedRate'; $config->product->dtable->fieldList['bugFixedRate']['title'] = $lang->product->bugFixedRate; @@ -240,13 +240,13 @@ $config->product->all->dtable->fieldList['plans']['show'] = true; $config->product->all->dtable->fieldList['plans']['sortType'] = false; $config->product->all->dtable->fieldList['plans']['group'] = 'g5'; -$config->product->all->dtable->fieldList['execution']['name'] = 'execution'; -$config->product->all->dtable->fieldList['execution']['title'] = $lang->execution->common; -$config->product->all->dtable->fieldList['execution']['width'] = 64; -$config->product->all->dtable->fieldList['execution']['type'] = 'number'; -$config->product->all->dtable->fieldList['execution']['show'] = true; -$config->product->all->dtable->fieldList['execution']['sortType'] = false; -$config->product->all->dtable->fieldList['execution']['group'] = 'g6'; +$config->product->all->dtable->fieldList['executions']['name'] = 'executions'; +$config->product->all->dtable->fieldList['executions']['title'] = $lang->execution->common; +$config->product->all->dtable->fieldList['executions']['width'] = 64; +$config->product->all->dtable->fieldList['executions']['type'] = 'number'; +$config->product->all->dtable->fieldList['executions']['show'] = true; +$config->product->all->dtable->fieldList['executions']['sortType'] = false; +$config->product->all->dtable->fieldList['executions']['group'] = 'g6'; $config->product->all->dtable->fieldList['testCaseCoverage']['name'] = 'testCaseCoverage'; $config->product->all->dtable->fieldList['testCaseCoverage']['title'] = $lang->product->testCaseCoverage; @@ -257,14 +257,14 @@ $config->product->all->dtable->fieldList['testCaseCoverage']['show'] = true; $config->product->all->dtable->fieldList['testCaseCoverage']['sortType'] = false; $config->product->all->dtable->fieldList['testCaseCoverage']['group'] = 'g7'; -$config->product->all->dtable->fieldList['totalActivatedBugs']['name'] = 'totalActivatedBugs'; -$config->product->all->dtable->fieldList['totalActivatedBugs']['title'] = $lang->product->activatedBug; -$config->product->all->dtable->fieldList['totalActivatedBugs']['width'] = 64; -$config->product->all->dtable->fieldList['totalActivatedBugs']['minWidth'] = 86; -$config->product->all->dtable->fieldList['totalActivatedBugs']['type'] = 'number'; -$config->product->all->dtable->fieldList['totalActivatedBugs']['show'] = true; -$config->product->all->dtable->fieldList['totalActivatedBugs']['sortType'] = false; -$config->product->all->dtable->fieldList['totalActivatedBugs']['group'] = 'g8'; +$config->product->all->dtable->fieldList['unresolvedBugs']['name'] = 'unresolvedBugs'; +$config->product->all->dtable->fieldList['unresolvedBugs']['title'] = $lang->product->activatedBug; +$config->product->all->dtable->fieldList['unresolvedBugs']['width'] = 64; +$config->product->all->dtable->fieldList['unresolvedBugs']['minWidth'] = 86; +$config->product->all->dtable->fieldList['unresolvedBugs']['type'] = 'number'; +$config->product->all->dtable->fieldList['unresolvedBugs']['show'] = true; +$config->product->all->dtable->fieldList['unresolvedBugs']['sortType'] = false; +$config->product->all->dtable->fieldList['unresolvedBugs']['group'] = 'g8'; $config->product->all->dtable->fieldList['totalBugs']['name'] = 'totalBugs'; $config->product->all->dtable->fieldList['totalBugs']['title'] = $lang->product->totalBugs; diff --git a/module/product/control.php b/module/product/control.php index 3dc80a840a..c3078285ee 100755 --- a/module/product/control.php +++ b/module/product/control.php @@ -587,6 +587,9 @@ class product extends control $this->app->loadClass('pager', true); $pager = new pager($recTotal, $recPerPage, $pageID); + + $this->product->refreshStats(); // Refresh stats fields of products. + $productStatList = $this->product->getStats(array_keys($products), $orderBy, $pager, 'story', $programID); /* Generate root program list. */ diff --git a/module/product/model.php b/module/product/model.php index 3985e7b5eb..5ea69bd7e1 100755 --- a/module/product/model.php +++ b/module/product/model.php @@ -880,26 +880,7 @@ class productModel extends model $projects = $this->getProjectListByProduct($productID, $browseType, $branch, $involved, $orderBy, $pager); if(empty($projects)) return array(); - $projectKeys = array_keys($projects); - - /* Get all tasks and compute totalEstimate, totalConsumed, totalLeft, progress according to them. */ - $tasks = $this->dao->select('id, project, estimate, consumed, `left`, status, closedReason') - ->from(TABLE_TASK) - ->where('project')->in($projectKeys) - ->andWhere('parent')->lt(1) - ->andWhere('deleted')->eq(0) - ->fetchGroup('project', 'id'); - $hours = $this->loadModel('program')->computeProjectHours($tasks); - - /* Get the number of project teams. */ - $teams = $this->dao->select('t1.root,t1.account')->from(TABLE_TEAM)->alias('t1') - ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account=t2.account') - ->where('t1.root')->in($projectKeys) - ->andWhere('t1.type')->eq('project') - ->andWhere('t2.deleted')->eq(0) - ->fetchGroup('root', 'account'); - - return $this->program->appendStatToProjects($projects, 'hours,teamCount', array('hours' => $hours, 'teams' => $teams)); + return $this->program->appendStatToProjects($projects); } /** @@ -1066,44 +1047,22 @@ class productModel extends model if(empty($productIdList)) return array(); - /* Get stats data. */ - $products = $this->getStatsProducts($productIdList, $programID == 0, $orderBy, $pager); - $unclosedStory = $this->loadModel('story')->getUnClosedTotal(); - $finishClosedStory = $this->story->getFinishClosedTotal(); + $stats = $this->getStatsProducts($productIdList, $programID == 0, $orderBy, $pager); - $modules = array('plans', 'releases', 'latestReleases', 'bugs', 'unResolved', 'activeBugs', 'fixedBugs', 'closedBugs', 'thisWeekBugs', 'assignToNull'); - foreach($modules as $module) $$module = $this->productTao->getStatisticByType($productIdList, $module); - list($stories, $requirements) = $this->getStatsStoriesAndRequirements($productIdList, $storyType); + $latestReleases = $this->productTao->getStatisticByType($productIdList, 'latestReleases'); $executionCountPairs = $this->productTao->getExecutionCountPairs($productIdList); $coveragePairs = $this->getCaseCoveragePairs($productIdList); - $projectsPairs = $this->productTao->getProjectCountPairs($productIdList); - /* Render statistic result to each product. */ - $stats = array(); - foreach($products as $productID => $product) + foreach($stats as $productID => $product) { - foreach($modules as $field) $product->$field = zget($$field, $productID, 0); - $product->stories = zget($stories, $product->id, array()); - $product->stories['finishClosed'] = zget($finishClosedStory, $product->id, 0); - $product->stories['unclosed'] = zget($unclosedStory, $product->id, 0); - $product->activeStories = zget($product->stories, 'active', 0); - $product->requirements = zget($requirements, $product->id, array()); $product->executions = zget($executionCountPairs, $product->id, 0); $product->coverage = zget($coveragePairs, $product->id, 0); - $product->projects = zget($projectsPairs, $product->id, 0); $latestRelease = isset($latestReleases[$product->id]) ? $latestReleases[$product->id][0] : null; $product->latestRelease = $latestRelease ? $latestRelease->name : ''; $product->latestReleaseDate = $latestRelease ? $latestRelease->date : ''; - - /* Calculate product progress. */ - $closedTotal = $product->stories['closed'] + $product->requirements['closed']; - $allTotal = array_sum($product->stories) + array_sum($product->requirements); - $product->progress = empty($closedTotal) ? 0 : round($closedTotal / $allTotal * 100, 1); - - $stats[$productID] = $product; } return $stats; @@ -1494,35 +1453,14 @@ class productModel extends model */ public function formatDataForList(object $product, array $users): object { - $totalStories = $product->stories['closed'] + $product->stories['unclosed']; - $totalBugs = $product->unResolved + $product->fixedBugs; + $product->type = 'product'; + $product->productLine = $product->lineName; + $product->PO = !empty($product->PO) ? zget($users, $product->PO) : ''; + $product->testCaseCoverage = $product->coverage; + $product->storyCompleteRate = $totalStories == 0 ? 0 : round($product->finishedStories / $totalStories, 3) * 100; + $product->bugFixedRate = ($product->unresolvedBugs + $product->fixedBugs) == 0 ? 0 : round($product->fixedBugs / ($product->unresolvedBugs + $product->fixedBugs), 3) * 100; - $item = new stdClass(); - $item->type = 'product'; - $item->id = $product->id; - $item->name = $product->name; - $item->productLine = $product->lineName; - $item->PO = !empty($product->PO) ? zget($users, $product->PO) : ''; - $item->createdDate = $product->createdDate; - $item->createdBy = $product->createdBy; - $item->draftStories = $product->stories['draft']; - $item->activeStories = $product->stories['active']; - $item->changingStories = $product->stories['changing']; - $item->reviewingStories = $product->stories['reviewing']; - $item->totalStories = $totalStories; - $item->storyCompleteRate = ($totalStories == 0 ? 0 : round($product->stories['finishClosed'] / $totalStories, 3) * 100); - $item->plans = $product->plans; - $item->execution = $product->executions; - $item->testCaseCoverage = $product->coverage; - $item->totalActivatedBugs= $product->activeBugs; - $item->totalBugs = $product->bugs; - $item->bugFixedRate = ($totalBugs == 0 ? 0 : round($product->fixedBugs / $totalBugs, 3) * 100); - $item->releases = $product->releases; - $item->latestReleaseDate = $product->latestReleaseDate; - $item->latestRelease = $product->latestRelease; - if(isset($product->actions)) $item->actions = $product->actions; - - return $item; + return $product; } /** @@ -2172,5 +2110,4 @@ class productModel extends model /* 4. Clear actions older than 30 days. */ $this->loadModel('action')->cleanActions(); } - } diff --git a/module/product/tao.php b/module/product/tao.php index 187567e512..bd69eaaaa9 100644 --- a/module/product/tao.php +++ b/module/product/tao.php @@ -804,41 +804,10 @@ class productTao extends productModel $data = $type == 'program' ? $programStructure[$product->program] : $programStructure[$product->program][$product->line]; foreach($this->config->product->statisticFields as $key => $fields) { - /* Get the total number of requirements and stories. */ - if(strpos('stories|requirements', $key) !== false) - { - $totalObjects = 0; - foreach($product->$key as $status => $number) if(isset($this->lang->story->statusList[$status])) $totalObjects += $number; - - $fieldType = $key == 'stories' ? 'Stories' : 'Requirements'; - if(!isset($data['total' . $fieldType])) $data['total' . $fieldType] = 0; - $data['total' . $fieldType] += $totalObjects; - } - elseif($key == 'bugs') - { - $fieldType = 'Bugs'; - } - foreach($fields as $field) { if(!isset($data[$field])) $data[$field] = 0; - - $status = $field; - if(strpos($field, 'Requirements') !== false or strpos($field, 'Stories') !== false or $field == 'unResolvedBugs') - { - $length = strpos($field, $fieldType); - $status = substr($field, 0, $length); - } - - if(strpos('requirements|stories', $key) !== false) - { - $objects = $product->$key; - $data[$field] += $objects[$status]; - } - else - { - $data[$field] += $product->$status; - } + $data[$field] += $product->$field; } } diff --git a/module/product/zen.php b/module/product/zen.php index 9130e595d0..8118fd7b0f 100644 --- a/module/product/zen.php +++ b/module/product/zen.php @@ -434,15 +434,7 @@ class productZen extends product { $product->line = zget($lines, $product->line, ''); $product->manager = zget($users, $product->PO, ''); - $product->draftStories = (int)$product->stories['draft']; - $product->activeStories = (int)$product->stories['active']; - $product->changedStories = (int)$product->stories['changing']; - $product->reviewingStories = (int)$product->stories['reviewing']; - $product->closedStories = (int)$product->stories['closed']; - $product->totalStories = $product->activeStories + $product->changedStories + $product->draftStories + $product->closedStories + $product->reviewingStories; $product->storyCompleteRate = ($product->totalStories == 0 ? 0 : round($product->closedStories / $product->totalStories, 3) * 100) . '%'; - $product->unResolvedBugs = (int)$product->unResolved; - $product->assignToNullBugs = (int)$product->assignToNull; $product->bugFixedRate = (($product->unResolved + $product->fixedBugs) == 0 ? 0 : round($product->fixedBugs / ($product->unResolved + $product->fixedBugs), 3) * 100) . '%'; $product->program = $product->programName; } diff --git a/module/program/control.php b/module/program/control.php index b107735f5a..9119dc06c0 100644 --- a/module/program/control.php +++ b/module/program/control.php @@ -45,6 +45,8 @@ class program extends control $this->app->loadClass('pager', $static = true); $pager = new pager($recTotal, $recPerPage, $pageID); + $this->program->refreshStats(); // Refresh stats fields of projects. + $programs = $this->programZen->getProgramsByType($status, $orderBy, $param, $pager); $PMList = $this->programZen->getPMListByPrograms($programs); @@ -53,16 +55,15 @@ class program extends control $this->config->program->search['actionURL'] = $actionURL; $this->loadModel('search')->setSearchParams($this->config->program->search); - $this->view->title = $this->lang->program->browse; - $this->view->programs = $programs; - $this->view->status = $status; - $this->view->orderBy = $orderBy; - $this->view->pager = $pager; - $this->view->users = $this->loadModel('user')->getPairs('noletter'); - $this->view->usersAvatar = $this->user->getAvatarPairs(''); - $this->view->PMList = $PMList; - $this->view->progressList = $this->program->getProgressList(); - $this->view->param = $param; + $this->view->title = $this->lang->program->browse; + $this->view->programs = $programs; + $this->view->status = $status; + $this->view->orderBy = $orderBy; + $this->view->pager = $pager; + $this->view->users = $this->loadModel('user')->getPairs('noletter'); + $this->view->usersAvatar = $this->user->getAvatarPairs(''); + $this->view->PMList = $PMList; + $this->view->param = $param; $this->display(); } diff --git a/module/program/model.php b/module/program/model.php index 6f1060b346..103846afe7 100644 --- a/module/program/model.php +++ b/module/program/model.php @@ -287,8 +287,8 @@ class programModel extends model $programs = $this->getTopPairs('noclosed'); /* Group data by product. */ - list($productGroup, $planGroup, $releaseGroup, $projectGroup, $doingExecutions, $hours, $projectHours) = $this->getKanbanStatisticData($programs); - $productGroup = $this->processProductsForKanban($productGroup, $planGroup, $releaseGroup, $projectGroup, $doingExecutions, $hours, $projectHours); + list($productGroup, $planGroup, $releaseGroup, $projectGroup, $doingExecutions) = $this->getKanbanStatisticData($programs); + $productGroup = $this->processProductsForKanban($productGroup, $planGroup, $releaseGroup, $projectGroup, $doingExecutions); /* Group data by program. */ $kanbanGroup = array(); @@ -359,20 +359,7 @@ class programModel extends model $executionPairs = array(); foreach($doingExecutions as $execution) $executionPairs[$execution->id] = $execution->id; - /* Compute executions and projects progress. */ - $tasks = $this->dao->select('id, project, estimate, consumed, `left`, status, closedReason, execution') - ->from(TABLE_TASK) - ->where('(execution')->in($executionPairs) - ->orWhere('project')->in($projectIdList) - ->markRight(1) - ->andWhere('parent')->lt(1) - ->andWhere('deleted')->eq(0) - ->fetchGroup('execution', 'id'); - - $hours = $this->computeProjectHours($tasks); - $projectHours = $this->getProgressList(); - - return array($productGroup, $planGroup, $releaseGroup, $projectGroup, $doingExecutions, $hours, $projectHours); + return array($productGroup, $planGroup, $releaseGroup, $projectGroup, $doingExecutions); } /** @@ -384,12 +371,10 @@ class programModel extends model * @param array $releaseGroup * @param array $projectGroup * @param array $doingExecutions - * @param array $hours - * @param array $projectHours * @access public * @return array */ - public function processProductsForKanban(array $productGroup, array $planGroup, array $releaseGroup, array $projectGroup, array $doingExecutions, array $hours, array $projectHours): array + public function processProductsForKanban(array $productGroup, array $planGroup, array $releaseGroup, array $projectGroup, array $doingExecutions): array { if(empty($productGroup)) return $productGroup; @@ -414,14 +399,10 @@ class programModel extends model $status = $project->status == 'wait' ? 'wait' : 'doing'; $execution = zget($doingExecutions, $project->id, array()); - if(!empty($execution)) - { - $execution->hours = zget($hours, $execution->id, array()); - if(helper::diffDate($today, $execution->end) > 0) $execution->delay = 1; - } + if(!empty($execution) && helper::diffDate($today, $execution->end) > 0) $execution->delay = 1; $project->execution = $execution; - $project->hours['progress'] = zget($projectHours, $project->id, array()); + $project->hours = array('progress' => $project->progress); /* Convert predefined HTML entities to characters. */ $project->name = htmlspecialchars_decode($project->name, ENT_QUOTES); @@ -527,67 +508,18 @@ class programModel extends model } /** - * 根据按照项目分组的任务,统计各个项目的工时和进度 - * Compute hours and progress for project or execution. - * - * @param array $tasks - * @access public - * @return array - */ - public function computeProjectHours(array $tasks): array - { - if(empty($tasks)) return array(); - - $hours = array(); - foreach($tasks as $projectID => $projectTasks) - { - /* Init hour. */ - $hour = new stdclass(); - $hour->totalConsumed = 0; - $hour->totalEstimate = 0; - $hour->totalLeft = 0; - - /* Compute totalEstimate, totalConsumed, totalLeft to them. */ - foreach($projectTasks as $task) - { - $hour->totalConsumed += $task->consumed; - $hour->totalEstimate += $task->estimate; - if(strpos('|cancel|closed|done|', "|{$task->status}|") === false) $hour->totalLeft += (float)$task->left; - } - $hours[$projectID] = $hour; - } - - /* 计算进度。四舍五入totalEstimate, totalConsumed, totalLeft. */ - $progressList = $this->loadModel('project')->getWaterfallProgress(array_keys($hours)); - foreach($hours as $projectID => $hour) - { - $hour->totalEstimate = round($hour->totalEstimate, 1) ; - $hour->totalConsumed = round($hour->totalConsumed, 1); - $hour->totalLeft = round($hour->totalLeft, 1); - $hour->totalReal = $hour->totalConsumed + $hour->totalLeft; - $hour->progress = zget($progressList, $projectID, ($hour->totalReal ? round($hour->totalConsumed / $hour->totalReal, 2) * 100 : 0)) ; - } - - return $hours; - } - - /** - * 将工时、团队人数、剩余任务数、团队成员等统计信息,追加到对应的项目中。 + * 将团队成员等统计信息,追加到对应的项目中。 * Append statistics fields to projects. * * @param array $projects - * @param string $appendFields hours,teamCount,leftTasks,teamMembers - * @param array $data array keys are hours, teams and leftTasks. + * @param array $teams array keys are hours, teams and leftTasks. * @access public * @return array */ - public function appendStatToProjects(array $projects, string $appendFields = '', array $data = array()): array + public function appendStatToProjects(array $projects, array $teams = array()): array { if(empty($projects)) return array(); - if(empty($appendFields) || empty($data)) return $projects; - $appendFields = explode(',', $appendFields); - $emptyHour = json_decode(json_encode(array('totalEstimate' => 0, 'totalConsumed' => 0, 'totalLeft' => 0, 'progress' => 0))); /* Process projects. */ $stats = array(); foreach($projects as $projectID => $project) @@ -601,20 +533,16 @@ class programModel extends model if($delay > 0) $project->delay = $delay; } - /* Merge the hours. */ - if(in_array('hours', $appendFields)) - { - $project->hours = $emptyHour; - if(isset($data['hours'])) $project->hours = zget($data['hours'], $project->id, $emptyHour); - } - - /* Merge the team and left tasks. */ - if(in_array('teamCount', $appendFields)) $project->teamCount = isset($data['teams'][$project->id]) ? count($data['teams'][$project->id]) : 0; - if(in_array('teamMembers', $appendFields)) $project->teamMembers = isset($data['teams'][$project->id]) ? array_keys($data['teams'][$project->id]) : array(); - if(in_array('leftTasks', $appendFields)) $project->leftTasks = isset($data['leftTasks'][$project->id]) ? $data['leftTasks'][$project->id]->tasks : '—'; + /* Merge project team. */ + if(!empty($teams)) + { + $project->teamCount = isset($teams[$project->id]) ? count($teams[$project->id]) : 0; + $project->teamMembers = isset($teams[$project->id]) ? array_keys($teams[$project->id]) : array(); + } $stats[$projectID] = $project; } + return $stats; } @@ -728,59 +656,6 @@ class programModel extends model ->fetchAll(); } - /** - * 计算项目和项目集的进度。 - * Get program and project progress list. - * - * @access public - * @return array - */ - public function getProgressList(): array - { - $totalProgress = array(); - $projectCount = array(); - $userPRJCount = array(); - $progressList = array(); - $programPairs = $this->getPairs(); - $projectStats = $this->getProjectStats(0, 'all', 0, 'id_desc', '', true); - - /* Add program progress. */ - foreach(array_keys($programPairs) as $programID) - { - $totalProgress[$programID] = 0; - $projectCount[$programID] = 0; - $userPRJCount[$programID] = 0; - $progressList[$programID] = 0; - - foreach($projectStats as $project) - { - if(strpos($project->path, ',' . $programID . ',') === false) continue; - - /* The number of projects under this program that the user can view. */ - if(strpos(',' . $this->app->user->view->projects . ',', ',' . $project->id . ',') !== false) $userPRJCount[$programID] ++; - - $totalProgress[$programID] += $project->hours->progress; - $projectCount[$programID] ++; - } - - if(empty($projectCount[$programID])) continue; - - /* Program progress can't see when this user don't have all projects priv. */ - if(!$this->app->user->admin && $userPRJCount[$programID] != $projectCount[$programID]) - { - unset($progressList[$programID]); - continue; - } - - $progressList[$programID] = round($totalProgress[$programID] / $projectCount[$programID]); - } - - /* Add project progress. */ - foreach($projectStats as $project) $progressList[$project->id] = $project->hours->progress; - - return $progressList; - } - /** * 创建项目集。 * Create a program. @@ -1324,44 +1199,10 @@ class programModel extends model { if(commonModel::isTutorialMode()) return $this->loadModel('tutorial')->getProjectStats($browseType); - /* Init vars. */ $projects = $this->getProjectList($programID, $browseType, $queryID, $orderBy, $programTitle, $queryAll, $pager); if(empty($projects)) return array(); - $projectKeys = array_keys($projects); - $executions = $this->loadModel('project')->getExecutionList($projectKeys); - - /* Get all tasks and compute totalEstimate, totalConsumed, totalLeft, progress according to them. */ - $tasks = $this->dao->select('id, project, estimate, consumed, `left`, status, closedReason, execution') - ->from(TABLE_TASK) - ->where('project')->in($projectKeys) - ->andWhere('execution')->in(array_keys($executions)) - ->andWhere('parent')->lt(1) - ->andWhere('deleted')->eq(0) - ->fetchGroup('project', 'id'); - $hours = $this->computeProjectHours($tasks); - - /* Get the number of left tasks. */ - $leftTasks = array(); - if($this->cookie->projectType && $this->cookie->projectType == 'bycard') - { - $leftTasks = $this->dao->select('t2.parent as project, count(*) as tasks')->from(TABLE_TASK)->alias('t1') - ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.execution = t2.id') - ->where('t1.execution')->in(array_keys($executions)) - ->andWhere('t1.status')->notIn('cancel,closed') - ->groupBy('t2.parent') - ->fetchAll('project'); - } - - /* Get the members of project teams. */ - $teamMembers = $this->dao->select('t1.root,t1.account')->from(TABLE_TEAM)->alias('t1') - ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account=t2.account') - ->where('t1.root')->in($projectKeys) - ->andWhere('t1.type')->eq('project') - ->andWhere('t2.deleted')->eq(0) - ->fetchGroup('root', 'account'); - - $stats = $this->appendStatToProjects($projects, 'hours,teamCount,teamMembers,leftTasks', array('hours' => $hours, 'teams' => $teamMembers, 'leftTasks' => $leftTasks)); + $stats = $this->appendStatToProjects($projects); foreach($stats as $project) $project->name = htmlspecialchars_decode($project->name, ENT_QUOTES); // Convert predefined HTML entities to characters. return $stats; @@ -1570,13 +1411,30 @@ class programModel extends model } /* 1. Get summary and members of executions to be refreshed. */ - $summary = $this->dao->select('execution, SUM(t1.estimate) AS totalEstimate, SUM(t1.consumed) AS totalConsumed, SUM(t1.`left`) AS totalLeft')->from(TABLE_TASK)->alias('t1') + $tasks = $this->dao->select('t1.id, execution, t1.estimate, t1.consumed, t1.`left`, t1.status')->from(TABLE_TASK)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id') ->where('t1.deleted')->eq(0) - ->andWhere('t1.parent')->le(0) // Ignore child task. + ->andWhere('t1.parent')->ge(0) // Ignore parent task. ->beginIF(!empty($projects))->andWhere('t1.project')->in(array_keys($projects))->fi() - ->groupBy('execution') - ->fetchAll(); + ->fetchAll('id'); + + $summary = array(); + foreach($tasks as $task) + { + if(empty($task->execution)) continue; + if(!isset($summary[$task->execution])) + { + $summary[$task->execution] = new stdclass(); + $summary[$task->execution]->totalEstimate = 0; + $summary[$task->execution]->totalConsumed = 0; + $summary[$task->execution]->totalLeft = 0; + } + + $summary[$task->execution]->execution = $task->execution; + $summary[$task->execution]->totalEstimate += $task->estimate; + $summary[$task->execution]->totalConsumed += $task->consumed; + $summary[$task->execution]->totalLeft += ($task->status == 'closed' or $task->status == 'cancel') ? 0 : $task->left; + } $teamMembers = $this->dao->select('t1.root, COUNT(1) AS members')->from(TABLE_TEAM)->alias('t1') ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account=t2.account') @@ -1606,6 +1464,8 @@ class programModel extends model $stats[$nodeID]['totalEstimate'] += $execution->totalEstimate; $stats[$nodeID]['totalConsumed'] += $execution->totalConsumed; $stats[$nodeID]['totalLeft'] += $execution->totalLeft; + + // Check $execution->execution and $nodeID(path) is not deleted. if(empty($projectsPairs[$execution->execution]) && empty($projectsPairs[$nodeID])) { $stats[$nodeID]['totalConsumedNotDel'] += $execution->totalConsumed; @@ -1614,22 +1474,6 @@ class programModel extends model } } - $this->loadModel('project'); - foreach($projects as $projectID => $project) - { - if($project->model != 'waterfall') continue; - - $projectStats = $this->project->getWaterfallPVEVAC($projectID); - $stats[$projectID]['totalEstimate'] = $projectStats['PV']; - $stats[$projectID]['totalConsumed'] = $projectStats['AC']; - $stats[$projectID]['totalLeft'] = $projectStats['left']; - if(empty($project->deleted)) - { - $stats[$projectID]['totalConsumedNotDel'] = $projectStats['AC']; - $stats[$projectID]['totalLeftNotDel'] = $projectStats['left']; - } - } - foreach($teamMembers as $projectID => $teamCount) { if(!isset($stats[$projectID])) $stats[$projectID] = array('totalEstimate' => 0, 'totalConsumed' => 0, 'totalLeft' => 0, 'teamCount' => 0, 'totalConsumedNotDel' => 0, 'totalLeftNotDel' => 0); @@ -1645,14 +1489,14 @@ class programModel extends model ->set('progress')->eq($progress) ->set('teamCount')->eq($project['teamCount']) ->set('estimate')->eq($project['totalEstimate']) - ->set('consumed')->eq($project['totalConsumed']) - ->set('left')->eq($project['totalLeft']) + ->set('consumed')->eq($project['totalConsumedNotDel']) + ->set('left')->eq($project['totalLeftNotDel']) ->where('id')->eq($projectID) ->exec(); } /* 5. Update programStatsTime. */ - $projectList = $this->dao->select('id,progress,path')->from(TABLE_PROJECT) + $projectList = $this->dao->select('id,progress,path,consumed,`left`')->from(TABLE_PROJECT) ->where('type')->eq('project') ->andWhere('parent')->ne(0) ->andWhere('deleted')->eq(0) @@ -1665,19 +1509,18 @@ class programModel extends model foreach($path as $programID) { if($programID == $projectID) continue; - $programProgress[$programID][] = $project->progress; + + if(!isset($programProgress[$programID])) $programProgress[$programID] = array('consumed' => 0, 'left' => 0); + $programProgress[$programID]['consumed'] += $project->consumed; + $programProgress[$programID]['left'] += $project->left; } } - foreach($programProgress as $programID => $progress) + foreach($programProgress as $programID => $hours) { - $count = count($progress); - $sum = array_sum($progress); - $average = round($sum / $count, 2); - $this->dao->update(TABLE_PROJECT) - ->set('progress')->eq($average) - ->where('id')->eq($programID) - ->exec(); + $progress = ($hours['consumed'] + $hours['left']) ? floor($hours['consumed'] / ($hours['consumed'] + $hours['left']) * 1000) / 1000 * 100 : 0; + + $this->dao->update(TABLE_PROJECT)->set('progress')->eq($progress)->where('id')->eq($programID)->exec(); } /* 6. Update projectStatsTime in config. */ diff --git a/module/program/ui/browse.html.php b/module/program/ui/browse.html.php index 57bedc53dd..6da58bc160 100644 --- a/module/program/ui/browse.html.php +++ b/module/program/ui/browse.html.php @@ -51,9 +51,7 @@ foreach($programs as $program) $program->budget = !empty($program->budget) ? zget($lang->project->currencySymbol, $program->budgetUnit) . ' ' . $programBudget : $lang->project->future; $parents[$program->parent][] = $program->id; - /* Progress. */ - if(isset($progressList[$program->id])) $program->progress = round($progressList[$program->id]); - + $program->progress = round($program->progress); $program->isParent = false; if($program->parent > 0 and isset($programs[$program->parent])) $programs[$program->parent]->isParent = true; if($program->parent > 0 and !isset($programs[$program->parent])) @@ -78,7 +76,8 @@ foreach($programs as $program) } /* Set invested hours. */ - if(!isset($program->invested)) $program->invested = 0; + $program->invested = !empty($this->config->execution->defaultWorkhours) ? round($program->consumed / $this->config->execution->defaultWorkhours, 2) : 0; + if(!is_null($program->end) && str_contains($program->end, LONG_TIME)) $program->end = $lang->program->longTime; /* Actions. */ diff --git a/module/project/control.php b/module/project/control.php index 794f616a09..5cfa4f6f53 100644 --- a/module/project/control.php +++ b/module/project/control.php @@ -323,8 +323,10 @@ class project extends control $actionURL = $this->createLink('project', 'browse', "&programID=$programID&browseType=bySearch&queryID=myQueryID"); $this->project->buildSearchForm($queryID, $actionURL); + $this->loadModel('program')->refreshStats(); // Refresh stats fields of projects. + $programTitle = $this->loadModel('setting')->getItem("owner={$this->app->user->account}&module=project&key=programTitle"); - $projectStats = $this->loadModel('program')->getProjectStats($programID, $browseType, $queryID, $orderBy, $programTitle, false, $pager); + $projectStats = $this->program->getProjectStats($programID, $browseType, $queryID, $orderBy, $programTitle, false, $pager); $this->view->title = $this->lang->project->browse; $this->view->projectStats = $this->projectZen->processProjectListData($projectStats); diff --git a/module/project/js/browse.ui.js b/module/project/js/browse.ui.js index 1e322cc19a..de2727dd3c 100644 --- a/module/project/js/browse.ui.js +++ b/module/project/js/browse.ui.js @@ -3,7 +3,7 @@ window.programMenuOnClick = function(data, url) location.href = url.replace('%d', data.item.key); } -window.renderReleaseCountCell = function(result, {col, row}) +window.renderCell = function(result, {col, row}) { if(col.name === 'name') { diff --git a/module/project/model.php b/module/project/model.php index 97fe03f0f2..bf3b8e635b 100755 --- a/module/project/model.php +++ b/module/project/model.php @@ -244,7 +244,7 @@ class projectModel extends model * @access public * @return object[] */ - public function getOverviewList(string $status = '', int $projectID = 0, string $orderBy = 'id_desc', int $limit = 15, string $excludedModel = ''): array + public function getOverviewList(string $status = '', int $projectID = 0, string $orderBy = 'id_desc', int $limit = 10, string $excludedModel = ''): array { /* Get project list by query. */ $projects = $this->projectTao->fetchProjectListByQuery($status, $projectID, $orderBy, $limit, $excludedModel); @@ -256,24 +256,15 @@ class projectModel extends model $projects = array($projectID => $projects[$projectID]); } - /* Get team members under the project. */ - $projectIdList = array_keys($projects); - $teamCount = $this->projectTao->fetchMemberCountByIdList($projectIdList); - - /* Get all consumed and all estimate under the project. */ - $hours = $this->projectTao->fetchTaskEstimateByIdList($projectIdList, 'consumed,estimate'); - /* Get bug, task and story summary under the project. */ - $bugSummary = $this->projectTao->getTotalBugByProject($projectIdList); - $taskSummary = $this->projectTao->getTotalTaskByProject($projectIdList); - $storySummary = $this->projectTao->getTotalStoriesByProject($projectIdList); + $projectIdList = array_keys($projects); + $bugSummary = $this->projectTao->getTotalBugByProject($projectIdList); + $taskSummary = $this->projectTao->getTotalTaskByProject($projectIdList); + $storySummary = $this->projectTao->getTotalStoriesByProject($projectIdList); /* Set project attribute. */ foreach($projects as $projectID => $project) { - $project->teamCount = zget($teamCount, $projectID, 0); - $project->consumed = isset($hours[$projectID]) ? round((float)$hours[$projectID]->consumed, 1) : 0; - $project->estimate = isset($hours[$projectID]) ? round((float)$hours[$projectID]->estimate, 1) : 0; $project->leftBugs = isset($bugSummary[$projectID]) ? $bugSummary[$projectID]->leftBugs : 0; $project->allBugs = isset($bugSummary[$projectID]) ? $bugSummary[$projectID]->allBugs : 0; $project->doneBugs = isset($bugSummary[$projectID]) ? $bugSummary[$projectID]->doneBugs : 0; @@ -2495,13 +2486,13 @@ class projectModel extends model $project->budget = $project->budget != 0 ? zget($this->lang->project->currencySymbol, $project->budgetUnit) . ' ' . $projectBudget : $this->lang->project->future; $project->statusTitle = $this->processStatus('project', $project); - $project->estimate = $project->hours->totalEstimate . $this->lang->project->workHourUnit; - $project->consume = $project->hours->totalConsumed . $this->lang->project->workHourUnit; - $project->surplus = $project->hours->totalLeft . $this->lang->project->workHourUnit; - $project->progress = $project->hours->progress; + $project->estimate = $project->estimate . $this->lang->project->workHourUnit; + $project->consume = $project->consumed . $this->lang->project->workHourUnit; + $project->surplus = $project->left . $this->lang->project->workHourUnit; + $project->progress = $project->progress; $project->end = $project->end == LONG_TIME ? $this->lang->project->longTime : $project->end; $project->hasProduct = zget($this->lang->project->projectTypeList, $project->hasProduct); - $project->invested = !empty($this->config->execution->defaultWorkhours) ? round($project->hours->totalConsumed / $this->config->execution->defaultWorkhours, 2) : 0; + $project->invested = !empty($this->config->execution->defaultWorkhours) ? round($project->consumed / $this->config->execution->defaultWorkhours, 2) : 0; if($project->PM) { diff --git a/module/project/ui/browsebylist.html.php b/module/project/ui/browsebylist.html.php index 61fca6eaf9..7183e97987 100644 --- a/module/project/ui/browsebylist.html.php +++ b/module/project/ui/browsebylist.html.php @@ -10,6 +10,9 @@ declare(strict_types=1); */ namespace zin; +jsVar('langManDay', $lang->program->manDay); +jsVar('langPostponed', $lang->project->statusList['delay']); + /* zin: Define the feature bar on main menu. */ featureBar ( @@ -108,6 +111,7 @@ dtable set::sortLink(createLink('project', 'browse', "programID=$programID&browseType=$browseType¶m=$param&orderBy={name}_{sortType}&recTotal={$pager->recTotal}&recPerPage={$pager->recPerPage}&pageID={$pager->pageID}")), set::footPager(usePager()), set::customCols(true), + set::onRenderCell(jsRaw('window.renderCell')), set::emptyTip($lang->project->empty), set::createTip($lang->project->create), set::createLink(hasPriv('project', 'create') ? createLink('project', 'createGuide') : ''), From 288a4f2fbb2542f9f81d7dc28b6dd415c0a0a67a Mon Sep 17 00:00:00 2001 From: caoyanyi Date: Tue, 21 Nov 2023 11:16:00 +0800 Subject: [PATCH 21/92] * Split repo diff method. --- module/repo/control.php | 135 +++++++++------------------------------- module/repo/model.php | 21 +++++++ module/repo/zen.php | 88 ++++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 104 deletions(-) diff --git a/module/repo/control.php b/module/repo/control.php index 5ab52a323a..97ea1975ca 100644 --- a/module/repo/control.php +++ b/module/repo/control.php @@ -539,32 +539,33 @@ class repo extends control $this->scm->setEngine($repo); $log = $this->scm->log('', $revision, $revision); - $history = $this->dao->select('*')->from(TABLE_REPOHISTORY)->where('revision')->eq($log[0]->revision)->andWhere('repo')->eq($repoID)->fetch(); - if($history) + $revision = $this->repo->getHistoryRevision($repoID, $log[0]->revision); + if($revision) { if(in_array($repo->SCM, $this->config->repo->gitTypeList)) { - $thisAndPrevRevisions = $this->scm->exec("rev-list -n 2 {$history->revision} --"); + $thisAndPrevRevisions = $this->scm->exec("rev-list -n 2 {$revision} --"); array_shift($thisAndPrevRevisions); if($thisAndPrevRevisions) $oldRevision = array_shift($thisAndPrevRevisions); } else { - $oldRevision = $this->dao->select('*')->from(TABLE_REPOHISTORY)->where('revision')->lt($history->revision)->andWhere('repo')->eq($repoID)->orderBy('revision_desc')->limit(1)->fetch('revision'); + $oldRevision = $this->repo->getHistoryRevision($repoID, $revision, false, 'lt'); } } if(empty($oldRevision)) { $oldRevision = '^'; - if($history and in_array($repo->SCM, $this->config->repo->gitTypeList)) $oldRevision = "{$history->revision}^"; + if($revision and in_array($repo->SCM, $this->config->repo->gitTypeList)) $oldRevision = "{$revision}^"; } $this->locate($this->repo->createLink('diff', "repoID=$repoID&objectID=$objectID&entry=&oldrevision=$oldRevision&newRevision={$log[0]->revision}")); } /** + * 代码blame信息。 * Blame repo file. * * @param int $repoID @@ -575,13 +576,14 @@ class repo extends control * @access public * @return void */ - public function blame($repoID, $objectID = 0, $entry = '', $revision = 'HEAD', $encoding = '') + public function blame(int $repoID, int $objectID = 0, string $entry = '', string $revision = 'HEAD', string $encoding = '') { $this->commonAction($repoID, $objectID); if($this->get->repoPath) $entry = $this->get->repoPath; if($repoID == 0) $repoID = $this->session->repoID; $repo = $this->repo->getByID($repoID); + $file = $entry; $entry = $this->repo->decodePath($entry); @@ -596,7 +598,7 @@ class repo extends control if($encoding != 'utf-8') $blames[$i]['content'] = helper::convertEncoding($blame['content'], $encoding); } - $log = in_array($repo->SCM, $this->config->repo->gitTypeList) ? $this->dao->select('revision,commit')->from(TABLE_REPOHISTORY)->where('revision')->eq($revision)->andWhere('repo')->eq($repo->id)->fetch() : ''; + $log = in_array($repo->SCM, $this->config->repo->gitTypeList) ? $this->repo->getHistoryRevision($repo->id, $revision, true) : ''; $this->view->title = $this->lang->repo->common; $this->view->repoID = $repoID; @@ -607,8 +609,7 @@ class repo extends control $this->view->entry = $entry; $this->view->file = $file; $this->view->encoding = str_replace('-', '_', $encoding); - $this->view->historys = in_array($repo->SCM, $this->config->repo->gitTypeList) ? $this->dao->select('revision,commit')->from(TABLE_REPOHISTORY)->where('revision')->in($revisions)->andWhere('repo')->eq($repo->id)->fetchPairs() : ''; - $this->view->revisionName = ($log and in_array($repo->SCM, $this->config->repo->gitTypeList)) ? $this->repo->getGitRevisionName($log->revision, $log->commit) : $revision; + $this->view->revisionName = ($log && in_array($repo->SCM, $this->config->repo->gitTypeList)) ? $this->repo->getGitRevisionName($log->revision, $log->commit) : $revision; $this->view->blames = $blames; $this->display(); } @@ -627,125 +628,51 @@ class repo extends control * @access public * @return void */ - public function diff($repoID, $objectID = 0, $entry = '', $oldRevision = '', $newRevision = '', $showBug = 'false', $encoding = '', $isBranchOrTag = false) + public function diff(int $repoID, int $objectID = 0, string $entry = '', string $oldRevision = '', string $newRevision = '', string $showBug = 'false', string $encoding = '', bool $isBranchOrTag = false) { + $newRevision = strtr($newRevision, '*', '-'); + $oldRevision = strtr($oldRevision, '*', '-'); + $oldRevision = urldecode(urldecode($oldRevision)); //Fix error. if($isBranchOrTag) { - $oldRevision = strtr($oldRevision, '*', '-'); - $newRevision = strtr($newRevision, '*', '-'); - if($isBranchOrTag) - { - $oldRevision = urldecode(helper::safe64Decode($oldRevision)); - $newRevision = urldecode(helper::safe64Decode($newRevision)); - } - } - else - { - $oldRevision = urldecode(urldecode($oldRevision)); //Fix error. + $oldRevision = urldecode(helper::safe64Decode($oldRevision)); + $newRevision = urldecode(helper::safe64Decode($newRevision)); } $this->commonAction($repoID, $objectID); + $repo = $this->repo->getByID($repoID); if($this->get->repoPath) $entry = $this->get->repoPath; $file = $entry; - $repo = $this->repo->getByID($repoID); $entry = $this->repo->decodePath($entry); - if($repo->SCM == 'Git' and !is_dir($repo->path)) - { - $error = sprintf($this->lang->repo->error->notFound, $repo->name, $repo->path); - return print(js::error($error) . js::locate($this->repo->createLink('maintain'))); - } - - $pathInfo = pathinfo($entry); - $suffix = ''; - if(isset($pathInfo["extension"])) $suffix = strtolower($pathInfo["extension"]); + if($repo->SCM == 'Git' && !is_dir($repo->path)) return $this->sendError(sprintf($this->lang->repo->error->notFound, $repo->name, $repo->path), $this->repo->createLink('maintain')); $arrange = $this->cookie->arrange ? $this->cookie->arrange : 'inline'; - if($this->server->request_method == 'POST') - { - $oldRevision = isset($this->post->revision[1]) ? $this->post->revision[1] : ''; - $newRevision = isset($this->post->revision[0]) ? $this->post->revision[0] : ''; + if($this->server->request_method == 'POST') return $this->repoZen->locateDiffPage($repoID, $objectID, $arrange, $isBranchOrTag, $file); - if($this->post->arrange) - { - $arrange = $this->post->arrange; - helper::setcookie('arrange', $arrange); - } - if($this->post->encoding) $encoding = $this->post->encoding; - if($this->post->isBranchOrTag) $isBranchOrTag = $this->post->isBranchOrTag; - - $this->locate($this->repo->createLink('diff', "repoID=$repoID&objectID=$objectID&entry=" . $this->repo->encodePath($entry) . "&oldrevision=$oldRevision&newRevision=$newRevision&showBug=&encoding=$encoding&isBranchOrTag=$isBranchOrTag")); - } - - $info = new stdClass(); $diffs = array(); $encoding = empty($encoding) ? $repo->encoding : $encoding; $encoding = strtolower(str_replace('_', '-', $encoding)); if($oldRevision !== '') { $this->scm->setEngine($repo); - $info = $this->scm->info($entry, $newRevision); $diffs = $this->scm->diff($entry, $oldRevision, $newRevision, 'yes', $isBranchOrTag ? 'isBranchOrTag': ''); } - foreach($diffs as $diff) - { - if($encoding != 'utf-8') - { - $diff->fileName = helper::convertEncoding($diff->fileName, $encoding); - if(empty($diff->contents)) continue; - foreach($diff->contents as $content) - { - if(empty($content->lines)) continue; - foreach($content->lines as $lines) - { - if(empty($lines->line)) continue; - $lines->line = helper::convertEncoding($lines->line, $encoding); - } - } - } - } - /* When arrange is appose then adjust data for show them easy.*/ - if($arrange == 'appose') - { - - foreach($diffs as $diffFile) - { - if(empty($diffFile->contents)) continue; - foreach($diffFile->contents as $content) - { - $old = array(); - $new = array(); - foreach($content->lines as $line) - { - if($line->type != 'new') $old[$line->oldlc] = $line->line; - if($line->type != 'old') $new[$line->newlc] = $line->line; - } - $content->old = $old; - $content->new = $new; - } - } - } - - $this->view->type = 'diff'; - $this->view->showBug = $showBug; - $this->view->entry = urldecode($entry); - $this->view->suffix = $suffix; - $this->view->file = $file; - $this->view->repoID = $repoID; - $this->view->branchID = (string) $this->cookie->repoBranch; - $this->view->objectID = $objectID; - $this->view->repo = $repo; - $this->view->encoding = str_replace('-', '_', $encoding); - $this->view->arrange = $arrange; - $this->view->diffs = $diffs; - $this->view->newRevision = $newRevision; - $this->view->oldRevision = $oldRevision; - $this->view->revision = $newRevision; - $this->view->historys = in_array($repo->SCM, $this->config->repo->gitTypeList) ? $this->dao->select('revision,commit')->from(TABLE_REPOHISTORY)->where('revision')->in("$oldRevision,$newRevision")->andWhere('repo')->eq($repo->id)->fetchPairs() : ''; - $this->view->info = $info; + if($encoding != 'utf-8') $diffs = $this->repoZen->encodingDiff($diffs, $encoding); + if($arrange == 'appose') $diffs = $this->repoZen->getApposeDiff($diffs); + $this->view->entry = urldecode($entry); + $this->view->encoding = str_replace('-', '_', $encoding); + $this->view->file = $file; + $this->view->repoID = $repoID; + $this->view->branchID = (string) $this->cookie->repoBranch; + $this->view->objectID = $objectID; + $this->view->repo = $repo; + $this->view->diffs = $diffs; + $this->view->newRevision = $newRevision; + $this->view->oldRevision = $oldRevision; $this->view->isBranchOrTag = $isBranchOrTag; $this->view->title = $this->lang->repo->common . $this->lang->colon . $this->lang->repo->diff; diff --git a/module/repo/model.php b/module/repo/model.php index 545782eb13..6c4442acca 100644 --- a/module/repo/model.php +++ b/module/repo/model.php @@ -3289,4 +3289,25 @@ class repoModel extends model return array_merge($folders, $files); } + + /** + * 查询提交记录的版本号。 + * Get history revision. + * + * @param int $repoID + * @param string $revision + * @param bool $withCommit + * @param string $condition + * @access public + * @return string|false + */ + public function getHistoryRevision(int $repoID, string $revision, bool $withCommit = false, string $condition = 'eq'): string|false + { + $field = $withCommit ? 'revision, commit' : 'revision'; + return $this->dao->select($field)->from(TABLE_REPOHISTORY) + ->where('repo')->eq($repoID) + ->beginIF($condition != 'lt')->andWhere('revision')->eq($revision)->fi() + ->beginIF($condition == 'lt')->andWhere('revision')->lt($revision)->fi() + ->fetch($withCommit ? '' : 'revision'); + } } diff --git a/module/repo/zen.php b/module/repo/zen.php index 42a8f70e76..1fb801148e 100644 --- a/module/repo/zen.php +++ b/module/repo/zen.php @@ -1210,4 +1210,92 @@ class repoZen extends repo return $error; } + + /** + * 跳转到版本库的diff页面。 + * Redirect to diff page. + * + * @param int $repoID + * @param int $objectID + * @param string $arrange + * @param bool $isBranchOrTag + * @param string $file + * @access protected + * @return void + */ + protected function locateDiffPage(int $repoID, int $objectID, string $arrange, bool $isBranchOrTag, string $file) + { + $oldRevision = isset($this->post->revision[1]) ? $this->post->revision[1] : ''; + $newRevision = isset($this->post->revision[0]) ? $this->post->revision[0] : ''; + + if($this->post->arrange) + { + $arrange = $this->post->arrange; + helper::setcookie('arrange', $arrange); + } + if($this->post->encoding) $encoding = $this->post->encoding; + if($this->post->isBranchOrTag) $isBranchOrTag = $this->post->isBranchOrTag; + + return $this->locate($this->repo->createLink('diff', "repoID={$repoID}&objectID={$objectID}&entry={$file}&oldrevision={$oldRevision}&newRevision={$newRevision}&showBug=&encoding={$encoding}&isBranchOrTag={$isBranchOrTag}")); + } + + /** + * 设置对比信息的编码格式。 + * Set encoding for diff. + * + * @param array $diffs + * @param string $encoding + * @access protected + * @return array + */ + protected function encodingDiff(array $diffs, string $encoding): array + { + foreach($diffs as $diff) + { + $diff->fileName = helper::convertEncoding($diff->fileName, $encoding); + if(empty($diff->contents)) continue; + + foreach($diff->contents as $content) + { + if(empty($content->lines)) continue; + + foreach($content->lines as $lines) + { + if(empty($lines->line)) continue; + $lines->line = helper::convertEncoding($lines->line, $encoding); + } + } + } + + return $diffs; + } + + /** + * 获取并列展示的对比信息。 + * Get appose diff. + * + * @param array $diffs + * @access protected + * @return array + */ + protected function getApposeDiff(array $diffs): array + { + foreach($diffs as $diffFile) + { + if(empty($diffFile->contents)) continue; + foreach($diffFile->contents as $content) + { + $old = array(); + $new = array(); + foreach($content->lines as $line) + { + if($line->type != 'new') $old[$line->oldlc] = $line->line; + if($line->type != 'old') $new[$line->newlc] = $line->line; + } + $content->old = $old; + $content->new = $new; + } + } + return $diffs; + } } From e472d57a8f73bce2692f03d2ac9e9308bc53453a Mon Sep 17 00:00:00 2001 From: sunhao Date: Tue, 21 Nov 2023 11:45:31 +0800 Subject: [PATCH 22/92] * zin: refactor widgets for compatible mode. --- lib/zin/wg/history/v1.php | 12 +++++++++++- lib/zin/wg/pagebase/v1.php | 12 +++++++++--- lib/zin/wg/searchform/v1.php | 7 +++++++ lib/zin/wg/searchtoggle/v1.php | 19 ++++++++++++++----- 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/lib/zin/wg/history/v1.php b/lib/zin/wg/history/v1.php index 96fc116292..3f812c92b2 100644 --- a/lib/zin/wg/history/v1.php +++ b/lib/zin/wg/history/v1.php @@ -20,6 +20,7 @@ class history extends wg 'actions?: array', // 操作列表数据。 'users?: array', // 用户 Map 数据。 'commentUrl?: string', // 备注对话框 URL。 + 'editCommentUrl?: string', // 修改备注对话框 URL。 'title?: string|array', // 标题。 'commentBtn?: string|array' // 是否允许添加备注。 ); @@ -74,7 +75,15 @@ class history extends wg protected function build(): wg { - list($panel, $objectID, $objectType, $title, $actions, $commentUrl, $commentBtn) = $this->prop(array('panel', 'objectID', 'objectType', 'title', 'actions', 'commentUrl', 'commentBtn')); + global $config; + + list($panel, $objectID, $objectType, $title, $actions, $commentUrl, $editCommentUrl, $commentBtn) = $this->prop(array('panel', 'objectID', 'objectType', 'title', 'actions', 'commentUrl', 'editCommentUrl', 'commentBtn')); + + if(isset($config->zin->mode) && $config->zin->mode == 'compatible') + { + if(is_null($commentUrl)) $commentUrl = createLink('action', 'commentZin', "objectType=$objectType&objectID=$objectID"); + if(is_null($editCommentUrl)) $editCommentUrl = createLink('action', 'editCommentZin', 'actionID={actionID}'); + } return zui::historyPanel ( @@ -84,6 +93,7 @@ class history extends wg set::actions($actions), set::title($title), set::commentUrl($commentUrl), + set::editCommentUrl($editCommentUrl), set::commentBtn($commentBtn), set($this->getRestProps()) ); diff --git a/lib/zin/wg/pagebase/v1.php b/lib/zin/wg/pagebase/v1.php index 49700cdb42..b16b497709 100644 --- a/lib/zin/wg/pagebase/v1.php +++ b/lib/zin/wg/pagebase/v1.php @@ -64,7 +64,8 @@ class pageBase extends wg $themeName = $app->cookie->theme; $zuiPath = $config->zin->zuiPath; - $jsConfig->zin = (isset($config->zin->mode) && $config->zin->mode) ? $config->zin->mode : true; + $zinMode = isset($config->zin->mode) ? $config->zin->mode : ''; + $jsConfig->zin = !empty($zinMode) ? $zinMode : true; $headImports = array(); if($zui) @@ -74,10 +75,15 @@ class pageBase extends wg $headImports[] = h::importJs($zuiPath . 'zui.zentao.umd.cjs', setID('zuiJS')); $headImports[] = h::jsCall('$.setLibRoot', $zuiPath); - if(isset($config->zin->extraCSS) && $config->zin->extraCSS) $headImports[] = h::importCss($webRoot . 'js/zui3/' . $config->zin->extraCSS); + $extraCSS = isset($config->zin->extraCSS) ? $config->zin->extraCSS : ''; + if(!empty($extraCSS)) $headImports[] = h::importCss($webRoot . 'js/zui3/' . $extraCSS); } $headImports[] = h::jsVar('window.config', $jsConfig, setID('configJS')); - if($zui) $headImports[] = h::importJs($webRoot . 'js/zui3/zin.js', setID('zinJS')); + if($zui) + { + $extraJS = isset($config->zin->extraJS) ? $config->zin->extraJS : 'zin.js'; + if(!empty($extraJS)) $headImports[] = h::importJs($webRoot . 'js/zui3/' . $extraJS); + } if($config->debug) { diff --git a/lib/zin/wg/searchform/v1.php b/lib/zin/wg/searchform/v1.php index 8deaaf5002..47acaeb4e3 100644 --- a/lib/zin/wg/searchform/v1.php +++ b/lib/zin/wg/searchform/v1.php @@ -33,6 +33,7 @@ class searchForm extends wg 'simple?: boolean', // 是否为简单模式,不包含保存搜索条件和已保存的查询条件侧边栏。 'setting?: array', // 默认配置。 'url?: string', // 配置加载地址,默认为 search-buildForm-module。 + 'searchUrl?: string', // 搜索时提交表单的 URL。 'searchLoader?: string|array' // 搜索时 loadPage 参数。 ); @@ -44,6 +45,12 @@ class searchForm extends wg */ protected function build(): wg { + global $config; + if(isset($config->zin->mode) && $config->zin->mode == 'compatible') + { + if(!$this->hasProp('url')) $this->setProp('url', createLink('search', 'buildZinForm', 'module=' . $this->prop('module'))); + if(!$this->hasProp('searchUrl')) $this->setProp('searchUrl', createLink('search', 'buildZinQuery')); + } return zui::searchForm(inherit($this)); } } diff --git a/lib/zin/wg/searchtoggle/v1.php b/lib/zin/wg/searchtoggle/v1.php index 4588b322ba..a95434f7e4 100644 --- a/lib/zin/wg/searchtoggle/v1.php +++ b/lib/zin/wg/searchtoggle/v1.php @@ -7,7 +7,9 @@ class searchToggle extends wg protected static array $defineProps = array( 'target?: string', 'open?: bool', - 'module?: string' + 'module?: string', + 'url?: string', + 'searchUrl?: string' ); protected function checkErrors() @@ -17,19 +19,26 @@ class searchToggle extends wg protected function build(): wg { - global $lang, $app; - list($target, $module, $open) = $this->prop(array('target', 'module', 'open')); + global $lang, $app, $config; + list($target, $module, $open, $url, $searchUrl) = $this->prop(array('target', 'module', 'open', 'url', 'searchUrl')); if(is_null($open) && !empty($_GET['browseType'])) $open = $_GET['browseType'] === 'bySearch'; if(is_null($module)) $module = $app->rawModule; + + if(isset($config->zin->mode) && $config->zin->mode == 'compatible') + { + if(is_null($url)) $url = createLink('search', 'buildZinForm', 'module=' . $this->prop('module')); + if(is_null($searchUrl)) $searchUrl = createLink('search', 'buildZinQuery'); + } + return btn ( set::className('ghost search-form-toggle'), set::icon('search'), set::active($open), set::text($lang->searchAB), - toggle::searchform(array('module' => $module, 'target' => $target)), - $open ? h::jsCall('~zui.toggleSearchForm', array('module' => $module, 'target' => $target, 'show' => true)) : null + toggle::searchform(array('module' => $module, 'target' => $target, 'url' => $url, 'searchUrl' => $searchUrl)), + $open ? h::jsCall('~zui.toggleSearchForm', array('module' => $module, 'target' => $target, 'show' => true, 'url' => $url, 'searchUrl' => $searchUrl)) : null ); } } From 8464662c36a811a8990a23e00163dbe40bb982d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E9=87=91=E5=8B=87?= Date: Tue, 21 Nov 2023 05:16:47 +0000 Subject: [PATCH 23/92] - Remove unused code. --- module/api/v1/entries/product.php | 7 +- module/kanban/model.php | 6 +- .../test/model/computeprojecthours.php | 32 ---------- module/program/test/model/getprogresslist.php | 49 -------------- module/program/test/program.class.php | 11 ---- module/project/model.php | 64 ------------------- module/project/test/model/computeprogress.php | 26 -------- 7 files changed, 2 insertions(+), 193 deletions(-) delete mode 100755 module/program/test/model/computeprojecthours.php delete mode 100755 module/program/test/model/getprogresslist.php delete mode 100755 module/project/test/model/computeprogress.php diff --git a/module/api/v1/entries/product.php b/module/api/v1/entries/product.php index fb87636161..00a7f35c51 100644 --- a/module/api/v1/entries/product.php +++ b/module/api/v1/entries/product.php @@ -76,7 +76,7 @@ class productEntry extends entry $product->actions = $this->loadModel('action')->processActionForAPI($actions, $users, $this->lang->product); break; case 'lastexecution': - $execution = $this->dao->select('t2.id,t2.name,t2.type')->from(TABLE_PROJECTPRODUCT)->alias('t1') + $execution = $this->dao->select('t2.id,t2.name,t2.type,t2.progress')->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id') ->where('t2.deleted')->eq(0) ->andWhere('t1.product')->eq($productID) @@ -84,11 +84,6 @@ class productEntry extends entry ->orderBy('t2.id desc') ->limit(1) ->fetch(); - if($execution) - { - $workhour = $this->loadModel('project')->computeProgress(array($execution->id => $execution)); - if(isset($workhour[$execution->id])) $execution->progress = $workhour[$execution->id]->progress; - } $product->lastExecution = $execution; break; diff --git a/module/kanban/model.php b/module/kanban/model.php index 81621f7dc7..5125291841 100755 --- a/module/kanban/model.php +++ b/module/kanban/model.php @@ -1444,10 +1444,6 @@ class kanbanModel extends model ->andWhere('action')->eq('opened') ->fetchPairs(); } - elseif($fromType == 'execution') - { - $executionProgress = $this->loadModel('project')->computeProgress($objects); - } /* Data for constructing the card. */ foreach($objectCards as $objectID => $cardsInfo) @@ -1473,7 +1469,7 @@ class kanbanModel extends model if($delay > 0) $objectCard->delay = $delay; } $objectCard->execType = $object->type; - $objectCard->progress = isset($executionProgress[$objectID]->progress) ? $executionProgress[$objectID]->progress : 0; + $objectCard->progress = $object->progress; $parentExecutions = $this->dao->select('id,name')->from(TABLE_EXECUTION)->where('id')->in(trim($object->path, ','))->andWhere('type')->in('stage,kanban,sprint')->orderBy('grade')->fetchPairs(); $objectCard->title = implode('/', $parentExecutions); diff --git a/module/program/test/model/computeprojecthours.php b/module/program/test/model/computeprojecthours.php deleted file mode 100755 index 13cc1e2fd4..0000000000 --- a/module/program/test/model/computeprojecthours.php +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env php -gen(5); -su('admin'); - -zdTable('project')->gen(50); - -$task = zdTable('task'); -$task->project->range('1-20'); -$task->gen(100); - -/** - -title=测试 programModel::computeProjectHours(); -timeout=0 -cid=1 - -*/ - -global $tester; -$tester->loadModel('program'); -$tasks = $tester->program->dao->select('id, project, estimate, consumed, `left`, status, closedReason, execution') - ->from(TABLE_TASK) - ->where('project')->eq('11') - ->andWhere('parent')->lt(1) - ->andWhere('deleted')->eq(0) - ->fetchGroup('project', 'id'); - -r($tester->program->computeProjectHours(array())) && p() && e('0'); -r($tester->program->computeProjectHours($tasks)) && p('11:totalConsumed,totalEstimate,totalLeft,progress') && e('15,30,10,60'); diff --git a/module/program/test/model/getprogresslist.php b/module/program/test/model/getprogresslist.php deleted file mode 100755 index 763c42c7a1..0000000000 --- a/module/program/test/model/getprogresslist.php +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env php -id->range('1-8'); -$program->name->setFields(array( - array('field' => 'name1', 'range' => '项目集{2},项目{3},迭代{3}'), - array('field' => 'name2', 'range' => '1-8') -)); -$program->type->range('program{2},project{3},sprint{3}'); -$program->status->range('doing'); -$program->parent->range('0,0,1,1,2,3,4,5'); -$program->grade->range('1{2},2{3},1{3}'); -$program->project->range('0{5},3-5'); -$program->path->range('1,2,`1,3`,`1,4`,`2,5`,`3,6`,`4,7`,`5,8`')->prefix(',')->postfix(','); -$program->begin->range('20220112 000000:0')->type('timestamp')->format('YYYY-MM-DD'); -$program->end->range('20230212 000000:0')->type('timestamp')->format('YYYY-MM-DD'); -$program->gen(8); - -$task = zdTable('task'); -$task->name->prefix('任务')->range('1-3'); -$task->project->range('3-5'); -$task->execution->range('6-8'); -$task->estimate->range('6-8'); -$task->consumed->range('1,3,2'); -$task->left->range('5,4,6'); -$task->status->range('doing'); -$task->assignedTo->range('admin'); -$task->gen(3); - -zdTable('team')->gen(0); - -/** - -title=测试 programModee::getProgressList(); -timeout=0 -cid=1 - -*/ - -$programTester = new programTest(); -$progressList = $programTester->getProgressListTest(); - -r(count($progressList)) && p() && e('5'); // 获取项目和项目集的个数 -r($progressList) && p('2') && e('25'); // 获取id=2的项目的进度 -r($progressList) && p('4') && e('43'); // 获取id=4的项目集的进度 diff --git a/module/program/test/program.class.php b/module/program/test/program.class.php index 3745e0135b..8c3cb56714 100644 --- a/module/program/test/program.class.php +++ b/module/program/test/program.class.php @@ -293,17 +293,6 @@ class programTest return $this->program->getTeamMemberPairs($programID); } - /** - * Get progress list. - * - * @access public - * @return array - */ - public function getProgressListTest() - { - return $this->program->getProgressList(); - } - /** * Test save state. * diff --git a/module/project/model.php b/module/project/model.php index bf3b8e635b..a0f2ec772a 100755 --- a/module/project/model.php +++ b/module/project/model.php @@ -2022,70 +2022,6 @@ class projectModel extends model return array($projectsStats, $ongoingExecutions); } - /** - * Computer execution progress. - * - * @param array $executions - * @access public - * @return array - */ - public function computeProgress($executions) - { - $hours = array(); - $emptyHour = array('totalEstimate' => 0, 'totalConsumed' => 0, 'totalLeft' => 0, 'progress' => 0); - $executionIdList = array_keys($executions); - - /* Get all tasks and compute totalEstimate, totalConsumed, totalLeft, progress according to them. */ - $tasks = $this->loadModel('execution')->getTaskGroupByExecution($executionIdList); - $projects = $this->dao->select('t1.id,t2.model') - ->from(TABLE_PROJECT)->alias('t1') - ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id') - ->where('t1.deleted')->eq('0') - ->andWhere('t1.id')->in($executionIdList) - ->fetchPairs(); - - /* Compute totalEstimate, totalConsumed, totalLeft. */ - foreach($tasks as $executionID => $executionTasks) - { - $hour = (object)$emptyHour; - foreach($executionTasks as $task) - { - $hour->totalEstimate += $task->estimate; - $hour->totalConsumed += $task->consumed; - if($task->status != 'cancel' and $task->status != 'closed') $hour->totalLeft += (float)$task->left; - } - $hours[$executionID] = $hour; - - if(isset($executions[$executionID]) && $executions[$executionID]->grade > 1 && isset($projects[$executionID]) && in_array($projects[$executionID], array('waterfall', 'waterfallplus'))) - { - $stageParents = $this->dao->select('id')->from(TABLE_EXECUTION)->where('id')->in(trim($executions[$executionID]->path, ','))->andWhere('type')->eq('stage')->andWhere('id')->ne($executions[$executionID]->id)->orderBy('grade')->fetchPairs(); - foreach($stageParents as $stageParent) - { - if(!isset($hours[$stageParent])) - { - $hours[$stageParent] = clone $hour; - continue; - } - - $hours[$stageParent]->totalEstimate += $hour->totalEstimate; - $hours[$stageParent]->totalConsumed += $hour->totalConsumed; - $hours[$stageParent]->totalLeft += $hour->totalLeft; - } - } - } - - /* Compute totalReal and progress. */ - foreach($hours as $hour) - { - $hour->totalEstimate = round($hour->totalEstimate, 1) ; - $hour->totalConsumed = round($hour->totalConsumed, 1); - $hour->totalLeft = round($hour->totalLeft, 1); - $hour->totalReal = $hour->totalConsumed + $hour->totalLeft; - $hour->progress = $hour->totalReal ? round($hour->totalConsumed / $hour->totalReal * 100, 2) : 0; - } - return $hours; - } - /** * 设置项目的导航菜单。 * Set menu of project module. diff --git a/module/project/test/model/computeprogress.php b/module/project/test/model/computeprogress.php deleted file mode 100755 index 21f6526d9b..0000000000 --- a/module/project/test/model/computeprogress.php +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env php -config('execution')->gen(10); -zdTable('task')->config('task')->gen(20); -zdTable('taskteam')->gen(20); -su('admin'); - -/** - -title=测试 projectModel->computeProgress(); -timeout=0 -cid=1 - -*/ - -global $tester; -$tester->loadModel('project'); -$tester->loadModel('execution'); - -$executions = $tester->execution->getList(11); -$executions = $tester->project->computeProgress($executions); - -r(count($executions)) && p('') && e('1'); // 查看项目11下的所有执行的数量 -r($executions) && p('101:totalEstimate,totalConsumed,totalLeft,progress,totalReal') && e('44,9,44,16.98,53'); // 查看计算后的执行工时汇总信息 From d96afd13dcaf0fe1bf5879f8a66bf297912028e2 Mon Sep 17 00:00:00 2001 From: daitingting Date: Tue, 21 Nov 2023 05:20:32 +0000 Subject: [PATCH 24/92] * Add pager on feature bar. * Format codes. --- module/project/control.php | 2 +- module/project/ui/batchedit.html.php | 2 +- module/project/ui/bug.html.php | 20 +++++++++++--------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/module/project/control.php b/module/project/control.php index 5cfa4f6f53..47e369ba48 100644 --- a/module/project/control.php +++ b/module/project/control.php @@ -1309,7 +1309,7 @@ class project extends control */ public function activate(int $projectID) { - $project = $this->project->getByID($projectID); + $project = $this->project->getByID($projectID); if(!empty($_POST)) { diff --git a/module/project/ui/batchedit.html.php b/module/project/ui/batchedit.html.php index 1ae5a280b6..640bcb6b90 100644 --- a/module/project/ui/batchedit.html.php +++ b/module/project/ui/batchedit.html.php @@ -13,7 +13,7 @@ namespace zin; $programAclList = array(); $projectAclList = array(); foreach($lang->program->subAcls as $acl => $label) $programAclList[] = array('text' => $label, 'value' => $acl); -foreach($lang->project->acls as $acl => $label) $projectAclList[] = array('text' => $label, 'value' => $acl); +foreach($lang->project->acls as $acl => $label) $projectAclList[] = array('text' => $label, 'value' => $acl); jsVar('LONG_TIME', LONG_TIME); jsVar('weekend', $config->execution->weekend); diff --git a/module/project/ui/bug.html.php b/module/project/ui/bug.html.php index 431389bc54..80438876c0 100644 --- a/module/project/ui/bug.html.php +++ b/module/project/ui/bug.html.php @@ -10,7 +10,7 @@ declare(strict_types=1); */ namespace zin; -$linkParams = "projectID={$project->id}&productID={$productID}&branch=$branchID&orderBy=status,id_desc&build=$buildID&type={key}" . ($type == 'bysearch' ? '' : "¶m=$param"); +$linkParams = "projectID={$project->id}&productID={$productID}&branch=$branchID&orderBy=status,id_desc&build=$buildID&type={key}" . ($type == 'bysearch' ? '' : "¶m=$param") . "&recTotal={$pager->recTotal}&recPerpage={$pager->recPerPage}"; featureBar ( set::current($type), @@ -29,14 +29,16 @@ toolbar 'url' => createLink('bug', 'export', "productID={$productID}&orderBy=$orderBy&browseType=&projectID={$project->id}"), 'data-toggle' => 'modal' ))) : null, - $canCreate ? item(set(array + $canCreate ? item ( - 'text' => $lang->bug->create, - 'icon' => 'plus', - 'type' => 'primary', - 'url' => createLink('bug', 'create', "productID={$productID}&branch={$branchID}&extras=projectID={$project->id}") - )), - setData('app', 'project') + set(array + ( + 'text' => $lang->bug->create, + 'icon' => 'plus', + 'type' => 'primary', + 'url' => createLink('bug', 'create', "productID={$productID}&branch={$branchID}&extras=projectID={$project->id}") + )), + setData('app', 'project') ) : null ); @@ -51,7 +53,7 @@ sidebar ))) ); -$canBatchAssignTo = common::hasPriv('bug', 'batchAssignTo'); +$canBatchAssignTo = hasPriv('bug', 'batchAssignTo'); $config->bug->dtable->fieldList['module']['map'] = $modulePairs; $config->bug->dtable->fieldList['story']['map'] = $stories; From 755f377e92eeef63b236cda76c9427c9ed6379fd Mon Sep 17 00:00:00 2001 From: wangyidong Date: Tue, 21 Nov 2023 13:29:20 +0800 Subject: [PATCH 25/92] * Add param type for action model methods. --- module/action/model.php | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/module/action/model.php b/module/action/model.php index 7825a7ec7d..d4f4d31fc7 100755 --- a/module/action/model.php +++ b/module/action/model.php @@ -638,7 +638,7 @@ class actionModel extends model * @access public * @return string */ - public function formatActionComment($comment): string + public function formatActionComment(string $comment): string { if(str_contains($comment, '
'))
         {
@@ -704,10 +704,7 @@ class actionModel extends model
     public function printAction(object $action, string $desc = '')
     {
         $content = $this->renderAction($action, $desc);
-        if(is_string($content))
-        {
-            echo $content;
-        }
+        if(is_string($content)) echo $content;
         return;
     }
 
@@ -1540,7 +1537,7 @@ class actionModel extends model
      * @access public
      * @return array
      */
-    public function processDynamicForAPI($dynamics): array
+    public function processDynamicForAPI(array $dynamics): array
     {
         /* 获取用户列表。 */
         /* Get user list. */

From 65a2b44328c170b5b955a85e5a04949bee9cc201 Mon Sep 17 00:00:00 2001
From: daitingting 
Date: Tue, 21 Nov 2023 05:38:04 +0000
Subject: [PATCH 26/92]  * Format codes.

---
 module/project/ui/build.html.php | 16 ++++------------
 1 file changed, 4 insertions(+), 12 deletions(-)

diff --git a/module/project/ui/build.html.php b/module/project/ui/build.html.php
index 8e35fcebde..d952ce1ef2 100644
--- a/module/project/ui/build.html.php
+++ b/module/project/ui/build.html.php
@@ -37,14 +37,11 @@ featureBar
 
 /* zin: Define the toolbar on main menu. */
 $canCreateBuild = hasPriv('projectbuild', 'create') && common::canModify('project', $project);
-if($canCreateBuild) $createItem = array('icon' => 'plus', 'class' => 'primary', 'text' => $lang->build->create, 'url' => $this->createLink('projectbuild', 'create', "projectID={$project->id}"));
-toolbar
-(
-    !empty($createItem) ? item(set($createItem)) : null
-);
+
+if($canCreateBuild) toolbar(item(set(array('icon' => 'plus', 'class' => 'primary', 'text' => $lang->build->create, 'url' => createLink('projectbuild', 'create', "projectID={$project->id}")))));
 
 jsVar('projectID', $project->id);
-jsVar('changeProductLink', helper::createLink($app->rawModule, $app->rawMethod, "projectID={$project->id}&type=product¶m={productID}&orderBy={$orderBy}&recTotal={$pager->recTotal}&recPerPage={$pager->recPerPage}&pageID={$pager->pageID}"));
+jsVar('changeProductLink', createLink($app->rawModule, $app->rawMethod, "projectID={$project->id}&type=product¶m={productID}&orderBy={$orderBy}&recTotal={$pager->recTotal}&recPerPage={$pager->recPerPage}&pageID={$pager->pageID}"));
 jsVar('scmPathTip', $lang->build->scmPath);
 jsVar('filePathTip', $lang->build->filePath);
 jsVar('integratedTip', $lang->build->integrated);
@@ -59,12 +56,7 @@ dtable
     set::sortLink(createLink($app->rawModule, $app->rawMethod, "projectID={$project->id}&type={$type}¶m={$param}&orderBy={name}_{sortType}&recTotal={$pager->recTotal}&recPerPage={$pager->recPerPage}&pageID={$pager->pageID}")),
     set::onRenderCell(jsRaw('window.renderCell')),
     set::getCellSpan(jsRaw('window.getCellSpan')),
-    set::footPager(
-        usePager
-        (
-            array('linkCreator' => helper::createLink($app->rawModule, $app->rawMethod, "projectID={$project->id}&type={$type}¶m={$param}&orderBy={$orderBy}&recTotal={recTotal}&recPerPage={recPerPage}&pageID={page}"), 'recTotal' => $pager->recTotal, 'recPerPage' => $pager->recPerPage)
-        )
-    ),
+    set::footPager(usePager(array('linkCreator' => createLink($app->rawModule, $app->rawMethod, "projectID={$project->id}&type={$type}¶m={$param}&orderBy={$orderBy}&recTotal={recTotal}&recPerPage={recPerPage}&pageID={page}"), 'recTotal' => $pager->recTotal, 'recPerPage' => $pager->recPerPage))),
     set::emptyTip($lang->build->noBuild),
     set::createTip($lang->build->create),
     set::createLink($canCreateBuild ? createLink('projectbuild', 'create', "projectID={$project->id}") : '')

From c99005393f24d6c667ac20f7a1222a97ab69e52a Mon Sep 17 00:00:00 2001
From: liumengyi 
Date: Tue, 21 Nov 2023 13:39:40 +0800
Subject: [PATCH 27/92] * Fix bug of linking cases in testsuite.

---
 module/testsuite/config/table.php  | 5 +----
 module/testsuite/js/linkcase.ui.js | 2 +-
 2 files changed, 2 insertions(+), 5 deletions(-)

diff --git a/module/testsuite/config/table.php b/module/testsuite/config/table.php
index 2121eba42a..8fc33dc180 100644
--- a/module/testsuite/config/table.php
+++ b/module/testsuite/config/table.php
@@ -98,6 +98,7 @@ $config->testsuite->testcase->dtable->fieldList['id']['type']  = 'checkID';
 $config->testsuite->testcase->dtable->fieldList['title']['name']  = 'title';
 $config->testsuite->testcase->dtable->fieldList['title']['title'] = $lang->testcase->title;
 $config->testsuite->testcase->dtable->fieldList['title']['type']  = 'title';
+$config->testsuite->testcase->dtable->fieldList['title']['link']  = array('module' => 'testcase', 'method' => 'view', 'params' => 'caseID={id}&version={caseVersion}');
 
 $config->testsuite->testcase->dtable->fieldList['pri']['name']  = 'pri';
 $config->testsuite->testcase->dtable->fieldList['pri']['title'] = $lang->testcase->pri;
@@ -166,10 +167,6 @@ $config->testsuite->linkcase->dtable->fieldList['title']['link']['params'] = 'ca
 $config->testsuite->linkcase->dtable->fieldList['title']['data-toggle']    = 'modal';
 $config->testsuite->linkcase->dtable->fieldList['title']['data-size']      = 'lg';
 
-$config->testsuite->linkcase->dtable->fieldList['pri']['type']         = 'control';
-$config->testsuite->linkcase->dtable->fieldList['pri']['control']      = 'picker';
-$config->testsuite->linkcase->dtable->fieldList['pri']['controlItems'] = $lang->testcase->priList;
-
 $config->testsuite->linkcase->dtable->fieldList['version']['name']         = 'version';
 $config->testsuite->linkcase->dtable->fieldList['version']['title']        = $lang->testsuite->linkVersion;
 $config->testsuite->linkcase->dtable->fieldList['version']['type']         = 'control';
diff --git a/module/testsuite/js/linkcase.ui.js b/module/testsuite/js/linkcase.ui.js
index fc28cbbf0c..37adeb9c05 100644
--- a/module/testsuite/js/linkcase.ui.js
+++ b/module/testsuite/js/linkcase.ui.js
@@ -9,7 +9,7 @@ $(document).off('click', '.batch-btn').on('click', '.batch-btn', function()
     checkedList.forEach((id) =>
     {
         form.append('cases[]', id);
-        form.append('versions[' + id + ']', $('#versions' + id).val());
+        form.append('versions[' + id + ']', $("[name='version\[" + id + "\]']").val());
     });
 
     if($(this).hasClass('ajax-btn'))

From c0c105ed51d326e564e6bbf82341021d2b93547d Mon Sep 17 00:00:00 2001
From: xieqiyu 
Date: Tue, 21 Nov 2023 13:50:53 +0800
Subject: [PATCH 28/92] * Modify testreport and caselib required items.

---
 module/caselib/model.php             | 1 +
 module/caselib/ui/create.html.php    | 6 ++----
 module/caselib/ui/edit.html.php      | 1 +
 module/testreport/ui/create.html.php | 5 ++++-
 module/testreport/ui/edit.html.php   | 3 +++
 5 files changed, 11 insertions(+), 5 deletions(-)

diff --git a/module/caselib/model.php b/module/caselib/model.php
index 62c1276670..8db7b90661 100644
--- a/module/caselib/model.php
+++ b/module/caselib/model.php
@@ -87,6 +87,7 @@ class caselibModel extends model
         $oldLib = $this->dao->select('*')->from(TABLE_TESTSUITE)->where('id')->eq($lib->id)->fetch();
 
         $this->lang->testsuite->name = $this->lang->caselib->name;
+        $this->lang->testsuite->desc = $this->lang->caselib->desc;
 
         $this->dao->update(TABLE_TESTSUITE)->data($lib, 'uid')
             ->autoCheck()
diff --git a/module/caselib/ui/create.html.php b/module/caselib/ui/create.html.php
index 2d44f367f7..94b2550e84 100644
--- a/module/caselib/ui/create.html.php
+++ b/module/caselib/ui/create.html.php
@@ -31,10 +31,8 @@ formPanel
     formGroup
     (
         set::label($lang->caselib->desc),
-        editor
-        (
-            set::name('desc')
-        )
+        set::control('editor'),
+        set::name('desc')
     )
 );
 
diff --git a/module/caselib/ui/edit.html.php b/module/caselib/ui/edit.html.php
index 5cfc0046d0..3510cd9495 100644
--- a/module/caselib/ui/edit.html.php
+++ b/module/caselib/ui/edit.html.php
@@ -24,6 +24,7 @@ formPanel
     formGroup
     (
         set::label($lang->caselib->desc),
+        set::required(strpos(",{$this->config->caselib->edit->requiredFields},", ",desc,") !== false),
         editor
         (
             set::name('desc'),
diff --git a/module/testreport/ui/create.html.php b/module/testreport/ui/create.html.php
index 0276ff1f36..6ea6667dfd 100644
--- a/module/testreport/ui/create.html.php
+++ b/module/testreport/ui/create.html.php
@@ -38,6 +38,7 @@ formPanel
         (
             set::width('1/2'),
             set::label($lang->testreport->startEnd),
+            set::required(strpos(",{$this->config->testreport->create->requiredFields},", ",begin,") !== false || strpos(",{$this->config->testreport->create->requiredFields},", ",end,") !== false),
             inputGroup
             (
                 datePicker
@@ -122,6 +123,7 @@ formPanel
         formGroup
         (
             set::label($lang->testreport->members),
+            set::required(strpos(",{$this->config->testreport->create->requiredFields},", ",members,") !== false),
             picker
             (
                 set::multiple(true),
@@ -178,7 +180,8 @@ formPanel
         formGroup
         (
             set::label($lang->testreport->report),
-            editor(set::name('report'))
+            set::control('editor'),
+            set::name('report')
         )
     ),
     formRow
diff --git a/module/testreport/ui/edit.html.php b/module/testreport/ui/edit.html.php
index 46b32cb830..d774faaec1 100644
--- a/module/testreport/ui/edit.html.php
+++ b/module/testreport/ui/edit.html.php
@@ -24,6 +24,7 @@ formPanel
         (
             set::width('1/2'),
             set::label($lang->testreport->startEnd),
+            set::required(strpos(",{$this->config->testreport->edit->requiredFields},", ",begin,") !== false || strpos(",{$this->config->testreport->edit->requiredFields},", ",end,") !== false),
             inputGroup
             (
                 datePicker
@@ -96,6 +97,7 @@ formPanel
         formGroup
         (
             set::label($lang->testreport->members),
+            set::required(strpos(",{$this->config->testreport->edit->requiredFields},", ",members,") !== false),
             picker
             (
                 set::multiple(true),
@@ -152,6 +154,7 @@ formPanel
         formGroup
         (
             set::label($lang->testreport->report),
+            set::required(strpos(",{$this->config->testreport->edit->requiredFields},", ",report,") !== false),
             editor
             (
                 set::name('report'),

From c1a40f4e2931f03c8eaa5dbe9f50816bfb54f9a9 Mon Sep 17 00:00:00 2001
From: songchenxuan <1097180981@qq.com>
Date: Tue, 21 Nov 2023 13:57:16 +0800
Subject: [PATCH 29/92] * Fix metric preview set className bug.

---
 module/metric/ui/previewsingle.html.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/module/metric/ui/previewsingle.html.php b/module/metric/ui/previewsingle.html.php
index 79d5ba0974..8e51ff1ec6 100644
--- a/module/metric/ui/previewsingle.html.php
+++ b/module/metric/ui/previewsingle.html.php
@@ -21,7 +21,7 @@ $fnGenerateSide = function() use($groupMetrics, $current, $viewType, $scope, $la
 
         $metricList[] = li
             (
-                set::class('metric-group'),
+                set::className('metric-group'),
                 $lang->metric->objectList[$key]
             );
 

From 08715ddcc58afb285d5704f2db1f655aefcaee8c Mon Sep 17 00:00:00 2001
From: tianshujie 
Date: Tue, 21 Nov 2023 14:05:04 +0800
Subject: [PATCH 30/92] * Refactor custom::set method.

---
 module/custom/control.php     | 255 ++----------------------
 module/custom/ui/set.html.php |   6 +-
 module/custom/zen.php         | 364 ++++++++++++++++++++++++++++++++++
 3 files changed, 383 insertions(+), 242 deletions(-)
 create mode 100644 module/custom/zen.php

diff --git a/module/custom/control.php b/module/custom/control.php
index 790594288b..2f877e40ca 100644
--- a/module/custom/control.php
+++ b/module/custom/control.php
@@ -12,277 +12,52 @@
 class custom extends control
 {
     /**
-     * Index
+     * Index.
      *
      * @access public
      * @return void
      */
     public function index()
     {
-        if($this->config->vision == 'lite') return print(js::locate(inlink('execution')));
+        if($this->config->vision == 'lite') return $this->locate(inlink('execution'));
 
-        if(common::hasPriv('custom', 'set'))       return print(js::locate(inlink('set', "module=project&field=" . key($this->lang->custom->project->fields))));
-        if(common::hasPriv('custom', 'product'))   return print(js::locate(inlink('product')));
-        if(common::hasPriv('custom', 'execution')) return print(js::locate(inlink('execution')));
+        if(common::hasPriv('custom', 'set'))       return $this->locate(inlink('set', "module=project&field=" . key($this->lang->custom->project->fields)));
+        if(common::hasPriv('custom', 'product'))   return $this->locate(inlink('product'));
+        if(common::hasPriv('custom', 'execution')) return $this->locate(inlink('execution'));
 
         foreach($this->lang->custom->system as $sysObject)
         {
-            if(common::hasPriv('custom', $sysObject)) return print(js::locate(inlink($sysObject)));
+            if(common::hasPriv('custom', $sysObject)) return $this->locate(inlink($sysObject));
         }
     }
 
     /**
-     * Custom
+     * 设置对象字段的语言项。
+     * Set the language items of the object fields.
      *
-     * @param  string $module
-     * @param  string $field
-     * @param  string $lang
+     * @param  string $module todo|story|task|bug|testcase|testtask|user|project
+     * @param  string $field  priList|typeList|statusList|sourceList|reasonList|stageList|reviewRules|reviewResultList|review|severityList|osList|browserList|resolutionList|longlife|resultList|roleList|contactField|deleted|unitList
+     * @param  string $lang   all|zh-cn|zh-tw|en|de|fr
      * @access public
      * @return void
      */
     public function set(string $module = 'story', string $field = 'priList', string $lang = '')
     {
         if(empty($lang)) $lang = $this->app->getClientLang();
-        if($module == 'user' and $field == 'priList') $field = 'statusList';
-        if($module == 'block' and $field == 'priList')$field = 'closed';
+        if($module == 'user' && $field == 'priList') $field = 'statusList';
+        if($module == 'block' && $field == 'priList')$field = 'closed';
         $currentLang = $this->app->getClientLang();
 
-        $this->app->loadLang($module);
-        if($lang == 'all')
-        {
-            $fieldList = array();
-            $items     = $this->custom->getItems("lang=all&module=$module§ion=$field&vision={$this->config->vision}");
-            foreach($items as $key => $item)
-            {
-                $fieldList[$key] = $item->value;
-            }
-        }
-        else
-        {
-            $fieldList = zget($this->lang->$module, $field, '');
-        }
-
-        if($module == 'project' and $field == 'unitList')
-        {
-            $this->app->loadConfig($module);
-            $unitList = zget($this->config->$module, 'unitList', '');
-            $this->view->unitList        = explode(',', $unitList);
-            $this->view->defaultCurrency = zget($this->config->$module, 'defaultCurrency', 'CNY');
-        }
-        if(($module == 'story' or $module == 'demand') and $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(($module == 'story' or $module == 'testcase' or $module == 'demand') and $field == 'review')
-        {
-            $this->app->loadConfig($module);
-            $this->loadModel('user');
-
-            if($module == 'story' or $module == 'demand')
-            {
-                $this->view->depts            = $this->loadModel('dept')->getDeptPairs();
-
-                $this->view->forceReviewRoles = zget($this->config->$module, 'forceReviewRoles', '');
-                $this->view->forceReviewDepts = zget($this->config->$module, 'forceReviewDepts', '');
-
-                $this->view->forceNotReviewRoles = zget($this->config->$module, 'forceNotReviewRoles', '');
-                $this->view->forceNotReviewDepts = zget($this->config->$module, 'forceNotReviewDepts', '');
-            }
-
-            $this->view->users          = $module == 'story' ? $this->user->getCanCreateStoryUsers() : $this->user->getPairs('noclosed|nodeleted');
-            $this->view->needReview     = zget($this->config->$module, 'needReview', 1);
-            $this->view->forceReview    = zget($this->config->$module, 'forceReview', '');
-            $this->view->forceNotReview = zget($this->config->$module, 'forceNotReview', '');
-        }
-        if($module == 'bug' and $field == 'longlife')
-        {
-            $this->app->loadConfig('bug');
-            $this->view->longlife = $this->config->bug->longlife;
-        }
-        if($module == 'block' and $field == 'closed')
-        {
-            $this->loadModel('block');
-            $closedBlock = isset($this->config->block->closed) ? $this->config->block->closed : '';
-
-            $this->view->blockPairs  = $this->block->getClosedBlockPairs($closedBlock);
-            $this->view->closedBlock = $closedBlock;
-        }
-        if($module == 'user' and $field == 'deleted')
-        {
-            $this->app->loadConfig('user');
-            $this->view->showDeleted = isset($this->config->user->showDeleted) ? $this->config->user->showDeleted : '0';
-        }
+        $this->customZen->assignVarsForSet($module, $field, $lang, $currentLang);
 
         if(strtolower($this->server->request_method) == "post")
         {
-            $postArray = fixer::input('post');
-            $keys      = array();
-            if(isset($postArray->data->keys))
-            {
-                foreach($postArray->data->keys as $key)
-                {
-                    if($module == 'testtask' and $field == 'typeList' and empty($key)) continue;
-                    if($key && in_array($key, $keys)) return $this->send(array('result' => 'fail', 'message' => sprintf($this->lang->custom->notice->repeatKey, $key)));
-                    $keys[] = $key;
-                }
-            }
-
-            if($module == 'project' and $field == 'unitList')
-            {
-                $data = fixer::input('post')->join('unitList', ',')->get();
-                if(empty($data->unitList)) return $this->send(array('result' => 'fail', 'message' => $this->lang->custom->currencyNotEmpty));
-                if(empty($data->defaultCurrency)) return $this->send(array('result' => 'fail', 'message' => $this->lang->custom->defaultNotEmpty));
-                $this->loadModel('setting')->setItems("system.$module", $data);
-            }
-            elseif(($module == 'story' or $module == 'demand') and $field == 'review')
-            {
-                $data = fixer::input('post')
-                    ->setDefault('forceReview', '')
-                    ->setDefault('forceNotReview', '')
-                    ->setDefault('forceReviewRoles', '')
-                    ->setDefault('forceNotReviewRoles', '')
-                    ->setDefault('forceReviewDepts', '')
-                    ->setDefault('forceNotReviewDepts', '')
-                    ->join('forceReview', ',')
-                    ->join('forceReviewRoles', ',')
-                    ->join('forceReviewDepts', ',')
-                    ->join('forceNotReview', ',')
-                    ->join('forceNotReviewRoles', ',')
-                    ->join('forceNotReviewDepts', ',')
-                    ->get();
-
-                foreach($data as $key => $value)
-                {
-                    if($key == 'needReview') continue;
-                    if(strpos($key, 'Not')  and $data->needReview == 0) $data->$key = '';
-                    if(!strpos($key, 'Not') and $data->needReview == 1) $data->$key = '';
-                }
-
-                $this->loadModel('setting')->setItems("system.$module@{$this->config->vision}", $data);
-            }
-            elseif(($module == 'story' or $module == 'demand') and $field == 'reviewRules')
-            {
-                $data = fixer::input('post')->setDefault('superReviewers', '')->join('superReviewers', ',')->get();
-                $this->loadModel('setting')->setItems("system.$module@{$this->config->vision}", $data);
-            }
-            elseif($module == 'testcase' and $field == 'review')
-            {
-                $review = fixer::input('post')->get();
-                if($review->needReview)  $data = fixer::input('post')->setDefault('forceNotReview', '')->join('forceNotReview', ',')->remove('forceReview')->get();
-                if(!$review->needReview) $data = fixer::input('post')->setDefault('forceReview', '')->join('forceReview', ',')->remove('forceNotReview')->get();
-                $this->loadModel('setting')->setItems("system.$module", $data);
-
-                $reviewCase = isset($review->reviewCase) ? $review->reviewCase : 0;
-                if($review->needReview == 0 and $reviewCase)
-                {
-                    $waitCases = $this->loadModel('testcase')->getByStatus(0, 0, 'all', 'wait');
-                    $this->testcase->batchReview(array_keys($waitCases), 'pass');
-                }
-            }
-            elseif($module == 'bug' and $field == 'longlife')
-            {
-                $this->loadModel('setting')->setItems('system.bug', fixer::input('post')->get());
-            }
-            elseif($module == 'block' and $field == 'closed')
-            {
-                $data = fixer::input('post')->join('closed', ',')->get();
-                $this->loadModel('setting')->setItem('system.block.closed', zget($data, 'closed', ''));
-            }
-            elseif($module == 'user' and $field == 'contactField')
-            {
-                $data = fixer::input('post')->join('contactField', ',')->get();
-                if(!isset($data->contactField)) $data->contactField = '';
-                $this->loadModel('setting')->setItem('system.user.contactField', $data->contactField);
-            }
-            elseif($module == 'user' and $field == 'deleted')
-            {
-                $data = fixer::input('post')->get();
-                $this->loadModel('setting')->setItem('system.user.showDeleted', $data->showDeleted);
-            }
-            else
-            {
-                if(!$this->post->keys) return $this->sendError(sprintf($this->lang->error->notempty, $this->lang->custom->key));
-                $lang = $_POST['lang'];
-                $oldCustoms = $this->custom->getItems("lang=$lang&module=$module§ion=$field");
-                foreach($_POST['keys'] as $index => $key)
-                {
-                    if(!empty($key)) $key = trim($key);
-                    /* Invalid key. It should be numbers. (It includes severityList in bug module and priList in story, task, bug, testcasea, testtask and todo module.) */
-                    if($field == 'priList' or $field == 'severityList')
-                    {
-                        if(!is_numeric($key) or $key > 255) return $this->send(array('result' => 'fail', 'message' => $this->lang->custom->notice->invalidNumberKey));
-                    }
-                    if(!empty($key) and !isset($oldCustoms[$key]) and $key != 'n/a' and !validater::checkREG($key, '/^[a-z_A-Z_0-9]+$/')) return $this->send(array('result' => 'fail', 'message' => $this->lang->custom->notice->invalidStringKey));
-
-                    /* The length of roleList in user module is less than 10. check it when saved. */
-                    if($module == 'user' and $field == 'roleList' and strlen($key) > 10) return $this->send(array('result' => 'fail', 'message' => $this->lang->custom->notice->invalidStrlen['ten']));
-
-                    /* The length of typeList in todo module is less than 15. check it when saved. */
-                    if($module == 'todo' and $field == 'typeList' and strlen($key) > 15) return $this->send(array('result' => 'fail', 'message' => $this->lang->custom->notice->invalidStrlen['fifteen']));
-
-                    /* The length of sourceList in story module and typeList in task module is less than 20, check it when saved. */
-                    if(($module == 'story' and $field == 'sourceList') or ($module == 'task' and $field == 'typeList'))
-                    {
-                        if(strlen($key) > 20) return $this->send(array('result' => 'fail', 'message' => $this->lang->custom->notice->invalidStrlen['twenty']));
-                    }
-
-                    /* The length of stageList in testcase module is less than 255, check it when saved. */
-                    if($module == 'testcase' and $field == 'stageList' and strlen($key) > 255) return $this->send(array('result' => 'fail', 'message' => $this->lang->custom->notice->invalidStrlen['twoHundred']));
-
-                    /* The length of field that in bug and testcase module and reasonList in story and task module is less than 30, check it when saved. */
-                    if(in_array($module, array('bug', 'testcase')) or (in_array($module, array('story', 'task')) and $field == 'reasonList'))
-                    {
-                        if(strlen($key) > 30) return $this->send(array('result' => 'fail', 'message' => $this->lang->custom->notice->invalidStrlen['thirty']));
-                    }
-                }
-
-                $this->custom->deleteItems("lang=$lang&module=$module§ion=$field&vision={$this->config->vision}");
-                if($lang == 'all') $this->custom->deleteItems("lang=$currentLang&module=$module§ion=$field&vision={$this->config->vision}");
-
-                $data     = fixer::input('post')->get();
-                $emptyKey = false;
-                foreach($data->keys as $index => $key)
-                {
-                    if(!$key && $emptyKey) continue;
-
-                    //if(!$system and (!$value or !$key)) continue; //Fix bug #951.
-
-                    $value  = $data->values[$index];
-                    $system = $data->systems[$index];
-                    if($key and trim($value) === '') return $this->send(array('result' => 'fail', 'message' => $this->lang->custom->notice->valueEmpty)); // Fix bug #23538.
-
-                    $this->custom->setItem("{$lang}.{$module}.{$field}.{$key}.{$system}", $value);
-
-                    if(!$key) $emptyKey = true;
-                }
-            }
+            $this->customZen->setFieldListForSet($module, $field);
             if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
             return $this->sendSuccess(array('load' => $this->createLink('custom', 'set', "module=$module&field=$field&lang=" . ($lang == 'all' ? $lang : ''))));
         }
 
-        /* Check whether the current language has been customized. */
-        $lang = str_replace('_', '-', $lang);
-        $dbFields = $this->custom->getItems("lang=$lang&module=$module§ion=$field&vision={$this->config->vision}");
-        if(empty($dbFields)) $dbFields = $this->custom->getItems("lang=" . ($lang == $currentLang ? 'all' : $currentLang) . "&module=$module§ion=$field");
-        if($dbFields)
-        {
-            $dbField = reset($dbFields);
-            if($lang != $dbField->lang)
-            {
-                $lang = str_replace('-', "_", $dbField->lang);
-                foreach($fieldList as $key => $value)
-                {
-                    if(isset($dbFields[$key]) and $value != $dbFields[$key]->value) $fieldList[$key] = $dbFields[$key]->value;
-                }
-            }
-        }
-
         $this->view->title       = $this->lang->custom->common . $this->lang->colon . $this->lang->$module->common;
-        $this->view->fieldList   = $fieldList;
-        $this->view->dbFields    = $dbFields;
         $this->view->field       = $field;
         $this->view->lang2Set    = str_replace('_', '-', $lang);
         $this->view->module      = $module;
diff --git a/module/custom/ui/set.html.php b/module/custom/ui/set.html.php
index f2a562d553..8ca5c05a47 100644
--- a/module/custom/ui/set.html.php
+++ b/module/custom/ui/set.html.php
@@ -407,7 +407,9 @@ else
 
         foreach($fieldList as $key => $value)
         {
-            $system = isset($dbFields[$key]) ? $dbFields[$key]->system : 1;
+            $system   = isset($dbFields[$key]) ? $dbFields[$key]->system : 1;
+            $keyLabel = $key === '' ? 'NULL' : $key;
+            if($key == 0) $keyLabel = '0';
 
             $formItems[] = formRow
             (
@@ -426,7 +428,7 @@ else
                 formGroup
                 (
                     set::width('1/2'),
-                    set::label($key === '' ? 'NULL' : $key),
+                    set::label($keyLabel),
                     set::name('values[]'),
                     set::value(isset($dbFields[$key]) ? $dbFields[$key]->value : $value),
                     set::readonly(empty($key))
diff --git a/module/custom/zen.php b/module/custom/zen.php
new file mode 100644
index 0000000000..2a82135681
--- /dev/null
+++ b/module/custom/zen.php
@@ -0,0 +1,364 @@
+
+ * @package     custom
+ * @link        https://www.zentao.net
+ */
+class customZen extends custom
+{
+    /**
+     * 设置自定义字段列表。
+     * Set the list of custom fields.
+     *
+     * @param  string    $module      todo|story|task|bug|testcase|testtask|user|project
+     * @param  string    $field       priList|typeList|statusList|sourceList|reasonList|stageList|reviewRules|reviewResultList|review|severityList|osList|browserList|resolutionList|longlife|resultList|roleList|contactField|deleted|unitList
+     * @param  string    $lang        all|zh-cn|zh-tw|en|de|fr
+     * @param  string    $currentLang all|zh-cn|zh-tw|en|de|fr
+     * @access protected
+     * @return void
+     */
+    protected function assignFieldListForSet(string $module = 'story', string $field = 'priList', string $lang = '', string $currentLang = ''): void
+    {
+        $this->app->loadLang($module);
+        if($lang == 'all')
+        {
+            $fieldList = array();
+            $items     = $this->custom->getItems("lang=all&module={$module}§ion={$field}&vision={$this->config->vision}");
+            foreach($items as $key => $item)
+            {
+                $fieldList[$key] = $item->value;
+            }
+        }
+        else
+        {
+            $fieldList = zget($this->lang->$module, $field, array());
+        }
+
+        /* Check whether the current language has been customized. */
+        $lang = str_replace('_', '-', $lang);
+        $dbFields = $this->custom->getItems("lang={$lang}&module={$module}§ion={$field}&vision={$this->config->vision}");
+        if(empty($dbFields)) $dbFields = $this->custom->getItems("lang=" . ($lang == $currentLang ? 'all' : $currentLang) . "&module={$module}§ion={$field}");
+        if($dbFields)
+        {
+            $dbField = reset($dbFields);
+            if($lang != $dbField->lang)
+            {
+                $lang = str_replace('-', "_", $dbField->lang);
+                foreach($fieldList as $key => $value)
+                {
+                    if(isset($dbFields[$key]) && $value != $dbFields[$key]->value) $fieldList[$key] = $dbFields[$key]->value;
+                }
+            }
+        }
+        $this->view->fieldList = $fieldList;
+        $this->view->dbFields  = $dbFields;
+    }
+
+    /**
+     * 设置自定义列表变量。
+     * Set the list of custom variables.
+     *
+     * @param  string    $module todo|story|task|bug|testcase|testtask|user|project
+     * @param  string    $field  priList|typeList|statusList|sourceList|reasonList|stageList|reviewRules|reviewResultList|review|severityList|osList|browserList|resolutionList|longlife|resultList|roleList|contactField|deleted|unitList
+     * @param  string    $lang        all|zh-cn|zh-tw|en|de|fr
+     * @param  string    $currentLang all|zh-cn|zh-tw|en|de|fr
+     * @access protected
+     * @return void
+     */
+    protected function assignVarsForSet(string $module = 'story', string $field = 'priList', string $lang = '', string $currentLang = ''): void
+    {
+        $this->assignFieldListForSet($module, $field, $lang, $currentLang);
+        if($module == 'project' && $field == 'unitList')
+        {
+            $this->app->loadConfig($module);
+            $unitList = zget($this->config->$module, 'unitList', '');
+            $this->view->unitList        = explode(',', $unitList);
+            $this->view->defaultCurrency = zget($this->config->$module, 'defaultCurrency', 'CNY');
+        }
+        if(in_array($module, array('story', 'demand')) && $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')
+        {
+            $this->app->loadConfig($module);
+            $this->loadModel('user');
+            if(in_array($module, array('story', 'demand')))
+            {
+                $this->view->depts               = $this->loadModel('dept')->getDeptPairs();
+                $this->view->forceReviewRoles    = zget($this->config->$module, 'forceReviewRoles', '');
+                $this->view->forceReviewDepts    = zget($this->config->$module, 'forceReviewDepts', '');
+                $this->view->forceNotReviewRoles = zget($this->config->$module, 'forceNotReviewRoles', '');
+                $this->view->forceNotReviewDepts = zget($this->config->$module, 'forceNotReviewDepts', '');
+            }
+            $this->view->users          = $module == 'story' ? $this->user->getCanCreateStoryUsers() : $this->user->getPairs('noclosed|nodeleted');
+            $this->view->needReview     = zget($this->config->$module, 'needReview', 1);
+            $this->view->forceReview    = zget($this->config->$module, 'forceReview', '');
+            $this->view->forceNotReview = zget($this->config->$module, 'forceNotReview', '');
+        }
+        if($module == 'bug' && $field == 'longlife')
+        {
+            $this->app->loadConfig('bug');
+            $this->view->longlife = $this->config->bug->longlife;
+        }
+        if($module == 'block' && $field == 'closed')
+        {
+            $this->loadModel('block');
+            $closedBlock = isset($this->config->block->closed) ? $this->config->block->closed : '';
+            $this->view->blockPairs  = $this->block->getClosedBlockPairs($closedBlock);
+            $this->view->closedBlock = $closedBlock;
+        }
+        if($module == 'user' && $field == 'deleted')
+        {
+            $this->app->loadConfig('user');
+            $this->view->showDeleted = isset($this->config->user->showDeleted) ? $this->config->user->showDeleted : '0';
+        }
+    }
+
+    /**
+     * 设置自定义字段的值。
+     * Set the value of the custom field.
+     *
+     * @param  string      $module todo|story|task|bug|testcase|testtask|user|project
+     * @param  string      $field  priList|typeList|statusList|sourceList|reasonList|stageList|reviewRules|reviewResultList|review|severityList|osList|browserList|resolutionList|longlife|resultList|roleList|contactField|deleted|unitList
+     * @access protected
+     * @return string|bool
+     */
+    protected function setFieldListForSet(string $module = 'story', string $field = 'priList'): string|bool
+    {
+        $data = $_POST;
+        if($module == 'project' && $field == 'unitList')
+        {
+            if(empty($data['unitList']))        return dao::$errors['message'] = $this->lang->custom->currencyNotEmpty;
+            if(empty($data['defaultCurrency'])) return dao::$errors['message'] = $this->lang->custom->defaultNotEmpty;
+            $this->loadModel('setting')->setItems("system.$module", $data);
+        }
+        elseif(($module == 'story' || $module == 'demand') && $field == 'review')
+        {
+            $this->setStoryReview($data);
+        }
+        elseif(($module == 'story' || $module == 'demand') && $field == 'reviewRules')
+        {
+            if(!isset($data['superReviewers'])) $data['superReviewers'] = array();
+            $data['superReviewers'] = implode(',', $data['superReviewers']);
+            $this->loadModel('setting')->setItems("system.$module@{$this->config->vision}", $data);
+        }
+        elseif($module == 'testcase' && $field == 'review')
+        {
+            $this->setTestcaseReview($data);
+        }
+        elseif($module == 'bug' && $field == 'longlife')
+        {
+            $this->loadModel('setting')->setItems('system.bug', $data);
+        }
+        elseif($module == 'block' && $field == 'closed')
+        {
+            $data['closed'] = implode(',', $data['closed']);
+            $this->loadModel('setting')->setItem('system.block.closed', zget($data, 'closed', ''));
+        }
+        elseif($module == 'user' && $field == 'contactField')
+        {
+            if(!isset($data['contactField'])) $data['contactField']= array();
+            $data['contactField'] = implode(',', $data['contactField']);
+            $this->loadModel('setting')->setItem('system.user.contactField', $data['contactField']);
+        }
+        elseif($module == 'user' && $field == 'deleted')
+        {
+            $this->loadModel('setting')->setItem('system.user.showDeleted', $data['showDeleted']);
+        }
+        else
+        {
+            if(!$this->post->keys) return dao::$errors['message'] = sprintf($this->lang->error->notempty, $this->lang->custom->key);
+            $this->checkKeysForSet($module, $field);
+        }
+
+        return !dao::isError();
+    }
+
+    /**
+     * 检查自定义key值是否合法。
+     * Check whether the key of custom is valid.
+     *
+     * @param  string    $module todo|story|task|bug|testcase|testtask|user|project
+     * @param  string    $field  priList|typeList|statusList|sourceList|reasonList|stageList|reviewRules|reviewResultList|review|severityList|osList|browserList|resolutionList|longlife|resultList|roleList|contactField|deleted|unitList
+     * @access protected
+     * @return bool
+     */
+    protected function checkKeysForSet(string $module = 'story', string $field = 'priList'): bool
+    {
+        $this->checkDuplicateKeys($module, $field);
+        if(dao::$errors) return false;
+
+        $this->checkInvalidKeys($module, $field);
+        if(dao::$errors) return false;
+
+        $lang = $_POST['lang'];
+        $this->custom->deleteItems("lang={$lang}&module={$module}§ion={$field}&vision={$this->config->vision}");
+        if($lang == 'all') $this->custom->deleteItems("lang={$currentLang}&module={$module}§ion={$field}&vision={$this->config->vision}");
+
+        $this->checkEmptyKeys($module, $field);
+        return !dao::isError();
+    }
+
+    /**
+     * 检查自定义key值是否重复。
+     * Check whether the key of custom is duplicate.
+     *
+     * @param  string    $module todo|story|task|bug|testcase|testtask|user|project
+     * @param  string    $field  priList|typeList|statusList|sourceList|reasonList|stageList|reviewRules|reviewResultList|review|severityList|osList|browserList|resolutionList|longlife|resultList|roleList|contactField|deleted|unitList
+     * @access protected
+     * @return bool|string
+     */
+    protected function checkDuplicateKeys(string $module = 'story', string $field = 'priList'): bool|string
+    {
+        $keys = array();
+        if(isset($_POST['keys']))
+        {
+            foreach($_POST['keys'] as $key)
+            {
+                if($module == 'testtask' && $field == 'typeList' && empty($key)) continue;
+                if($key && in_array($key, $keys)) return dao::$errors['message'] = sprintf($this->lang->custom->notice->repeatKey, $key);
+                $keys[] = $key;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 检查自定义key值是否正确。
+     * Check whether the key of custom is valid.
+     *
+     * @param  string    $module todo|story|task|bug|testcase|testtask|user|project
+     * @param  string    $field  priList|typeList|statusList|sourceList|reasonList|stageList|reviewRules|reviewResultList|review|severityList|osList|browserList|resolutionList|longlife|resultList|roleList|contactField|deleted|unitList
+     * @access protected
+     * @return bool|string
+     */
+    protected function checkInvalidKeys(string $module = 'story', string $field = 'priList'): bool|string
+    {
+        $lang       = $_POST['lang'];
+        $oldCustoms = $this->custom->getItems("lang={$lang}&module={$module}§ion={$field}");
+        foreach($_POST['keys'] as $index => $key)
+        {
+            if(!empty($key)) $key = trim($key);
+
+            /* Invalid key. It should be numbers. (It includes severityList in bug module && priList in story, task, bug, testcasea, testtask && todo module.) */
+            if(($field == 'priList' || $field == 'severityList') && (!is_numeric($key) || $key > 255)) return dao::$errors['message'] = $this->lang->custom->notice->invalidNumberKey;
+
+            if(!empty($key) && !isset($oldCustoms[$key]) && $key != 'n/a' && !validater::checkREG($key, '/^[a-z_A-Z_0-9]+$/')) return dao::$errors['message'] = $this->lang->custom->notice->invalidStringKey;
+
+            /* The length of roleList in user module is less than 10. check it when saved. */
+            if($module == 'user' && $field == 'roleList' && strlen($key) > 10) return dao::$errors['message'] = $this->lang->custom->notice->invalidStrlen['ten'];
+
+            /* The length of typeList in todo module is less than 15. check it when saved. */
+            if($module == 'todo' && $field == 'typeList' && strlen($key) > 15) return dao::$errors['message'] = $this->lang->custom->notice->invalidStrlen['fifteen'];
+
+            /* The length of sourceList in story module && typeList in task module is less than 20, check it when saved. */
+            if((($module == 'story' && $field == 'sourceList') || ($module == 'task' && $field == 'typeList')) && strlen($key) > 20) return dao::$errors['message'] = $this->lang->custom->notice->invalidStrlen['twenty'];
+
+            /* The length of field that in bug && testcase module && reasonList in story && task module is less than 30, check it when saved. */
+            if((in_array($module, array('bug', 'testcase')) || (in_array($module, array('story', 'task')) && $field == 'reasonList')) && strlen($key) > 30) return dao::$errors['message'] = $this->lang->custom->notice->invalidStrlen['thirty'];
+        }
+
+        return true;
+    }
+
+    /**
+     * 检查自定义key值是否为空。
+     * Check whether the key of custom is empty.
+     *
+     * @param  string    $module todo|story|task|bug|testcase|testtask|user|project
+     * @param  string    $field  priList|typeList|statusList|sourceList|reasonList|stageList|reviewRules|reviewResultList|review|severityList|osList|browserList|resolutionList|longlife|resultList|roleList|contactField|deleted|unitList
+     * @access protected
+     * @return bool|string
+     */
+    protected function checkEmptyKeys(string $module = 'story', string $field = 'priList'): bool|string
+    {
+        $lang     = $_POST['lang'];
+        $emptyKey = false;
+        $keys     = array();
+        foreach($_POST['keys'] as $index => $key)
+        {
+            if(!$key && $emptyKey) continue;
+
+            $value  = $_POST['values'][$index];
+            $system = $_POST['systems'][$index];
+            if($key && trim($value) === '') return dao::$errors['message'] = $this->lang->custom->notice->valueEmpty; // Fix bug #23538.
+
+            $this->custom->setItem("{$lang}.{$module}.{$field}.{$key}.{$system}", $value);
+
+            if(!$key) $emptyKey = true;
+        }
+        return true;
+    }
+
+    /**
+     * 设置需求评审规则。
+     * Set the review rules of story.
+     *
+     * @param  array     $data
+     * @access protected
+     * @return bool
+     */
+    protected function setStoryReview(array $data): bool
+    {
+        $forceFields = array('forceReview', 'forceNotReview', 'forceReviewRoles', 'forceNotReviewRoles', 'forceReviewDepts', 'forceNotReviewDepts');
+        foreach($forceFields as $forceField)
+        {
+            if(!isset($data[$forceField])) $data[$forceField] = array();
+            $data[$forceField] = implode(',', $data[$forceField]);
+        }
+
+        foreach($data as $key => $value)
+        {
+            if($key == 'needReview') continue;
+            if(strpos($key, 'Not')  && $data['needReview'] == 0) $data[$key] = '';
+            if(!strpos($key, 'Not') && $data['needReview'] == 1) $data[$key] = '';
+        }
+
+        $this->loadModel('setting')->setItems("system.$module@{$this->config->vision}", $data);
+
+        return !dao::isError();
+    }
+
+    /**
+     * 设置用例评审规则。
+     * Set testcase review rules.
+     *
+     * @param  array     $data
+     * @access protected
+     * @return bool
+     */
+    protected function setTestcaseReview(array $data): bool
+    {
+        if($data['needReview'])
+        {
+            unset($data['forceReview']);
+            if(!isset($data['forceNotReview'])) $data['forceNotReview'] = array();
+            $data['forceNotReview'] = implode(',', $data['forceNotReview']);
+        }
+        else
+        {
+            unset($data['forceNotReview']);
+            if(!isset($data['forceReview'])) $data['forceReview'] = array();
+            $data['forceReview'] = implode(',', $data['forceReview']);
+        }
+        $this->loadModel('setting')->setItems("system.testcase", $data);
+
+        $reviewCase = isset($data['reviewCase']) ? $data['reviewCase'] : 0;
+        if($data['needReview'] == 0 && $reviewCase)
+        {
+            $waitCases = $this->loadModel('testcase')->getByStatus(0, 0, 'all', 'wait');
+            $this->testcase->batchReview(array_keys($waitCases), 'pass');
+        }
+
+        return !dao::isError();
+    }
+}

From c855db99a4e20981125952267c9a1daae233475f Mon Sep 17 00:00:00 2001
From: tianshujie 
Date: Tue, 21 Nov 2023 14:06:12 +0800
Subject: [PATCH 31/92] * Change the empty key to 0;

---
 module/story/lang/de.php    | 10 +++++-----
 module/story/lang/en.php    | 10 +++++-----
 module/story/lang/fr.php    | 10 +++++-----
 module/story/lang/zh-cn.php | 10 +++++-----
 4 files changed, 20 insertions(+), 20 deletions(-)

diff --git a/module/story/lang/de.php b/module/story/lang/de.php
index e3e35f8144..48c6cd2f25 100644
--- a/module/story/lang/de.php
+++ b/module/story/lang/de.php
@@ -270,11 +270,11 @@ $lang->story->sourceList['bug']        = 'Bug';
 $lang->story->sourceList['forum']      = 'Forum';
 $lang->story->sourceList['other']      = 'Andere';
 
-$lang->story->priList[''] = '';
-$lang->story->priList[1]  = '1';
-$lang->story->priList[2]  = '2';
-$lang->story->priList[3]  = '3';
-$lang->story->priList[4]  = '4';
+$lang->story->priList[0] = '';
+$lang->story->priList[1] = '1';
+$lang->story->priList[2] = '2';
+$lang->story->priList[3] = '3';
+$lang->story->priList[4] = '4';
 
 $lang->story->changeList = array();
 $lang->story->changeList['no']  = 'Cancel';
diff --git a/module/story/lang/en.php b/module/story/lang/en.php
index 1a2e184953..f16d0a34ca 100644
--- a/module/story/lang/en.php
+++ b/module/story/lang/en.php
@@ -270,11 +270,11 @@ $lang->story->sourceList['bug']        = 'Bug';
 $lang->story->sourceList['forum']      = 'Forum';
 $lang->story->sourceList['other']      = 'Others';
 
-$lang->story->priList[''] = '';
-$lang->story->priList[1]  = '1';
-$lang->story->priList[2]  = '2';
-$lang->story->priList[3]  = '3';
-$lang->story->priList[4]  = '4';
+$lang->story->priList[0] = '';
+$lang->story->priList[1] = '1';
+$lang->story->priList[2] = '2';
+$lang->story->priList[3] = '3';
+$lang->story->priList[4] = '4';
 
 $lang->story->changeList = array();
 $lang->story->changeList['no']  = 'Cancel';
diff --git a/module/story/lang/fr.php b/module/story/lang/fr.php
index 92580348d7..afb551e843 100644
--- a/module/story/lang/fr.php
+++ b/module/story/lang/fr.php
@@ -270,11 +270,11 @@ $lang->story->sourceList['bug']        = 'Bug';
 $lang->story->sourceList['forum']      = 'Forum';
 $lang->story->sourceList['other']      = 'Autre';
 
-$lang->story->priList[''] = '';
-$lang->story->priList[1]  = '1';
-$lang->story->priList[2]  = '2';
-$lang->story->priList[3]  = '3';
-$lang->story->priList[4]  = '4';
+$lang->story->priList[0] = '';
+$lang->story->priList[1] = '1';
+$lang->story->priList[2] = '2';
+$lang->story->priList[3] = '3';
+$lang->story->priList[4] = '4';
 
 $lang->story->changeList = array();
 $lang->story->changeList['no']  = 'Cancel';
diff --git a/module/story/lang/zh-cn.php b/module/story/lang/zh-cn.php
index e592590c8f..8db85c510b 100644
--- a/module/story/lang/zh-cn.php
+++ b/module/story/lang/zh-cn.php
@@ -270,11 +270,11 @@ $lang->story->sourceList['bug']        = 'Bug';
 $lang->story->sourceList['forum']      = '论坛';
 $lang->story->sourceList['other']      = '其他';
 
-$lang->story->priList[''] = '';
-$lang->story->priList[1]  = '1';
-$lang->story->priList[2]  = '2';
-$lang->story->priList[3]  = '3';
-$lang->story->priList[4]  = '4';
+$lang->story->priList[0] = '';
+$lang->story->priList[1] = '1';
+$lang->story->priList[2] = '2';
+$lang->story->priList[3] = '3';
+$lang->story->priList[4] = '4';
 
 $lang->story->changeList = array();
 $lang->story->changeList['no']  = '不变更';

From 5d54dc54d0fde2640e038dfb46bf1d6b190b27a8 Mon Sep 17 00:00:00 2001
From: xieqiyu 
Date: Tue, 21 Nov 2023 14:11:22 +0800
Subject: [PATCH 32/92] * Modify doc required items.

---
 module/doc/js/create.ui.js    | 2 +-
 module/doc/ui/create.html.php | 6 ++----
 module/doc/ui/edit.html.php   | 8 +++-----
 module/doc/ui/view.html.php   | 2 +-
 4 files changed, 7 insertions(+), 11 deletions(-)

diff --git a/module/doc/js/create.ui.js b/module/doc/js/create.ui.js
index 50fc4317ff..be554bdcc4 100644
--- a/module/doc/js/create.ui.js
+++ b/module/doc/js/create.ui.js
@@ -10,7 +10,7 @@ $(function()
 
         if(requiredFields.indexOf('content') >= 0)
         {
-            if($('[name="content"]').val() == '')
+            if($('input[name="content"]').val() == '')
             {
                 zui.Modal.alert(contentNotEmpty);
                 return false;
diff --git a/module/doc/ui/create.html.php b/module/doc/ui/create.html.php
index 525aeb38c5..4d9a023cdd 100644
--- a/module/doc/ui/create.html.php
+++ b/module/doc/ui/create.html.php
@@ -183,10 +183,8 @@ else
                 formGroup
                 (
                     set::label($lang->doc->keywords),
-                    input
-                    (
-                        set::name('keywords')
-                    )
+                    set::control('input'),
+                    set::name('keywords')
                 ),
                 formGroup
                 (
diff --git a/module/doc/ui/edit.html.php b/module/doc/ui/edit.html.php
index 9c2420a3b2..26acf1752b 100644
--- a/module/doc/ui/edit.html.php
+++ b/module/doc/ui/edit.html.php
@@ -135,11 +135,9 @@ form
             formGroup
             (
                 set::label($lang->doc->keywords),
-                input
-                (
-                    set::name('keywords'),
-                    set::value($doc->keywords)
-                )
+                set::control('input'),
+                set::name('keywords'),
+                set::value($doc->keywords)
             ),
             formGroup
             (
diff --git a/module/doc/ui/view.html.php b/module/doc/ui/view.html.php
index 6be6d485c6..8b10c4893c 100644
--- a/module/doc/ui/view.html.php
+++ b/module/doc/ui/view.html.php
@@ -81,7 +81,7 @@ if($doc->keywords)
         {
             $keywordsLabel[] = span
             (
-                setClass('label secondary-outline'),
+                setClass('label secondary-outline ml-2'),
                 $keywords
             );
         }

From 754b80130bd099c25a1a89026438861040fda105 Mon Sep 17 00:00:00 2001
From: tianshujie 
Date: Tue, 21 Nov 2023 14:17:18 +0800
Subject: [PATCH 33/92] * Modify the logic of setting the default currency
 drop-down option.

---
 module/custom/js/set.ui.js | 15 ++++++++++-----
 1 file changed, 10 insertions(+), 5 deletions(-)

diff --git a/module/custom/js/set.ui.js b/module/custom/js/set.ui.js
index 2b134a8e5d..3055f5f889 100644
--- a/module/custom/js/set.ui.js
+++ b/module/custom/js/set.ui.js
@@ -30,8 +30,9 @@ window.changeReview = function(e)
 
 window.changeUnit = function()
 {
-    const defaultCurrency = $('#defaultCurrency').val();
-    $('#defaultCurrency').empty().append('');
+    const defaultCurrency      = $('[name=defaultCurrency]').val();
+    let   defaultCurrencyItems = new Array();
+    let   index                = 0;
     $('[name^=unitList]').each(function()
     {
         if($(this).prop('checked'))
@@ -40,11 +41,15 @@ window.changeUnit = function()
             const firstStr = $(this).val() + '">';
 
             text = text.substring(text.lastIndexOf(firstStr) + firstStr.length, text.lastIndexOf('<'));
-            $('#defaultCurrency').append("');
-                                                                                }
+            defaultCurrencyItems[index] = {'value': $(this).val(), 'text': text};
+
+            index ++;
+        }
     });
 
-     $('#defaultCurrency').val(defaultCurrency);
+    let $defaultCurrencyPicker = $('[name=defaultCurrency]').zui('picker');
+    $defaultCurrencyPicker.render({items: defaultCurrencyItems});
+    $defaultCurrencyPicker.$.setValue(defaultCurrency);
 }
 
 window.savaTestcaseReview = function()

From 4a22da17e5fa2e0c98f9be2cdbdfc8bbecc3f2ea Mon Sep 17 00:00:00 2001
From: liumengyi 
Date: Tue, 21 Nov 2023 14:19:03 +0800
Subject: [PATCH 34/92] * Add version drop down and deleted tip.

---
 module/testcase/lang/zh-cn.php   |  2 +-
 module/testcase/ui/view.html.php | 12 +++++++++---
 2 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/module/testcase/lang/zh-cn.php b/module/testcase/lang/zh-cn.php
index 43f228f789..f0517c5c91 100644
--- a/module/testcase/lang/zh-cn.php
+++ b/module/testcase/lang/zh-cn.php
@@ -95,7 +95,7 @@ $lang->testcase->fromCaselib      = '来自用例库用例';
 $lang->testcase->fromCaseID       = '用例来源ID';
 $lang->testcase->fromCaseVersion  = '用例来源版本';
 $lang->testcase->mailto           = '抄送给';
-$lang->testcase->deleted          = '是否删除';
+$lang->testcase->deleted          = '已删除';
 $lang->testcase->browseUnits      = '单元测试';
 $lang->testcase->suite            = '套件';
 $lang->testcase->executionStatus  = '执行状态';
diff --git a/module/testcase/ui/view.html.php b/module/testcase/ui/view.html.php
index 9c8f7b875c..b1dd9f3431 100644
--- a/module/testcase/ui/view.html.php
+++ b/module/testcase/ui/view.html.php
@@ -51,8 +51,6 @@ $app->loadLang('product');
 $productLink = $case->product && hasPriv('product', 'view') ? $this->createLink('product', 'view', "productID={$case->product}") : '';
 $branchLink  = $case->branch  && hasPriv('testcase', 'browse') ? $this->createLink('testcase', 'browse', "productID={$case->product}&branch={$case->branch}") : '';
 
-\a();
-
 $fromCaseItem      = array();
 $libItem           = array();
 $productItem       = array();
@@ -319,6 +317,8 @@ foreach(explode(',', $case->reviewedBy) as $account)
 $reviewedBy = trim($reviewedBy);
 
 $isInModal = isAjaxRequest('modal');
+$versions  = array();
+for($i = $case->version; $i >= 1; $i --) $versions[] = array('text' => "#{$i}", 'url' => inlink('view', "caseID={$case->id}&version={$i}"), 'active' => $i == $version);
 detailHeader
 (
     to::title
@@ -328,7 +328,13 @@ detailHeader
             set::entityID($case->id),
             set::level(1),
             span(setStyle('color', $case->color), $case->title)
-        )
+        ),
+        count($versions) > 1 ? dropdown
+        (
+            btn(setClass('btn-link'), "#{$version}"),
+            set::items($versions)
+        ) : null,
+        $case->deleted ? span(setClass('label danger'), $lang->case->deleted) : null
     ),
     $isInModal ? to::prefix('') : null,
     !$isInModal ? to::suffix

From 9205553d929cd3ee704ae47367a23a4c8c5081d8 Mon Sep 17 00:00:00 2001
From: sunguangming 
Date: Tue, 21 Nov 2023 14:21:55 +0800
Subject: [PATCH 35/92] * Add exclude drop menu method.

---
 config/zentaopms.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/config/zentaopms.php b/config/zentaopms.php
index 0d1338efc1..1f49c257cf 100644
--- a/config/zentaopms.php
+++ b/config/zentaopms.php
@@ -694,7 +694,7 @@ $config->bi->pickerHeight = 150;
 $config->db->sqliteBlacklist  = array('sqlite_queue', 'cron');
 $config->hasDropmenuApps      = array('program', 'project', 'product', 'execution', 'qa', 'admin', 'bi');
 $config->hasBranchMenuModules = array('product', 'story', 'release', 'bug', 'testcase', 'testtask');
-$config->excludeDropmenuList  = array('program-browse', 'program-productview', 'product-all', 'product-index', 'execution-all', 'execution-executionkanban', 'project-browse', 'project-batchedit', 'product-batchedit', 'admin-index', 'product-create', 'project-create', 'execution-create', 'program-create', 'execution-batchedit', 'metric-preview', 'qa-index');
+$config->excludeDropmenuList  = array('program-browse', 'program-productview', 'program-kanban', 'product-all', 'product-index', 'execution-all', 'execution-executionkanban', 'project-browse', 'project-batchedit', 'product-batchedit', 'admin-index', 'product-create', 'project-create', 'execution-create', 'program-create', 'execution-batchedit', 'metric-preview', 'qa-index');
 $config->hasSwitcherModules   = array('design');
 $config->hasSwitcherMethods   = array('project-bug', 'project-testcase', 'execution-bug', 'execution-testcase', 'testtask-cases', 'testtask-view', 'testtask-report', 'testtask-groupcase', 'testtask-linkcase');
 $config->excludeSwitcherList  = array();

From 7a0ab93f7374c91ca900340a603280126d53ec61 Mon Sep 17 00:00:00 2001
From: liumengyi 
Date: Tue, 21 Nov 2023 14:24:21 +0800
Subject: [PATCH 36/92] * Fix bug of duplicate key after deleting users from
 `zt_user`.

---
 module/user/model.php | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/module/user/model.php b/module/user/model.php
index d34ec256eb..36afa724b8 100644
--- a/module/user/model.php
+++ b/module/user/model.php
@@ -381,7 +381,7 @@ class userModel extends model
                 $data->account = $this->post->account;
                 $data->group   = $groupID;
                 $data->project = '';
-                $this->dao->insert(TABLE_USERGROUP)->data($data)->exec();
+                $this->dao->replace(TABLE_USERGROUP)->data($data)->exec();
             }
         }
 
@@ -517,7 +517,7 @@ class userModel extends model
                     $groups->account = $user->account;
                     $groups->group   = $group;
                     $groups->project = '';
-                    $this->dao->insert(TABLE_USERGROUP)->data($groups)->exec();
+                    $this->dao->replace(TABLE_USERGROUP)->data($groups)->exec();
                 }
             }
             unset($user->group);

From 512fd7661fbcf4ac3ea3bd507070443f505f2818 Mon Sep 17 00:00:00 2001
From: sunguangming 
Date: Tue, 21 Nov 2023 14:28:18 +0800
Subject: [PATCH 37/92] * Adjust code style.

---
 module/product/model.php | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/module/product/model.php b/module/product/model.php
index 5ea69bd7e1..ae73c948c0 100755
--- a/module/product/model.php
+++ b/module/product/model.php
@@ -1049,16 +1049,14 @@ class productModel extends model
 
         $stats = $this->getStatsProducts($productIdList, $programID == 0, $orderBy, $pager);
 
-
         $latestReleases       = $this->productTao->getStatisticByType($productIdList, 'latestReleases');
         $executionCountPairs  = $this->productTao->getExecutionCountPairs($productIdList);
         $coveragePairs        = $this->getCaseCoveragePairs($productIdList);
 
-        foreach($stats as $productID => $product)
+        foreach($stats as $product)
         {
-
-            $product->executions              = zget($executionCountPairs, $product->id, 0);
-            $product->coverage                = zget($coveragePairs, $product->id, 0);
+            $product->executions = zget($executionCountPairs, $product->id, 0);
+            $product->coverage   = zget($coveragePairs, $product->id, 0);
 
             $latestRelease = isset($latestReleases[$product->id]) ? $latestReleases[$product->id][0] : null;
             $product->latestRelease     = $latestRelease ? $latestRelease->name : '';

From 1943e2cd43ca7dd6dc4548a0b662740c89c76e70 Mon Sep 17 00:00:00 2001
From: tianshujie 
Date: Tue, 21 Nov 2023 14:30:29 +0800
Subject: [PATCH 38/92] * Refactor custorm::restore method.

---
 module/custom/control.php | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/module/custom/control.php b/module/custom/control.php
index 2f877e40ca..4bd3894409 100644
--- a/module/custom/control.php
+++ b/module/custom/control.php
@@ -68,6 +68,7 @@ class custom extends control
     }
 
     /**
+     * 还原默认语言项。删除相关项。
      * Restore the default lang. Delete the related items.
      *
      * @param  string $module
@@ -77,13 +78,13 @@ class custom extends control
      */
     public function restore(string $module, string $field)
     {
-        if($module == 'user' and $field == 'contactField')
+        if($module == 'user' && $field == 'contactField')
         {
-            $this->loadModel('setting')->deleteItems("module=$module&key=$field");
+            $this->loadModel('setting')->deleteItems("module={$module}&key={$field}");
         }
         else
         {
-            $this->custom->deleteItems("module=$module§ion=$field");
+            $this->custom->deleteItems("module={$module}§ion={$field}");
         }
 
         return $this->sendSuccess(array('load' => true));

From 04d259b049f4ae77181bc147f175be4a327b5d5b Mon Sep 17 00:00:00 2001
From: tianshujie 
Date: Tue, 21 Nov 2023 14:30:56 +0800
Subject: [PATCH 39/92] * Refactor custom::deleteItems method.

---
 module/custom/model.php | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/module/custom/model.php b/module/custom/model.php
index 19d3045f6c..b74ca46519 100644
--- a/module/custom/model.php
+++ b/module/custom/model.php
@@ -124,15 +124,17 @@ class customModel extends model
     }
 
     /**
+     * 删除自定义项。
      * Delete items.
      *
-     * @param  string   $paramString    see parseItemParam();
+     * @param  string $paramString    see parseItemParam();
      * @access public
-     * @return void
+     * @return bool
      */
-    public function deleteItems($paramString)
+    public function deleteItems($paramString): bool
     {
         $this->prepareSQL($this->parseItemParam($paramString), 'delete')->exec();
+        return !dao::isError();
     }
 
     /**

From ac7403a65fc3671797e6b718a89cfe1a5989297e Mon Sep 17 00:00:00 2001
From: tianshujie 
Date: Tue, 21 Nov 2023 14:31:37 +0800
Subject: [PATCH 40/92] * Refactor custom::deleteItems case.

---
 module/custom/test/custom.class.php      | 12 ++++++------
 module/custom/test/model/deleteitems.php | 23 ++++++++---------------
 2 files changed, 14 insertions(+), 21 deletions(-)

diff --git a/module/custom/test/custom.class.php b/module/custom/test/custom.class.php
index 8cdf495085..d6dba39bc1 100644
--- a/module/custom/test/custom.class.php
+++ b/module/custom/test/custom.class.php
@@ -68,19 +68,19 @@ class customTest
     }
 
     /**
+     * 删除自定义项。
      * Test delete items.
      *
-     * @param  string   $paramString
+     * @param  string     $paramString
      * @access public
-     * @return int
+     * @return array|bool
      */
-    public function deleteItemsTest($paramString)
+    public function deleteItemsTest($paramString): array|bool
     {
-        $objects = $this->objectModel->deleteItems($paramString);
+        $result = $this->objectModel->deleteItems($paramString);
 
         if(dao::isError()) return dao::getError();
-
-        return $objects;
+        return $result;
     }
 
     /**
diff --git a/module/custom/test/model/deleteitems.php b/module/custom/test/model/deleteitems.php
index b684b27809..8803f1ae4a 100755
--- a/module/custom/test/model/deleteitems.php
+++ b/module/custom/test/model/deleteitems.php
@@ -7,15 +7,8 @@ su('admin');
 /**
 
 title=测试 customModel->deleteItems();
+timeout=0
 cid=1
-pid=1
-
-测试参数为空 >> 0
-测试参数为lang >> 0
-测试参数为lang,module >> 0
-测试参数为lang,key,section >> 0
-测试参数为lang,key,section,module >> 0
-测试参数为lang,key,section,module,vision >> 0
 
 */
 
@@ -26,11 +19,11 @@ $key         = 'key=1';
 $vision      = 'vision=rnd';
 $paramString = array('', $lang, $lang.'&'.$module, $lang.'&'.$key.'&'.$section, $lang.'&'.$key.'&'.$section.'&'.$module, $lang.'&'.$key.'&'.$section.'&'.$module.'&'.$vision);
 
-$custom = new customTest();
+$customTester = new customTest();
 
-r($custom->deleteItemsTest($paramString[0])) && p() && e('0');  //测试参数为空
-r($custom->deleteItemsTest($paramString[1])) && p() && e('0');  //测试参数为lang
-r($custom->deleteItemsTest($paramString[2])) && p() && e('0');  //测试参数为lang,module
-r($custom->deleteItemsTest($paramString[3])) && p() && e('0');  //测试参数为lang,key,section
-r($custom->deleteItemsTest($paramString[4])) && p() && e('0');  //测试参数为lang,key,section,module
-r($custom->deleteItemsTest($paramString[5])) && p() && e('0');  //测试参数为lang,key,section,module,vision
+r($customTester->deleteItemsTest($paramString[0])) && p() && e('1');  //测试参数为空
+r($customTester->deleteItemsTest($paramString[1])) && p() && e('1');  //测试参数为lang
+r($customTester->deleteItemsTest($paramString[2])) && p() && e('1');  //测试参数为lang,module
+r($customTester->deleteItemsTest($paramString[3])) && p() && e('1');  //测试参数为lang,key,section
+r($customTester->deleteItemsTest($paramString[4])) && p() && e('1');  //测试参数为lang,key,section,module
+r($customTester->deleteItemsTest($paramString[5])) && p() && e('1');  //测试参数为lang,key,section,module,vision

From 0b9cd784f5944a1d16b9a511903fb7757315021e Mon Sep 17 00:00:00 2001
From: liumengyi 
Date: Tue, 21 Nov 2023 14:43:24 +0800
Subject: [PATCH 41/92] * Fix bug #40801.

---
 config/zentaopms.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/config/zentaopms.php b/config/zentaopms.php
index 1f49c257cf..0da8b3e969 100644
--- a/config/zentaopms.php
+++ b/config/zentaopms.php
@@ -236,7 +236,7 @@ $config->openMethods[] = 'testcase.showImport';
 $config->openMethods[] = 'testsuite.index';
 $config->openMethods[] = 'caselib.index';
 $config->openMethods[] = 'company.index';
-$config->openMethods[] = 'user.cropAvatar';
+$config->openMethods[] = 'user.cropavatar';
 $config->openMethods[] = 'custom.index';
 $config->openMethods[] = 'testcase.getxmindimport';
 $config->openMethods[] = 'testcase.showxmindimport';

From 74a14cf9e6e0dbb1c2f0ded2b4f30bb6f6289e46 Mon Sep 17 00:00:00 2001
From: sunhao 
Date: Tue, 21 Nov 2023 14:28:22 +0800
Subject: [PATCH 42/92] * zin: fix zin header not work in compatible mode.

---
 lib/zin/wg/header/v1.php | 79 +++++++++++++++++++++-------------------
 1 file changed, 42 insertions(+), 37 deletions(-)

diff --git a/lib/zin/wg/header/v1.php b/lib/zin/wg/header/v1.php
index 6a39baec9d..529973a29f 100644
--- a/lib/zin/wg/header/v1.php
+++ b/lib/zin/wg/header/v1.php
@@ -139,38 +139,40 @@ class header extends wg
 
         if(!isset($app->user)) return;
 
-        $user    = $app->user;
-        $isGuest = $user->account == 'guest';
-        $items   = array();
+        $user       = $app->user;
+        $isGuest    = $user->account == 'guest';
+        $items      = array();
+        $modalClass = (isset($config->zin->mode) && $config->zin->mode == 'compatible') ? 'open-in-parent' : null;
 
         if(!$isGuest)
         {
             $noRole = empty($user->role) || !isset($lang->user->roleList[$user->role]);
             $items[] = array
             (
-                'url'       => createLink('my', 'profile', '', '', true),
+                'url'          => createLink('my', 'profile', '', '', true),
                 'leadingClass' => 'row items-center gap-2 px-2 py-1 text-inherit',
-                'icon' => ' hidden',
-                'title'     => empty($user->realname) ? $user->account : $user->realname,
-                'titleClass' => 'text-lg',
-                'subtitle'  => $noRole ? null : $lang->user->roleList[$user->role],
-                // 'style'     => array('padding-left' => 0),
-                'data-toggle' => 'modal',
-                'data-size' => 700,
-                'data-id'   => 'profile',
-                'leading'   => array('html' => userAvatar(set::user($user))->render(), 'className' => 'center mr-1')
+                'icon'         => ' hidden',
+                'title'        => empty($user->realname) ? $user->account : $user->realname,
+                'titleClass'   => 'text-lg',
+                'subtitle'     => $noRole ? null : $lang->user->roleList[$user->role],
+                'innerClass'   => $modalClass,
+                'data-toggle'  => 'modal',
+                'data-size'    => 700,
+                'data-id'      => 'profile',
+                'leading'      => array('html' => userAvatar(set::user($user))->render(), 'className' => 'center mr-1')
             );
 
             $items[] = array('type' => 'divider');
 
             $items[] = array
             (
-                'url' => createLink('my', 'profile', '', '', true),
-                'icon' => 'account',
-                'text' => $lang->profile,
+                'url'         => createLink('my', 'profile', '', '', true),
+                'icon'        => 'account',
+                'text'        => $lang->profile,
+                'innerClass'  => $modalClass,
                 'data-toggle' => 'modal',
-                'data-size' => 700,
-                'data-id'   => 'profile'
+                'data-size'   => 700,
+                'data-id'     => 'profile'
             );
 
             if($app->config->vision === 'rnd')
@@ -179,26 +181,28 @@ class header extends wg
                 {
                     $items[] = array
                     (
-                        'url' => createLink('tutorial', 'start'),
-                        'icon' => 'guide',
-                        'text' => $lang->tutorialAB,
-                        'class' => '800',
-                        'outerClass' => 'user-tutorial',
-                        'data-width' => 700,
+                        'url'             => createLink('tutorial', 'start'),
+                        'icon'            => 'guide',
+                        'text'            => $lang->tutorialAB,
+                        'class'           => '800',
+                        'outerClass'      => 'user-tutorial',
+                        'data-width'      => 700,
                         'data-class-name' => 'modal-inverse',
                         'data-headerless' => true,
-                        'data-backdrop' => true,
-                        'data-keyboard' => true,
-                        'data-toggle' => 'modal'
+                        'data-backdrop'   => true,
+                        'data-keyboard'   => true,
+                        'innerClass'      => $modalClass,
+                        'data-toggle'     => 'modal'
                     );
                 }
 
                 $items[] = array
                 (
-                    'url' => createLink('my', 'preference', 'showTip=false', '', true),
-                    'icon' => 'controls',
-                    'text' => $lang->preference,
-                    'data-width' => 700,
+                    'url'         => createLink('my', 'preference', 'showTip=false', '', true),
+                    'icon'        => 'controls',
+                    'text'        => $lang->preference,
+                    'data-width'  => 700,
+                    'innerClass'  => $modalClass,
                     'data-toggle' => 'modal'
                 );
             }
@@ -207,11 +211,12 @@ class header extends wg
             {
                 $items[] = array
                 (
-                    'url' => createLink('my', 'changepassword', '', '', true),
-                    'icon' => 'cog-outline',
-                    'text' => $lang->changePassword,
+                    'url'         => createLink('my', 'changepassword', '', '', true),
+                    'icon'        => 'cog-outline',
+                    'text'        => $lang->changePassword,
+                    'innerClass'  => $modalClass,
                     'data-toggle' => 'modal',
-                    'data-size' => 'sm'
+                    'data-size'   => 'sm'
                 );
             }
 
@@ -241,12 +246,12 @@ class header extends wg
         $helpItems = array();
         $manualUrl = ((!empty($config->isINT)) ? $config->manualUrl['int'] : $config->manualUrl['home']) . '&theme=' . $_COOKIE['theme'];
         $helpItems[] = array('text' => $lang->manual, 'url' => $manualUrl, 'attrs' => array('data-app' => 'help'));
-        $helpItems[] = array('text' => $lang->changeLog, 'url' => createLink('misc', 'changeLog'), 'data-toggle' => 'modal');
+        $helpItems[] = array('text' => $lang->changeLog, 'url' => createLink('misc', 'changeLog'), 'data-toggle' => 'modal', 'innerClass' => $modalClass);
         $items[] = array('text' => $lang->help, 'icon' => 'help', 'items' => $helpItems);
 
         /* printClientLink */
 
-        $items[] = array('text' => $lang->aboutZenTao, 'icon' => 'about', 'url' => createLink('misc', 'about'), 'data-toggle' => 'modal');
+        $items[] = array('text' => $lang->aboutZenTao, 'icon' => 'about', 'url' => createLink('misc', 'about'), 'data-toggle' => 'modal', 'innerClass' => $modalClass);
         $items[] = array('type' => 'html', 'className' => 'menu-item', 'html' => $lang->designedByAIUX);
 
         $items[] = array('type' => 'divider');

From 3e905450eac362d0ef8ca95ad8b0458f07623376 Mon Sep 17 00:00:00 2001
From: daitingting 
Date: Tue, 21 Nov 2023 06:18:22 +0000
Subject: [PATCH 43/92]  * Format codes.  * Set name field as nested title when
 it will show task.

---
 module/project/ui/execution.html.php | 23 +++++++++++++----------
 1 file changed, 13 insertions(+), 10 deletions(-)

diff --git a/module/project/ui/execution.html.php b/module/project/ui/execution.html.php
index d989ef02e6..553cc7027f 100644
--- a/module/project/ui/execution.html.php
+++ b/module/project/ui/execution.html.php
@@ -22,15 +22,17 @@ jsVar('pageExecSummary', $lang->execution->pageExecSummary);
 jsVar('checkedExecSummary', $lang->execution->checkedExecSummary);
 
 $footToolbar = array();
-$canBatchEdit         = common::hasPriv('execution', 'batchEdit');
-$canBatchChangeStatus = common::hasPriv('execution', 'batchChangeStatus');
+$canBatchEdit         = hasPriv('execution', 'batchEdit');
+$canBatchChangeStatus = hasPriv('execution', 'batchChangeStatus');
 $canBatchAction       = $canBatchEdit || $canBatchChangeStatus;
 if($canBatchAction)
 {
     $editClass = $canBatchEdit ? 'batch-btn' : 'disabled';
-    $footToolbar['items'][] = array(
+    $footToolbar['items'][] = array
+    (
         'type'  => 'btn-group',
-        'items' => array(
+        'items' => array
+        (
             array('text' => $lang->edit, 'className' => "secondary size-sm {$editClass}", 'btnType' => 'primary', 'data-url' => createLink('execution', 'batchEdit')),
         )
     );
@@ -58,6 +60,12 @@ $fnGenerateCols = function() use ($config, $project)
         $fieldList['actions']['actionsMap']['edit']['url'] = createLink('programplan', 'edit', "stageID={rawID}&projectID={projectID}");
     }
 
+    if(!$this->cookie->showTask)
+    {
+        $fieldList['name']['type'] = 'title';
+        unset($fieldList['name']['nestedToggle']);
+    }
+
     return array_values($fieldList);
 };
 
@@ -126,12 +134,7 @@ dtable
     set::canRowCheckable(jsRaw("function(rowID){return this.getRowInfo(rowID).data.id.indexOf('pid') > -1;}")),
     set::checkInfo(jsRaw("function(checkedIDList){ return window.footerSummary(this, checkedIDList);}")),
     set::footToolbar($footToolbar),
-    set::footPager(
-        usePager
-        (
-            array('linkCreator' => helper::createLink('project', 'execution', "status={$status}&projectID=$projectID&orderBy={$orderBy}&productID={$productID}&recTotal={recTotal}&recPerPage={recPerPage}&page={page}"))
-        )
-    ),
+    set::footPager(usePager(array('linkCreator' => createLink('project', 'execution', "status={$status}&projectID=$projectID&orderBy={$orderBy}&productID={$productID}&recTotal={recTotal}&recPerPage={recPerPage}&page={page}")))),
     set::emptyTip($lang->execution->noExecution),
     set::createTip($isStage ? $lang->programplan->create : $lang->execution->create),
     set::createLink(hasPriv('execution', 'create') ? $createLink : '')

From b686ff43205ca9694be604ea35b5b20b75b0c5e0 Mon Sep 17 00:00:00 2001
From: daitingting 
Date: Tue, 21 Nov 2023 06:45:40 +0000
Subject: [PATCH 44/92]  * Set link params for feature bar.

---
 module/project/ui/group.html.php | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/module/project/ui/group.html.php b/module/project/ui/group.html.php
index b333db1ceb..4a2a5cb78f 100644
--- a/module/project/ui/group.html.php
+++ b/module/project/ui/group.html.php
@@ -17,7 +17,8 @@ jsVar('confirmDelete', $lang->group->confirmDelete);
 /* zin: Define the feature bar on main menu. */
 featureBar
 (
-    set::current('all'),
+    set::linkParams("projectID={$projectID}&programID={$programID}"),
+    set::current('all')
 );
 
 /* zin: Define the toolbar on main menu. */

From 8ff2c13e8205d7f89a7ee44cca6c321c378d8405 Mon Sep 17 00:00:00 2001
From: wangxuepeng 
Date: Tue, 21 Nov 2023 14:48:44 +0800
Subject: [PATCH 45/92] * Change to new writing method.

---
 module/kanban/ui/create.html.php          | 78 +++++++++++------------
 module/kanban/ui/createlane.html.php      |  2 +-
 module/kanban/ui/importexecution.html.php |  8 +--
 module/kanban/ui/importrelease.html.php   |  8 +--
 module/system/ui/backupplatform.html.php  |  2 +-
 module/system/ui/editdomain.html.php      |  6 +-
 module/system/ui/ossview.html.php         |  8 +--
 7 files changed, 54 insertions(+), 58 deletions(-)

diff --git a/module/kanban/ui/create.html.php b/module/kanban/ui/create.html.php
index 749d14bfd4..8760a95bea 100644
--- a/module/kanban/ui/create.html.php
+++ b/module/kanban/ui/create.html.php
@@ -27,8 +27,7 @@ formPanel
             setClass('primary-pale'),
             set::icon('copy'),
             set::url('#copyKanbanModal'),
-            set('data-destoryOnHide', true),
-            set('data-toggle', 'modal'),
+            setData(array('destoryOnHide' => true, 'toggle' => 'modal')),
             $lang->kanban->copy . $lang->kanban->common
         )
     ),
@@ -48,7 +47,7 @@ formPanel
     ),
     formRow
     (
-        set::id('WIPCountBox'),
+        setID('WIPCountBox'),
         formGroup
         (
             set::label($lang->kanban->WIPCount),
@@ -63,7 +62,7 @@ formPanel
     ),
     formRow
     (
-        set::id('spaceBox'),
+        setID('spaceBox'),
         formGroup
         (
             set::width('1/2'),
@@ -79,7 +78,7 @@ formPanel
     ),
     formRow
     (
-        set::id('nameBox'),
+        setID('nameBox'),
         formGroup
         (
             set::width('1/2'),
@@ -90,8 +89,8 @@ formPanel
     ),
     formRow
     (
-        set::id('ownerBox'),
-        set::className($type == 'private' ? 'hidden' : ''),
+        setID('ownerBox'),
+        setClass($type == 'private' ? 'hidden' : ''),
         formGroup
         (
             set::width('1/2'),
@@ -106,10 +105,10 @@ formPanel
                 ),
                 span
                 (
-                    set('class', 'input-group-addon'),
+                    setClass('input-group-addon'),
                     a
                     (
-                        set('id', 'allUsers'),
+                        setID('allUsers'),
                         set('href', 'javascript:;'),
                         $lang->kanban->allUsers
                     )
@@ -119,8 +118,8 @@ formPanel
     ),
     formRow
     (
-        set::id('teamBox'),
-        set::className($type == 'private' ? 'hidden' : ''),
+        setID('teamBox'),
+        setClass($type == 'private' ? 'hidden' : ''),
         formGroup
         (
             set::label($lang->kanban->team),
@@ -135,7 +134,7 @@ formPanel
     ),
     formRow
     (
-        set::id('fixedColBox'),
+        setID('fixedColBox'),
         formGroup
         (
             setClass('items-center'),
@@ -149,10 +148,10 @@ formPanel
             ),
             div
             (
-                set::className('flex items-center ml-8 py-1'),
+                setClass('flex items-center ml-8 py-1'),
                 set::style(array('padding-left' => '1px')),
                 span($lang->kanban->colWidth),
-                input(set::type('number'), set::min($config->colWidth), set::name('colWidth'), set::className('w-16 size-sm mx-1'), set::value(isset($copyKanban->colWidth) ? $copyKanban->colWidth : $config->colWidth)),
+                input(set::type('number'), set::min($config->colWidth), set::name('colWidth'), setClass('w-16 size-sm mx-1'), set::value(isset($copyKanban->colWidth) ? $copyKanban->colWidth : $config->colWidth)),
                 span('px')
             ),
             div
@@ -165,7 +164,7 @@ formPanel
     ),
     formRow
     (
-        set::id('autoColBox'),
+        setID('autoColBox'),
         set::style(array('margin-top' => '0px')),
         formGroup
         (
@@ -180,12 +179,12 @@ formPanel
             ),
             div
             (
-                set::className('flex items-center ml-5 py-1'),
+                setClass('flex items-center ml-5 py-1'),
                 span($lang->kanban->colWidth),
-                input(set::type('number'), set::min($config->minColWidth), set::name('minColWidth'), set::className('w-16 size-sm mx-1'), set::value(isset($copyKanban->minColWidth) ? $copyKanban->minColWidth : $config->minColWidth)),
+                input(set::type('number'), set::min($config->minColWidth), set::name('minColWidth'), setClass('w-16 size-sm mx-1'), set::value(isset($copyKanban->minColWidth) ? $copyKanban->minColWidth : $config->minColWidth)),
                 span('px'),
-                span('~', set::className('mx-1')),
-                input(set::type('number'), set::min($config->maxColWidth), set::name('maxColWidth'), set::className('w-16 size-sm mx-1'), set::value(isset($copyKanban->maxColWidth) ? $copyKanban->maxColWidth : $config->maxColWidth)),
+                span('~', setClass('mx-1')),
+                input(set::type('number'), set::min($config->maxColWidth), set::name('maxColWidth'), setClass('w-16 size-sm mx-1'), set::value(isset($copyKanban->maxColWidth) ? $copyKanban->maxColWidth : $config->maxColWidth)),
                 span('px')
             ),
             div
@@ -228,7 +227,7 @@ formPanel
     ),
     formRow
     (
-        set::id('archiveBox'),
+        setID('archiveBox'),
         formGroup
         (
             set::label($lang->kanban->archive),
@@ -243,7 +242,7 @@ formPanel
     ),
     formRow
     (
-        set::id('manageProgressBox'),
+        setID('manageProgressBox'),
         formGroup
         (
             set::label($lang->kanban->manageProgress),
@@ -258,7 +257,7 @@ formPanel
     ),
     formRow
     (
-        set::id('alignmentBox'),
+        setID('alignmentBox'),
         formGroup
         (
             set::label($lang->kanban->alignment),
@@ -273,7 +272,7 @@ formPanel
     ),
     formRow
     (
-        set::id('descBox'),
+        setID('descBox'),
         formGroup
         (
             set::label($lang->kanban->desc),
@@ -286,8 +285,8 @@ formPanel
     ),
     formRow
     (
-        set::id('whitelistBox'),
-        set::className($type != 'private' ? 'hidden' : ''),
+        setID('whitelistBox'),
+        setClass($type != 'private' ? 'hidden' : ''),
         formGroup
         (
             set::label($lang->whitelist),
@@ -299,8 +298,8 @@ formPanel
                 set::value(isset($copyKanban->whitelist) ? $copyKanban->whitelist : '')
             )
         ),
-        input(set::className('hidden'), set::name('copyKanbanID'), set::value($copyKanbanID)),
-        input(set::className('hidden'), set::name('copyRegion'), set::value($copyRegion))
+        input(setClass('hidden'), set::name('copyKanbanID'), set::value($copyKanbanID)),
+        input(setClass('hidden'), set::name('copyRegion'), set::value($copyRegion))
     )
 );
 
@@ -310,11 +309,11 @@ if($copyKanbanID != 0)
     $kanbanList[] = cell
     (
         set::width('1/3'),
-        set::className('p-2'),
+        setClass('p-2'),
         div
         (
-            set::className('copy-card p-2 border rounded-md text-danger'),
-            icon('cancel', set::className('pr-2')),
+            setClass('copy-card p-2 border rounded-md text-danger'),
+            icon('cancel', setClass('pr-2')),
             $lang->kanban->cancelCopy
         )
     );
@@ -327,15 +326,12 @@ if(!empty($kanbans))
         $kanbanList[] = cell
             (
                 set::width('1/3'),
-                set::className('p-2'),
+                setClass('p-2'),
                 div
                 (
-                    set('data-on', 'click'),
-                    set('data-call', 'clickCopyCard'),
-                    set('data-params', 'event'),
-                    set('data-id', $id),
-                    set::className('copy-card p-2 border rounded-md'),
-                    icon('kanban', set::className('pr-2')),
+                    setData(array('on' => 'click', 'call' => 'clickCopyCard', 'params' => 'event', 'id' => $id)),
+                    setClass('copy-card p-2 border rounded-md'),
+                    icon('kanban', setClass('pr-2')),
                     $name
                 )
             );
@@ -349,7 +345,7 @@ else
             icon('exclamation-sign icon-2x pl-2 text-warning'),
             span
             (
-                set::className('font-bold ml-2'),
+                setClass('font-bold ml-2'),
                 $lang->kanban->copyNoKanban
             )
         );
@@ -359,12 +355,12 @@ modalTrigger
 (
     modal
     (
-        set::id('copyKanbanModal'),
+        setID('copyKanbanModal'),
         to::header
         (
             span
             (
-                set::className('copy-title article-h1'),
+                setClass('copy-title article-h1'),
                 $lang->kanban->copyTitle,
             ),
             span
@@ -381,7 +377,7 @@ modalTrigger
         ),
         div
         (
-            set::id('copyKanbans'),
+            setID('copyKanbans'),
             setClass('flex flex-wrap'),
             $kanbanList
         )
diff --git a/module/kanban/ui/createlane.html.php b/module/kanban/ui/createlane.html.php
index ea1baabeb1..1251b1efa4 100644
--- a/module/kanban/ui/createlane.html.php
+++ b/module/kanban/ui/createlane.html.php
@@ -39,7 +39,7 @@ formPanel
     ),
     formRow
     (
-        set::id('otherLaneBox'),
+        setID('otherLaneBox'),
         formGroup
         (
             set::label($lang->kanbanlane->otherlane),
diff --git a/module/kanban/ui/importexecution.html.php b/module/kanban/ui/importexecution.html.php
index 779cf67dbd..6eebb6494e 100644
--- a/module/kanban/ui/importexecution.html.php
+++ b/module/kanban/ui/importexecution.html.php
@@ -26,9 +26,9 @@ featureBar
 (
     inputGroup
     (
-        span(set::className('input-group-addon'), $lang->kanban->selectedProduct),
+        span(setClass('input-group-addon'), $lang->kanban->selectedProduct),
         picker(set::name('project'), set::items($projects), set::value($selectedProjectID), set::style(array('width' => '200px')), set('data-on', 'change'), set('data-call', 'changeProject'), set::required(true)),
-        span(set::className('input-group-addon'), $lang->kanban->selectedLane),
+        span(setClass('input-group-addon'), $lang->kanban->selectedLane),
         picker(set::name('lane'), set::items($lanePairs), set::style(array('width' => '200px')), set::required(true))
     )
 );
@@ -50,9 +50,9 @@ unset($config->execution->dtable->fieldList['actions']);
 
 formBase
 (
-    set::id('linkForm'),
+    setID('linkForm'),
     set::actions(''),
-    set::className('mt-2'),
+    setClass('mt-2'),
     dtable
     (
         set::fixedLeftWidth('0.33'),
diff --git a/module/kanban/ui/importrelease.html.php b/module/kanban/ui/importrelease.html.php
index f4af4de6a1..658e22d46d 100644
--- a/module/kanban/ui/importrelease.html.php
+++ b/module/kanban/ui/importrelease.html.php
@@ -35,9 +35,9 @@ featureBar
 (
     inputGroup
     (
-        span(set::className('input-group-addon'), $lang->kanban->selectedProduct),
+        span(setClass('input-group-addon'), $lang->kanban->selectedProduct),
         picker(set::name('product'), set::items($products), set::value($selectedProductID), set::style(array('width' => '200px')), set('data-on', 'change'), set('data-call', 'changeProduct'), set::required(true)),
-        span(set::className('input-group-addon'), $lang->kanban->selectedLane),
+        span(setClass('input-group-addon'), $lang->kanban->selectedLane),
         picker(set::name('lane'), set::items($lanePairs), set::style(array('width' => '200px')), set::required(true))
     )
 );
@@ -49,9 +49,9 @@ unset($config->release->dtable->fieldList['actions']);
 
 formBase
 (
-    set::id('linkForm'),
+    setID('linkForm'),
     set::actions(''),
-    set::className('mt-2'),
+    setClass('mt-2'),
     dtable
     (
         set::fixedLeftWidth('0.33'),
diff --git a/module/system/ui/backupplatform.html.php b/module/system/ui/backupplatform.html.php
index a5c0bae861..47eb9b548b 100644
--- a/module/system/ui/backupplatform.html.php
+++ b/module/system/ui/backupplatform.html.php
@@ -16,7 +16,7 @@ modalHeader(set::title($lang->system->backup->backup));
 formPanel
 (
     on::click('#startBackup', 'startBackup'),
-    set::id('backupForm'),
+    setID('backupForm'),
     formGroup
     (
         set::label($lang->comment),
diff --git a/module/system/ui/editdomain.html.php b/module/system/ui/editdomain.html.php
index 48d2cfe56d..e6642d77e9 100644
--- a/module/system/ui/editdomain.html.php
+++ b/module/system/ui/editdomain.html.php
@@ -13,7 +13,7 @@ namespace zin;
 formPanel
 (
     set::title($lang->system->domain->config),
-    set::id('domainForm'),
+    setID('domainForm'),
     formRow
     (
         formGroup
@@ -83,13 +83,13 @@ formPanel
             btn
             (
                 $lang->system->verify,
-                set::id('validateCertBtn'),
+                setID('validateCertBtn'),
                 on::click('checkCert')
             ),
             h::span
             (
                 setClass('tips-git leading-8 ml-2'),
-                set::id('validateMsg')
+                setID('validateMsg')
             )
         )
     )
diff --git a/module/system/ui/ossview.html.php b/module/system/ui/ossview.html.php
index 3ed48469f2..8e50e1b792 100644
--- a/module/system/ui/ossview.html.php
+++ b/module/system/ui/ossview.html.php
@@ -25,7 +25,7 @@ panel
             set::name($lang->system->oss->appURL),
             a
             (
-                set::id('ossVisitUrl'),
+                setID('ossVisitUrl'),
                 set::target('_blank'),
                 $lang->system->visit
             )
@@ -35,7 +35,7 @@ panel
             set::name($lang->system->oss->user),
             span
             (
-                set::id('ossAdmin')
+                setID('ossAdmin')
             )
         ),
         item
@@ -44,7 +44,7 @@ panel
             h::input
             (
                 setClass('hidden'),
-                set::id('ossPassword'),
+                setID('ossPassword'),
                 set::value(zget($ossAccount, 'password', ''))
             ),
             btn
@@ -69,7 +69,7 @@ panel
             h::input
             (
                 setClass('hidden'),
-                set::id('ossSK'),
+                setID('ossSK'),
                 set::value(zget($ossAccount, 'password', ''))
             ),
             btn

From 437d4e2151e34918da72105ade5c60d171477083 Mon Sep 17 00:00:00 2001
From: liumengyi 
Date: Tue, 21 Nov 2023 14:52:55 +0800
Subject: [PATCH 46/92] * Fix bug #40795.

---
 module/product/model.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/module/product/model.php b/module/product/model.php
index ae73c948c0..a4ba91bd2c 100755
--- a/module/product/model.php
+++ b/module/product/model.php
@@ -1455,7 +1455,7 @@ class productModel extends model
          $product->productLine        = $product->lineName;
          $product->PO                 = !empty($product->PO) ? zget($users, $product->PO) : '';
          $product->testCaseCoverage   = $product->coverage;
-         $product->storyCompleteRate  = $totalStories == 0 ? 0 : round($product->finishedStories / $totalStories, 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;

From ae9919d055c923925c0fa850466b18c81d60603f Mon Sep 17 00:00:00 2001
From: caoyanyi 
Date: Tue, 21 Nov 2023 14:56:34 +0800
Subject: [PATCH 47/92] * Split repo control method.

---
 module/repo/control.php | 155 ++++++++--------------------------------
 module/repo/model.php   |  26 +++----
 module/repo/zen.php     | 128 +++++++++++++++++++++++++++++++++
 3 files changed, 172 insertions(+), 137 deletions(-)

diff --git a/module/repo/control.php b/module/repo/control.php
index 97ea1975ca..5cc7971fcf 100644
--- a/module/repo/control.php
+++ b/module/repo/control.php
@@ -615,7 +615,8 @@ class repo extends control
     }
 
     /**
-     * Show diff.
+     * 代码diff信息。
+     * Show repo diff.
      *
      * @param  int    $repoID
      * @param  int    $objectID
@@ -680,6 +681,7 @@ class repo extends control
     }
 
     /**
+     * 代码下载。
      * Download repo file.
      *
      * @param  int    $repoID
@@ -691,7 +693,7 @@ class repo extends control
      * @access public
      * @return void
      */
-    public function download($repoID, $path, $fromRevision = 'HEAD', $toRevision = '', $type = 'file', $isBranchOrTag = false)
+    public function download(int $repoID, string $path, string $fromRevision = 'HEAD', string $toRevision = '', string $type = 'file', bool $isBranchOrTag = false)
     {
         if($this->get->repoPath) $path = $this->get->repoPath;
         $entry = $this->repo->decodePath($path);
@@ -717,8 +719,9 @@ class repo extends control
 
         $fileName = basename(urldecode($entry));
         if($type != 'file') $fileName .= "r$fromRevision--r$toRevision.patch";
+
         $extension = ltrim(strrchr($fileName, '.'), '.');
-        $this->fetch('file', 'sendDownHeader', array("fileName" => $fileName, "fileType" => $extension,  "content" => $content));
+        $this->fetch('file', 'sendDownHeader', array("fileName" => $fileName, "fileType" => $extension, "content" => $content));
     }
 
     /**
@@ -750,7 +753,8 @@ class repo extends control
     }
 
     /**
-     * Show sync comment.
+     * 显示提交同步进度。
+     * Show sync commit.
      *
      * @param  int    $repoID
      * @param  int    $objectID  projectID|executionID
@@ -758,16 +762,15 @@ class repo extends control
      * @access public
      * @return void
      */
-    public function showSyncCommit($repoID = 0, $objectID = 0, $branch = '')
+    public function showSyncCommit(int $repoID = 0, int $objectID = 0, string $branch = '')
     {
         $this->commonAction($repoID, $objectID);
 
         if($repoID == 0) $repoID = $this->session->repoID;
         if($branch) $branch = base64_decode(helper::safe64Decode($branch));
 
-        $this->view->title      = $this->lang->repo->common . $this->lang->colon . $this->lang->repo->showSyncCommit;
-
         $latestInDB = $this->repo->getLatestCommit($repoID);
+        $this->view->title      = $this->lang->repo->common . $this->lang->colon . $this->lang->repo->showSyncCommit;
         $this->view->version    = $latestInDB ? (int)$latestInDB->commit : 1;
         $this->view->repoID     = $repoID;
         $this->view->repo       = $this->repo->getByID($repoID);
@@ -778,6 +781,7 @@ class repo extends control
     }
 
     /**
+     * 根据提交信息关联需求。
      * Link story to commit.
      *
      * @param  int    $repoID
@@ -825,6 +829,7 @@ class repo extends control
     }
 
     /**
+     * 根据提交信息关联Bug。
      * Link bug to commit.
      *
      * @param  int    $repoID
@@ -872,6 +877,7 @@ class repo extends control
     }
 
     /**
+     * 根据提交信息关联任务。
      * Link task to commit.
      *
      * @param  int    $repoID
@@ -926,6 +932,7 @@ class repo extends control
     }
 
     /**
+     * 取消提交信息的关联记录。
      * Unlink object and commit revision.
      *
      * @param  int    $repoID
@@ -935,7 +942,7 @@ class repo extends control
      * @access public
      * @return void
      */
-    public function unlink($repoID, $revision, $objectType, $objectID)
+    public function unlink(int $repoID, string $revision, string $objectType, int $objectID)
     {
         $this->repo->unlink($repoID, $revision, $objectType, $objectID);
 
@@ -969,10 +976,8 @@ class repo extends control
         $gitlab     = empty($server) ? array_shift($gitlabList) : $this->gitlab->getById($server);
 
         $repoList = $gitlab ? $this->repoZen->getGitlabNotExistRepos($gitlab) : array();
-
         $products = $this->loadModel('product')->getPairs('', 0, '', 'all');
 
-
         $this->view->title       = $this->lang->repo->common . $this->lang->colon . $this->lang->repo->importAction;
         $this->view->gitlabPairs = $this->gitlab->getPairs();
         $this->view->products    = $products;
@@ -983,6 +988,7 @@ class repo extends control
     }
 
     /**
+     * 获取代码对比编辑器内容。
      * Get diff editor content by ajax.
      *
      * @param  int    $repoID
@@ -990,12 +996,12 @@ class repo extends control
      * @param  string $entry
      * @param  string $oldRevision
      * @param  string $newRevision
-     * @param  string $showBug
+     * @param  string $showBug     // Used for biz.
      * @param  string $encoding
      * @access public
      * @return void
      */
-    public function ajaxGetDiffEditorContent($repoID, $objectID = 0, $entry = '', $oldRevision = '', $newRevision = '', $showBug = 'false', $encoding = '')
+    public function ajaxGetDiffEditorContent(int $repoID, int $objectID = 0, string $entry = '', string $oldRevision = '', string $newRevision = '', string $showBug = 'false', string $encoding = '')
     {
         if(!$entry) $entry = (string) $this->cookie->repoCodePath;
 
@@ -1033,6 +1039,7 @@ class repo extends control
     }
 
     /**
+     * 获取代码详情的编辑器内容。
      * Get editor content by ajax.
      *
      * @param  int    $repoID
@@ -1044,7 +1051,7 @@ class repo extends control
      * @access public
      * @return void
      */
-    public function ajaxGetEditorContent($repoID, $objectID = 0, $entry = '', $revision = 'HEAD', $showBug = 'false', $encoding = '')
+    public function ajaxGetEditorContent(int $repoID, int $objectID = 0, string $entry = '', string $revision = 'HEAD', string $showBug = 'false', string $encoding = '')
     {
         if(!$entry) $entry = (string) $this->cookie->repoCodePath;
 
@@ -1057,6 +1064,7 @@ class repo extends control
         $info = $this->scm->info($entry, $revision);
         $path = $entry ? $info->path : '';
         if($info->kind == 'dir') $this->locate($this->repo->createLink('browse', "repoID=$repoID&branchID=&objectID=$objectID&path=" . $this->repo->encodePath($path) . "&revision=$revision"));
+
         $content  = $this->scm->cat($entry, $revision);
         $entry    = urldecode($entry);
         $pathInfo = pathinfo($entry);
@@ -1079,17 +1087,11 @@ class repo extends control
         $this->view->title       = $this->lang->repo->common . $this->lang->colon . $this->lang->repo->view;
         $this->view->type        = 'view';
         $this->view->showBug     = $showBug;
-        $this->view->encoding    = str_replace('-', '_', $encoding);
         $this->view->repoID      = $repoID;
-        $this->view->branchID    = $this->cookie->repoBranch;
-        $this->view->objectID    = $objectID;
-        $this->view->repo        = $repo;
         $this->view->revision    = $revision;
         $this->view->oldRevision = '';
         $this->view->file        = $file;
         $this->view->entry       = $entry;
-        $this->view->path        = $entry;
-        $this->view->info        = $info;
         $this->view->suffix      = $suffix;
         $this->view->content     = $content ? $content : '';
         $this->view->pathInfo    = $pathInfo;
@@ -1098,6 +1100,7 @@ class repo extends control
     }
 
     /**
+     * 异步同步代码提交记录。
      * Ajax sync comment.
      *
      * @param  int    $repoID
@@ -1105,136 +1108,38 @@ class repo extends control
      * @access public
      * @return void
      */
-    public function ajaxSyncCommit($repoID = 0, $type = 'batch')
+    public function ajaxSyncCommit(int $repoID = 0, string $type = 'batch')
     {
         set_time_limit(0);
         $repo = $this->repo->getByID($repoID);
-        if(empty($repo)) return;
+        if(empty($repo)) return print($this->config->repo->repoSyncLog->finish);
         if($repo->synced) return print($this->config->repo->repoSyncLog->finish);
 
         if(in_array($repo->SCM, array('Gitea', 'Gogs')))
         {
-            $logFile = realPath($this->app->getTmpRoot() . $this->config->repo->repoSyncLog->logFilePrefix . strtolower($repo->SCM) . ".{$repo->name}.log");
-            if($logFile)
-            {
-                $content  = file($logFile);
-                foreach($content as $line)
-                {
-                    if($this->repo->strposAry($line, $this->config->repo->repoSyncLog->fatal) !== false) return print($line);
-                    if($this->repo->strposAry($line, $this->config->repo->repoSyncLog->failed) !== false) return print($line);
-                }
-
-                $lastLine = $content[count($content) - 1];
-                if($this->repo->strposAry($lastLine, $this->config->repo->repoSyncLog->done) === false)
-                {
-                    if($this->repo->strposAry($lastLine, $this->config->repo->repoSyncLog->emptyRepo) !== false)
-                    {
-                        @unlink($logFile);
-                    }
-                    elseif($this->repo->strposAry($lastLine, $this->config->repo->repoSyncLog->total) !== false)
-                    {
-                        $logContent = file_get_contents($logFile);
-                        if($this->repo->strposAry($logContent, $this->config->repo->repoSyncLog->finishCount) !== false and $this->repo->strposAry($logContent, $this->config->repo->repoSyncLog->finishCompress) !== false)
-                        {
-                            @unlink($logFile);
-                        }
-                        else
-                        {
-                            return print($this->config->repo->repoSyncLog->one);
-                        }
-                    }
-                    else
-                    {
-                        return print($this->config->repo->repoSyncLog->one);
-                    }
-                }
-                else
-                {
-                    @unlink($logFile);
-                }
-            }
+            $syncLog = $this->repoZen->syncLocalCommit($repo);
+            if($syncLog) return print($syncLog);
         }
 
         $this->commonAction($repoID);
         $this->scm->setEngine($repo);
 
         $branchID = '';
-        if(in_array($repo->SCM, $this->config->repo->gitTypeList) and empty($branchID))
-        {
-            $branches = $this->scm->branch();
-            if($branches)
-            {
-                /* Init branchID. */
-                if($this->cookie->syncBranch) $branchID = $this->cookie->syncBranch;
-                if(!isset($branches[$branchID])) $branchID = '';
-                if(empty($branchID)) $branchID = 'master';
-
-                /* Get unsynced branches. */
-                unset($branches['master']);
-                if($branchID != 'master')
-                {
-                    foreach($branches as $branch)
-                    {
-                        unset($branches[$branch]);
-                        if($branch == $branchID) break;
-                    }
-                }
-
-                $this->repo->setRepoBranch($branchID);
-                helper::setcookie("syncBranch", $branchID, 0, $this->config->webRoot, '', $this->config->cookieSecure, true);
-            }
-        }
+        $branches = $this->repoZen->getSyncBranches($repo, $branchID);
 
         $logs    = array();
         $version = 1;
         if($repo->SCM != 'Gitlab')
         {
-            $latestInDB = $this->dao->select('t1.*')->from(TABLE_REPOHISTORY)->alias('t1')
-                ->leftJoin(TABLE_REPOBRANCH)->alias('t2')->on('t1.id=t2.revision')
-                ->where('t1.repo')->eq($repoID)
-                ->beginIF($repo->SCM == 'Git' and $this->cookie->repoBranch)->andWhere('t2.branch')->eq($this->cookie->repoBranch)->fi()
-                ->beginIF($repo->SCM == 'Gitlab' and $this->cookie->repoBranch)->andWhere('t2.branch')->eq($this->cookie->repoBranch)->fi()
-                ->orderBy('t1.time')
-                ->limit(1)
-                ->fetch();
+            $latestInDB = $this->repo->getLatestCommit($repoID, false);
 
             $version  = empty($latestInDB) ? 1 : $latestInDB->commit + 1;
             $revision = $version == 1 ? 'HEAD' : (in_array($repo->SCM, array('Git', 'Gitea', 'Gogs')) ? $latestInDB->commit : $latestInDB->revision);
-            if($type == 'batch')
-            {
-                $logs = $this->scm->getCommits($revision, $this->config->repo->batchNum, $branchID);
-            }
-            else
-            {
-                $logs = $this->scm->getCommits($revision, 0, $branchID);
-            }
+            $logs     = $this->scm->getCommits($revision, $type == 'batch' ? $this->config->repo->batchNum : 0, $branchID);
         }
 
         $commitCount = $this->repo->saveCommit($repoID, $logs, $version, $branchID);
-        if(empty($commitCount))
-        {
-            if(!$repo->synced)
-            {
-                if(in_array($repo->SCM, $this->config->repo->gitTypeList))
-                {
-                    if($branchID) $this->repo->saveExistCommits4Branch($repo->id, $branchID);
-
-                    $branchID = reset($branches);
-                    setcookie("syncBranch", $branchID, 0, $this->config->webRoot, '', $this->config->cookieSecure, true);
-
-                    if($branchID) $this->repo->fixCommit($repoID);
-                }
-
-                if(empty($branchID))
-                {
-                    $this->repo->markSynced($repoID);
-                    return print($this->config->repo->repoSyncLog->finish);
-                }
-            }
-        }
-
-        $this->dao->update(TABLE_REPO)->set('commits=commits + ' . $commitCount)->where('id')->eq($repoID)->exec();
-        echo $type == 'batch' ?  $commitCount : $this->config->repo->repoSyncLog->finish;
+        echo $this->repoZen->checkSyncResult($repo, $branches, $branchID, $commitCount);
     }
 
     /**
diff --git a/module/repo/model.php b/module/repo/model.php
index 6c4442acca..4683d50332 100644
--- a/module/repo/model.php
+++ b/module/repo/model.php
@@ -983,28 +983,30 @@ class repoModel extends model
      * Get latest commit.
      *
      * @param  int    $repoID
+     * @param  bool   $checkCount
      * @access public
      * @return object
      */
-    public function getLatestCommit($repoID)
+    public function getLatestCommit($repoID, $checkCount = true)
     {
-        $count = $this->dao->select('count(DISTINCT t1.id) as count')->from(TABLE_REPOHISTORY)->alias('t1')
-            ->leftJoin(TABLE_REPOBRANCH)->alias('t2')->on('t1.id=t2.revision')
-            ->where('t1.repo')->eq($repoID)
-            ->beginIF($this->cookie->repoBranch)->andWhere('t2.branch')->eq($this->cookie->repoBranch)->fi()
-            ->fetch('count');
-
+        $repo        = $this->fetchByID($repoID);
+        $branchID    = (string)$this->cookie->repoBranch;
         $lastComment = $this->dao->select('t1.*')->from(TABLE_REPOHISTORY)->alias('t1')
             ->leftJoin(TABLE_REPOBRANCH)->alias('t2')->on('t1.id=t2.revision')
             ->where('t1.repo')->eq($repoID)
-            ->beginIF($this->cookie->repoBranch)->andWhere('t2.branch')->eq($this->cookie->repoBranch)->fi()
+            ->beginIF($repo->SCM != 'Subversion' && $branchID)->andWhere('t2.branch')->eq($branchID)->fi()
             ->orderBy('t1.time desc')
-            ->limit(1)
             ->fetch();
-        if(empty($lastComment)) return null;
+        if(empty($lastComment)) return new stdclass();
+        if(!$checkCount) return $lastComment;
 
-        $repo = $this->getByID($repoID);
-        if($repo->SCM == 'Git' and $lastComment->commit != $count)
+        $count = $this->dao->select('count(DISTINCT t1.id) as count')->from(TABLE_REPOHISTORY)->alias('t1')
+            ->leftJoin(TABLE_REPOBRANCH)->alias('t2')->on('t1.id=t2.revision')
+            ->where('t1.repo')->eq($repoID)
+            ->beginIF($repo->SCM != 'Subversion' && $branchID)->andWhere('t2.branch')->eq($branchID)->fi()
+            ->fetch('count');
+
+        if($repo->SCM == 'Git' && $lastComment->commit != $count)
         {
             $this->fixCommit($repo->id);
             $lastComment->commit = $count;
diff --git a/module/repo/zen.php b/module/repo/zen.php
index 1fb801148e..a3f0b511b1 100644
--- a/module/repo/zen.php
+++ b/module/repo/zen.php
@@ -1298,4 +1298,132 @@ class repoZen extends repo
         }
         return $diffs;
     }
+
+    /**
+     * 获取代码同步本地的日志。
+     * Get sync log.
+     *
+     * @param  object    $repo
+     * @access protected
+     * @return string
+     */
+    protected function syncLocalCommit(object $repo): string
+    {
+        $logFile = realPath($this->app->getTmpRoot() . $this->config->repo->repoSyncLog->logFilePrefix . strtolower($repo->SCM) . ".{$repo->name}.log");
+        if($logFile)
+        {
+            $content  = file($logFile);
+            foreach($content as $line)
+            {
+                if($this->repo->strposAry($line, $this->config->repo->repoSyncLog->fatal) !== false) return $line;
+                if($this->repo->strposAry($line, $this->config->repo->repoSyncLog->failed) !== false) return $line;
+            }
+
+            $lastLine = $content[count($content) - 1];
+            if($this->repo->strposAry($lastLine, $this->config->repo->repoSyncLog->done) === false)
+            {
+                if($this->repo->strposAry($lastLine, $this->config->repo->repoSyncLog->emptyRepo) !== false)
+                {
+                    @unlink($logFile);
+                }
+                elseif($this->repo->strposAry($lastLine, $this->config->repo->repoSyncLog->total) !== false)
+                {
+                    $logContent = file_get_contents($logFile);
+                    if($this->repo->strposAry($logContent, $this->config->repo->repoSyncLog->finishCount) !== false and $this->repo->strposAry($logContent, $this->config->repo->repoSyncLog->finishCompress) !== false)
+                    {
+                        @unlink($logFile);
+                    }
+                    else
+                    {
+                        return $this->config->repo->repoSyncLog->one;
+                    }
+                }
+                else
+                {
+                    return $this->config->repo->repoSyncLog->one;
+                }
+            }
+            else
+            {
+                @unlink($logFile);
+            }
+        }
+
+        return '';
+    }
+
+    /**
+     * 获取需要同步的分支。
+     * Get sync branches.
+     *
+     * @param  object    $repo
+     * @param  string    $branchID
+     * @access protected
+     * @return array
+     */
+    protected function getSyncBranches(object $repo, string &$branchID = ''): array
+    {
+        $branches = array();
+        if(in_array($repo->SCM, $this->config->repo->gitTypeList))
+        {
+            $branches = $this->scm->branch();
+            if($branches)
+            {
+                /* Init branchID. */
+                if($this->cookie->syncBranch) $branchID = $this->cookie->syncBranch;
+                if(!isset($branches[$branchID])) $branchID = '';
+                if(empty($branchID)) $branchID = 'master';
+
+                /* Get unsynced branches. */
+                unset($branches['master']);
+                if($branchID != 'master')
+                {
+                    foreach($branches as $branch)
+                    {
+                        unset($branches[$branch]);
+                        if($branch == $branchID) break;
+                    }
+                }
+
+                $this->repo->setRepoBranch($branchID);
+                helper::setcookie("syncBranch", $branchID, 0, $this->config->webRoot, '', $this->config->cookieSecure, true);
+            }
+        }
+
+        return $branches;
+    }
+
+    /**
+     * 获取同步结果。
+     * Get sync result.
+     *
+     * @param  object    $repo
+     * @param  array     $branches
+     * @param  string    $branchID
+     * @param  int       $commitCount
+     * @access protected
+     * @return string
+     */
+    protected function checkSyncResult(object $repo, array $branches, string $branchID, int $commitCount): string
+    {
+        if(empty($commitCount) && !$repo->synced)
+        {
+            if(in_array($repo->SCM, $this->config->repo->gitTypeList))
+            {
+                if($branchID) $this->repo->saveExistCommits4Branch($repo->id, $branchID);
+                helper::setcookie("syncBranch", reset($branches));
+
+                if($branchID) $this->repo->fixCommit($repo->id);
+            }
+
+            if(empty($branchID))
+            {
+                $this->repo->markSynced($repo->id);
+                return $this->config->repo->repoSyncLog->finish;
+            }
+        }
+
+        $this->dao->update(TABLE_REPO)->set('commits=commits + ' . $commitCount)->where('id')->eq($repo->id)->exec();
+        return $type == 'batch' ?  $commitCount : $this->config->repo->repoSyncLog->finish;
+    }
 }

From 51daf74a4a6e6f2aec42f143a7f91aebe490eca1 Mon Sep 17 00:00:00 2001
From: caoyanyi 
Date: Tue, 21 Nov 2023 15:21:24 +0800
Subject: [PATCH 48/92] * Replace gitlab repo attribute.

---
 module/repo/control.php | 1 +
 1 file changed, 1 insertion(+)

diff --git a/module/repo/control.php b/module/repo/control.php
index 5cc7971fcf..f4ec90f44e 100644
--- a/module/repo/control.php
+++ b/module/repo/control.php
@@ -322,6 +322,7 @@ class repo extends control
         $entry    = urldecode($entry);
         $pathInfo = helper::mbPathinfo($entry);
 
+        if($repo->SCM == 'Gitlab') $repo = $this->repo->processGitService($repo, true);
         if($this->app->tab == 'execution') $this->view->executionID = $objectID;
         $this->view->title       = $this->lang->repo->common . $this->lang->colon . $this->lang->repo->view;
         $this->view->dropMenus   = $this->repoZen->getBranchAndTagItems($repo, $this->cookie->repoBranch);

From ada11298831e991032e43bade716afa42b386378 Mon Sep 17 00:00:00 2001
From: xieqiyu 
Date: Tue, 21 Nov 2023 15:26:20 +0800
Subject: [PATCH 49/92] * Modify story required items.

---
 module/story/tao.php            |  1 +
 module/story/ui/change.html.php |  2 +-
 module/story/ui/close.html.php  |  6 ++----
 module/story/ui/create.html.php | 10 ++++++++--
 4 files changed, 12 insertions(+), 7 deletions(-)

diff --git a/module/story/tao.php b/module/story/tao.php
index af7d5a8bda..8cfd361361 100644
--- a/module/story/tao.php
+++ b/module/story/tao.php
@@ -674,6 +674,7 @@ class storyTao extends storyModel
         $this->dao->insert(TABLE_STORY)->data($story, 'spec,verify,reviewer,URS,region,lane,branches,plans,modules,uploadImage')
             ->autoCheck()
             ->checkIF(!empty($story->notifyEmail), 'notifyEmail', 'email')
+            ->batchCheck($this->config->story->create->requiredFields, 'notempty')
             ->checkFlow()
             ->exec();
         if(dao::isError()) return false;
diff --git a/module/story/ui/change.html.php b/module/story/ui/change.html.php
index 1f99385162..8b09848e67 100644
--- a/module/story/ui/change.html.php
+++ b/module/story/ui/change.html.php
@@ -118,7 +118,7 @@ $formItems['spec'] = formGroup
 (
     set::width('full'),
     set::label($fields['spec']['title']),
-    set::required($fields['spec']['required']),
+    set::required(strpos(",{$this->config->story->change->requiredFields},", ",spec,") !== false),
     set::tip($lang->story->specTemplate),
     editor
     (
diff --git a/module/story/ui/close.html.php b/module/story/ui/close.html.php
index e68db95ffa..502f661b97 100644
--- a/module/story/ui/close.html.php
+++ b/module/story/ui/close.html.php
@@ -45,10 +45,8 @@ formPanel
     formGroup
     (
         set::label($lang->comment),
-        editor
-        (
-            set::name('comment')
-        )
+        set::control('editor'),
+        set::name('comment')
     )
 );
 hr();
diff --git a/module/story/ui/create.html.php b/module/story/ui/create.html.php
index ea3fb99c69..c371a33710 100644
--- a/module/story/ui/create.html.php
+++ b/module/story/ui/create.html.php
@@ -64,6 +64,7 @@ formPanel
         isset($fields['branch']) && $type == 'story' ? null : formGroup
         (
             set::label($lang->story->module),
+            set::required(strpos(",{$this->config->story->create->requiredFields},", ",module,") !== false),
             inputGroup
             (
                 setID('moduleIdBox'),
@@ -118,6 +119,7 @@ formPanel
         (
             set::width('1/4'),
             set::label($lang->story->module),
+            set::required(strpos(",{$this->config->story->create->requiredFields},", ",module,") !== false),
             inputGroup
             (
                 setID('moduleIdBox'),
@@ -127,6 +129,7 @@ formPanel
         formGroup
         (
             set::label($lang->story->plan),
+            set::required(strpos(",{$this->config->story->create->requiredFields},", ",plan,") !== false),
             inputGroup
             (
                 setID('planIdBox'),
@@ -156,6 +159,7 @@ formPanel
         (
             set::width('1/2'),
             set::label($lang->story->planAB),
+            set::required(strpos(",{$this->config->story->create->requiredFields},", ",plan,") !== false),
             inputGroup
             (
                 span
@@ -362,21 +366,21 @@ formPanel
     formGroup
     (
         set::label($lang->story->spec),
+        set::required(strpos(",{$this->config->story->create->requiredFields},", ",spec,") !== false),
         editor
         (
             set::name('spec'),
             set::placeholder($lang->story->specTemplate . "\n" . $lang->noticePasteImg),
-            set::required($fields['spec']['required']),
             html($fields['spec']['default'])
         )
     ),
     formGroup
     (
         set::label($lang->story->verify),
+        set::required(strpos(",{$this->config->story->create->requiredFields},", ",verify,") !== false),
         editor
         (
             set::name('verify'),
-            set::required($fields['verify']['required']),
             html($fields['verify']['default'])
         )
     ),
@@ -425,6 +429,7 @@ isset($fields['branches']) && $type == 'story' ? formRow
     (
         set::width('1/4'),
         set::label($lang->story->module),
+        set::required(strpos(",{$this->config->story->create->requiredFields},", ",module,") !== false),
         inputGroup
         (
             setID('moduleIdBox'),
@@ -434,6 +439,7 @@ isset($fields['branches']) && $type == 'story' ? formRow
     formGroup
     (
         set::label($lang->story->plan),
+        set::required(strpos(",{$this->config->story->create->requiredFields},", ",plan,") !== false),
         inputGroup
         (
             setID('planIdBox'),

From a144c88700ab9f65ff98088ef26a685c11b4004a Mon Sep 17 00:00:00 2001
From: liugang 
Date: Tue, 21 Nov 2023 15:29:01 +0800
Subject: [PATCH 50/92] + user: declare strict type.

---
 module/user/control.php | 1 +
 1 file changed, 1 insertion(+)

diff --git a/module/user/control.php b/module/user/control.php
index 32a002a282..5d81dab9d9 100755
--- a/module/user/control.php
+++ b/module/user/control.php
@@ -1,4 +1,5 @@
 
Date: Tue, 21 Nov 2023 15:31:31 +0800
Subject: [PATCH 51/92] * Refactor testreport::__construct.

---
 module/testreport/control.php | 17 ++++++-----------
 1 file changed, 6 insertions(+), 11 deletions(-)

diff --git a/module/testreport/control.php b/module/testreport/control.php
index 5bde9e22fd..979afa1db4 100644
--- a/module/testreport/control.php
+++ b/module/testreport/control.php
@@ -29,7 +29,7 @@ class testreport extends control
      * @access public
      * @return void
      */
-    public function __construct($moduleName = '', $methodName = '')
+    public function __construct(string $moduleName = '', string $methodName = '')
     {
         parent::__construct($moduleName, $methodName);
         $this->loadModel('project');
@@ -47,22 +47,17 @@ class testreport extends control
         /* Get product data. */
         $products = array();
         $objectID = 0;
-        $tab      = ($this->app->tab == 'project' or $this->app->tab == 'execution') ? $this->app->tab : 'qa';
+        $tab      = $this->app->tab == 'project' || $this->app->tab == 'execution' ? $this->app->tab : 'qa';
         if(!isInModal())
         {
-            if($this->app->tab == 'project')
+            if($tab == 'qa')
             {
-                $objectID = $this->session->project;
-                $products  = $this->product->getProducts($objectID, 'all', '', false);
-            }
-            elseif($this->app->tab == 'execution')
-            {
-                $objectID = $this->session->execution;
-                $products = $this->product->getProducts($objectID, 'all', '', false);
+                $products = $this->product->getPairs();
             }
             else
             {
-                $products = $this->product->getPairs();
+                $objectID = $this->session->{$tab};
+                $products = $this->product->getProducts($objectID, 'all', '', false);
             }
             if(empty($products) && !isInModal() && (helper::isAjaxRequest('zin') || helper::isAjaxRequest('fetch'))) $this->locate($this->createLink('product', 'showErrorNone', "moduleName={$tab}&activeMenu=testreport&objectID=$objectID"));
         }

From 4b940440fd069f0911e30e0a4cff29963d390819 Mon Sep 17 00:00:00 2001
From: liumengyi 
Date: Tue, 21 Nov 2023 15:35:38 +0800
Subject: [PATCH 52/92] * Optimize code.

---
 module/testreport/control.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/module/testreport/control.php b/module/testreport/control.php
index 979afa1db4..7c76f3a763 100644
--- a/module/testreport/control.php
+++ b/module/testreport/control.php
@@ -87,7 +87,7 @@ class testreport extends control
         if(strpos('product|execution|project', $objectType) === false) return $this->send(array('result' => 'fail', 'message' => 'Type Error!'));
 
         $objectID = $this->testreportZen->commonAction($objectID, $objectType);
-        $object   = $this->$objectType->getById($objectID);
+        $object   = $this->{$objectType}->getById($objectID);
         if($extra) $task = $this->testtask->getByID($extra);
 
         $reports = $this->testreportZen->getReportsForBrowse($objectID, $objectType, $extra, $orderBy, $recTotal, $recPerPage, $pageID);

From a9ebcd4ffb9e997f39d3314d5c56845008805959 Mon Sep 17 00:00:00 2001
From: liugang 
Date: Tue, 21 Nov 2023 15:42:03 +0800
Subject: [PATCH 53/92] * user: set default value and add comment of the
 referer property.

---
 module/user/control.php | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/module/user/control.php b/module/user/control.php
index 5d81dab9d9..4d426bc24e 100755
--- a/module/user/control.php
+++ b/module/user/control.php
@@ -12,7 +12,14 @@ declare(strict_types=1);
  */
 class user extends control
 {
-    public $referer;
+    /**
+     * 来源地址。
+     * Origin url.
+     *
+     * @var    array
+     * @access public
+     */
+    public $referer = '';
 
     /**
      * Construct

From f0bf35b690685fa6508f060103e3ce90bb17314b Mon Sep 17 00:00:00 2001
From: tianshujie 
Date: Tue, 21 Nov 2023 15:47:20 +0800
Subject: [PATCH 54/92] * Refactor custom::parseItemParam method.

---
 module/custom/model.php | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/module/custom/model.php b/module/custom/model.php
index b74ca46519..ae96db182f 100644
--- a/module/custom/model.php
+++ b/module/custom/model.php
@@ -138,13 +138,14 @@ class customModel extends model
     }
 
     /**
+     * 解析选择或删除项的参数字符串。
      * Parse the param string for select or delete items.
      *
-     * @param  string    $paramString     lang=xxx&module=story§ion=sourceList&key=customer and so on.
+     * @param  string $paramString lang=xxx&module=story§ion=sourceList&key=customer and so on.
      * @access public
      * @return array
      */
-    public function parseItemParam($paramString)
+    public function parseItemParam(string $paramString): array
     {
         /* Parse the param string into array. */
         parse_str($paramString, $params);
@@ -152,7 +153,11 @@ class customModel extends model
         /* Init fields not set in the param string. */
         $fields = 'lang,module,section,key,vision';
         $fields = explode(',', $fields);
-        foreach($fields as $field) if(!isset($params[$field])) $params[$field] = '';
+        foreach($fields as $field)
+        {
+            if(isset($params[$field])) continue;
+            $params[$field] = '';
+        }
 
         return $params;
     }

From 6c51de826621d2f70d7a33df1482d0c8be32f085 Mon Sep 17 00:00:00 2001
From: tianshujie 
Date: Tue, 21 Nov 2023 15:47:38 +0800
Subject: [PATCH 55/92] * Refactor custom::parseItemParam case.

---
 module/custom/test/custom.class.php         | 10 ++++-----
 module/custom/test/model/parseitemparam.php | 24 +++++++--------------
 2 files changed, 13 insertions(+), 21 deletions(-)

diff --git a/module/custom/test/custom.class.php b/module/custom/test/custom.class.php
index d6dba39bc1..93cce391a6 100644
--- a/module/custom/test/custom.class.php
+++ b/module/custom/test/custom.class.php
@@ -84,19 +84,19 @@ class customTest
     }
 
     /**
+     * 解析选择或删除项的参数字符串。
      * Test parse the param string for select or delete items.
      *
-     * @param  string    $paramString
+     * @param  string $paramString lang=xxx&module=story§ion=sourceList&key=customer and so on.
      * @access public
      * @return array
      */
-    public function parseItemParamTest($paramString)
+    public function parseItemParamTest(string $paramString): array
     {
-        $objects = $this->objectModel->parseItemParam($paramString);
+        $params = $this->objectModel->parseItemParam($paramString);
 
         if(dao::isError()) return dao::getError();
-
-        return $objects;
+        return $params;
     }
 
     /**
diff --git a/module/custom/test/model/parseitemparam.php b/module/custom/test/model/parseitemparam.php
index 6ddacb3e31..f95733906e 100755
--- a/module/custom/test/model/parseitemparam.php
+++ b/module/custom/test/model/parseitemparam.php
@@ -7,15 +7,8 @@ su('admin');
 /**
 
 title=测试 customModel->parseItemParam();
+timeout=0
 cid=1
-pid=1
-
-测试参数为空 >> ,,,,
-测试参数为lang >> zh-cn,,,,
-测试参数为lang,module >> zh-cn,custom,,,
-测试参数为lang,key,section >> zh-cn,,URSRList,1,
-测试参数为lang,key,section,module >> zh-cn,custom,URSRList,1,
-测试参数为lang,key,section,module,vision >> zh-cn,custom,URSRList,1,rnd
 
 */
 
@@ -26,11 +19,10 @@ $key         = 'key=1';
 $vision      = 'vision=rnd';
 $paramString = array('', $lang, $lang.'&'.$module, $lang.'&'.$key.'&'.$section, $lang.'&'.$key.'&'.$section.'&'.$module, $lang.'&'.$key.'&'.$section.'&'.$module.'&'.$vision);
 
-$custom = new customTest();
-
-r($custom->parseItemParamTest($paramString[0])) && p('lang,module,section,key,vision') && e(',,,,');                        //测试参数为空
-r($custom->parseItemParamTest($paramString[1])) && p('lang,module,section,key,vision') && e('zh-cn,,,,');                   //测试参数为lang
-r($custom->parseItemParamTest($paramString[2])) && p('lang,module,section,key,vision') && e('zh-cn,custom,,,');             //测试参数为lang,module
-r($custom->parseItemParamTest($paramString[3])) && p('lang,module,section,key,vision') && e('zh-cn,,URSRList,1,');          //测试参数为lang,key,section
-r($custom->parseItemParamTest($paramString[4])) && p('lang,module,section,key,vision') && e('zh-cn,custom,URSRList,1,');    //测试参数为lang,key,section,module
-r($custom->parseItemParamTest($paramString[5])) && p('lang,module,section,key,vision') && e('zh-cn,custom,URSRList,1,rnd'); //测试参数为lang,key,section,module,vision
\ No newline at end of file
+$customTester = new customTest();
+r($customTester->parseItemParamTest($paramString[5])) && p('lang,module,section,key,vision') && e('zh-cn,custom,URSRList,1,rnd'); // 测试参数为lang,key,section,module,vision
+r($customTester->parseItemParamTest($paramString[4])) && p('lang,module,section,key,vision') && e('zh-cn,custom,URSRList,1,~~');  // 测试参数为lang,key,section,module
+r($customTester->parseItemParamTest($paramString[3])) && p('lang,module,section,key,vision') && e('zh-cn,~~,URSRList,1,~~');      // 测试参数为lang,key,section
+r($customTester->parseItemParamTest($paramString[2])) && p('lang,module,section,key,vision') && e('zh-cn,custom,~~,~~,~~');       // 测试参数为lang,module
+r($customTester->parseItemParamTest($paramString[1])) && p('lang,module,section,key,vision') && e('zh-cn,N/A,N/A,N/A,N/A');       // 测试参数为lang
+r($customTester->parseItemParamTest($paramString[0])) && p('lang,module,section,key,vision') && e('N/A,N/A,N/A,N/A,N/A');         // 测试参数为空

From 46ee558a59bc5728f7478bf700684857ee9b9ada Mon Sep 17 00:00:00 2001
From: sunguangming 
Date: Tue, 21 Nov 2023 15:58:14 +0800
Subject: [PATCH 56/92] * Fix code error, remove useless code.

---
 db/update18.8.sql                      |  1 -
 module/product/config.php              |  9 ++++-----
 module/product/model.php               |  2 ++
 module/program/ui/productview.html.php | 18 ++++++++----------
 4 files changed, 14 insertions(+), 16 deletions(-)

diff --git a/db/update18.8.sql b/db/update18.8.sql
index dceb5ab615..c095016d75 100644
--- a/db/update18.8.sql
+++ b/db/update18.8.sql
@@ -90,4 +90,3 @@ INSERT INTO `zt_config` ( `vision`, `owner`, `module`, `section`, `key`, `value`
 
 REPLACE INTO `zt_chart`(`id`, `name`, `dimension`, `type`, `group`, `dataset`, `desc`, `settings`, `filters`, `step`, `fields`, `langs`, `sql`, `stage`, `builtin`, `objects`, `createdBy`, `createdDate`, `editedBy`, `editedDate`, `deleted`) VALUES (1030, '宏观数据-禅道使用时长', 1, 'card', '58', '', '', '{\"value\": {\"type\": \"value\", \"field\": \"period\", \"agg\": \"value\"}, \"title\": {\"type\": \"text\", \"name\": \"\"},\n\"type\": \"value\"\n}', '[]', 0, '', NULL, '	SELECT if(t2.`year` > 0, concat(t2.`year`, \'年\', t2.`day`, \'天\'), concat(t2.`day`, \'天\')) as period from (\r\nSELECT TIMESTAMPDIFF(YEAR,t1.firstDay,t1.today) AS `year`,DATEDIFF(DATE_SUB(t1.today,INTERVAL TIMESTAMPDIFF(YEAR,t1.firstDay,t1.today) YEAR), t1.firstDay) AS `day`  \r\nFROM (SELECT `value` AS firstDay, now() AS today FROM zt_config WHERE `owner` = \'system\' AND `key` = \'installedDate\') AS t1\r\n) t2', 'published', 1, '', 'system', '2022-12-07 14:59:41', '', '2022-12-07 14:59:41', 0);
 REPLACE INTO `zt_chart`(`id`, `name`, `dimension`, `type`, `group`, `dataset`, `desc`, `settings`, `filters`, `step`, `fields`, `langs`, `sql`, `stage`, `builtin`, `objects`, `createdBy`, `createdDate`, `editedBy`, `editedDate`, `deleted`) VALUES (20015, '使用数据分析-上线时间', 1, 'card', '58', '0', ' ', '{\"value\": {\"type\": \"value\", \"field\": \"date\", \"agg\": \"value\"}, \"title\": {\"type\": \"text\", \"name\": \"\"}, \"type\": \"value\"}', '[]', 0, ' ', NULL, 'select `value` as date from zt_config where `owner` = \'system\' and `key` = \'installedDate\'', 'published', 1, ' ', 'system', '2023-08-16 15:32:10', 'admin', '2023-08-16 15:32:17', 0);
-
diff --git a/module/product/config.php b/module/product/config.php
index 546b8bf50c..4562fefac1 100644
--- a/module/product/config.php
+++ b/module/product/config.php
@@ -46,11 +46,10 @@ $config->product->report->planLabels = array();
 $config->product->report->planLabels[] = '';
 
 $config->product->statisticFields = array();
-$config->product->statisticFields['requirements'] = array('draftRequirements', 'activeRequirements', 'changingRequirements', 'reviewingRequirements', 'closedRequirements');
-$config->product->statisticFields['stories']      = array('draftStories', 'activeStories', 'changingStories', 'reviewingStories', 'closedStories', 'finishedStories', 'totalStories');
-$config->product->statisticFields['bugs']         = array('unresolvedBugs', 'closedBugs', 'fixedBugs');
-$config->product->statisticFields['plans']        = array('plans');
-$config->product->statisticFields['releases']     = array('releases');
+$config->product->statisticFields['stories']  = array('draftStories', 'activeStories', 'changingStories', 'reviewingStories', 'closedStories', 'finishedStories', 'totalStories');
+$config->product->statisticFields['bugs']     = array('unresolvedBugs', 'closedBugs', 'fixedBugs');
+$config->product->statisticFields['plans']    = array('plans');
+$config->product->statisticFields['releases'] = array('releases');
 
 $config->product->skipRedirectMethod = ',create,index,showerrornone,ajaxgetdropmenu,kanban,all,manageline,export,ajaxgetplans,';
 $config->product->memberFields       = array('PO', 'QD', 'RD', 'feedback', 'ticket', 'createdBy;');
diff --git a/module/product/model.php b/module/product/model.php
index a4ba91bd2c..4e4390b101 100755
--- a/module/product/model.php
+++ b/module/product/model.php
@@ -1050,11 +1050,13 @@ class productModel extends model
         $stats = $this->getStatsProducts($productIdList, $programID == 0, $orderBy, $pager);
 
         $latestReleases       = $this->productTao->getStatisticByType($productIdList, 'latestReleases');
+        $projectCountPairs    = $this->productTao->getProjectCountPairs($productIdList);
         $executionCountPairs  = $this->productTao->getExecutionCountPairs($productIdList);
         $coveragePairs        = $this->getCaseCoveragePairs($productIdList);
 
         foreach($stats as $product)
         {
+            $product->projects   = zget($projectCountPairs, $product->id, 0);
             $product->executions = zget($executionCountPairs, $product->id, 0);
             $product->coverage   = zget($coveragePairs, $product->id, 0);
 
diff --git a/module/program/ui/productview.html.php b/module/program/ui/productview.html.php
index 1b8b068fd6..72c2bf22d2 100644
--- a/module/program/ui/productview.html.php
+++ b/module/program/ui/productview.html.php
@@ -52,7 +52,7 @@ $fnGenerateProgramRowData = function($programID, $program) use ($config, $users)
     if(!isset($program['programName']) || $config->systemMode != 'ALM') return null;
 
     /* ALM mode with more data. */
-    $totalStories = $program['finishClosedStories'] + $program['unclosedStories'];
+    $totalStories = $program['totalStories'];
     $pmName       = '';
     if(!empty($program['programPM']))
     {
@@ -70,9 +70,9 @@ $fnGenerateProgramRowData = function($programID, $program) use ($config, $users)
     $item->PM                   = $pmName;
     $item->createdDate          = '';
     $item->createdBy            = '';
-    $item->totalUnclosedStories = $program['unclosedStories'];
+    $item->totalUnclosedStories = $totalStories - $program['closedStories'];
     $item->totalStories         = $totalStories;
-    $item->closedStoryRate      = ($totalStories == 0 ? 0 : round($program['finishClosedStories'] / $totalStories, 3) * 100);
+    $item->closedStoryRate      = ($totalStories == 0 ? 0 : round($program['closedStories'] / $totalStories, 3) * 100);
     $item->totalPlans           = $program['plans'];
     $item->totalProjects        = 0;
     $item->totalExecutions      = 0;
@@ -125,8 +125,6 @@ $fnGenerateLineRowData = function($programID, $lineID, $line) use ($config, &$li
 /* Closure for generating product row data. */
 $fnGenerateProductRowData = function($lineID, $product) use ($users)
 {
-    $totalStories = $product->stories['finishClosed'] + $product->stories['unclosed'];
-
     $item = new stdClass();
     $item->type                 = 'product';
     $item->id                   = $product->id;
@@ -135,15 +133,15 @@ $fnGenerateProductRowData = function($lineID, $product) use ($users)
     $item->PM                   = !empty($product->PO) ? zget($users, $product->PO) : '';
     $item->createdDate          = $product->createdDate;
     $item->createdBy            = $product->createdBy;
-    $item->totalUnclosedStories = $product->stories['unclosed'];
-    $item->totalStories         = $totalStories;
-    $item->closedStoryRate      = empty($totalStories) ? 0 : round($product->stories['finishClosed'] / $totalStories, 3) * 100;
+    $item->totalUnclosedStories = $product->totalStories - $product->closedStories;
+    $item->totalStories         = $product->totalStories;
+    $item->closedStoryRate      = empty($product->totalStories) ? 0 : round($product->closedStories / $product->totalStories, 3) * 100;
     $item->totalPlans           = $product->plans;
     $item->totalProjects        = $product->projects;
     $item->totalExecutions      = $product->executions;
     $item->testCaseCoverage     = $product->coverage;
-    $item->totalActivatedBugs   = $product->activeBugs;
-    $item->totalBugs            = $product->bugs;
+    $item->totalActivatedBugs   = $product->unresolvedBugs;
+    $item->totalBugs            = $product->totalBugs;
     $item->fixedRate            = empty($item->totalBugs) ? 0 : round($product->fixedBugs / $item->totalBugs, 3) * 100;
     $item->totalReleases        = $product->releases;
     $item->latestReleaseDate    = $product->latestReleaseDate;

From 75e3f8f54b21b2d026643151c367abed362fd7b6 Mon Sep 17 00:00:00 2001
From: liumengyi 
Date: Tue, 21 Nov 2023 16:03:07 +0800
Subject: [PATCH 57/92] * Refactor testreport::view.

---
 module/testreport/control.php | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/module/testreport/control.php b/module/testreport/control.php
index 7c76f3a763..d5bec56fbf 100644
--- a/module/testreport/control.php
+++ b/module/testreport/control.php
@@ -22,6 +22,7 @@ class testreport extends control
     public $products = array();
 
     /**
+     * 构造函数。
      * Construct
      *
      * @param  string $moduleName
@@ -249,7 +250,7 @@ class testreport extends control
     public function view(int $reportID, string $tab = 'basic', int $recTotal = 0, int $recPerPage = 100, int $pageID = 1)
     {
         $report = $this->testreport->getById($reportID);
-        if(!$report) return $this->send(array('result' => 'fail', 'message' => $this->lang->error->notFount, 'load' => array('alert' => $this->lang->error->notFount, 'locate' => $this->createLink('qa', 'index'))));
+        if(!$report) return $this->send(array('result' => 'fail', 'message' => $this->lang->notFound, 'load' => array('alert' => $this->lang->notFound, 'locate' => array('load' => true))));
 
         /* Set session. */
         $this->session->project = $report->project;
@@ -277,7 +278,7 @@ class testreport extends control
         }
 
         /* Load pager. */
-        $this->app->loadClass('pager', $static = true);
+        $this->app->loadClass('pager', true);
         if($this->app->getViewType() == 'mhtml') $recPerPage = 10;
         $pager = pager::init($recTotal, $recPerPage, $pageID);
 

From 8ec4edae48f3bcfe1c5f963ed5d45e957ac99e26 Mon Sep 17 00:00:00 2001
From: liumengyi 
Date: Tue, 21 Nov 2023 16:04:04 +0800
Subject: [PATCH 58/92] * Optimize code.

---
 module/testreport/control.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/module/testreport/control.php b/module/testreport/control.php
index d5bec56fbf..1971dd3237 100644
--- a/module/testreport/control.php
+++ b/module/testreport/control.php
@@ -300,7 +300,7 @@ class testreport extends control
      * @access public
      * @return void
      */
-    public function delete($reportID)
+    public function delete(int $reportID)
     {
         $testreport = $this->testreport->getByID($reportID);
         $locateLink = $this->session->reportList ? $this->session->reportList : inlink('browse', "productID={$testreport->product}");

From f5da86968146e812b2346e3cef6ce88c9f710a6f Mon Sep 17 00:00:00 2001
From: qixinzhi 
Date: Tue, 21 Nov 2023 13:37:41 +0800
Subject: [PATCH 59/92] * Fix screen and chart preview not keep same,bug#40239.

---
 module/screen/model.php | 50 ++++++++++++++++++++++++++++++++---------
 1 file changed, 40 insertions(+), 10 deletions(-)

diff --git a/module/screen/model.php b/module/screen/model.php
index d55455b9eb..8f662eadd4 100644
--- a/module/screen/model.php
+++ b/module/screen/model.php
@@ -357,10 +357,12 @@ class screenModel extends model
 
             list($group, $metrics, $aggs, $xLabels, $yStats) = $this->loadModel('chart')->getMultiData($settings, $chart->sql, $filters);
 
-            $fields     = json_decode($chart->fields);
-            $dimensions = array($settings['xaxis'][0]['field']);
-            $sourceData = array();
-            $clientLang = $this->app->getClientLang();
+            $fields       = json_decode($chart->fields, true);
+            $dimensions   = array($settings['xaxis'][0]['field']);
+            $sourceData   = array();
+            $clientLang   = $this->app->getClientLang();
+            $xLabelValues = $this->processXLabel($xLabels, $fields[$group]['type'], $fields[$group]['object'], $fields[$group]['field']);
+
             foreach($yStats as $index => $dataList)
             {
                 $field     = zget($fields, $metrics[$index]);
@@ -371,6 +373,7 @@ class screenModel extends model
 
                 foreach($dataList as $valueField => $value)
                 {
+                    $valueField = $xLabelValues[$valueField];
                     if(empty($sourceData[$valueField]))
                     {
                         $sourceData[$valueField] = new stdclass();
@@ -406,10 +409,12 @@ class screenModel extends model
 
             list($group, $metrics, $aggs, $xLabels, $yStats) = $this->loadModel('chart')->getMultiData($settings, $chart->sql, $filters);
 
-            $fields     = json_decode($chart->fields);
-            $dimensions = array($settings['xaxis'][0]['field']);
-            $sourceData = array();
-            $clientLang = $this->app->getClientLang();
+            $fields       = json_decode($chart->fields, true);
+            $dimensions   = array($settings['xaxis'][0]['field']);
+            $sourceData   = array();
+            $clientLang   = $this->app->getClientLang();
+            $xLabelValues = $this->processXLabel($xLabels, $fields[$group]['type'], $fields[$group]['object'], $fields[$group]['field']);
+
             foreach($yStats as $index => $dataList)
             {
                 $field     = zget($fields, $metrics[$index]);
@@ -420,6 +425,7 @@ class screenModel extends model
 
                 foreach($dataList as $valueField => $value)
                 {
+                    $valueField = $xLabelValues[$valueField];
                     if(empty($sourceData[$valueField]))
                     {
                         $sourceData[$valueField] = new stdclass();
@@ -509,11 +515,13 @@ class screenModel extends model
 
             list($group, $metrics, $aggs, $xLabels, $yStats) = $this->loadModel('chart')->getMultiData($settings, $chart->sql, $filters);
 
-            $fields         = json_decode($chart->fields);
+            $fields         = json_decode($chart->fields, true);
             $radarIndicator = array();
             $seriesData     = array();
             $max            = 0;
             $clientLang     = $this->app->getClientLang();
+            $xLabelValues   = $this->processXLabel($xLabels, $fields[$group]['type'], $fields[$group]['object'], $fields[$group]['field']);
+
             foreach($yStats as $index => $dataList)
             {
                 $field     = zget($fields, $metrics[$index]);
@@ -539,7 +547,7 @@ class screenModel extends model
                 foreach($dataList as $valueField => $value)
                 {
                     $indicator = new stdclass();
-                    $indicator->name   = $valueField;
+                    $indicator->name   = $xLabelValues[$valueField];
                     $indicator->max    = $max;
                     $radarIndicator[]  = $indicator;;
                 }
@@ -722,6 +730,28 @@ class screenModel extends model
         return $screenFilters;
     }
 
+    /**
+     * Process xLabel with lang
+     *
+     * @param  array   $xLabel
+     * @param  string  $type
+     * @param  string  $object
+     * @param  string  $field
+     * @access public
+     * @return array
+     */
+    public function processXLabel($xLabels, $type, $object, $field)
+    {
+        $options = $this->getSysOptions($type, $object, $field);
+        $xLabelValues = array();
+        foreach($xLabels as $index => $label)
+        {
+            $xLabelValues[$label] = isset($options[$label]) ? $options[$label] : $label;
+        }
+
+        return $xLabelValues;
+    }
+
     /**
      * Get system options.
      *

From fdbb4a27b557a944ab096e8ee997d29e261da6e9 Mon Sep 17 00:00:00 2001
From: qixinzhi 
Date: Tue, 21 Nov 2023 16:00:20 +0800
Subject: [PATCH 60/92] * Fix screen display text with draft and deleted
 chart,bug#40778.

---
 module/screen/model.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/module/screen/model.php b/module/screen/model.php
index 8f662eadd4..1ea6869315 100644
--- a/module/screen/model.php
+++ b/module/screen/model.php
@@ -237,7 +237,7 @@ class screenModel extends model
 
         list($component, $typeChanged) = $this->initComponent($chart, $type, $component);
 
-        if(empty($chart) || ($chart->stage == 'draft' || $chart->deleted === '1'))
+        if(empty($chart) || ($chart->stage == 'draft' || $chart->deleted == '1'))
         {
             $component->option = new stdclass();
             if($type == 'chart')

From 4df1749ff76ac9ce9046f2913b49a5eeb1ba3c98 Mon Sep 17 00:00:00 2001
From: tianshujie 
Date: Tue, 21 Nov 2023 16:09:45 +0800
Subject: [PATCH 61/92] * Refactor custom::prepareSQL method.

---
 module/custom/model.php | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/module/custom/model.php b/module/custom/model.php
index ae96db182f..0162c16b3f 100644
--- a/module/custom/model.php
+++ b/module/custom/model.php
@@ -163,14 +163,15 @@ class customModel extends model
     }
 
     /**
+     * 创建一个DAO对象来选择或删除一条或多条记录。
      * Create a DAO object to select or delete one or more records.
      *
-     * @param  array  $params     the params parsed by parseItemParam() method.
-     * @param  string $method     select|delete.
+     * @param  array  $params the params parsed by parseItemParam() method.
+     * @param  string $method select|delete.
      * @access public
      * @return object
      */
-    public function prepareSQL($params, $method = 'select')
+    public function prepareSQL(array $params, string $method = 'select'): object
     {
         return $this->dao->$method('*')->from(TABLE_LANG)->where('1 = 1')
             ->beginIF($params['lang'])->andWhere('lang')->in($params['lang'])->fi()

From 9cdc28a1238a04b8bed447f003560bf11f97cb85 Mon Sep 17 00:00:00 2001
From: tianshujie 
Date: Tue, 21 Nov 2023 16:10:02 +0800
Subject: [PATCH 62/92] * Refactor custom::prepareSQLTest case.

---
 module/custom/test/custom.class.php     |  5 ++-
 module/custom/test/model/preparesql.php | 52 +++++++++++--------------
 2 files changed, 25 insertions(+), 32 deletions(-)

diff --git a/module/custom/test/custom.class.php b/module/custom/test/custom.class.php
index 93cce391a6..843e4b8a56 100644
--- a/module/custom/test/custom.class.php
+++ b/module/custom/test/custom.class.php
@@ -100,14 +100,15 @@ class customTest
     }
 
     /**
+     * 创建一个DAO对象来选择或删除一条或多条记录。
      * Test create a DAO object to select or delete one or more records.
      *
-     * @param  string  $paramString
+     * @param  string $paramString
      * @param  string $method
      * @access public
      * @return array|int
      */
-    public function prepareSQLTest($paramString, $method = 'select')
+    public function prepareSQLTest(string $paramString, string $method = 'select'): array|int
     {
         $params  = $this->objectModel->parseItemParam($paramString);
         if($method == 'delete')
diff --git a/module/custom/test/model/preparesql.php b/module/custom/test/model/preparesql.php
index b2ab5e1f57..1e2dede96c 100755
--- a/module/custom/test/model/preparesql.php
+++ b/module/custom/test/model/preparesql.php
@@ -7,43 +7,35 @@ su('admin');
 /**
 
 title=测试 customModel->prepareSQL();
+timeout=0
 cid=1
-pid=1
-
-测试method为select,paramString参数为空 >> zh-cn,custom
-测试method为select,paramString参数为lang >> zh-cn,custom
-测试method为select,paramString参数为lang,module >> zh-cn,custom
-测试method为select,paramString参数为lang,key,section >> zh-cn,custom
-测试method为select,paramString参数为lang,key,section,module >> zh-cn,custom
-测试method为select,paramString参数为lang,key,section,module,vision >> zh-cn,custom
-测试method为delete,paramString参数为空 >> 14
-测试method为delete,paramString参数为lang >> 0
-测试method为delete,paramString参数为lang,module >> 0
-测试method为delete,paramString参数为lang,key,section >> 0
-测试method为delete,paramString参数为lang,key,section,module >> 0
-测试method为delete,paramString参数为lang,key,section,module,vision >> 0
 
 */
 
+$lang = zdTable('lang');
+$lang->lang->range('zh-cn');
+$lang->module->range('custom');
+$lang->vision->range('rnd');
+$lang->gen(1, true);
+
 $lang        = 'lang=zh-cn';
 $module      = 'module=custom';
-$section     = 'section=URSRList';
-$key         = 'key=1';
+$section     = 'section=section1';
+$key         = 'key=key1';
 $vision      = 'vision=rnd';
 $paramString = array('', $lang, $lang.'&'.$module, $lang.'&'.$key.'&'.$section, $lang.'&'.$key.'&'.$section.'&'.$module, $lang.'&'.$key.'&'.$section.'&'.$module.'&'.$vision);
 $method      = array('select', 'delete');
 
-$custom = new customTest();
-
-r($custom->prepareSQLTest($paramString[0], $method[0])) && p('1:lang,module') && e('zh-cn,custom');  //测试method为select,paramString参数为空
-r($custom->prepareSQLTest($paramString[1], $method[0])) && p('1:lang,module') && e('zh-cn,custom');  //测试method为select,paramString参数为lang
-r($custom->prepareSQLTest($paramString[2], $method[0])) && p('1:lang,module') && e('zh-cn,custom');  //测试method为select,paramString参数为lang,module
-r($custom->prepareSQLTest($paramString[3], $method[0])) && p('1:lang,module') && e('zh-cn,custom');  //测试method为select,paramString参数为lang,key,section
-r($custom->prepareSQLTest($paramString[4], $method[0])) && p('1:lang,module') && e('zh-cn,custom');  //测试method为select,paramString参数为lang,key,section,module
-r($custom->prepareSQLTest($paramString[5], $method[0])) && p('1:lang,module') && e('zh-cn,custom');  //测试method为select,paramString参数为lang,key,section,module,vision
-r($custom->prepareSQLTest($paramString[0], $method[1])) && p()                && e('14');            //测试method为delete,paramString参数为空
-r($custom->prepareSQLTest($paramString[1], $method[1])) && p()                && e('0');             //测试method为delete,paramString参数为lang
-r($custom->prepareSQLTest($paramString[2], $method[1])) && p()                && e('0');             //测试method为delete,paramString参数为lang,module
-r($custom->prepareSQLTest($paramString[3], $method[1])) && p()                && e('0');             //测试method为delete,paramString参数为lang,key,section
-r($custom->prepareSQLTest($paramString[4], $method[1])) && p()                && e('0');             //测试method为delete,paramString参数为lang,key,section,module
-r($custom->prepareSQLTest($paramString[5], $method[1])) && p()                && e('0');             //测试method为delete,paramString参数为lang,key,section,module,vision
+$customTester = new customTest();
+r($customTester->prepareSQLTest($paramString[0], $method[0])) && p('key1:lang,module') && e('zh-cn,custom');  //测试method为select,paramString参数为空
+r($customTester->prepareSQLTest($paramString[1], $method[0])) && p('key1:lang,module') && e('zh-cn,custom');  //测试method为select,paramString参数为lang
+r($customTester->prepareSQLTest($paramString[2], $method[0])) && p('key1:lang,module') && e('zh-cn,custom');  //测试method为select,paramString参数为lang,module
+r($customTester->prepareSQLTest($paramString[3], $method[0])) && p('key1:lang,module') && e('zh-cn,custom');  //测试method为select,paramString参数为lang,key,section
+r($customTester->prepareSQLTest($paramString[4], $method[0])) && p('key1:lang,module') && e('zh-cn,custom');  //测试method为select,paramString参数为lang,key,section,module
+r($customTester->prepareSQLTest($paramString[5], $method[0])) && p('key1:lang,module') && e('zh-cn,custom');  //测试method为select,paramString参数为lang,key,section,module,vision
+r($customTester->prepareSQLTest($paramString[0], $method[1])) && p()                   && e('1');             //测试method为delete,paramString参数为空
+r($customTester->prepareSQLTest($paramString[1], $method[1])) && p()                   && e('0');             //测试method为delete,paramString参数为lang
+r($customTester->prepareSQLTest($paramString[2], $method[1])) && p()                   && e('0');             //测试method为delete,paramString参数为lang,module
+r($customTester->prepareSQLTest($paramString[3], $method[1])) && p()                   && e('0');             //测试method为delete,paramString参数为lang,key,section
+r($customTester->prepareSQLTest($paramString[4], $method[1])) && p()                   && e('0');             //测试method为delete,paramString参数为lang,key,section,module
+r($customTester->prepareSQLTest($paramString[5], $method[1])) && p()                   && e('0');             //测试method为delete,paramString参数为lang,key,section,module,vision

From 7046aae015554f326fb6f79437ca9bae19629e52 Mon Sep 17 00:00:00 2001
From: liugang 
Date: Tue, 21 Nov 2023 16:13:59 +0800
Subject: [PATCH 63/92] * user: load model before call its method.

---
 module/user/control.php | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/module/user/control.php b/module/user/control.php
index 4d426bc24e..aa43b93969 100755
--- a/module/user/control.php
+++ b/module/user/control.php
@@ -82,7 +82,7 @@ class user extends control
 
         /* Get user, totos. */
         $account = $user->account;
-        $todos   = $this->todo->getList($type, $account, $status, 0, $pager, $sort);
+        $todos   = $this->loadModel('todo')->getList($type, $account, $status, 0, $pager, $sort);
         $date    = (int)$type == 0 ? helper::today() : $type;
         $deptID  = $this->app->user->admin ? 0 : $this->app->user->dept;
         $users   = $this->loadModel('dept')->getDeptUserPairs($deptID, 'id');
@@ -483,7 +483,7 @@ class user extends control
         $this->view->title        = "USER #$user->id $user->account/" . $this->lang->user->profile;
         $this->view->user         = $user;
         $this->view->groups       = $this->loadModel('group')->getByAccount($account);
-        $this->view->deptPath     = $this->dept->getParents($user->dept);
+        $this->view->deptPath     = $this->loadModel('dept')->getParents($user->dept);
         $this->view->personalData = $this->user->getPersonalData($user->account);
         $this->view->userList     = $this->user->setUserList($users, $userID);
 
@@ -554,7 +554,7 @@ class user extends control
         $position[] = $this->lang->user->create;
         $this->view->title     = $title;
         $this->view->position  = $position;
-        $this->view->depts     = $this->dept->getOptionMenu();
+        $this->view->depts     = $this->loadModel('dept')->getOptionMenu();
         $this->view->groupList = $groupList;
         $this->view->roleGroup = $roleGroup;
         $this->view->deptID    = $deptID;
@@ -613,7 +613,7 @@ class user extends control
         $this->view->showFields   = join(',', $showFields);
 
         $this->view->title      = $this->lang->company->common . $this->lang->colon . $this->lang->user->batchCreate;
-        $this->view->depts      = $this->dept->getOptionMenu();
+        $this->view->depts      = $this->loadModel('dept')->getOptionMenu();
         $this->view->deptID     = $deptID;
         $this->view->groupList  = $groupList;
         $this->view->roleGroup  = $roleGroup;
@@ -654,7 +654,7 @@ class user extends control
         $this->view->title      = $title;
         $this->view->position   = $position;
         $this->view->user       = $user;
-        $this->view->depts      = $this->dept->getOptionMenu();
+        $this->view->depts      = $this->loadModel('dept')->getOptionMenu();
         $this->view->userGroups = implode(',', array_keys($userGroups));
         $this->view->companies  = $this->loadModel('company')->getOutsideCompanies();
         $this->view->groups     = $this->dao->select('id, name')->from(TABLE_GROUP)->where('project')->eq(0)->fetchPairs('id', 'name');
@@ -701,7 +701,7 @@ class user extends control
         $this->view->showFields   = join(',', $showFields);
 
         $this->view->title      = $this->lang->company->common . $this->lang->colon . $this->lang->user->batchEdit;
-        $this->view->depts      = $this->dept->getOptionMenu();
+        $this->view->depts      = $this->loadModel('dept')->getOptionMenu();
         $this->view->rand       = $this->user->updateSessionRandom();
         $this->view->visionList = $this->user->getVisionList();
 

From c135725bb673ec4d13ddda6c59bea89171710b79 Mon Sep 17 00:00:00 2001
From: liumengyi 
Date: Tue, 21 Nov 2023 16:14:08 +0800
Subject: [PATCH 64/92] + Add unit test of testreportModel::create.

---
 module/testreport/test/model/create.php | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/module/testreport/test/model/create.php b/module/testreport/test/model/create.php
index 0a34b649df..ed37bddad1 100755
--- a/module/testreport/test/model/create.php
+++ b/module/testreport/test/model/create.php
@@ -26,9 +26,13 @@ $noOwner      = array('owner' => '',      'members' => $members, 'title' => $tit
 $noTitle      = array('owner' => 'user3', 'members' => $members, 'title' => $title[3], 'report' => '', 'cases' => $cases) ;
 $noMembers    = array('owner' => 'user3', 'members' => '',       'title' => $title[2], 'report' => '', 'cases' => $cases) ;
 $endLtBegin   = array('owner' => 'user3', 'members' => $members, 'title' => $title[0], 'report' => '', 'cases' => $cases, 'begin' => '2017-01-01', 'end' => '2016-01-01') ;
+$emptyBegin   = array('owner' => 'user3', 'members' => $members, 'title' => $title[0], 'report' => '', 'cases' => $cases, 'begin' => '') ;
+$emptyEnd     = array('owner' => 'user3', 'members' => $members, 'title' => $title[0], 'report' => '', 'cases' => $cases, 'end' => '') ;
 
 r($testreport->createTest($normalReport)) && p('id,title') && e('1,正常新增');                             // 正常新增
 r($testreport->createTest($noOwner))      && p('owner:0')  && e('『负责人』不能为空。');                   // 负责人为空测试
 r($testreport->createTest($noTitle))      && p('title:0')  && e('『报告标题』不能为空。');                 // 标题为空测试
 r($testreport->createTest($noMembers))    && p('id,title') && e('2,参与人员为空测试');                     // 参与人员测试
 r($testreport->createTest($endLtBegin))   && p('end:0')    && e('『结束时间』应当不小于『2017-01-01』。'); // 结束时间小于开始时间测试
+r($testreport->createTest($emptyBegin))   && p('begin:0')  && e('『开始时间』不能为空。');                 // 开始时间为空测试
+r($testreport->createTest($emptyEnd))     && p('end:0')    && e('『结束时间』不能为空。');                 // 结束时间为空测试

From 24bb596eda10d8680412b8bf6612034336457543 Mon Sep 17 00:00:00 2001
From: liugang 
Date: Tue, 21 Nov 2023 16:17:15 +0800
Subject: [PATCH 65/92] * user: remove unused code.

---
 module/user/control.php | 25 ++++---------------------
 1 file changed, 4 insertions(+), 21 deletions(-)

diff --git a/module/user/control.php b/module/user/control.php
index aa43b93969..c185992052 100755
--- a/module/user/control.php
+++ b/module/user/control.php
@@ -522,9 +522,6 @@ class user extends control
      */
     public function create($deptID = 0)
     {
-        $this->lang->user->menu      = $this->lang->company->menu;
-        $this->lang->user->menuOrder = $this->lang->company->menuOrder;
-
         if(!empty($_POST))
         {
             if(strtolower($_POST['account']) == 'guest')
@@ -550,10 +547,7 @@ class user extends control
             if($group->role) $roleGroup[$group->role] = $group->id;
         }
 
-        $title      = $this->lang->company->common . $this->lang->colon . $this->lang->user->create;
-        $position[] = $this->lang->user->create;
-        $this->view->title     = $title;
-        $this->view->position  = $position;
+        $this->view->title     = $this->lang->user->create;
         $this->view->depts     = $this->loadModel('dept')->getOptionMenu();
         $this->view->groupList = $groupList;
         $this->view->roleGroup = $roleGroup;
@@ -594,9 +588,6 @@ class user extends control
             if($group->role) $roleGroup[$group->role] = $group->id;
         }
 
-        $this->lang->user->menu      = $this->lang->company->menu;
-        $this->lang->user->menuOrder = $this->lang->company->menuOrder;
-
         /* Set custom. */
         foreach(explode(',', $this->config->user->availableBatchCreateFields) as $field)
         {
@@ -612,7 +603,7 @@ class user extends control
         $this->view->customFields = $customFields;
         $this->view->showFields   = join(',', $showFields);
 
-        $this->view->title      = $this->lang->company->common . $this->lang->colon . $this->lang->user->batchCreate;
+        $this->view->title      = $this->lang->user->batchCreate;
         $this->view->depts      = $this->loadModel('dept')->getOptionMenu();
         $this->view->deptID     = $deptID;
         $this->view->groupList  = $groupList;
@@ -633,8 +624,6 @@ class user extends control
      */
     public function edit($userID)
     {
-        $this->lang->user->menu      = $this->lang->company->menu;
-        $this->lang->user->menuOrder = $this->lang->company->menuOrder;
         if(!empty($_POST))
         {
             $this->user->update($userID);
@@ -649,10 +638,7 @@ class user extends control
         $user       = $this->user->getById($userID, 'id');
         $userGroups = $this->loadModel('group')->getByAccount($user->account, count($userVisionList) > 1 ? true : false);
 
-        $title      = $this->lang->company->common . $this->lang->colon . $this->lang->user->edit;
-        $position[] = $this->lang->user->edit;
-        $this->view->title      = $title;
-        $this->view->position   = $position;
+        $this->view->title      = $this->lang->user->edit;
         $this->view->user       = $user;
         $this->view->depts      = $this->loadModel('dept')->getOptionMenu();
         $this->view->userGroups = implode(',', array_keys($userGroups));
@@ -682,9 +668,6 @@ class user extends control
         }
         if(isset($_POST['users'])) $this->view->users = $this->dao->select('*')->from(TABLE_USER)->where('id')->in($this->post->users)->orderBy('id')->fetchAll('id');
 
-        $this->lang->user->menu      = $this->lang->company->menu;
-        $this->lang->user->menuOrder = $this->lang->company->menuOrder;
-
         /* Set custom. */
         foreach(explode(',', $this->config->user->availableBatchEditFields) as $field)
         {
@@ -700,7 +683,7 @@ class user extends control
         $this->view->customFields = $customFields;
         $this->view->showFields   = join(',', $showFields);
 
-        $this->view->title      = $this->lang->company->common . $this->lang->colon . $this->lang->user->batchEdit;
+        $this->view->title      = $this->lang->user->batchEdit;
         $this->view->depts      = $this->loadModel('dept')->getOptionMenu();
         $this->view->rand       = $this->user->updateSessionRandom();
         $this->view->visionList = $this->user->getVisionList();

From 057314265a73c1b47b3917c9ffe6909015359ca6 Mon Sep 17 00:00:00 2001
From: liugang 
Date: Tue, 21 Nov 2023 16:17:32 +0800
Subject: [PATCH 66/92] - user: remove unused __construct() method.

---
 module/user/control.php | 15 ---------------
 1 file changed, 15 deletions(-)

diff --git a/module/user/control.php b/module/user/control.php
index c185992052..691cb5400e 100755
--- a/module/user/control.php
+++ b/module/user/control.php
@@ -21,21 +21,6 @@ class user extends control
      */
     public $referer = '';
 
-    /**
-     * Construct
-     *
-     * @access public
-     * @return void
-     */
-    public function __construct($module = '', $method = '')
-    {
-        parent::__construct($module, $method);
-        $this->loadModel('company')->setMenu();
-        $this->loadModel('dept');
-        $this->loadModel('todo');
-        $this->app->loadLang('project');
-    }
-
     /**
      * View a user.
      *

From 072677e2ebfce525f893c30589127142c60a3c94 Mon Sep 17 00:00:00 2001
From: liumengyi 
Date: Tue, 21 Nov 2023 16:18:29 +0800
Subject: [PATCH 67/92] + Add unit test of testreportModel::update.

---
 module/testreport/test/model/update.php | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/module/testreport/test/model/update.php b/module/testreport/test/model/update.php
index 203d0f7180..dd96ae1985 100755
--- a/module/testreport/test/model/update.php
+++ b/module/testreport/test/model/update.php
@@ -32,9 +32,13 @@ $noOwner      = array('owner' => '',      'members' => $members, 'title' => $tit
 $noMembers    = array('owner' => 'user3', 'members' => '',       'title' => $title[2], 'report' => '', 'cases' => $cases);
 $noTitle      = array('owner' => 'user3', 'members' => $members, 'title' => $title[3], 'report' => '', 'cases' => $cases);
 $endLtBegin   = array('owner' => 'user3', 'members' => $members, 'title' => $title[0], 'report' => '', 'cases' => $cases, 'begin' => '2017-01-01', 'end' => '2016-01-01') ;
+$emptyBegin   = array('owner' => 'user3', 'members' => $members, 'title' => $title[0], 'report' => '', 'cases' => $cases, 'begin' => '') ;
+$emptyEnd     = array('owner' => 'user3', 'members' => $members, 'title' => $title[0], 'report' => '', 'cases' => $cases, 'end' => '') ;
 
 r($testreport->updateTest($reportID, $normalReport)) && p('id,title') && e('1,正常修改');                             // 正常修改
 r($testreport->updateTest($reportID, $noOwner))      && p('owner:0')  && e('『负责人』不能为空。');                   // 负责人为空测试
 r($testreport->updateTest($reportID, $noMembers))    && p('id,title') && e('1,参与人员为空测试,');                    // 参与人员为空测试
 r($testreport->updateTest($reportID, $noTitle))      && p('title:0')  && e('『报告标题』不能为空。');                 // 标题为空测试
 r($testreport->updateTest($reportID, $endLtBegin))   && p('end:0')    && e('『结束时间』应当不小于『2017-01-01』。'); // 结束时间小于开始时间测试
+r($testreport->updateTest($reportID, $emptyBegin))   && p('begin:0')  && e('『开始时间』不能为空。');                 // 开始时间为空测试
+r($testreport->updateTest($reportID, $emptyEnd))     && p('end:0')    && e('『结束时间』不能为空。');                 // 结束时间为空测试

From 9f1d0f0122bcd1c04833be18d8f7efb5157ea313 Mon Sep 17 00:00:00 2001
From: liumengyi 
Date: Tue, 21 Nov 2023 16:19:38 +0800
Subject: [PATCH 68/92] * Optimize code.

---
 module/testreport/model.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/module/testreport/model.php b/module/testreport/model.php
index bd83cc95a7..e23531c5ec 100644
--- a/module/testreport/model.php
+++ b/module/testreport/model.php
@@ -64,7 +64,7 @@ class testreportModel extends model
      * 通过 id 获取测试报告。
      * Get report by id.
      *
-     * @param  int    $reportID
+     * @param  int          $reportID
      * @access public
      * @return object|false
      */

From 6f875a17c753e30c8a77c0cc126058cb3f8d3ff6 Mon Sep 17 00:00:00 2001
From: songchenxuan <1097180981@qq.com>
Date: Tue, 21 Nov 2023 16:20:12 +0800
Subject: [PATCH 69/92] * Set max-width 50% for metric preview and use
 flex-basis.

---
 module/metric/control.php               |  3 ++-
 module/metric/css/preview.ui.css        |  4 ++--
 module/metric/ui/previewsingle.html.php |  1 +
 module/metric/zen.php                   | 13 ++++++++++++-
 4 files changed, 17 insertions(+), 4 deletions(-)

diff --git a/module/metric/control.php b/module/metric/control.php
index 70f1cdab38..4ee7c94ab4 100755
--- a/module/metric/control.php
+++ b/module/metric/control.php
@@ -67,7 +67,7 @@ class metric extends control
             $resultHeader = $this->metricZen->getViewTableHeader($metric);
             $resultData   = $this->metricZen->getViewTableData($metric, $result);
         }
-
+        
         $this->view->metrics       = $metrics;
         $this->view->groupMetrics  = $groupMetrics;
         $this->view->current       = $current;
@@ -80,6 +80,7 @@ class metric extends control
         $this->view->filtersBase64 = $filtersBase64;
         $this->view->resultHeader  = $resultHeader;
         $this->view->resultData    = $resultData;
+        $this->view->tableWidth    = $this->metricZen->getViewTableWidth($resultHeader);
         $this->view->chartTypeList = $this->metric->getChartTypeList($resultHeader);
         $this->view->echartOptions = $this->metric->getEchartsOptions($resultHeader, $resultData);
         $this->display();
diff --git a/module/metric/css/preview.ui.css b/module/metric/css/preview.ui.css
index bd632a7df1..5cb8622bc1 100644
--- a/module/metric/css/preview.ui.css
+++ b/module/metric/css/preview.ui.css
@@ -48,8 +48,8 @@
 i.star-empty {color: var(--color-slate-400);}
 i.star {color: var(--color-warning-300);}
 
-.table-and-chart .table-side {flex: 0 0 480px;}
-.table-and-chart .chart-side {flex: 0 0 calc(100% - 480px);}
+.table-and-chart .table-side {flex-shrink: 1; max-width: 50%;}
+.table-and-chart .chart-side {flex-grow: 1;}
 .table-and-chart .chart-center {display:flex; justify-content: center; align-items: center;}
 
 .icon-18 {font-size: 18px;}
diff --git a/module/metric/ui/previewsingle.html.php b/module/metric/ui/previewsingle.html.php
index 8e51ff1ec6..5f77bb1677 100644
--- a/module/metric/ui/previewsingle.html.php
+++ b/module/metric/ui/previewsingle.html.php
@@ -350,6 +350,7 @@ div
             div
             (
                 setClass('table-side'),
+                setStyle(array('flex-basis' => $tableWidth . 'px')),
                 div
                 (
                     $resultData ? dtable
diff --git a/module/metric/zen.php b/module/metric/zen.php
index 27daa768c5..34b552b700 100644
--- a/module/metric/zen.php
+++ b/module/metric/zen.php
@@ -397,7 +397,7 @@ class metricZen extends metric
 
         if(!$result) return array
         (
-            array('name' => 'value', 'title' => $this->lang->metric->value),
+            array('name' => 'value', 'title' => $this->lang->metric->value, 'width' => 96),
             array('name' => 'calcTime', 'title' => $this->lang->metric->calcTime, 'width' => 150)
         );
 
@@ -415,6 +415,17 @@ class metricZen extends metric
         return $header;
     }
 
+    protected function getViewTableWidth($headers)
+    {
+        $width = 0;
+        foreach($headers as $header)
+        {
+            $width += isset($header['width']) ? $header['width'] : 160;
+        }
+    
+        return $width;
+    }
+
     /**
      * 获取度量数据表的数据。
      * Get data of result table.

From d536e04df2b59adf00a266a0ba75d2ccfb40e999 Mon Sep 17 00:00:00 2001
From: tianshujie 
Date: Tue, 21 Nov 2023 16:21:13 +0800
Subject: [PATCH 70/92] - Remove unused method of custom::working.

---
 module/custom/control.php | 18 ------------------
 1 file changed, 18 deletions(-)

diff --git a/module/custom/control.php b/module/custom/control.php
index 4bd3894409..db72e133d4 100644
--- a/module/custom/control.php
+++ b/module/custom/control.php
@@ -90,24 +90,6 @@ class custom extends control
         return $this->sendSuccess(array('load' => true));
     }
 
-    /**
-     * Set working mode function.
-     *
-     * @access public
-     * @return void
-     */
-    public function working()
-    {
-        if($_POST)
-        {
-            $this->loadModel('setting')->setItem('system.common.global.flow', $this->post->flow);
-            return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => 'reload'));
-        }
-
-        $this->view->title      = $this->lang->custom->working;
-        $this->display();
-    }
-
     /**
      * Set Required.
      *

From 0e0cd2774e88b8719314132b3e27c0919201d0a8 Mon Sep 17 00:00:00 2001
From: sunhao 
Date: Tue, 21 Nov 2023 15:36:00 +0800
Subject: [PATCH 71/92] * zin: fix undefined property error.

---
 lib/zin/wg/mainnavbar/v1.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/zin/wg/mainnavbar/v1.php b/lib/zin/wg/mainnavbar/v1.php
index a3dce327ec..699e460849 100644
--- a/lib/zin/wg/mainnavbar/v1.php
+++ b/lib/zin/wg/mainnavbar/v1.php
@@ -139,7 +139,7 @@ class mainNavbar extends nav
 
         if(in_array("$moduleName-$methodName", is_array($config->excludeSwitcherList) ? $config->excludeSwitcherList : array())) return null;
 
-        if(in_array($moduleName, is_array($config->hasSwitcherModules) ? $config->hasSwitcherModules : array()) || in_array("$moduleName-$methodName", is_array($config->hasSwitcherMethods) ? $config->hasSwitcherMethods : array()))
+        if(in_array($moduleName, (isset($config->hasSwitcherModules) && is_array($config->hasSwitcherModules)) ? $config->hasSwitcherModules : array()) || in_array("$moduleName-$methodName", (isset($config->hasSwitcherMethods) && is_array($config->hasSwitcherMethods)) ? $config->hasSwitcherMethods : array()))
         {
             $ajaxMethod = 'ajaxSwitcherMenu';
             if($moduleName == 'testcase' && $app->tab == 'project') $moduleName = 'project';

From 6c792b91f19555ad893c82543aa0ec167aba64a1 Mon Sep 17 00:00:00 2001
From: sunhao 
Date: Tue, 21 Nov 2023 16:30:20 +0800
Subject: [PATCH 72/92] * zin: fix quick add menu not work in zin compatible
 mode.

---
 lib/zin/wg/header/v1.php | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/lib/zin/wg/header/v1.php b/lib/zin/wg/header/v1.php
index 529973a29f..32e68e021e 100644
--- a/lib/zin/wg/header/v1.php
+++ b/lib/zin/wg/header/v1.php
@@ -295,6 +295,7 @@ class header extends wg
 
         /* Initialize the default values. */
         $showCreateList = $needPrintDivider = false;
+        $modalClass     = (isset($config->zin->mode) && $config->zin->mode == 'compatible') ? 'open-in-parent' : null;
 
         /* Get default product id. */
         $productID = isset($_SESSION['product']) ? $_SESSION['product'] : 0;
@@ -352,6 +353,7 @@ class header extends wg
                 case 'doc':
                     $params              = "objectType=&objectID=0&libID=0";
                     $createMethod        = 'selectLibType';
+                    $item['innerClass']  = $modalClass;
                     $item['data-toggle'] = 'modal';
                     break;
                 case 'project':
@@ -363,6 +365,7 @@ class header extends wg
                     {
                         $params              = "programID=0&from=global";
                         $createMethod        = 'createGuide';
+                        $item['innerClass']  = $modalClass;
                         $item['data-toggle'] = 'modal';
                     }
                     else
@@ -416,11 +419,13 @@ class header extends wg
                     break;
                 case 'kanbanspace':
                     $isOnlyBody          = true;
+                    $item['innerClass']  = $modalClass;
                     $item['data-toggle'] = 'modal';
                     $item['data-width']  = '75%';
                     break;
                 case 'kanban':
                     $isOnlyBody          = true;
+                    $item['innerClass']  = $modalClass;
                     $item['data-toggle'] = 'modal';
                     $item['data-width']  = '75%';
                     break;

From d3bf894ad4e9f57364327b76c115ef3681cdf993 Mon Sep 17 00:00:00 2001
From: sunhao 
Date: Tue, 21 Nov 2023 16:30:25 +0800
Subject: [PATCH 73/92] * zin: refactor js log.

---
 www/js/zui3/zin.js | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/www/js/zui3/zin.js b/www/js/zui3/zin.js
index 383102a16e..18a6601df4 100644
--- a/www/js/zui3/zin.js
+++ b/www/js/zui3/zin.js
@@ -636,7 +636,12 @@
             const newOptions = window.beforePageLoad(options);
             if(newOptions) $.extend(options, newOptions);
         }
-        if(DEBUG) console.log('[APP] ', 'load:', options.url);
+        if(DEBUG)
+        {
+            console.groupCollapsed('[APP] ', 'load:', options.url);
+            console.trace('options', options);
+            console.groupEnd();
+        }
         fetchContent(options.url, options.selector, options);
     }
 

From 5af80f64e07196a05a37df54d5554b35ea5c6d57 Mon Sep 17 00:00:00 2001
From: liugang 
Date: Tue, 21 Nov 2023 16:38:50 +0800
Subject: [PATCH 74/92] * user: add the parameter type and modify comment.

---
 module/user/control.php | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/module/user/control.php b/module/user/control.php
index 691cb5400e..90183a31c9 100755
--- a/module/user/control.php
+++ b/module/user/control.php
@@ -22,13 +22,14 @@ class user extends control
     public $referer = '';
 
     /**
-     * View a user.
+     * 查看用户详情。
+     * View user details.
      *
-     * @param  string $userID
+     * @param  int    $userID
      * @access public
      * @return void
      */
-    public function view($userID)
+    public function view(int $userID)
     {
         $this->locate($this->createLink('user', 'todo', "userID=$userID&type=all"));
     }

From 9820b9941933ce794198e43a1b8ef4be973370d1 Mon Sep 17 00:00:00 2001
From: liumengyi 
Date: Tue, 21 Nov 2023 16:44:23 +0800
Subject: [PATCH 75/92] + Add inited data for the unit test of
 testreportModel::getTaskCases.

---
 module/testreport/test/model/gettaskcases.php | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/module/testreport/test/model/gettaskcases.php b/module/testreport/test/model/gettaskcases.php
index daf0d0fab8..50a7cb303b 100755
--- a/module/testreport/test/model/gettaskcases.php
+++ b/module/testreport/test/model/gettaskcases.php
@@ -2,6 +2,11 @@
 gen(2);
+zdTable('testtask')->gen(30);
+zdTable('user')->gen(1);
+
 su('admin');
 
 /**
@@ -31,4 +36,4 @@ $testreport = new testreportTest();
 
 r($testreport->getTaskCasesTest($taskIdList[0], $reportID, $idList[0])[1]) && p('1:id;1:lastRunResult;2:id;2:lastRunResult') && e('1;pass;2;fail'); //正常查询
 r($testreport->getTaskCasesTest($taskIdList[0], $reportID, $idList[1])[1]) && p('1:id;2:id;3:id;4:id')                       && e('1;2;3;4');       //idList为空查询
-r($testreport->getTaskCasesTest($taskIdList[1], $reportID, $idList[0]))    && p()                                            && e('0');             //tasksID为空查询
\ No newline at end of file
+r($testreport->getTaskCasesTest($taskIdList[1], $reportID, $idList[0]))    && p()                                            && e('0');             //tasksID为空查询

From 430922d9ab9dea4889bc7d12b72a6bbd2365f248 Mon Sep 17 00:00:00 2001
From: tianshujie 
Date: Tue, 21 Nov 2023 16:47:45 +0800
Subject: [PATCH 76/92] * Refactor custom::required method.

---
 module/custom/control.php | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/module/custom/control.php b/module/custom/control.php
index db72e133d4..f88714fffc 100644
--- a/module/custom/control.php
+++ b/module/custom/control.php
@@ -91,7 +91,8 @@ class custom extends control
     }
 
     /**
-     * Set Required.
+     * 设置表单必填字段。
+     * Set the required fields.
      *
      * @param  string $moduleName
      * @access public

From a88c4804a72cea388da87d3686e037107a45a95e Mon Sep 17 00:00:00 2001
From: wangyuting 
Date: Tue, 21 Nov 2023 08:45:37 +0000
Subject: [PATCH 77/92] * Fix side menu action bug.

---
 module/dept/ui/browse.html.php | 78 ++++++++++++++++------------------
 1 file changed, 37 insertions(+), 41 deletions(-)

diff --git a/module/dept/ui/browse.html.php b/module/dept/ui/browse.html.php
index f32940bb89..119c217e9e 100644
--- a/module/dept/ui/browse.html.php
+++ b/module/dept/ui/browse.html.php
@@ -10,48 +10,23 @@ declare(strict_types=1);
  */
 namespace zin;
 
-jsVar('editLinkTemp', createLink('dept', 'edit', "deptID={id}"));
-jsVar('deleteLinkTemp', createLink('dept', 'delete', "deptID={id}"));
-jsVar('deleteTip', $lang->dept->confirmDelete);
-
-$deptActions = array();
-if(hasPriv('dept', 'edit'))
+$processTreeAction = function($tree) use (&$processTreeAction)
 {
-    $deptAction = array();
-    $deptAction['key']  = 'edit';
-    $deptAction['icon'] = 'edit';
-    $deptAction['hint'] = $lang->dept->edit;
-    $deptAction['onClick'] = jsRaw('window.operateDept');
+    $canEditDept   = hasPriv('dept', 'edit');
+    $canDeleteDept = hasPriv('dept', 'delete');
+    foreach($tree as $node)
+    {
+        $actions = array();
+        if($canEditDept)                          $actions[] = array('key' => 'edit',   'icon' => 'edit',  'hint' => $lang->dept->edit,   'onClick' => jsRaw('window.operateDept'));
+        if(empty($node->items) && $canDeleteDept) $actions[] = array('key' => 'delete', 'icon' => 'trash', 'hint' => $lang->dept->delete, 'onClick' => jsRaw('window.operateDept'));
+        if(!empty($node->items)) $node->items = $processTreeAction($node->items);
+        $node->actions = $actions;
+    }
 
-    $deptActions[] = $deptAction;
-}
-if(hasPriv('dept', 'delete'))
-{
-    $deptAction = array();
-    $deptAction['key']  = 'delete';
-    $deptAction['icon'] = 'trash';
-    $deptAction['hint'] = $lang->dept->delete;
-    $deptAction['onClick'] = jsRaw('window.operateDept');
+    return $tree;
+};
 
-    $deptActions[] = $deptAction;
-}
-
-sidebar
-(
-    width('400px'),
-    set::showToggle(false),
-    panel
-    (
-        set::title($title),
-        tree
-        (
-            set::id('deptTree'),
-            set::items($tree),
-            set::hover(true),
-            set::itemActions($deptActions)
-        )
-    )
-);
+$tree = $processTreeAction($tree);
 
 $parentNames = array();
 foreach($parentDepts as $dept)
@@ -86,8 +61,9 @@ foreach($sons as $dept)
         set::value($dept->name)
     );
 }
+
 $emptyInputs = array();
-for($i = 0; $i < \DEPT::NEW_CHILD_COUNT ; $i ++)
+for($i = 0; $i < 10; $i ++)
 {
     $emptyInputs[] = formGroup
     (
@@ -97,6 +73,27 @@ for($i = 0; $i < \DEPT::NEW_CHILD_COUNT ; $i ++)
     );
 }
 
+jsVar('editLinkTemp', createLink('dept', 'edit', "deptID={id}"));
+jsVar('deleteLinkTemp', createLink('dept', 'delete', "deptID={id}"));
+jsVar('deleteTip', $lang->dept->confirmDelete);
+
+sidebar
+(
+    width('400px'),
+    set::showToggle(false),
+    panel
+    (
+        set::title($title),
+        tree
+        (
+            set::id('deptTree'),
+            set::items($tree),
+            set::hover(true),
+            set::itemActions($deptActions)
+        )
+    )
+);
+
 panel
 (
     set::title($lang->dept->manageChild),
@@ -147,4 +144,3 @@ panel
 );
 
 render();
-

From 9ffcb7ee9dfd3ca7c88ab68cb09315214f86bb18 Mon Sep 17 00:00:00 2001
From: wangyuting 
Date: Tue, 21 Nov 2023 08:46:05 +0000
Subject: [PATCH 78/92] * Optimize side menu action.

---
 module/dept/model.php | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/module/dept/model.php b/module/dept/model.php
index cce333ba2f..4a782223d3 100644
--- a/module/dept/model.php
+++ b/module/dept/model.php
@@ -471,8 +471,7 @@ class deptModel extends model
                 $node->text        = $node->name;
                 if(isset($tree[$deptID]))
                 {
-                    $node->items   = $tree[$deptID];
-                    $node->actions = array('delete' => false);
+                    $node->items = $tree[$deptID];
                     unset($tree[$deptID]);
                 }
                 $tree[$node->parent][] = $node;

From 226ee7ff0efdc5735d89b3eb7aa7d5dd3b415b9f Mon Sep 17 00:00:00 2001
From: wangyuting 
Date: Tue, 21 Nov 2023 08:46:34 +0000
Subject: [PATCH 79/92] * Remove NEW_CHILD_COUNT.

---
 module/dept/view/browse.html.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/module/dept/view/browse.html.php b/module/dept/view/browse.html.php
index 93861a70cb..af037e0710 100644
--- a/module/dept/view/browse.html.php
+++ b/module/dept/view/browse.html.php
@@ -53,7 +53,7 @@
                     if($sonDept->order > $maxOrder) $maxOrder = $sonDept->order;
                     echo html::input("depts[id$sonDept->id]", $sonDept->name, "class='form-control'");
                 }
-                for($i = 0; $i < DEPT::NEW_CHILD_COUNT ; $i ++) echo html::input("depts[]", '', "class='form-control'");
+                for($i = 0; $i < 10; $i ++) echo html::input("depts[]", '', "class='form-control'");
                ?>
               
               

From 8295058fdcf3de86073c7ed612f9c9705708ad08 Mon Sep 17 00:00:00 2001
From: wangyuting 
Date: Tue, 21 Nov 2023 08:48:17 +0000
Subject: [PATCH 80/92] * Optimize browse function.

---
 module/dept/control.php | 36 ++++++++++++------------------------
 1 file changed, 12 insertions(+), 24 deletions(-)

diff --git a/module/dept/control.php b/module/dept/control.php
index d1973b96db..1e1d775164 100644
--- a/module/dept/control.php
+++ b/module/dept/control.php
@@ -1,4 +1,5 @@
 loadModel('company')->setMenu();
-    }
-
-    /**
-     * Browse a department.
+     * 部门结构维护页面。
+     * Department management page.
      *
      * @param  int    $deptID
      * @access public
      * @return void
      */
-    public function browse($deptID = 0)
+    public function browse(int $deptID = 0)
     {
-        $parentDepts = $this->dept->getParents($deptID);
         $this->view->title       = $this->lang->dept->manage . $this->lang->colon . $this->app->company->name;
         $this->view->deptID      = $deptID;
-        $this->view->depts       = $this->dept->getTreeMenu($rootDeptID = 0, array('deptmodel', 'createManageLink'));
-        $this->view->parentDepts = $parentDepts;
+        $this->view->depts       = $this->dept->getTreeMenu(0, array('deptmodel', 'createManageLink'));
         $this->view->sons        = $this->dept->getSons($deptID);
         $this->view->tree        = $this->dept->getDataStructure();
+        $this->view->parentDepts = $this->dept->getParents($deptID);
         $this->display();
     }
 
     /**
-     * Update the departments order.
+     * 部门结构排序。
+     * Sort of departments.
      *
      * @access public
      * @return void
@@ -55,12 +43,13 @@ class dept extends control
         if(!empty($_POST))
         {
             $this->dept->updateOrder($_POST['orders']);
-            return print(js::reload('parent'));
+            return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => true));
         }
     }
 
     /**
-     * Manage childs.
+     * 修改子部门结构。
+     * Update child departments.
      *
      * @access public
      * @return void
@@ -70,8 +59,7 @@ class dept extends control
         if(!empty($_POST))
         {
             $deptIDList = $this->dept->manageChild($_POST['parentDeptID'], $_POST['depts']);
-            if($this->viewType == 'json') return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'idList' => $deptIDList));
-            return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => true));
+            return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => true, 'idList' => $deptIDList));
         }
     }
 

From 7e66a87b5de4000fd6fdf0f5834adf128d5fa491 Mon Sep 17 00:00:00 2001
From: wangyuting 
Date: Tue, 21 Nov 2023 08:48:41 +0000
Subject: [PATCH 81/92] * Adjust NEW_CHILD_COUNT to config.

---
 module/dept/config.php           | 2 ++
 module/dept/ui/browse.html.php   | 2 +-
 module/dept/view/browse.html.php | 2 +-
 3 files changed, 4 insertions(+), 2 deletions(-)
 create mode 100644 module/dept/config.php

diff --git a/module/dept/config.php b/module/dept/config.php
new file mode 100644
index 0000000000..c977033212
--- /dev/null
+++ b/module/dept/config.php
@@ -0,0 +1,2 @@
+dept->newChildCount = 10;
diff --git a/module/dept/ui/browse.html.php b/module/dept/ui/browse.html.php
index 119c217e9e..ae1efbbe87 100644
--- a/module/dept/ui/browse.html.php
+++ b/module/dept/ui/browse.html.php
@@ -63,7 +63,7 @@ foreach($sons as $dept)
 }
 
 $emptyInputs = array();
-for($i = 0; $i < 10; $i ++)
+for($i = 0; $i < $config->dept->newChildCount; $i ++)
 {
     $emptyInputs[] = formGroup
     (
diff --git a/module/dept/view/browse.html.php b/module/dept/view/browse.html.php
index af037e0710..d5036c7aac 100644
--- a/module/dept/view/browse.html.php
+++ b/module/dept/view/browse.html.php
@@ -53,7 +53,7 @@
                     if($sonDept->order > $maxOrder) $maxOrder = $sonDept->order;
                     echo html::input("depts[id$sonDept->id]", $sonDept->name, "class='form-control'");
                 }
-                for($i = 0; $i < 10; $i ++) echo html::input("depts[]", '', "class='form-control'");
+                for($i = 0; $i < $config->dept->newChildCount; $i ++) echo html::input("depts[]", '', "class='form-control'");
                ?>
               
               

From ce8a21a25405b6847cc3fe0aad650cdb3750b724 Mon Sep 17 00:00:00 2001
From: liumengyi 
Date: Tue, 21 Nov 2023 16:48:44 +0800
Subject: [PATCH 82/92] * Optimize code for testportModel::getCaseIdList and
 its unit test.

---
 module/testreport/model.php                    | 2 +-
 module/testreport/test/model/getcaseidlist.php | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/module/testreport/model.php b/module/testreport/model.php
index e23531c5ec..bccf0b8d72 100644
--- a/module/testreport/model.php
+++ b/module/testreport/model.php
@@ -153,7 +153,7 @@ class testreportModel extends model
 
     /**
      * 获取测试报告的用例列表。
-     * Get caseID list.
+     * Get case id list.
      *
      * @param  int    $reportID
      * @access public
diff --git a/module/testreport/test/model/getcaseidlist.php b/module/testreport/test/model/getcaseidlist.php
index aac03078fd..9ea8d79539 100755
--- a/module/testreport/test/model/getcaseidlist.php
+++ b/module/testreport/test/model/getcaseidlist.php
@@ -4,6 +4,8 @@ include dirname(__FILE__, 5) . '/test/lib/init.php';
 include dirname(__FILE__, 2) . '/testreport.class.php';
 
 zdTable('testreport')->gen(10);
+zdTable('case')->gen(10);
+zdTable('testrun')->gen(10);
 zdTable('user')->gen(1);
 
 su('admin');

From 29b532123dd73dc841d86ac2ec4007a945b5f41a Mon Sep 17 00:00:00 2001
From: liumengyi 
Date: Tue, 21 Nov 2023 17:06:52 +0800
Subject: [PATCH 83/92] * Fix bug #40429.

---
 module/execution/ui/story.html.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/module/execution/ui/story.html.php b/module/execution/ui/story.html.php
index 5605890aa3..fc05a6fad8 100644
--- a/module/execution/ui/story.html.php
+++ b/module/execution/ui/story.html.php
@@ -225,7 +225,7 @@ modal
             set::text($lang->execution->next),
             set::btnType('submit'),
             set::type('primary'),
-            setData(array('dismiss' => 'primary'))
+            setData(array('dismiss' => 'modal'))
         )
     )
 );

From 792d80feb15c8dd3f1b1fff106cd1760a5880c5a Mon Sep 17 00:00:00 2001
From: caoyanyi 
Date: Tue, 21 Nov 2023 16:26:45 +0800
Subject: [PATCH 84/92] * Adjust mr approval page error.

---
 module/mr/ui/approval.html.php | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/module/mr/ui/approval.html.php b/module/mr/ui/approval.html.php
index d766afdef2..b12e846ef9 100644
--- a/module/mr/ui/approval.html.php
+++ b/module/mr/ui/approval.html.php
@@ -45,7 +45,4 @@ formPanel
         set::rows('6')
     )
 );
-history();
-
-render();
-
+history(set::objectID($MR->id));

From 2f598715e998a1ea444442a8406210d9b95a9088 Mon Sep 17 00:00:00 2001
From: caoyanyi 
Date: Tue, 21 Nov 2023 17:07:24 +0800
Subject: [PATCH 85/92] + Add open method.

---
 config/zentaopms.php | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/config/zentaopms.php b/config/zentaopms.php
index 0da8b3e969..24a1f1ded9 100644
--- a/config/zentaopms.php
+++ b/config/zentaopms.php
@@ -230,7 +230,8 @@ $config->openMethods[] = 'project.createguide';
 $config->openMethods[] = 'task.editteam';
 $config->openMethods[] = 'feedback.mergeproductmodule';
 $config->openMethods[] = 'zanode.nodelist';
-$config->openMethods[] = 'action.restoreStages';
+$config->openMethods[] = 'action.restorestages';
+$config->openMethods[] = 'action.commentzin';
 $config->openMethods[] = 'execution.browse';
 $config->openMethods[] = 'testcase.showImport';
 $config->openMethods[] = 'testsuite.index';

From 6a33382a111d4c6e08a84d904faa20329e2c363a Mon Sep 17 00:00:00 2001
From: liugang 
Date: Tue, 21 Nov 2023 17:25:39 +0800
Subject: [PATCH 86/92] * user: refactor the todo method.

---
 module/user/control.php            | 49 +++++++++++++-----------------
 module/user/ui/featurebar.html.php |  2 +-
 2 files changed, 22 insertions(+), 29 deletions(-)

diff --git a/module/user/control.php b/module/user/control.php
index 90183a31c9..e4eb063d72 100755
--- a/module/user/control.php
+++ b/module/user/control.php
@@ -35,10 +35,11 @@ class user extends control
     }
 
     /**
-     * Todos of a user.
+     * 查看某个用户的待办。
+     * View user's todo.
      *
-     * @param  string $userID
-     * @param  string $type         the todo type, today|lastweek|thisweek|all|undone, or a date.
+     * @param  int    $userID
+     * @param  string $type       the todo type, all|before|future|thisWeek|thisMonth|thisYear|assignedToOther|cycle
      * @param  string $status
      * @param  string $orderBy
      * @param  int    $recTotal
@@ -47,45 +48,37 @@ class user extends control
      * @access public
      * @return void
      */
-    public function todo($userID, $type = 'today', $status = 'all', $orderBy = 'date,status,begin', $recTotal = 0, $recPerPage = 20, $pageID = 1)
+    public function todo(int $userID, string $type = 'today', string $status = 'all', string $orderBy = 'date,status,begin', int $recTotal = 0, int $recPerPage = 20, int $pageID = 1)
     {
         $user = $this->user->getById($userID, 'id');
-        if(empty($user)) return print(js::error($this->lang->notFound) . js::locate($this->createLink('my', 'team')));
-        if($user->deleted == 1) return print(js::error($this->lang->user->noticeHasDeleted) . js::locate('back'));
+        if(empty($user)) return $this->send(array('result' => 'fail', 'load' => array('alert' => $this->lang->notFound, 'locate' => $this->createLink('my', 'team'))));
+        if($user->deleted) return $this->send(array('result' => 'fail', 'load' => array('alert' => $this->lang->user->noticeHasDeleted, 'locate' => array('back' => true))));
 
         /* Set this url to session. */
         $uri = $this->app->getURI(true);
         $this->session->set('todoList', $uri, 'my');
-        $this->session->set('bugList',  $uri, 'qa');
-        $this->session->set('taskList', $uri, 'execution');
 
         /* Load pager. */
-        $this->app->loadClass('pager', $static = true);
+        $this->app->loadClass('pager', true);
         $pager = pager::init($recTotal, $recPerPage, $pageID);
 
         /* Append id for second sort. */
         $sort = common::appendOrder($orderBy);
 
-        /* Get user, totos. */
-        $account = $user->account;
-        $todos   = $this->loadModel('todo')->getList($type, $account, $status, 0, $pager, $sort);
-        $date    = (int)$type == 0 ? helper::today() : $type;
-        $deptID  = $this->app->user->admin ? 0 : $this->app->user->dept;
-        $users   = $this->loadModel('dept')->getDeptUserPairs($deptID, 'id');
-        if(!isset($users[$userID])) return print(js::error($this->lang->user->error->noAccess) . js::locate('back'));
+        /* Get users and todos. */
+        $todos  = $this->loadModel('todo')->getList($type, $user->account, $status, 0, $pager, $sort);
+        $deptID = $this->app->user->admin ? 0 : $this->app->user->dept;
+        $users  = $this->loadModel('dept')->getDeptUserPairs($deptID, 'id');
+        if(!isset($users[$userID])) return $this->send(array('result' => 'fail', 'load' => array('alert' => $this->lang->user->error->noAccess, 'locate' => array('back' => true))));
 
-        /* set menus. */
-        $this->view->userList = $this->user->setUserList($users, $userID);
-
-        $this->view->title      = $this->lang->user->common . $this->lang->colon . $this->lang->user->todo;
-        $this->view->tabID      = 'todo';
-        $this->view->date       = $date;
-        $this->view->todos      = $todos;
-        $this->view->user       = $user;
-        $this->view->type       = $type;
-        $this->view->status     = $status;
-        $this->view->orderBy    = $orderBy;
-        $this->view->pager      = $pager;
+        $this->view->title   = $this->lang->user->common . $this->lang->colon . $this->lang->user->todo;
+        $this->view->users   = $users;
+        $this->view->todos   = $todos;
+        $this->view->user    = $user;
+        $this->view->type    = $type;
+        $this->view->status  = $status;
+        $this->view->orderBy = $orderBy;
+        $this->view->pager   = $pager;
 
         $this->display();
     }
diff --git a/module/user/ui/featurebar.html.php b/module/user/ui/featurebar.html.php
index 30b425251b..ee856b9a49 100755
--- a/module/user/ui/featurebar.html.php
+++ b/module/user/ui/featurebar.html.php
@@ -12,7 +12,7 @@ featureBar
         picker
         (
             width('120px'),
-            set::items($userList),
+            set::items($users),
             set::value($user->id),
             set::required(true),
             set::onChange(jsRaw("(value) => switchAccount(value)"))

From d6260b26d2df104fedd1da2cb5535e1ae7709c62 Mon Sep 17 00:00:00 2001
From: liumengyi 
Date: Tue, 21 Nov 2023 17:26:11 +0800
Subject: [PATCH 87/92] * Optimize code for testreport::getResultSummary.

---
 module/testreport/model.php                       | 10 +++++-----
 module/testreport/test/model/getresultsummary.php |  5 +++--
 2 files changed, 8 insertions(+), 7 deletions(-)

diff --git a/module/testreport/model.php b/module/testreport/model.php
index bccf0b8d72..60d2f69b31 100644
--- a/module/testreport/model.php
+++ b/module/testreport/model.php
@@ -204,12 +204,12 @@ class testreportModel extends model
             ->orderBy('date')
             ->fetchAll('id');
 
-        $failResults = 0;
-        $runCasesNum = array();
-        foreach($results as $result) $runCasesNum[$result->run] = $result->caseResult;
-        foreach($runCasesNum as $lastResult) if($lastResult == 'fail') $failResults++;
+        $failResults     = 0;
+        $runCasesResults = array();
+        foreach($results as $result) $runCasesResults[$result->run] = $result->caseResult;
+        foreach($runCasesResults as $lastResult) if($lastResult == 'fail') $failResults++;
 
-        return sprintf($this->lang->testreport->caseSummary, $caseCount, count($runCasesNum), count($results), $failResults);
+        return sprintf($this->lang->testreport->caseSummary, $caseCount, count($runCasesResults), count($results), $failResults);
     }
 
     /**
diff --git a/module/testreport/test/model/getresultsummary.php b/module/testreport/test/model/getresultsummary.php
index dce5b7f7cc..a13ebaf812 100755
--- a/module/testreport/test/model/getresultsummary.php
+++ b/module/testreport/test/model/getresultsummary.php
@@ -2,7 +2,6 @@
 gen(10);
 zdTable('testtask')->gen(10);
@@ -10,6 +9,8 @@ zdTable('testrun')->gen(10);
 zdTable('testresult')->gen(10);
 zdTable('testreport')->gen(10);
 
+su('admin');
+
 /**
 
 title=测试 testreportModel->getResultSummary();
@@ -25,4 +26,4 @@ $reportID = '1';
 
 $testreport = new testreportTest();
 r($testreport->getResultSummaryTest($taskID[0], $reportID)) && p() && e('共有4个用例,共执行4个用例,产生了4个结果,失败的用例有2个。'); //正常查询
-r($testreport->getResultSummaryTest($taskID[1], $reportID)) && p() && e('共有0个用例,共执行0个用例,产生了0个结果,失败的用例有0个。'); //taskID为空查询
\ No newline at end of file
+r($testreport->getResultSummaryTest($taskID[1], $reportID)) && p() && e('共有0个用例,共执行0个用例,产生了0个结果,失败的用例有0个。'); //taskID为空查询

From a61c81cd353a72e399b9bd986abadf743178ddd0 Mon Sep 17 00:00:00 2001
From: caoyanyi 
Date: Tue, 21 Nov 2023 17:32:05 +0800
Subject: [PATCH 88/92] * Adjust job create linkage.

---
 module/job/js/create.ui.js | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/module/job/js/create.ui.js b/module/job/js/create.ui.js
index 222449c903..b888a72864 100644
--- a/module/job/js/create.ui.js
+++ b/module/job/js/create.ui.js
@@ -13,11 +13,13 @@ function changeEngine(event)
 
     var repos     = engine == 'gitlab' ? gitlabRepos : repoPairs;
     var repoItems = [];
+    var repoID    = 0;
     for(i in repos)
     {
+        if(repoID == 0) repoID = i;
         repoItems.push({'text': repos[i], 'value': i});
     }
-    zui.Picker.query('#repo').render({items: repoItems});
+    zui.Picker.query('#repo').render({items: repoItems, value: repoID});
 
     if(engine == 'gitlab')
     {
@@ -36,6 +38,8 @@ function changeEngine(event)
         if(engine == 'jenkins' || frame != 'sonarqube') items.push({'text': frameList[frame], 'value': frame});
     }
     zui.Picker.query('#frame').render({items: items});
+
+    changeRepo();
 }
 
 function changeFrame(event)
@@ -56,7 +60,7 @@ function changeFrame(event)
 
 function changeRepo(event)
 {
-    const repoID = $(event.target).val();
+    const repoID = $('input[name="repo"]').val();
     if(repoID <= 0) return;
 
     var link = $.createLink('repo', 'ajaxLoadProducts', 'repoID=' + repoID);

From c0cf8f1a74e1fdafe65d7d4b942cea255b8ef7b2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9C=B1=E9=87=91=E5=8B=87?= 
Date: Tue, 21 Nov 2023 09:32:40 +0000
Subject: [PATCH 89/92] * Set max consumer to 2.

---
 module/cron/config.php  | 5 +++--
 module/cron/control.php | 4 ++--
 2 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/module/cron/config.php b/module/cron/config.php
index aba956851f..b002b13c87 100644
--- a/module/cron/config.php
+++ b/module/cron/config.php
@@ -5,8 +5,9 @@ $config->cron->edit   = new stdclass();
 $config->cron->create->requiredFields = 'm,h,dom,mon,dow,command';
 $config->cron->edit->requiredFields   = 'm,h,dom,mon,dow,command';
 
-$config->cron->maxRunDays = 8;
-$config->cron->maxRunTime = 65;
+$config->cron->maxRunDays  = 8;
+$config->cron->maxRunTime  = 65;
+$config->cron->maxConsumer = 2;
 
 global $lang;
 $config->cron->dtable = new stdclass();
diff --git a/module/cron/control.php b/module/cron/control.php
index 2aa3dda32f..5c5c302e1b 100644
--- a/module/cron/control.php
+++ b/module/cron/control.php
@@ -257,8 +257,8 @@ class cron extends control
             {
                 if($setting->value > $expirDate)
                 {
-                    if($setting->key == strval($execId)) $roles[] = 'consumer';
                     $consumerCount ++;
+                    if($consumer < $this->config->cron->maxConsumer && $setting->key == strval($execId)) $roles[] = 'consumer';
                 }
                 else
                 {
@@ -268,7 +268,7 @@ class cron extends control
         }
 
         if(in_array($scheduler['execId'], array(0, $execId)) || $scheduler['lastTime'] < $expirDate) $roles[] = 'scheduler';
-        if($consumerCount < 4) $roles[] = 'consumer';
+        if($consumerCount < $this->config->cron->maxConsumer) $roles[] = 'consumer';
 
         return $roles;
     }

From adcbc6c8cc2df20279ca243b8b3b0a3df0883037 Mon Sep 17 00:00:00 2001
From: qixinzhi 
Date: Tue, 21 Nov 2023 21:16:35 +0800
Subject: [PATCH 90/92] * Fix user name with pre letter,bug#35720.

---
 module/pivot/model.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/module/pivot/model.php b/module/pivot/model.php
index f1aabdb1b1..7b3acbc4b7 100644
--- a/module/pivot/model.php
+++ b/module/pivot/model.php
@@ -2112,7 +2112,7 @@ class pivotModel extends model
         switch($type)
         {
             case 'user':
-                $options = $this->loadModel('user')->getPairs();
+                $options = $this->loadModel('user')->getPairs('noletter');
                 break;
             case 'product':
                 $options = $this->loadModel('product')->getPairs();

From 7067fc66d8cad3c39d41007d2160055a77da3c49 Mon Sep 17 00:00:00 2001
From: qixinzhi 
Date: Tue, 21 Nov 2023 22:11:57 +0800
Subject: [PATCH 91/92] * Fix the bug assign name of pivot,bug#40551.

---
 module/pivot/lang/zh-cn.php  | 4 ++--
 module/pivot/lang/zh-tw.php  | 4 ++--
 module/report/lang/zh-cn.php | 2 +-
 module/report/lang/zh-tw.php | 2 +-
 4 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/module/pivot/lang/zh-cn.php b/module/pivot/lang/zh-cn.php
index eda94bb74c..8af106b5d9 100644
--- a/module/pivot/lang/zh-cn.php
+++ b/module/pivot/lang/zh-cn.php
@@ -100,7 +100,7 @@ $lang->pivot->singleColor[] = 'F6BD0F';
 $lang->pivot->projectDeviation = "{$lang->execution->common}偏差报表";
 $lang->pivot->productSummary   = $lang->productCommon . '汇总表';
 $lang->pivot->bugCreate        = 'Bug创建表';
-$lang->pivot->bugAssign        = 'Bug指派表';
+$lang->pivot->bugAssign        = '未解决Bug指派表';
 $lang->pivot->workload         = '员工负载表';
 $lang->pivot->workloadAB       = '工作负载';
 $lang->pivot->bugOpenedDate    = 'Bug创建时间';
@@ -119,7 +119,7 @@ $lang->pivotList->staff   = new stdclass();
 $lang->pivotList->product->lists[10] = $lang->productCommon . '汇总表|pivot|productsummary';
 $lang->pivotList->project->lists[10] = "{$lang->execution->common}偏差报表|pivot|projectdeviation";
 $lang->pivotList->test->lists[10]    = 'Bug创建表|pivot|bugcreate';
-$lang->pivotList->test->lists[13]    = 'Bug指派表|pivot|bugassign';
+$lang->pivotList->test->lists[13]    = '未解决Bug指派表|pivot|bugassign';
 $lang->pivotList->staff->lists[10]   = '员工负载表|pivot|workload';
 
 $lang->pivot->id            = '编号';
diff --git a/module/pivot/lang/zh-tw.php b/module/pivot/lang/zh-tw.php
index 38d70ecb3c..9fad2ba0bb 100644
--- a/module/pivot/lang/zh-tw.php
+++ b/module/pivot/lang/zh-tw.php
@@ -41,7 +41,7 @@ $lang->pivot->singleColor[] = 'F6BD0F';
 $lang->pivot->projectDeviation = "{$lang->execution->common}偏差報表";
 $lang->pivot->productSummary   = $lang->productCommon . '彙總表';
 $lang->pivot->bugCreate        = 'Bug創建表';
-$lang->pivot->bugAssign        = 'Bug指派表';
+$lang->pivot->bugAssign        = '未解决Bug指派表';
 $lang->pivot->workload         = '員工負載表';
 $lang->pivot->workloadAB       = '工作負載';
 $lang->pivot->bugOpenedDate    = 'Bug創建時間';
@@ -60,7 +60,7 @@ $lang->pivotList->staff   = new stdclass();
 $lang->pivotList->product->lists[10] = $lang->productCommon . '彙總表|pivot|productsummary';
 $lang->pivotList->project->lists[10] = "{$lang->execution->common}偏差報表|pivot|projectdeviation";
 $lang->pivotList->test->lists[10]    = 'Bug創建表|pivot|bugcreate';
-$lang->pivotList->test->lists[13]    = 'Bug指派表|pivot|bugassign';
+$lang->pivotList->test->lists[13]    = '未解决Bug指派表|pivot|bugassign';
 $lang->pivotList->staff->lists[10]   = '員工負載表|pivot|workload';
 
 $lang->pivot->id            = '編號';
diff --git a/module/report/lang/zh-cn.php b/module/report/lang/zh-cn.php
index 5688e10bab..9e207da871 100644
--- a/module/report/lang/zh-cn.php
+++ b/module/report/lang/zh-cn.php
@@ -39,7 +39,7 @@ $lang->report->singleColor[] = 'F6BD0F';
 $lang->report->projectDeviation = "{$lang->execution->common}偏差报表";
 $lang->report->productSummary   = $lang->productCommon . '汇总表';
 $lang->report->bugCreate        = 'Bug创建表';
-$lang->report->bugAssign        = 'Bug指派表';
+$lang->report->bugAssign        = '未解决Bug指派表';
 $lang->report->workload         = '员工负载表';
 $lang->report->workloadAB       = '工作负载';
 $lang->report->bugOpenedDate    = 'Bug创建时间';
diff --git a/module/report/lang/zh-tw.php b/module/report/lang/zh-tw.php
index edc44b91bc..3a84671f24 100644
--- a/module/report/lang/zh-tw.php
+++ b/module/report/lang/zh-tw.php
@@ -39,7 +39,7 @@ $lang->report->singleColor[] = 'F6BD0F';
 $lang->report->projectDeviation = "{$lang->execution->common}偏差報表";
 $lang->report->productSummary   = $lang->productCommon . '彙總表';
 $lang->report->bugCreate        = 'Bug創建表';
-$lang->report->bugAssign        = 'Bug指派表';
+$lang->report->bugAssign        = '未解决Bug指派表';
 $lang->report->workload         = '員工負載表';
 $lang->report->workloadAB       = '工作負載';
 $lang->report->bugOpenedDate    = 'Bug創建時間';

From 55b76ba55ae81eba871349c85911c7ccbc18fcf7 Mon Sep 17 00:00:00 2001
From: caoyanyi 
Date: Wed, 22 Nov 2023 08:37:03 +0800
Subject: [PATCH 92/92] * Adjust search form data for repo.

---
 module/repo/config.php           | 2 --
 module/repo/ui/maintain.html.php | 2 +-
 2 files changed, 1 insertion(+), 3 deletions(-)

diff --git a/module/repo/config.php b/module/repo/config.php
index 79e4db0ac6..3c524ee28c 100644
--- a/module/repo/config.php
+++ b/module/repo/config.php
@@ -161,11 +161,9 @@ $config->repo->fileExt["yaml"]             = array('.yaml', '.yml');
 
 $config->repo->search['module'] = 'repo';
 $config->repo->search['fields']['name']     = $lang->repo->name;
-$config->repo->search['fields']['id']       = $lang->repo->id;
 $config->repo->search['fields']['product']  = $lang->repo->product;
 $config->repo->search['fields']['projects'] = $lang->repo->projects;
 $config->repo->search['fields']['SCM']      = $lang->repo->SCM;
-$config->repo->search['fields']['desc']     = $lang->repo->desc;
 
 $config->repo->search['params']['name']     = array('operator' => 'include', 'control' => 'input', 'values' => '');
 $config->repo->search['params']['product']  = array('operator' => 'include', 'control' => 'select', 'values' => '');
diff --git a/module/repo/ui/maintain.html.php b/module/repo/ui/maintain.html.php
index edfa0a637e..5d3500aa46 100644
--- a/module/repo/ui/maintain.html.php
+++ b/module/repo/ui/maintain.html.php
@@ -62,7 +62,7 @@ $queryMenuLink = createLink('repo', 'maintain', "objectID=$objectID&orderBy=&rec
 (
     set::current('all'),
     set::queryMenuLinkCallback(fn($key) => str_replace('{queryID}', (string)$key, $queryMenuLink)),
-    li(searchToggle(set::open($type == 'bySearch')))
+    li(searchToggle(set::module('repo'), set::open($type == 'bySearch')))
 );
 
 toolBar