Merge branch 'main' into feature/doctemplate

This commit is contained in:
daitingting
2025-03-31 06:58:16 +00:00
259 changed files with 13277 additions and 1823 deletions
+1
View File
@@ -118,6 +118,7 @@ zentaoxx:
sed -i "s/text NOT NULL DEFAULT ''/text NULL/" zentaoxx/db_bak/*.sql
sed -i "s/text NOT NULL/text NULL/" zentaoxx/db_bak/*.sql
sed -i "s/ENGINE=MyISAM/ENGINE=InnoDB/" zentaoxx/db_bak/*.sql
sed -i "s/utf8/utf8mb4/" zentaoxx/db_bak/*.sql
sed -i "/`xxb_user` ADD `clientStatus`/d; /`xxb_user` ADD `clientLang`/d; /`xxb_file` CHANGE `pathname`/d" zentaoxx/db_bak/xuanxuan.sql
cp zentaoxx/db_bak/upgradexuanxuan*.sql zentaoxx/db_bak/xuanxuan.sql zentaoxx/db/
rm -rf zentaoxx/db_bak/
+1 -1
View File
@@ -1 +1 @@
21.5
21.6
+5 -4
View File
@@ -11,7 +11,7 @@
**/
class bugActiveEntry extends entry
{
/**
/**
* POST method.
*
* @param int $bugID
@@ -19,11 +19,12 @@ class bugActiveEntry extends entry
* @return string
*/
public function post($bugID)
{
{
$control = $this->loadController('bug', 'activate');
$fields = 'assignedTo,uid,openedBuild,comment';
$this->batchSetPost($fields);
$control = $this->loadController('bug', 'activate');
$control->activate($bugID);
$data = $this->getData();
@@ -33,6 +34,6 @@ class bugActiveEntry extends entry
$bug = $this->loadModel('bug')->getByID($bugID);
return $this->send(200, $this->format($bug, 'openedBy:user,openedDate:time,assignedTo:user,assignedDate:time,reviewedBy:user,reviewedDate:time,lastEditedBy:user,lastEditedDate:time,closedBy:user,closedDate:time,deleted:bool,mailto:userList'));
}
}
}
+5 -4
View File
@@ -11,7 +11,7 @@
**/
class bugAssignEntry extends entry
{
/**
/**
* POST method.
*
* @param int $bugID
@@ -19,11 +19,12 @@ class bugAssignEntry extends entry
* @return string
*/
public function post($bugID)
{
{
$control = $this->loadController('bug', 'assignTo');
$fields = 'assignedTo,mailto,comment';
$this->batchSetPost($fields);
$control = $this->loadController('bug', 'assignTo');
$control->assignTo($bugID);
$data = $this->getData();
@@ -33,6 +34,6 @@ class bugAssignEntry extends entry
$bug = $this->loadModel('bug')->getByID($bugID);
return $this->send(200, $this->format($bug, 'openedBy:user,openedDate:time,assignedTo:user,assignedDate:time,reviewedBy:user,reviewedDate:time,lastEditedBy:user,lastEditedDate:time,closedBy:user,closedDate:time,deleted:bool,mailto:userList'));
}
}
}
+5 -4
View File
@@ -11,7 +11,7 @@
**/
class bugCloseEntry extends entry
{
/**
/**
* POST method.
*
* @param int $bugID
@@ -19,11 +19,12 @@ class bugCloseEntry extends entry
* @return string
*/
public function post($bugID)
{
{
$control = $this->loadController('bug', 'close');
$fields = 'comment';
$this->batchSetPost($fields);
$control = $this->loadController('bug', 'close');
$control->close($bugID);
$data = $this->getData();
@@ -32,6 +33,6 @@ class bugCloseEntry extends entry
$bug = $this->loadModel('bug')->getByID($bugID);
return $this->send(200, $this->format($bug, 'openedBy:user,openedDate:time,assignedTo:user,assignedDate:time,reviewedBy:user,reviewedDate:time,lastEditedBy:user,lastEditedDate:time,closedBy:user,closedDate:time,deleted:bool,mailto:userList'));
}
}
}
+2 -1
View File
@@ -20,11 +20,12 @@ class bugConfirmEntry extends entry
**/
public function post($bugID)
{
$control = $this->loadController('bug', 'confirm');
$fields = 'assignedTo,mailto,comment,pri,type,status,deadline';
$bug = $this->loadModel('bug')->getByID($bugID);
$this->batchSetPost($fields, $bug);
$control = $this->loadController('bug', 'confirm');
$control->confirm($bugID);
$data = $this->getData();
+2 -1
View File
@@ -45,9 +45,10 @@ class bugRecordEstimateEntry extends entry
{
if($this->config->edition == 'open') return $this->send400('ZenTaoPMS does not have bug effort function.');
$control = $this->loadController('effort', 'createForObject');
$fields = 'id,dates,consumed,objectType,objectID,work';
$this->batchSetPost($fields);
$control = $this->loadController('effort', 'createForObject');
$control->createForObject('bug', $bugID);
$data = $this->getData();
+2 -1
View File
@@ -20,10 +20,11 @@ class bugResolveEntry extends entry
*/
public function post($bugID)
{
$control = $this->loadController('bug', 'resolve');
$fields = 'resolution,resolvedBuild,resolvedDate,duplicateBug,assignedTo,uid,comment';
$this->batchSetPost($fields);
$control = $this->loadController('bug', 'resolve');
$control->resolve($bugID);
$data = $this->getData();
+2 -2
View File
@@ -80,6 +80,8 @@ class executionsEntry extends entry
{
$useCode = $this->checkCodeUsed();
$control = $this->loadController('execution', 'create');
$fields = 'project,name,begin,end,lifetime,desc,days,percent,parent';
if($useCode) $fields .= ',code';
@@ -96,8 +98,6 @@ class executionsEntry extends entry
$this->setPost('products', $this->request('products', array()));
$this->setPost('plans', $this->request('plans', array()));
$control = $this->loadController('execution', 'create');
$requireFields = 'name,begin,end';
if($useCode) $requireFields .= ',code';
$this->requireFields($requireFields);
+2 -1
View File
@@ -55,12 +55,13 @@ class feedbacksEntry extends entry
*/
public function post()
{
$control = $this->loadController('feedback', 'create');
$fields = 'module,product,type,title,public,desc,status,feedbackBy,notify,uid,pri';
$this->batchSetPost($fields);
$this->setPost('notifyEmail', $this->request('notifyEmail', ''));
$control = $this->loadController('feedback', 'create');
$this->requireFields('title,product');
$control->create();
+3
View File
@@ -30,6 +30,9 @@ class gitlabWebhookEntry extends baseEntry
$this->app->user = new stdclass();
$this->app->user->account = '';
$this->app->user->admin = false;
$this->app->user->rights['rights'] = array();
$this->app->user->rights['acls'] = array();
$repo = $this->loadModel('repo')->getByID($repoID);
if(empty($repo)) return;
+2 -1
View File
@@ -19,10 +19,11 @@ class mrEntry extends baseEntry
*/
public function post()
{
$this->loadController('mr', 'create');
$fields = 'repoID,jobID,sourceBranch,targetBranch,diffs,mergeStatus';
$this->batchSetPost($fields);
$this->loadController('mr', 'create');
$MRID = $this->loadModel('mr')->apiCreate();
if(dao::isError()) $this->sendError(400, dao::getError());
-1
View File
@@ -20,7 +20,6 @@ class pipelinesEntry extends entry
*/
public function get()
{
$pipeline = $this->param('pipeline', '');
$repoUrl = $this->param('repoUrl', '');
if(empty($pipeline) and empty($repoUrl)) return $this->sendError(400, 'The parameter is incorrect!');
+2 -1
View File
@@ -20,10 +20,11 @@ class productplanLinkBugsEntry extends entry
*/
public function post($planID)
{
$control = $this->loadController('productplan', 'linkBug');
$fields = 'bugs';
$this->batchSetPost($fields);
$control = $this->loadController('productplan', 'linkBug');
$control->linkBug($planID);
$data = $this->getData();
+2 -1
View File
@@ -20,10 +20,11 @@ class productplanLinkStoriesEntry extends entry
*/
public function post($planID)
{
$control = $this->loadController('productplan', 'linkStory');
$fields = 'stories';
$this->batchSetPost($fields);
$control = $this->loadController('productplan', 'linkStory');
$control->linkStory($planID);
$data = $this->getData();
+2 -1
View File
@@ -70,13 +70,14 @@ class productplansEntry extends entry
if(!$productID) $productID = $this->param('product', 0);
if(!$productID) return $this->sendError(400, 'No product id.');
$control = $this->loadController('productplan', 'create');
$fields = 'branch,begin,end,title,desc';
$this->batchSetPost($fields);
$this->setPost('product', $productID);
$this->setPost('parent', $this->request('parent', 0));
$this->setPost('branch', $this->request('branch', 0));
$control = $this->loadController('productplan', 'create');
$control->create($productID, $this->param('branch', 0), $this->param('parent', 0));
$data = $this->getData();
+2 -1
View File
@@ -63,6 +63,8 @@ class productProjectsEntry extends entry
*/
public function post()
{
$control = $this->loadController('project', 'create');
$fields = 'name,begin,end,products';
$this->batchSetPost($fields);
@@ -73,7 +75,6 @@ class productProjectsEntry extends entry
$this->setPost('PM', $this->request('PM', ''));
$this->setPost('model', $this->request('model', 'scrum'));
$control = $this->loadController('project', 'create');
$this->requireFields('name,code,begin,end,products');
$control->create($this->request('model', 'scrum'));
+2 -1
View File
@@ -72,12 +72,13 @@ class programsEntry extends entry
*/
public function post()
{
$control = $this->loadController('program', 'create');
$fields = 'name,PM,budget,budgetUnit,desc,begin,end';
$this->batchSetPost($fields);
$this->setPost('acl', $this->request('acl', 'open'));
$this->setPost('whitelist', $this->request('whitelist', array()));
$control = $this->loadController('program', 'create');
$this->requireFields('name,begin,end');
$control->create($this->request('parent', 0));
+2 -1
View File
@@ -67,6 +67,8 @@ class stakeholdersEntry extends entry
*/
public function post()
{
$control = $this->loadController('project', 'create');
$fields = 'name,begin,end,products';
$this->batchSetPost($fields);
@@ -77,7 +79,6 @@ class stakeholdersEntry extends entry
$this->setPost('PM', $this->request('PM', ''));
$this->setPost('model', $this->request('model', 'scrum'));
$control = $this->loadController('project', 'create');
$this->requireFields('name,code,begin,end,products');
$control->create($this->request('model', 'scrum'));
+2 -1
View File
@@ -69,6 +69,8 @@ class storiesEntry extends entry
if(!$productID and isset($this->requestBody->product)) $productID = $this->requestBody->product;
if(!$productID) return $this->sendError(400, 'Need product id.');
$control = $this->loadController('story', 'create');
$fields = 'title,spec,verify,module,reviewer,type,parent,moduleOptionMenu,source,sourceNote,category,pri,estimate,mailto,keywords,notifyemail,uid,status';
$this->batchSetPost($fields);
@@ -83,7 +85,6 @@ class storiesEntry extends entry
$this->setPost('type', $this->param('type', 'story'));
$this->setPost('status', $this->param('status', 'draft'));
$control = $this->loadController('story', 'create');
$this->requireFields('title,spec,pri,category');
$control->create($productID, $this->param('branch', 0), $this->param('moduleID', 0), $this->param('storyID', 0), $this->param('objectID', 0), $this->param('bugID', 0), 0, 0, '', $this->param('type', 'story'));
+2 -2
View File
@@ -21,11 +21,11 @@ class storyActiveEntry extends entry
*/
public function post($storyID)
{
$control = $this->loadController('story', 'activate');
$fields = 'assignedTo,status,comment';
$this->batchSetPost($fields);
$control = $this->loadController('story', 'activate');
$control->activate($storyID);
$data = $this->getData();
+2 -1
View File
@@ -20,10 +20,11 @@ class storyAssignToEntry extends entry
*/
public function post($storyID)
{
$control = $this->loadController('story', 'assignTo');
$fields = 'assignedTo,comment';
$this->batchSetPost($fields);
$control = $this->loadController('story', 'assignTo');
$control->assignTo($storyID);
$data = $this->getData();
+2 -1
View File
@@ -20,10 +20,11 @@ class storyCloseEntry extends entry
*/
public function post($storyID)
{
$control = $this->loadController('story', 'close');
$fields = 'closedReason,duplicateStory,comment';
$this->batchSetPost($fields);
$control = $this->loadController('story', 'close');
$control->close($storyID);
$data = $this->getData();
+2 -1
View File
@@ -45,9 +45,10 @@ class storyRecordEstimateEntry extends entry
{
if($this->config->edition == 'open') return $this->send400('ZenTaoPMS does not have story effort function.');
$control = $this->loadController('effort', 'createForObject');
$fields = 'id,dates,consumed,objectType,objectID,work';
$this->batchSetPost($fields);
$control = $this->loadController('effort', 'createForObject');
$control->createForObject('story', $storyID);
$data = $this->getData();
+2 -2
View File
@@ -21,11 +21,11 @@ class storyReviewEntry extends entry
*/
public function post($storyID)
{
$control = $this->loadController('story', 'review');
$fields = 'reviewedDate,result,closedReason,pri,estimate,comment';
$this->batchSetPost($fields);
$control = $this->loadController('story', 'review');
$control->review($storyID);
$data = $this->getData();
+5 -4
View File
@@ -11,7 +11,7 @@
**/
class taskActiveEntry extends entry
{
/**
/**
* POST method.
*
* @param int $taskID
@@ -19,11 +19,12 @@ class taskActiveEntry extends entry
* @return string
*/
public function post($taskID)
{
{
$control = $this->loadController('task', 'activate');
$fields = 'assignedTo,left,comment';
$this->batchSetPost($fields);
$control = $this->loadController('task', 'activate');
$control->activate($taskID);
$data = $this->getData();
@@ -33,6 +34,6 @@ class taskActiveEntry extends entry
$task = $this->loadModel('task')->getByID($taskID);
return $this->send(200, $this->format($task, 'openedBy:user,openedDate:time,assignedTo:user,assignedDate:time,reviewedBy:user,reviewedDate:time,lastEditedBy:user,lastEditedDate:time,closedBy:user,closedDate:time,deleted:bool,mailto:userList'));
}
}
}
+2 -1
View File
@@ -23,6 +23,8 @@ class taskBatchCreateEntry extends entry
if(!$executionID) $executionID = $this->param('execution', 0);
if(!$executionID) return $this->send400('Need execution id.');
$control = $this->loadController('task', 'batchCreate');
$storyID = $this->param('story', 0);
$moduleID = $this->param('module', 0);
$taskID = $this->param('task', 0);
@@ -72,7 +74,6 @@ class taskBatchCreateEntry extends entry
$this->setPost('pri', $pri);
$this->setPost('story', $stories);
$control = $this->loadController('task', 'batchCreate');
$control->batchCreate($executionID, $storyID, $moduleID, $taskID);
$data = $this->getData();
+2 -1
View File
@@ -50,9 +50,10 @@ class taskRecordEstimateEntry extends entry
*/
public function post($taskID)
{
$control = $this->loadController('task', 'recordWorkhour');
$fields = 'date,consumed,left,work';
$this->batchSetPost($fields);
$control = $this->loadController('task', 'recordWorkhour');
$control->recordWorkhour($taskID);
$data = $this->getData();
+2 -1
View File
@@ -101,6 +101,8 @@ class tasksEntry extends entry
*/
public function post($executionID)
{
$control = $this->loadController('task', 'create');
$fields = 'name,type,assignedTo,estimate,story,execution,project,module,pri,desc,estStarted,deadline,mailto,team,teamEstimate,multiple,uid';
$this->batchSetPost($fields);
@@ -116,7 +118,6 @@ class tasksEntry extends entry
$this->setPost('teamSource', array_fill(0, count($this->request('team')), ''));
}
$control = $this->loadController('task', 'create');
$this->requireFields('name,assignedTo,type,estStarted,deadline');
$control->create($executionID, $this->request('storyID', 0), $this->request('moduleID', 0), $this->request('copyTaskID', 0), $this->request('copyTodoID', 0));
+10 -5
View File
@@ -74,6 +74,8 @@ class testcasesEntry extends entry
if(!$productID and isset($this->requestBody->product)) $productID = $this->requestBody->product;
if(!$productID) return $this->sendError(400, 'Need product id.');
$control = $this->loadController('testcase', 'create');
$fields = 'module,type,stage,story,title,precondition,pri';
$this->batchSetPost($fields);
$this->setPost('product', $productID);
@@ -84,20 +86,23 @@ class testcasesEntry extends entry
$steps = array();
$expects = array();
$stepType = array();
$index = 1;
$useName = count(array_column($this->requestBody->steps, 'name')) == count($this->requestBody->steps);
foreach($this->requestBody->steps as $step)
{
$type = isset($step->type) ? $step->type : 'step';
$name = $useName ? $step->name : $index;
$type = isset($step->type) ? $step->type : 'step';
$index ++;
$steps[] = $step->desc;
$expects[] = $type == 'group' ? '' : $step->expect;
$stepType[] = $type;
$steps[$name] = $step->desc;
$expects[$name] = $type == 'group' ? '' : $step->expect;
$stepType[$name] = $type;
}
$this->setPost('steps', $steps);
$this->setPost('expects', $expects);
$this->setPost('stepType', $stepType);
}
$control = $this->loadController('testcase', 'create');
$this->requireFields('title,type,pri,steps');
$control->create(0);
+2 -1
View File
@@ -57,13 +57,14 @@ class testsuitesEntry extends entry
if(!$productID and isset($this->requestBody->product)) $productID = $this->requestBody->product;
if(!$productID) return $this->sendError(400, 'Need product id.');
$control = $this->loadController('testsuite', 'create');
$fields = 'name,type';
$this->batchSetPost($fields);
$this->setPost('product', $productID);
$this->setPost('desc', $this->request('desc', ''));
$this->setPost('type', $this->request('type', 'private'));
$control = $this->loadController('testsuite', 'create');
$this->requireFields('name');
$control->create($productID);
+2 -1
View File
@@ -54,10 +54,11 @@ class ticketsEntry extends entry
*/
public function post()
{
$control = $this->loadController('ticket', 'create');
$fields = 'module,product,type,openedBuild,assignedTo,deadline,title,desc,status,notify,uid,pri';
$this->batchSetPost($fields);
$control = $this->loadController('ticket', 'create');
$this->requireFields('title,product,module');
$control->create();
+2 -1
View File
@@ -43,6 +43,8 @@ class todosEntry extends entry
*/
public function post()
{
$control = $this->loadController('todo', 'create');
$fields = 'name,desc,begin,end,private';
$this->batchSetPost($fields);
@@ -53,7 +55,6 @@ class todosEntry extends entry
$this->setPost('end', str_replace(':', '', $this->request('end')));
$this->setPost('pri', $this->request('pri', '3'));
$control = $this->loadController('todo', 'create');
$this->requireFields('name');
$control->create();
+2 -1
View File
@@ -55,6 +55,8 @@ class usersEntry extends entry
*/
public function post()
{
$control = $this->loadController('user', 'create');
$fields = 'type,dept,account,password,visions,realname,join,role,email,commiter,gender,group,passwordStrength';
$this->batchSetPost($fields);
@@ -74,7 +76,6 @@ class usersEntry extends entry
$this->setPost('passwordLength', strlen($_POST['password1']));
unset($_POST['password']);
$control = $this->loadController('user', 'create');
$this->requireFields('account,gender,password1,realname');
$control->create();
+3 -1
View File
@@ -16,7 +16,7 @@ if(!class_exists('config')){class config{}}
if(!function_exists('getWebRoot')){function getWebRoot(){}}
/* 基本设置。Basic settings. */
$config->version = '21.5'; // ZenTaoPHP的版本。 The version of ZenTaoPHP. Don't change it.
$config->version = '21.6'; // ZenTaoPHP的版本。 The version of ZenTaoPHP. Don't change it.
$config->liteVersion = '1.2'; // 迅捷版版本。 The version of Lite.
$config->charset = 'UTF-8'; // ZenTaoPHP的编码。 The encoding of ZenTaoPHP.
$config->cookieLife = time() + 2592000; // Cookie的生存时间。The cookie life time.
@@ -270,3 +270,5 @@ else
unset($config->maxVersion);
unset($config->ipdVersion);
}
if(strtolower($config->db->encoding) == 'utf8') $config->db->encoding = 'utf8mb4';
+4
View File
@@ -582,6 +582,7 @@ define('TABLE_RISK', '`' . $config->db->prefix . 'risk`');
define('TABLE_RISKISSUE', '`' . $config->db->prefix . 'riskissue`');
define('TABLE_SOLUTIONS', '`' . $config->db->prefix . 'solutions`');
define('TABLE_SQLVIEW', '`' . $config->db->prefix . 'sqlview`');
define('TABLE_SQLBUILDER', '`' . $config->db->prefix . 'sqlbuilder`');
define('TABLE_TICKET', '`' . $config->db->prefix . 'ticket`');
define('TABLE_TICKETRELATION', '`' . $config->db->prefix . 'ticketrelation`');
define('TABLE_TICKETSOURCE', '`' . $config->db->prefix . 'ticketsource`');
@@ -638,6 +639,9 @@ if(!defined('TABLE_MARKETREPORT')) define('TABLE_MARKETREPORT', '`' . $confi
if(!defined('TABLE_MARKETRESEARCH')) define('TABLE_MARKETRESEARCH', '`' . $config->db->prefix . 'project`');
if(!defined('TABLE_SYSTEM')) define('TABLE_SYSTEM', '`' . $config->db->prefix . 'system`');
define('JIRA_TMPRELATION', '`jiratmprelation`');
define('CONFLUENCE_TMPRELATION', '`confluencetmprelation`');
$config->objectTables['dept'] = TABLE_DEPT;
$config->objectTables['product'] = TABLE_PRODUCT;
$config->objectTables['productplan'] = TABLE_PRODUCTPLAN;
-3
View File
@@ -1,3 +0,0 @@
ALTER TABLE `zt_workflowgroup` MODIFY `name` varchar(255) NOT NULL DEFAULT '';
ALTER TABLE `zt_workflowgroup` ADD COLUMN `exclusive` enum('0','1') NOT NULL DEFAULT '0' AFTER `main`;
ALTER TABLE `zt_doclib` ADD COLUMN `archived` enum('0','1') NOT NULL DEFAULT '0';
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+6
View File
@@ -3,3 +3,9 @@ ALTER TABLE `zt_feedback` ADD COLUMN `prevAssignedTo` varchar(255) NOT NULL DEFA
ALTER TABLE `zt_doc` ADD COLUMN `readGroups` varchar(255) NOT NULL DEFAULT '' AFTER `users`;
ALTER TABLE `zt_doc` ADD COLUMN `readUsers` text NULL AFTER `readGroups`;
ALTER TABLE `zt_workflowgroup` MODIFY `name` varchar(255) NOT NULL DEFAULT '';
ALTER TABLE `zt_workflowgroup` ADD COLUMN `exclusive` enum('0','1') NOT NULL DEFAULT '0' AFTER `main`;
ALTER TABLE `zt_doclib` ADD COLUMN `archived` enum('0','1') NOT NULL DEFAULT '0';
UPDATE `zt_cron` SET `command` = 'moduleName=compile&methodName=ajaxSyncCompile' WHERE `command` = 'moduleName=compile&methodName=syncCompile';
+53
View File
@@ -0,0 +1,53 @@
ALTER TABLE `zt_compile` CHANGE `status` `status` varchar(100) NOT NULL DEFAULT '';
ALTER TABLE `zt_measqueue` CHANGE `status` `status` varchar(100) NOT NULL DEFAULT '';
REPLACE INTO `zt_workflowaction` (`module`, `action`, `method`, `name`, `type`, `batchMode`, `extensionType`, `open`, `position`, `layout`, `show`, `order`, `buildin`, `role`, `virtual`, `conditions`, `verifications`, `hooks`, `linkages`, `js`, `css`, `toList`, `blocks`, `desc`, `status`, `vision`, `createdBy`, `createdDate`, `editedBy`, `editedDate`) VALUES
('feedback', 'batchcreate', 'batchcreate', '批量创建', 'batch', 'different', 'none', 'normal', 'browse', 'normal', 'direct', 0, 1, 'buildin', 0, NULL, NULL, NULL, NULL, NULL, NULL, '', NULL, NULL, 'enable', 'rnd', 'admin', '2025-03-24 14:50:45', '', NULL),
('feedback', 'exporttemplate', 'exporttemplate', '导出模板', 'single', 'different', 'none', 'modal', 'browse', 'normal', 'direct', 0, 1, 'buildin', 0, NULL, NULL, NULL, NULL, NULL, NULL, '', NULL, '', 'enable', 'rnd', 'admin', '2025-03-24 14:50:45', '', NULL),
('feedback', 'import', 'import', '导入', 'single', 'different', 'none', 'modal', 'browse', 'normal', 'direct', 0, 1, 'buildin', 0, NULL, NULL, NULL, NULL, NULL, NULL, '', NULL, NULL, 'enable', 'rnd', 'admin', '2025-03-24 14:50:45', '', NULL),
('feedback', 'showimport', 'showimport', '显示导入内容', 'single', 'different', 'none', 'modal', 'browse', 'normal', 'direct', 0, 1, 'buildin', 0, NULL, NULL, NULL, NULL, NULL, NULL, '', NULL, NULL, 'enable', 'rnd', 'admin', '2025-03-24 14:50:45', '', NULL);
DELETE FROM `zt_workflowdatasource` WHERE `code` IN ('color', 'litecolor');
INSERT INTO `zt_workflowdatasource` (`type`, `name`, `code`, `buildin`, `vision`, `createdBy`, `createdDate`, `datasource`, `view`, `keyField`, `valueField`) VALUES
('option', '颜色', 'color', '1', 'rnd', 'admin', '1970-01-01 00:00:01', '{"#ef4444":"#ef4444","#f97316":"#f97316","#eab308":"#eab308","#84cc16":"#84cc16","#22c55e":"#22c55e","#14b8a6":"#14b8a6","#0ea5e9":"#0ea5e9","#6366f1":"#6366f1","#a855f7":"#a855f7","#d946ef":"#d946ef","#ec4899":"#ec4899"}', '', '', ''),
('option', '颜色', 'litecolor', '1', 'lite', 'admin', '1970-01-01 00:00:01', '{"#ef4444":"#ef4444","#f97316":"#f97316","#eab308":"#eab308","#84cc16":"#84cc16","#22c55e":"#22c55e","#14b8a6":"#14b8a6","#0ea5e9":"#0ea5e9","#6366f1":"#6366f1","#a855f7":"#a855f7","#d946ef":"#d946ef","#ec4899":"#ec4899"}', '', '', '');
UPDATE `zt_workflowfield` SET `control` = 'select', `options` = (SELECT id FROM `zt_workflowdatasource` WHERE `code` = 'color' limit 1) WHERE `module` = 'testcase' AND `field` = 'color';
UPDATE `zt_workflowfield` SET `control` = 'select', `options` = (SELECT id FROM `zt_workflowdatasource` WHERE `code` = 'bugs' limit 1) WHERE `module` = 'testcase' AND `field` = 'fromBug';
UPDATE `zt_workflowfield` SET `control` = 'select', `options` = (SELECT id FROM `zt_workflowdatasource` WHERE `code` = 'color' limit 1) WHERE `module` = 'story' AND `field` = 'color';
UPDATE `zt_workflowfield` SET `control` = 'select', `options` = (SELECT id FROM `zt_workflowdatasource` WHERE `code` = 'bugs' limit 1) WHERE `module` = 'story' AND `field` = 'fromBug';
UPDATE `zt_workflowfield` SET `control` = 'select', `options` = (SELECT id FROM `zt_workflowdatasource` WHERE `code` = 'feedbacks' limit 1) WHERE `module` = 'story' AND `field` = 'feedback';
UPDATE `zt_workflowfield` SET `control` = 'select', `options` = (SELECT id FROM `zt_workflowdatasource` WHERE `code` = 'stories' limit 1) WHERE `module` = 'story' AND `field` = 'duplicateStory';
UPDATE `zt_workflowfield` SET `control` = 'select', `options` = (SELECT id FROM `zt_workflowdatasource` WHERE `code` = 'color' limit 1) WHERE `module` = 'requirement' AND `field` = 'color';
UPDATE `zt_workflowfield` SET `control` = 'select', `options` = (SELECT id FROM `zt_workflowdatasource` WHERE `code` = 'feedbacks' limit 1) WHERE `module` = 'requirement' AND `field` = 'feedback';
UPDATE `zt_workflowfield` SET `control` = 'select', `options` = (SELECT id FROM `zt_workflowdatasource` WHERE `code` = 'stories' limit 1) WHERE `module` = 'requirement' AND `field` = 'duplicateStory';
UPDATE `zt_workflowfield` SET `control` = 'select', `options` = (SELECT id FROM `zt_workflowdatasource` WHERE `code` = 'color' limit 1) WHERE `module` = 'epic' AND `field` = 'color';
UPDATE `zt_workflowfield` SET `control` = 'select', `options` = (SELECT id FROM `zt_workflowdatasource` WHERE `code` = 'feedbacks' limit 1) WHERE `module` = 'epic' AND `field` = 'feedback';
UPDATE `zt_workflowfield` SET `control` = 'select', `options` = (SELECT id FROM `zt_workflowdatasource` WHERE `code` = 'stories' limit 1) WHERE `module` = 'epic' AND `field` = 'duplicateStory';
UPDATE `zt_workflowfield` SET `control` = 'select', `options` = (SELECT id FROM `zt_workflowdatasource` WHERE `code` = 'color' limit 1) WHERE `module` = 'bug' AND `field` = 'color';
UPDATE `zt_workflowfield` SET `control` = 'select', `options` = (SELECT id FROM `zt_workflowdatasource` WHERE `code` = 'color' limit 1) WHERE `module` = 'task' AND `field` = 'color';
UPDATE `zt_workflowfield` SET `control` = 'select', `options` = (SELECT id FROM `zt_workflowdatasource` WHERE `code` = 'bugs' limit 1) WHERE `module` = 'task' AND `field` = 'fromBug';
UPDATE `zt_workflowfield` SET `name` = REPLACE(`name`, '版本', '代码') WHERE `module` = 'task' AND `field` = 'repo';
DELETE FROM `zt_workflowfield` WHERE `module` = 'testcase' AND `field` in ('order', 'frequency');
DELETE FROM `zt_workflowfield` WHERE `module` = 'product' AND `field` in ('order', 'createdVersion');
DELETE FROM `zt_workflowfield` WHERE `module` = 'story' AND `field` in ('childStories', 'linkStories');
DELETE FROM `zt_workflowfield` WHERE `module` = 'requirement' AND `field` in ('childStories', 'linkStories', 'toBug', 'fromBug');
DELETE FROM `zt_workflowfield` WHERE `module` = 'epic' AND `field` in ('childStories', 'linkStories', 'toBug', 'fromBug');
DELETE FROM `zt_workflowfield` WHERE `module` = 'bug' AND `field` = 'storyVersion';
DELETE FROM `zt_workflowfield` WHERE `module` = 'task' AND `field` in ('storyVersion', 'designVersion', 'v1', 'v2', 'vision');
DELETE FROM `zt_workflowfield` WHERE `module` = 'project' AND `field` = 'project';
DELETE FROM `zt_workflowfield` WHERE `module` in ('project', 'execution') AND `field` in ('budgetUnit', 'output', 'path', 'grade', 'version', 'parentVersion', 'openedVersion', 'order', 'vision', 'team');
UPDATE `zt_workflowfield` SET `control` = 'multi-select' WHERE `module` = 'testcase' AND `field` = 'stage';
UPDATE `zt_workflowfield` SET `control` = 'multi-select' WHERE `module` = 'bug' AND `field` = 'os';
UPDATE `zt_workflowfield` SET `control` = 'multi-select' WHERE `module` = 'bug' AND `field` = 'browser';
UPDATE `zt_workflowfield` SET `control` = 'select' WHERE `module` = 'project' AND `field` = 'model';
UPDATE `zt_workflowfield` SET `control` = 'select' WHERE `module` = 'project' AND `field` = 'type';
UPDATE `zt_workflowfield` SET `control` = 'select', `options` = "{\"extend\":\"\\u7ee7\\u627f\",\"reset\":\"\\u91cd\\u65b0\\u5b9a\\u4e49\"}" WHERE `module` = 'project' AND `field` = 'auth';
UPDATE `zt_workflowaction` SET `layout` = 'side' WHERE `module` = 'ticket' AND `action` = 'edit';
DELETE FROM `zt_block` WHERE `dashboard` = 'my' AND `module` = 'product' AND `code` IN ('overview', 'statistic') AND `vision` = 'lite';
DELETE FROM `zt_block` WHERE `dashboard` = 'my' AND `module` = 'qa' AND `code` = 'statistic' AND `vision` = 'lite';
ALTER TABLE `zt_charter` ADD COLUMN `branch` text NULL AFTER `product`;
+275 -272
View File
File diff suppressed because it is too large Load Diff
+166
View File
@@ -1,3 +1,169 @@
2025-04-03 21.6
完成的需求
修复的Bug
2025-03-21 21.6.beta
完成的需求
开源版:
72457 jira数据导入可以导入bug的影响版本字段
72350 jira导入支持版本下的子研发需求和bug
72284 jira导入方式的提示语优化
72151 jira导入页面,对象映射默认匹配选项
72150 jira对象字段导入逻辑调整
72106 jira导入增加进度提示
72103 去掉文档创建点击发布按钮出现的弹窗
72102 去掉文档创建点击存为草稿按钮出现的弹窗
71965 jira导入字段映射过滤已选字段
71964 jira导入字段映射选项修改
71870 jira数据导入页面增加提示语
71765 支持将已发布的文档存为草稿
71749 文档编辑页面点击发布按钮的逻辑调整
71748 文档编辑页面点击存为草稿按钮的逻辑调整
71747 打印创建文档的弹窗
71746 创建文档时先出弹窗设置所属位置和权限
71736 支持在章节下添加子章节或添加同级章节
71674 修改团队空间中文档设置弹窗、移动弹窗内的字段
71673 修改我的空间中文档设置弹窗、移动弹窗内的字段
71672 修改项目空间中文档设置弹窗、移动弹窗内的字段
71671 修改产品空间中文档设置弹窗、移动弹窗内的字段
71651 发布中的应用版本号取消格式判断
71639 jira云版本回收站的项目不导入禅道
71528 支持对删除章节进行权限控制
71527 支持对编辑章节进行权限控制
71526 支持对添加章节进行权限控制
71476 在文档模块中打印删除子文档的弹窗
71474 修改编辑目录弹窗中的上级目录字段
71396 修改文档模块中删除目录的弹窗提示语和逻辑
71384 支持删除章节
71383 在文档列表页打印编辑章节的弹窗
71271 在文档列表页打印添加子章节的弹窗
71260 支持编辑章节
71258 支持在文档下添加子章节
71163 jira导入后执行自动匹配关系
71135 在文档列表页打印删除章节的二次确认弹窗
70729 jira导入后层级关系自动匹配关系
70728 jira导入后评论自动匹配关系
70727 jira导入后工时自动匹配关系
70725 jira导入后项目自动匹配关系
70682 导入页面默认匹配字段
70572 jira导入后数据能够正常操作
70571 再次进入导入页面回填上次信息
70570 jira导入后用户信息自动匹配关系
70568 jira导入支持对象解决方案映射
70567 jira导入支持对象动作映射
70566 jira导入支持对象状态映射
70565 jira导入支持对象字段映射
70564 jira导入支持对象映射
70563 jira导入页面交互优化
企业版:
72529 Confluence导入去掉空间映射步骤的页面
72316 新增Confluence 文档列表获取接口
72310 新增Confluence 子页面列表获取接口
72309 新增获取 Confluence 用户信息接口
72308 新增Confluence 空间列表获取接口
72307 新增Jira 事务列表获取接口
72306 新增Confluence 对象获取接口
72305 新增Jira 事务获取接口
72304 新增Confluence 附件下载接口
72276 支持导入Confluence编辑器中插入的“信息面板”和“备注面板”
72275 支持导入Confluence编辑器中插入的“日期”
72268 支持导入Confluence编辑器中插入的“决定”
72251 jira导入后内置字段的处理
72200 支持导入Confluence编辑器中插入的“附件”
72185 工作流支持发布到项目的更多菜单中
72175 修改Confluence导入文件夹映射关系
72149 jira对象动作导入逻辑调整
72133 升级后实现禅道原来文档的权限迁移为协作文档的权限
72131 支持导入Confluence页面中的“评论”内容
72123 实现协作文档的权限继承后台文档的权限
72111 支持导入Confluence编辑器中插入的“贡献者”
72032 实现禅道章节增加权限控制
72027 调整创建文档弹窗和设置弹窗的文档协作权限
72008 支持导入Confluence编辑器中插入的“用户信息”
72007 支持导入Confluence编辑器中插入的“展开”
72006 支持导入Confluence编辑器中插入的“摘录”
71968 jira导入后的项目流程模板映射逻辑
71879 支持导入Confluence编辑器中插入的“操作项”
71877 支持导入Confluence编辑器中插入的“分隔条”
71876 Confluence导入页面自定义布局后的映射规则
71875 实现Confluence数据格式转Block suite格式的适配机制
71873 支持Confluence导入页面中上传的附件
71871 Confluence中文件夹的映射关系调整
71863 支持将Confluence中已归档的空间导入到禅道产品空间中
71862 支持将Confluence中已归档的空间导入到禅道项目空间中
71861 支持将Confluence中已归档的空间导入到禅道团队空间中
71859 在Confluence导入空间映射页面,增加已归档的空间的显示
71842 支持导入Confluence编辑器中插入“任务报告”
71840 支持导入Confluence编辑器中插入的“PDF”文件
71839 支持导入Confluence编辑器中插入的“Powerpoint”文件
71838 支持导入Confluence编辑器中插入的“Excel”文件
71837 支持导入Confluence编辑器中插入的“子页面”
71833 支持导入Confluence编辑器中插入的“无序列表”
71832 支持导入Confluence编辑器中插入的“Jira事务”
71831 支持导入Confluence编辑器中插入的“卡片”
71830 Confluence页面中无法导入的内容,在导入后的文档中增加标识
71829 支持导入Confluence编辑器中插入的“锚点链接”
71827 在Confluence空间映射表单页打印标识字段
71743 Confluence导入支持白板的映射
71690 支持导入Confluence编辑器中插入的“表情符号”
71644 后台数据导入页新增导入Confluence入口
71643 支持导入Confluence用户
71451 反馈中存在未关闭的转化对象单个关闭反馈给出提示信息
71450 关闭处理中的反馈时给未关闭转换对象的指派人发送消息通知
71439 支持Confluence导入博文状态的映射
71436 支持Confluence导入页面状态的映射
71330 支持Confluence导入填写授权信息
71312 地盘的日志列表支持高级表格功能
71305 支持导入Confluence编辑器中插入的“Confluence页面”
71294 Confluence导入后支持文档的版本切换
71285 支持导入Confluence编辑器中插入的“Draw.io Diagrams”数据
71279 Confluence导入支持子文件夹映射
71245 Confluence导入后博文的权限匹配
71244 支持导入Confluence编辑器中插入的“音视频”文件
71243 支持导入Confluence编辑器中插入的“Word”文件
71242 支持导入confluence编辑器中插入的“图片”文件
71241 保留Confluence页面导入后的段落格式
71240 支持导入Confluence编辑器中插入的“引用”
71239 支持导入Confluence编辑器中插入的“代码片段”
71236 保留Confluence页面导入后的文本样式
71235 保留Confluence页面导入后的标题结构
71234 支持导入Confluence编辑器中插入的“链接”
71192 支持导入Confluence编辑器中插入的“有序列表”
71181 支持将Confluence中已归档的页面导入到禅道
71164 jira从文件导增加API接口对接
71157 Confluence导入后智能链接的权限匹配
71156 Confluence导入后数据库的权限匹配
71155 Confluence导入后页面的权限匹配
71154 Confluence导入后文件夹的权限匹配
71153 Confluence导入后空间的权限匹配
71143 Confluence导入支持博文的映射
71141 Confluence导入支持智能链接页面映射
71140 Confluence导入支持数据库页面表格视图布局的映射
71101 反馈列表中批量关闭反馈时过滤掉存在未关闭的转化对象的反馈
71068 已处理状态的反馈支持激活功能
70726 jira导入后工作流自动匹配关系
70722 支持将Confluence中已归档的空间导入到禅道我的空间中
70717 Confluence导入支持空间映射的页面展示
70675 支持导入Confluence编辑器中插入的“表格”
70670 Confluence导入支持页面映射
70668 Confluence导入支持文件夹映射
70667 Confluence导入支持空间映射
70569 jira导入全局关联关系映射(企业版)
60842 已关闭反馈激活时根据实际情况更新状态
60841 已关闭的反馈激活转化的对象后状态更新为处理中
60840 已处理的反馈激活转化的对象后状态更新为处理中
60837 反馈的历史记录中提供详细的系统更新状态的信息
修复的Bug
开源版:
60409 导入jira数据后内置字段中没有解决时间/解决方案和环境信息
60473 confluence数据导入后用户无法修改密码
60646 jira故事的story point导入后内容没有带入
60668 文档添加子章节后,文档一直显示加载中
企业版:
59343 自定义列勾选富文本类型字段后有报错
旗舰版:
60648 评审时回退到开始节点并选择按审批流程顺序审批后发起人未收到邮件
2025-03-06 21.5
完成的需求
开源版:
@@ -195,6 +195,7 @@ unset($lang->createIcons['product']);
unset($lang->createIcons['program']);
if($config->edition != 'open') unset($lang->workflow->menu->flowgroup);
if($config->edition != 'open') unset($lang->workflow->menu->ruler);
$lang->createObjects['story'] = 'Story';
$lang->createObjects['task'] = 'Task';
@@ -195,6 +195,7 @@ unset($lang->createIcons['product']);
unset($lang->createIcons['program']);
if($config->edition != 'open') unset($lang->workflow->menu->flowgroup);
if($config->edition != 'open') unset($lang->workflow->menu->ruler);
$lang->createObjects['story'] = 'Story';
$lang->createObjects['task'] = 'Task';
@@ -195,6 +195,7 @@ unset($lang->createIcons['product']);
unset($lang->createIcons['program']);
if($config->edition != 'open') unset($lang->workflow->menu->flowgroup);
if($config->edition != 'open') unset($lang->workflow->menu->ruler);
$lang->createObjects['story'] = 'Story';
$lang->createObjects['task'] = 'Task';
@@ -195,6 +195,7 @@ unset($lang->createIcons['product']);
unset($lang->createIcons['program']);
if($config->edition != 'open') unset($lang->workflow->menu->flowgroup);
if($config->edition != 'open') unset($lang->workflow->menu->ruler);
$lang->createObjects['story'] = '目标';
$lang->createObjects['task'] = '任务';
@@ -45,6 +45,7 @@ else
);
}
$lang->task->statusList['changed'] = $lang->task->storyChange;
featureBar
(
li
@@ -49,7 +49,7 @@ if(common::canModify('execution', $execution))
$cols = $this->loadModel('datatable')->getSetting('execution');
$tableData = initTableData($tasks, $cols, $this->task);
$lang->task->statusList['changed'] = $lang->my->storyChanged;
$lang->task->statusList['changed'] = $lang->task->storyChange;
foreach($tableData as $task)
{
$task->rawStory = $task->story;
@@ -595,7 +595,6 @@ $lang->resource->doc->sortCatalog = 'sortCatalog';
$lang->resource->doc->sortDoc = 'sortDoc';
$lang->resource->doc->deleteCatalog = 'deleteCatalog';
// $lang->resource->doc->displaySetting = 'displaySetting';
$lang->resource->doc->exportFiles = 'exportFiles';
$lang->doc->methodOrder[5] = 'index';
$lang->doc->methodOrder[10] = 'mySpace';
+1 -1
View File
@@ -919,7 +919,7 @@ class baseControl
ob_start();
call_user_func_array(array($module, $methodName), $fetchParams);
$output = ob_get_contents();
ob_end_clean();
if(ob_get_level() > 0) ob_end_clean();
unset($module);
-6
View File
@@ -843,12 +843,6 @@ class baseRouter
*/
public function setDebug()
{
/* 如果没有启用 debug 模式,但是用户已登录且在请求头中有 X-ZIN-DEBUG,则启用 debug 模式。If debug is not enabled, but the user is logged in and X-ZIN-DEBUG is in the request header, enable debug mode. */
if(empty($this->config->debug) && !empty($_SERVER['HTTP_X_ZIN_DEBUG']) && !empty($_SESSION['user']->account) && $_SESSION['user']->account != 'guest')
{
$this->config->debug = 1;
}
if(!empty($this->config->debug)) error_reporting(E_ALL & ~ E_STRICT);
}
+16
View File
@@ -400,6 +400,22 @@ class helper extends baseHelper
$checkFunc = 'check' . $operator;
return validater::$checkFunc($value1, $value2);
}
/**
* 替换 Emoji 为指定字符串。
* Replace Emoji to a specified string.
*
* @param string $subject
* @param string $replace
* @access public
* @return string
*/
public static function replaceEmoji(string $subject, string $replace = '[Emoji]'): string
{
/* 匹配大部分常见 Emoji 范围(包括符号、旗帜、交通工具等)。 */
$pattern = '/[\x{1F300}-\x{1F5FF}\x{1F600}-\x{1F64F}\x{1F680}-\x{1F6FF}\x{2600}-\x{26FF}\x{2700}-\x{27BF}\x{1F900}-\x{1F9FF}]/u';
return preg_replace($pattern, $replace, $subject);
}
}
/**
+2
View File
@@ -215,6 +215,8 @@ class dao extends baseDAO
$module = $this->app->getModuleName();
$method = $this->app->getMethodName();
if($module == 'story' && $this->app->rawModule == 'requirement') $module = 'requirement';
if($module == 'story' && $this->app->rawModule == 'epic') $module = 'epic';
$flowAction = $this->dbh->query("SELECT * FROM " . TABLE_WORKFLOWACTION . " WHERE `module` = '{$module}' AND `action` = '{$method}' AND `buildin` = '1' AND `extensionType` = 'extend' AND `vision` = '{$this->config->vision}'")->fetch(PDO::FETCH_OBJ);
if(!$flowAction) return $this;
+11 -5
View File
@@ -300,11 +300,19 @@ class dbh
switch($this->config->driver)
{
case 'oceanbase':
$sql = "CREATE DATABASE `{$this->config->name}`";
return $this->rawQuery($sql);
case 'mysql':
$sql = "CREATE DATABASE `{$this->config->name}`";
if($version > 4.1) $sql .= " DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci";
if(version_compare($version, '5.6', '>='))
{
$sql .= " DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci";
}
elseif(version_compare($version, '4.1', '>='))
{
$sql .= " DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci";
}
return $this->rawQuery($sql);
case 'dm':
/*
$tableSpace = strtoupper($this->config->name);
@@ -318,11 +326,9 @@ class dbh
$this->rawQuery($createTableSpace);
$this->rawQuery($createUser);
}
*/
*/
$createSchema = "CREATE SCHEMA {$this->config->name} AUTHORIZATION {$this->config->user}";
return $this->rawQuery($createSchema);
default:
return false;
}
+7
View File
@@ -119,6 +119,10 @@ class form extends fixer
if($moduleName == 'projectrelease') $moduleName = 'release';
if($moduleName == 'projectbuild') $moduleName = 'build';
/* 用户需求和业务需求用自己的工作流。*/
if($moduleName == 'story' && $app->rawModule == 'requirement') $moduleName = 'requirement';
if($moduleName == 'story' && $app->rawModule == 'epic') $moduleName = 'epic';
$flow = $app->control->loadModel('workflow')->getByModule($moduleName);
if(!$flow) return $configObject;
@@ -214,6 +218,9 @@ class form extends fixer
if($app->rawMethod == 'requirement' && $module == 'story') $module = 'requirement';
if($app->rawMethod == 'epic' && $module == 'story') $module = 'epic';
if($app->rawModule == 'requirement' && $module == 'story') $module = 'requirement';
if($app->rawModule == 'epic' && $module == 'story') $module = 'epic';
if($method == 'batchcreate') $method = 'create';
if($method == 'batchedit') $method = 'edit';
@@ -17,7 +17,7 @@ $PHPMAILER_LANG['from_failed'] = '发送地址错误:';
$PHPMAILER_LANG['instantiate'] = '未知函数调用。';
//$PHPMAILER_LANG['invalid_email'] = 'Not sending, email address is invalid: ';
$PHPMAILER_LANG['mailer_not_supported'] = '发信客户端不被支持。';
$PHPMAILER_LANG['provide_address'] = '收件人没有设置邮箱,请到后台-人员-用户编辑中添加上邮箱地址。';
$PHPMAILER_LANG['provide_address'] = '收件人没有设置邮箱,请到后台-人员管理-用户-编辑中添加上邮箱地址。';
$PHPMAILER_LANG['recipients_failed'] = 'SMTP 错误:收件人地址错误,或检查SMTP服务设置,是否允许发送该邮件地址:';
//$PHPMAILER_LANG['signing'] = 'Signing Error: ';
//$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() failed.';
@@ -17,7 +17,7 @@ $PHPMAILER_LANG['from_failed'] = '發送地址錯誤:';
$PHPMAILER_LANG['instantiate'] = '未知函數調用。';
//$PHPMAILER_LANG['invalid_email'] = 'Not sending, email address is invalid: ';
$PHPMAILER_LANG['mailer_not_supported'] = '發信客戶端不被支持。';
$PHPMAILER_LANG['provide_address'] = '收件人沒有設置郵箱,請到後台-人員-用戶編輯中添加上郵箱地址。';
$PHPMAILER_LANG['provide_address'] = '收件人沒有設置郵箱,請到後台-人員管理-用戶-編輯中添加上郵箱地址。';
$PHPMAILER_LANG['recipients_failed'] = 'SMTP 錯誤:收件人地址錯誤,或檢查SMTP服務設置,是否允許發送該郵件地址:';
//$PHPMAILER_LANG['signing'] = 'Signing Error: ';
//$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() failed.';
+1 -1
View File
@@ -164,7 +164,7 @@ class giteaRepo
for($page = $pageID; true; $page ++)
{
$params['page'] = $page;
$branchList = json_decode(commonModel::http($url . '?' . http_build_query($params)));
$branchList = json_decode(commonModel::http($url . '&' . http_build_query($params)));
if(empty($branchList) || !is_array($branchList)) break;
foreach($branchList as $branch)
+1 -1
View File
@@ -164,7 +164,7 @@ class gogsRepo
for($page = $pageID; true; $page ++)
{
$params['page'] = $page;
$branchList = json_decode(commonModel::http($url . '?' . http_build_query($params)));
$branchList = json_decode(commonModel::http($url . '&' . http_build_query($params)));
if(empty($branchList) || !is_array($branchList)) break;
foreach($branchList as $branch)
+4 -2
View File
@@ -173,8 +173,9 @@ class command
return $list;
}
public static function first(array $nodes, string|array|object $selectors)
public static function first(array $nodes, string|array|object|null $selectors = null)
{
if($selectors === null) return reset($nodes);
foreach($nodes as $node)
{
$result = $node->findFirst($selectors);
@@ -183,8 +184,9 @@ class command
return array();
}
public static function last(array $nodes, string|array|object $selectors)
public static function last(array $nodes, string|array|object|null $selectors = null)
{
if($selectors === null) return end($nodes);
foreach($nodes as $node)
{
$result = $node->findLast($selectors);
+2 -2
View File
@@ -176,13 +176,13 @@ class node implements \JsonSerializable
public function findFirst(string|array|object $selectors): ?node
{
$results = $this->find($selectors, true);
return empty($results) ? null : $results[0];
return empty($results) ? null : reset($results);
}
public function findLast(string|array|object $selectors): ?node
{
$results = $this->find($selectors, true, true);
return empty($results) ? null : $results[0];
return empty($results) ? null : end($results);
}
public function prop(array|string $name, mixed $defaultValue = null): mixed
+1 -1
View File
@@ -46,7 +46,7 @@ class backBtn extends btn
'task' => 'execution-task,my-work,my-contribute,execution-tree,execution-grouptask,project-execution,product-track,repo-view,story-change,execution-kanban,execution-taskkanban,my-index,feedback-adminview,projectstory-track,execution-calendar,my-effort,company-effort,company-calendar,product-dynamic,project-dynamic,execution-dynamic,project-view,execution-view,task-view',
'story' => 'product-browse,projectstory-story,execution-story,my-work,my-contribute,productplan-view,build-view,projectbuild-view,product-track,repo-view,testcase-zerocase,my-index,feedback-adminview,projectstory-track,task-view,my-effort,company-effort,company-calendar,demandpool-track,product-dynamic,project-dynamic,execution-dynamic,project-view,execution-view,ticket-browse,ticket-view,roadmap-view,charter-view,demand-view,review-assess,review-audit',
'bug' => 'bug-browse,project-bug,my-work,my-contribute,execution-bug,bug-view,qa-index,execution-task,product-track,execution-task,task-view,repo-view,story-change,repo-review,feedback-adminview,my-index,projectstory-track,my-effort,company-effort,company-calendar,product-dynamic,project-dynamic,execution-dynamic,project-view,execution-view,ticket-browse,ticket-view',
'testcase' => 'testcase-browse,project-testcase,my-work,my-contribute,execution-testcase,testtask-cases,testsuite-view,product-browse,testcase-view,qa-index,caselib-browse,product-track,story-change,my-index,projectstory-track,my-effort,company-effort,company-calendar,product-dynamic,project-dynamic,execution-dynamic,project-view,execution-view,testreport-view',
'testcase' => 'testcase-browse,project-testcase,my-work,my-contribute,execution-testcase,testtask-cases,testsuite-view,product-browse,testcase-view,qa-index,caselib-browse,product-track,story-change,my-index,projectstory-track,my-effort,company-effort,company-calendar,product-dynamic,project-dynamic,execution-dynamic,project-view,execution-view,testreport-view,bug-browse,project-bug,bug-view',
'testsuite' => 'testsuite-browse,testsuite-view,testtask-cases,my-index,product-dynamic,project-dynamic,execution-dynamic,project-view,execution-view',
'testtask' => 'testtask-browse,testtask-cases,qa-index,testcase-browse,execution-build,my-index,my-effort,company-effort,company-calendar,product-dynamic,project-dynamic,execution-dynamic,project-view,execution-view',
'testreport' => 'testreport-browse,project-testreport,execution-testreport,execution-testtask',
+1 -1
View File
@@ -144,7 +144,7 @@ class bugBasicInfo extends wg
$items[$lang->bug->deadline] = array
(
'control' => 'span',
'control' => 'html',
'content' => html(formatTime($bug->deadline) . (isset($bug->delay) ? sprintf($lang->bug->notice->delayWarning, $bug->delay) : ''))
);
+1
View File
@@ -53,6 +53,7 @@ class datalist extends wg
div
(
setClass('datalist-item-label', $labelClass),
set::title($label),
$label
),
div
+1
View File
@@ -1,5 +1,6 @@
.doc-app-header > .nav {--nav-active-bg: var(--color-primary-100); --nav-active-color: var(--color-fore); --nav-item-height: 24px; --nav-divider-margin: 8px;}
.doc-space-card-libs {min-height: 118px;}
.doc-app-header > .doc-app-nav > .row{min-width: 350px;}
.icon-api {text-align: center; --api-color-rgb: var(--color-success-500-rgb); color: rgba(var(--api-color-rgb), 1); margin: 0 3px;}
.icon-api::before {content: ' '; display: inline-block; width: 8px; height: 8px; border-radius: 2px; background-color: currentColor; min-width: 0;}
+167 -62
View File
@@ -333,16 +333,15 @@ function handleSwitchView(mode, location, info)
}
}
function showDocBasicModal(docID, isDraft, withTitle = false)
function showDocBasicModal(parentID, docID, isDraft, modalType = 'doc')
{
const docApp = getDocApp();
const spaceType = docApp.spaceType;
const spaceID = docApp.spaceID;
const libID = docApp.libID;
const moduleID = docApp.moduleID;
const url = $.createLink('doc', 'setDocBasic', `objectType=${spaceType}&objectID=${spaceID}&libID=${libID}&moduleID=${moduleID}&docID=${docID || 0}&isDraft=${isDraft ? 'yes' : 'no'}&withTitle=${withTitle ? 'yes' : 'no'}`);
const url = $.createLink('doc', 'setDocBasic', `objectType=${spaceType}&objectID=${spaceID}&libID=${libID}&moduleID=${moduleID}&parentID=${parentID || 0}&docID=${docID || 0}&isDraft=${isDraft ? 'yes' : 'no'}&modalType=${modalType}`);
console.log('showDocBasicModal', {docBasicModal: {...docBasicModal}, docID});
if(docBasicModal.doc === docID && docBasicModal.current)
{
docBasicModal.current.show();
@@ -364,7 +363,7 @@ function showDocBasicModal(docID, isDraft, withTitle = false)
}
},
onHidden: () => {
if(!docBasicModal.keepOnHide) docBasicModal.current.destroy();
if(!docBasicModal.keepOnHide && docBasicModal.current) docBasicModal.current.destroy();
},
$onDestroy: () => {
if(docBasicModal.current) docBasicModal = {};
@@ -447,9 +446,10 @@ function submitNewDoc(doc, spaceID, libID, moduleID, formData, afterCreate)
content : removeEmoji(doc.html),
status : doc.status || 'normal',
contentType: doc.contentType,
type : 'text',
type : doc.type || 'text',
lib : libID,
module : moduleID,
parent : doc.parent,
title : doc.title,
keywords : doc.keywords,
acl : 'private',
@@ -487,9 +487,12 @@ function submitNewDoc(doc, spaceID, libID, moduleID, formData, afterCreate)
newDoc.space = `${spaceType}.${docData[spaceType]}`;
}
resolve(newDoc);
docApp.load(null, null, null, {noLoading: true, picks: 'doc'}).then(() => {
afterCreate?.(newDoc);
});
if(afterCreate)
{
docApp.load(null, null, null, {noLoading: true, picks: 'doc'}).then(() => {
afterCreate(newDoc);
});
}
}
catch (error)
{
@@ -506,13 +509,51 @@ function submitNewDoc(doc, spaceID, libID, moduleID, formData, afterCreate)
});
}
function submitEditDoc(doc, formData)
{
const url = $.createLink('doc', 'edit', `docID=${doc.id}`);
const docApp = getDocApp();
const docData = {status: 'normal', type: 'chapter'};
if(formData) mergeDocFormData(docData, formData);
return new Promise((resolve) => {
$.post(url, docData, (res) =>
{
try
{
const data = JSON.parse(res);
if(!checkResponse(data)) return;
if(typeof data !== 'object' || data.result === 'fail')
{
let message = data.message || data.error || getLang('errorOccurred');
if(typeof message === 'object') message = Object.values(message).map(x => Array.isArray(x) ? x.join('\n') : x).join('\n');
throw new Error(message);
}
resolve(doc);
}
catch (error)
{
resolve(false);
showSaveFailedAlert(error);
if(!error.message) return;
}
}).fail(error => {
resolve(false);
showSaveFailedAlert(error);
}).complete(() => {
docApp.load(null, null, null, {noLoading: true, picks: 'doc'});
});
});
}
/**
* 处理创建文档的操作请求向服务器发送请求并返回创建的文档对象
* Handle the create doc operation request, send a request to the server and return the created doc object.
*/
function handleCreateDoc(doc, spaceID, libID, moduleID)
{
return showDocBasicModal(0, doc.status === 'draft').then((formData) => {
return showDocBasicModal(0, 0, doc.status === 'draft').then((formData) => {
return submitNewDoc(doc, spaceID, libID, moduleID, formData);
});
}
@@ -540,6 +581,7 @@ function handleSaveDoc(doc)
type : 'text',
lib : libID,
module : moduleID,
parent : doc.parent,
title : doc.title,
keywords : doc.keywords,
acl : doc.acl,
@@ -593,6 +635,22 @@ function handleSaveDoc(doc)
}).complete(() => {
docApp.isSavingDoc = false;
$(docApp.element).find('[zui-command^="saveDoc"],[zui-command^="saveNewDoc"]').removeAttr('disabled');
const {id, acl, users, addedBy} = doc;
if(acl == 'private' && !users.includes(currentUser) && addedBy !== currentUser)
{
if(docApp.hasEditingDoc)
{
docApp.cancelEditDoc().then(() => {
docApp.delete('doc', id);
docApp.load(null, null, null, {noLoading: true, picks: 'doc'});
});
}
else
{
docApp.delete('doc', id);
docApp.load(null, null, null, {noLoading: true, picks: 'doc'});
}
}
});
});
}
@@ -703,6 +761,10 @@ function importFileToEditor(type, options)
});
}
function isOpenVersion(version) {
return /^\d/.test(version || $('#zuiCSS').attr('href').split('?v=').pop());
}
/**
* 定义文档各个试图和 UI 元素的上的操作方法
* Define the operation methods on the views and UI elements of the doc.
@@ -768,15 +830,15 @@ const actionsMap =
const moreItems =
[
hasPriv('addChapter') ? {icon: 'plus', text: lang.addSubChapter, command: `addChapter/${doc.id}`} : null,
hasPriv('addChapter') ? {icon: 'plus', text: lang.addSameChapter, command: `addChapter/${doc.parent}`} : null,
hasPriv('create') ? {icon: 'plus', text: lang.addSubDoc, command: `addDoc/${doc.id}`} : null,
hasPriv('addChapter') && doc.parent != 0 ? {icon: 'plus', text: lang.addSameChapter, command: `addChapter/${doc.parent}`} : null,
hasPriv('create') ? {icon: 'plus', text: lang.addSubDoc, command: `startCreateDoc/${doc.id}`} : null,
!isChapter && canMoveDoc(doc) ? {icon: 'folder-move', text: lang.moveDoc, command: `moveDoc/${doc.id}`} : null,
!isChapter && hasPriv('delete') ? {icon: 'trash', text: lang.delete, command: `deleteDoc/${doc.id}`} : null,
isChapter && hasPriv('editChapter') ? {icon: 'pencil', text: lang.editChapter, command: `editChapter/${doc.id}`} : null,
isChapter && hasPriv('deleteChapter') ? {icon: 'trash', text: lang.deleteChapter, command: `deleteChapter/${doc.id}`} : null,
isChapter && hasPriv('deleteChapter') ? {icon: 'trash', text: lang.deleteChapter, command: `deleteDoc/${doc.id}`} : null,
];
return [
hasPriv('sortDoc') ? {icon: 'move cursor-move', className: 'sort-handler', hint: lang.sortDoc, size: 'xs'} : null,
(!isChapter && hasPriv('sortDoc')) || (isChapter && hasPriv('sortChapter')) ? {icon: 'move cursor-move', className: 'sort-handler', hint: lang.sortDoc, size: 'xs'} : null,
moreItems.length ? {icon: 'ellipsis-v', caret: false, placement: 'bottom-end', size: 'xs', items: moreItems} : null,
];
}
@@ -784,7 +846,6 @@ const actionsMap =
const moreItems = [];
if(canModify && canMoveDoc(doc)) moreItems.push({icon: 'folder-move', text: lang.moveDoc, command: `moveDoc/${doc.id}`});
if(canModify && hasPriv('delete')) moreItems.push({icon: 'trash', text: lang.delete, command: `deleteDoc/${doc.id}`});
if(canEditDoc && ['html', 'attachment', 'markdown'].includes(doc.contentType)) moreItems.push({icon: 'edit', text: lang.convertToNewDoc, command: 'startConvertDoc'});
if(!isApi && canModify && hasPriv('effort')) moreItems.push({icon: 'time', text: lang.effort, command: `effortDoc/${doc.id}`});
if(!isApi && !isTemplate)
{
@@ -807,7 +868,7 @@ const actionsMap =
}
}
const docApp = this;
const isOpen = /^\d/.test(config.version || $('#zuiCSS').attr('href').split('?v=').pop());
const isOpen = isOpenVersion(config.version);
if(!isOpen && docApp && docApp.mode === 'view' && doc.status != 'draft' && !doc.api && !isTemplate)
{
moreItems.push({icon: 'link', text: getLang('relateObject'), className: 'relateObject-btn', command: 'toggleViewSideTab/relateObject'})
@@ -926,6 +987,7 @@ const actionsMap =
const lang = getLang();
const canModify = canModifySpace();
const libID = docApp.libID;
const docID = docApp.docID;
if(isApiLib(docApp.lib))
{
@@ -962,12 +1024,12 @@ const actionsMap =
/* 文档列表没有文档时的按钮。Actions for empty doc list. */
if(info.ui === 'doc-list-empty')
{
return canCreateDoc ? [{icon: 'plus', text: lang.createDoc, btnType: 'primary', command: 'startCreateDoc'}] : null;
return canCreateDoc ? [{icon: 'plus', text: lang.createDoc, btnType: 'primary', command: `startCreateDoc/${docID}`}] : null;
}
const items = canModify ?
[
canCreateDoc ? {icon: 'plus', text: lang.createDoc, command: 'startCreateDoc'} : null,
canCreateDoc ? {icon: 'plus', text: lang.createDoc, command: `startCreateDoc/${docID}`} : null,
canCreateDoc ? {type: 'divider'} : null,
canCreateOffice ? {icon: 'file-word', text: lang.createList.word, command: 'startCreateOffice/word'} : null,
canCreateOffice ? {icon: 'file-powerpoint', text: lang.createList.ppt, command: 'startCreateOffice/ppt'} : null,
@@ -1004,7 +1066,7 @@ const actionsMap =
}
const moreItems = [];
if((doc.contentType === 'doc' || doc.contentType === 'markdown') && !isApiDoc(doc))
if(isOpenVersion() && (doc.contentType === 'doc' || doc.contentType === 'markdown') && !isApiDoc(doc))
{
if(config.debug > 5) moreItems.push({text: lang.importConfluence, command: 'importFile/confluence'});
moreItems.push(
@@ -1105,17 +1167,26 @@ function getActions(type, info)
const commands =
{
/* 创建文档。 Create doc. */
startCreateDoc: function()
startCreateDoc: function(_, args)
{
const docApp = getDocApp();
if(!docApp.libList.length) return zui.Modal.alert(getLang('createLibFirst'));
const {spaceType} = docApp;
if(spaceType === 'api') return docApp.startCreateDoc();
const {spaceID, libID, moduleID} = docApp;
return showDocBasicModal(0, true, true).then((formData) => {
const doc = {contentType: 'doc', status: 'draft'};
return submitNewDoc(doc, spaceID, libID, moduleID, formData, (newDoc) => {
docApp.startEditDoc(newDoc.id);
return showDocBasicModal(args?.[0] ?? 0, 0, true, 'doc').then((formData) => {
zui.Editor.loadModule().then(() => {
const emptyDoc = zui.Editor.createEmptyDoc();
const snapshot = zui.Editor.toSnapshot(emptyDoc);
const doc = {contentType: 'doc', status: 'draft', content: JSON.stringify(snapshot)};
return submitNewDoc(doc, spaceID, libID, moduleID, formData, (newDoc) => {
docApp.selectDoc(newDoc.id);
docApp.startEditDoc(newDoc.id);
});
});
});
},
startEditDoc: function(_, [doc], options)
@@ -1271,12 +1342,13 @@ const commands =
},
deleteModule: function(_, args)
{
const docApp = getDocApp();
const moduleID = args[0] || docApp.moduleID;
const docApp = getDocApp();
const moduleID = args[0] || docApp.moduleID;
const moduleInfo = docApp.treeMap.modules.get(+moduleID);
$.ajaxSubmit(
{
confirm: getLang('confirmDeleteModule'),
confirm: getLang(moduleInfo.modules.length > 0 || moduleInfo.docs.length > 0 ? 'confirmDeleteModuleWithSub' : 'confirmDeleteModule'),
url: $.createLink('doc', 'deleteCatalog', `moduleID=${moduleID}`),
load: false,
onSuccess: function(res)
@@ -1323,48 +1395,27 @@ const commands =
},
addChapter: function(_, args)
{
zui.Modal.prompt({message: getLang('chapterName')}).then(newChapterName =>
{
if(!newChapterName) return;
const docApp = getDocApp();
const {spaceType, spaceID, libID, moduleID} = docApp;
const url = $.createLink('doc', 'create', `objectType=${spaceType}&objectID=${Math.max(spaceID, 0)}&libID=${libID}&moduleID=${moduleID}`);
const docData =
{
status : 'normal',
type : 'chapter',
lib : libID,
module : moduleID,
title : newChapterName,
acl : 'open',
space : spaceType,
parent : args[0] || 0,
};
$.post(url, docData, (res) =>
{
try
const docApp = getDocApp();
const {spaceID, libID, moduleID} = docApp;
return showDocBasicModal(args[0], 0, false, 'chapter').then((formData) => {
const doc = {type: 'chapter', status: 'normal', parent: args[0]};
return submitNewDoc(doc, spaceID, libID, moduleID, formData).then(() => {
docApp.load(null, null, null, {noLoading: false, picks: 'doc'});
if(docBasicModal.current)
{
const data = JSON.parse(res);
if(!checkResponse(data)) return;
if(typeof data !== 'object' || data.result === 'fail') throw new Error(getErrorMessage(data));
docBasicModal.current.destroy();
docBasicModal = {};
}
catch (error)
{
showSaveFailedAlert(error);
}
}).fail(error => {
showSaveFailedAlert(error);
}).complete(() => {
docApp.load(null, null, null, {noLoading: true, picks: 'doc'});
});
});
},
editChapter: function(_, args)
{
const docApp = getDocApp();
const {spaceID, libID, moduleID} = docApp;
return showDocBasicModal(args[0], false, true)
return showDocBasicModal(0, args[0], false, 'chapter').then((formData) => {
return submitEditDoc({id: args[0]}, formData);
});
},
moveDoc: function(_, args)
{
@@ -1409,9 +1460,13 @@ const commands =
const docApp = getDocApp();
const docID = args[0] || docApp.docID;
const isApi = isApiDoc(docID);
const docInfo = docApp.treeMap.docs.get(docID)
const doc = docInfo.data;
const isChapter = doc?.type === 'chapter';
$.ajaxSubmit(
{
confirm: getLang('confirmDelete'),
confirm: getLang(isChapter ? (docInfo.docs.length > 0 ? 'confirmDeleteChapterWithSub' : 'confirmDeleteChapter') : (docInfo.docs.length > 0 ? 'confirmDeleteWithSub' : 'confirmDelete')),
url: $.createLink(isApi ? 'api' : 'doc', 'delete', `${isApi ? 'apiID' : 'docID'}=${isApi ? getApiID(docID) : docID}`),
load: false,
onSuccess: function()
@@ -1471,7 +1526,7 @@ const commands =
const docID = args[0] || doc.id;
const docType = args[1] || doc.contentType;
const saveEdited = args[2] || 0;
showDocBasicModal(docID).then(formData => {
showDocBasicModal(0, docID).then(formData => {
savingDocData[docID] = formData;
if(saveEdited == 1)
{
@@ -2108,4 +2163,54 @@ window.getFileActions = function(file)
fileActions.push({icon: 'download', title: downloadLang, url: downloadLink.replace('{id}', file.id).replace('\\', ''), className: 'text-primary', target: '_blank'});
return fileActions;
};
window.useHocuspocus = function(app)
{
if(vision !== 'rnd') return false;
return enableHocuspocus && app.spaceType !== 'mine';
};
let lastDisconnectMessager = null;
window.onHocuspocusConnect = function(app)
{
if(app.spaceType === 'mine') return;
lastDisconnectMessager && lastDisconnectMessager.hide();
lastDisconnectMessager = null;
zui.Messager.show({
content: hocuspocusConnect,
type: 'success',
time: 3000,
});
};
window.onHocuspocusDisconnect = function(app)
{
if(app.spaceType === 'mine') return;
lastDisconnectMessager && lastDisconnectMessager.hide();
lastDisconnectMessager = zui.Messager.show({
content: hocuspocusDisconnect,
type: 'danger',
time: 0,
});
};
window.onAuthenticationFailed = function(app)
{
if(app.spaceType === 'mine') return;
zui.Messager.show({
content: needReadable,
type: 'danger',
time: 3000,
});
};
window.onDocAppModeChange = function()
{
lastDisconnectMessager && lastDisconnectMessager.hide();
lastDisconnectMessager = null;
}
+13 -3
View File
@@ -129,6 +129,12 @@ class docApp extends wg
{
global $app, $lang, $config;
jsVar('hocuspocusConnect', $lang->doc->hocuspocusConnect);
jsVar('hocuspocusDisconnect', $lang->doc->hocuspocusDisconnect);
jsVar('enableHocuspocus', $config->docHocuspocus->enable);
jsVar('needReadable', $lang->doc->needReadable);
jsVar('vision', $config->vision);
/**
* 定义文档应用接口链接。
* Define the fetcher links for doc app.
@@ -138,7 +144,7 @@ class docApp extends wg
$filesFetcher = createLink('doc', 'ajaxGetFiles', 'type={objectType}&objectID={objectID}');
$libSummariesFetcher = createLink('doc', 'ajaxGetLibSummaries', 'spaceType={spaceType}&spaceList={spaceList}');
$uploadUrl = createLink('file', 'ajaxUpload', 'uid={uid}&objectType={objectType}&objectID={objectID}&extra={extra}&field={field}&api={api}&gid={gid}');
$downloadUrl = createLink('file', 'ajaxQuery', 'fileID={gid}');
$downloadUrl = createLink('file', 'ajaxQuery', 'fileID={gid}&objectType={objectType}&objectID={objectID}&title={title}&extra={extra}');
/**
* 定义文档界面上的文件下载链接。
@@ -275,9 +281,13 @@ class docApp extends wg
set::_style(array('height' => 'calc(100vh - 72px)')),
set::_id('docApp'),
set::token(session_id()),
set::awarenessUser(array('id' => $app->user->id, 'name' => $app->user->realname)),
set::useHocuspocus($config->docHocuspocus->enable),
set::awarenessUser(array('id' => $app->user->id, 'account' => $app->user->account, 'name' => $app->user->realname, 'avatar' => $app->user->avatar)),
set::useHocuspocus(jsRaw('window.useHocuspocus')),
set::hocuspocusUrls(array($config->docHocuspocus->http, $config->docHocuspocus->websocket)),
set::onHocuspocusConnect(jsRaw('window.onHocuspocusConnect')),
set::onHocuspocusDisconnect(jsRaw('window.onHocuspocusDisconnect')),
set::onAuthenticationFailed(jsRaw('window.onAuthenticationFailed')),
set::onModeChange(jsRaw('window.onDocAppModeChange')),
set::spaceType($spaceType),
set::spaceID(data('spaceID')),
set::libID(data('libID')),
+3
View File
@@ -214,7 +214,10 @@ class dtable extends wg
foreach($modifier as $subModifier)
{
if(!is_callable($subModifier)) continue;
if($subModifier == 'strip_tags' && empty(zget($item, $key, ''))) continue;
if($subModifier instanceof \Closure) $subModifier = $subModifier->bindTo($item);
if(is_object($item)) $item->$key = $subModifier($item->$key);
else $item[$key] = $subModifier($item[$key]);
}
+3 -2
View File
@@ -31,7 +31,7 @@ class fileSelector extends wg
'fileIcons?: string|array= "paper-clip"', // 文件图标。
'uploadBtn?: string|array', // 上传按钮。
'renameBtn?: bool|string|array|callback=true', // 重命名按钮。
'removeBtn?: bool|string|array|callback=true', // 删除按钮。
'removeBtn?: bool|string|array|callback', // 删除按钮。
'removeConfirm?: string|array', // 删除确认提示。
'maxFileSize?: int|string', // 限制文件大小。
'maxFileCount?: int=0', // 限制文件数目,如果设置为非大于 `0` 的数则不限制。
@@ -53,6 +53,7 @@ class fileSelector extends wg
protected function created()
{
if(!$this->hasProp('removeBtn')) $this->setProp('removeBtn', common::hasPriv('file', 'delete'));
if(!$this->hasProp('totalFileSize'))
{
$maxFileSize = ini_get('post_max_size');
@@ -115,7 +116,7 @@ class fileSelector extends wg
->const('acceptFileTypes', $acceptFileTypes)
->do(<<<'JS'
const typeIndex = file.name.lastIndexOf(".");
const fileType = file.name.slice(typeIndex + 1);
const fileType = "," + file.name.slice(typeIndex + 1) + ",";
if(acceptFileTypes)
{
if(acceptFileTypes.indexOf(fileType) == -1)
+1
View File
@@ -259,6 +259,7 @@ class formPanel extends panel
set::value($field->value),
set::placeholder($field->placeholder)
);
if((bool)$field->readonly) $extraMain[] = formHidden($field->field, $field->value);
}
return $extraMain;
}
+3
View File
@@ -0,0 +1,3 @@
.history-panel-action .avatar {width: 21px !important; height: 21px !important;}
.history-panel-body .listitem {padding: 0.4rem;}
.history-panel-action > .item-inner {padding-left: 1.5rem;}
+5
View File
@@ -25,6 +25,11 @@ class history extends wg
'commentBtn?: string|array' // 是否允许添加备注。
);
public static function getPageCSS(): ?string
{
return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css');
}
public static function getPageJS(): ?string
{
return file_get_contents(__DIR__ . DS . 'js' . DS . 'v1.js');
+1 -1
View File
@@ -147,7 +147,7 @@ class moduleMenu extends wg
$lang = $app->loadLang('datatable')->datatable;
$items = array();
if($settingLink)
if($settingLink && common::hasPriv('tree', 'browse'))
{
$tab = $this->prop('settingApp', $app->tab);
$settingText = $this->prop('settingText', $lang->moduleSetting);
+1 -1
View File
@@ -40,7 +40,7 @@ class modulePicker extends wg
setID('moduleBox'),
$this->prop('label', null),
picker(set($this->props->pick(array('id', 'name', 'value', 'required', 'items')))),
$items || empty($this->prop('manageLink'))? null : span
$items || empty($this->prop('manageLink')) || !common::hasPriv('tree', 'browse') ? null : span
(
setClass('input-group-btn'),
a
+15
View File
@@ -0,0 +1,15 @@
.monaco-editor .wave-decoration {
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 4'%3E%3Cpath fill='none' stroke='%23FF0000' d='M0 3.5c5 0 5-3 10-3s5 3 10 3 5-3 10-3 5 3 10 3'/%3E%3C/svg%3E") repeat-x 0 100%;
background-size: 8px 4px;
background-color: white;
animation: waveMove 1s infinite linear;
}
@keyframes waveMove {
from { background-position: 0 100%; }
to { background-position: -20px 100%; }
}
.monaco-editor .yellow-decoration {
background-color: rgba(255, 255, 0, 0.2); /* 半透明黄色 */
}
+89 -11
View File
@@ -1,7 +1,10 @@
setTimeout(function()
{
createMonaco(id, action, options, diffContent, onMouseDown, onMouseMove, vsPath, clientLang);
resize(id);
$.getLib(config.webRoot + 'js/monaco-editor/min/vs/loader.js', {root: false}, function()
{
createMonaco(id, action, options, diffContent, onMouseDown, onMouseMove, vsPath, clientLang);
resize(id);
});
}, 200);
function createMonaco(id, action, options, diffContent, onMouseDown, onMouseMove, vsPath, clientLang)
@@ -16,18 +19,33 @@ function createMonaco(id, action, options, diffContent, onMouseDown, onMouseMove
}
});
let decorations = [];
let programmaticSelectionRange = null;
require(['vs/editor/editor.main'], function ()
{
if(!lineMap && action == 'diff' && diffContent && diffContent.line) lineMap = diffContent.line.new;
if(lineMap) options.lineNumbers = (line) => {return lineMap[line - 1];};
$.extend(options, {
// 关键配置项
automaticLayout: true, // 自动调整布局
scrollBeyondLastLine: false, // 禁用底部滚动区域
minimap: {enabled: false}, // 可选:禁用缩略图以节省空间
// 编辑器尺寸配置
fontSize: 14,
lineHeight: 24,
lineNumbersMinChars: 2,
// 内边距设置
padding: {top: 8, bottom: 8}
});
if(action == 'diff')
{
options.renderSideBySide = $.cookie.get('renderSideBySide') == 'true';
options.lineNumbers = function(number)
{
const newlc = diffContent.line.new;
return newlc[number - 1];
};
modifiedEditor = monaco.editor.createDiffEditor(document.getElementById(id), options);
window.modifiedEditor = modifiedEditor;
@@ -60,9 +78,69 @@ function createMonaco(id, action, options, diffContent, onMouseDown, onMouseMove
if(selectedLines)
{
const lines = selectedLines.split(',');
const startLine = parseInt(lines[0]);
const endLine = parseInt(lines[1] || startLine + 1);
editor.setSelection(new monaco.Range(startLine, 1, endLine + 1, 1))
let startLine = parseInt(lines[0]);
let endLine = parseInt(lines[1] || startLine + 1);
let startCol = 1;
let endCol = 0;
if(lines.length == 4)
{
startCol = parseInt(lines[2]);
endCol = parseInt(lines[3]);
}
if(endCol == 0) endLine += 1;
if(lineMap)
{
lineMap.forEach((line, index) =>
{
if(line == startLine) startLine = index + 1;
if(line == endLine) endLine = index + 1;
});
}
const range = new monaco.Range(startLine, startCol, endLine, endCol);
programmaticSelectionRange = range;
editor.setSelection(range);
updateDecorations();
}
function updateDecorations()
{
editor.deltaDecorations(decorations, []);
decorations = [];
if(programmaticSelectionRange)
{
decorations.push({
range: programmaticSelectionRange,
options: {
className: selectedClass,
isWholeLine: false
}
});
}
decorations = editor.deltaDecorations([], decorations);
}
editor.onDidChangeCursorSelection(function()
{
// 只有当没有程序化选择时才更新装饰器
if(!programmaticSelectionRange) return;
// 如果用户手动改变了选择,清除程序化选择的样式
if(e.selections.some(sel => !rangesEqual(sel, programmaticSelectionRange)))
{
programmaticSelectionRange = null;
updateDecorations();
}
});
function rangesEqual(r1, r2)
{
return r1.startLineNumber === r2.startLineNumber &&
r1.startColumn === r2.startColumn &&
r1.endLineNumber === r2.endLineNumber &&
r1.endColumn === r2.endColumn;
}
});
}
+21 -10
View File
@@ -5,13 +5,15 @@ namespace zin;
class monaco extends wg
{
protected static array $defineProps = array(
'id:string',
'action?:string',
'options?:array',
'diffContent?:array',
'selectedLines?:string',
'onMouseDown?:string',
'onMouseMove?:string'
'id: string',
'action?: string',
'options?: array',
'diffContent?: array',
'selectedLines?: string', // startLine,endLine 或者 startLine,endLine,startCol,endCol
'selectedClass?: string', // yellow-decoration: 黄色背景,wave-decoration: 红色波浪线
'lineMap?: array', // 用于设置行号的显示规则,如 array('编辑器行号' => '实际行号')
'onMouseDown?: string',
'onMouseMove?: string'
);
protected static array $defaultProps = array(
@@ -21,11 +23,17 @@ class monaco extends wg
'onMouseDown' => '',
'onMouseMove' => ''
);
public static function getPageJS(): ?string
{
return file_get_contents(__DIR__ . DS . 'js' . DS . 'v1.js');
}
public static function getPageCSS(): ?string
{
return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css');
}
protected function build()
{
global $app;
@@ -36,8 +44,10 @@ class monaco extends wg
$options = $this->prop('options');
$diffContent = $this->prop('diffContent');
$selectedLines = $this->prop('selectedLines');
$onMouseDown = $this->prop('onMouseDown');
$onMouseMove = $this->prop('onMouseMove');
$selectedClass = $this->prop('selectedClass');
$lineMap = $this->prop('lineMap');
$onMouseDown = $this->prop('onMouseDown');
$onMouseMove = $this->prop('onMouseMove');
if(!$options) $options = new stdclass();
return div
@@ -51,7 +61,8 @@ class monaco extends wg
jsVar('vsPath', $vsPath),
jsVar('clientLang', $clientLang),
jsVar('selectedLines', $selectedLines),
h::import($app->getWebRoot() . 'js/monaco-editor/min/vs/loader.js'),
jsVar('selectedClass', $selectedClass),
jsVar('+lineMap', empty($lineMap) ? null : $lineMap),
setID($id)
);
}
+3 -1
View File
@@ -187,6 +187,7 @@ class navbar extends wg
'text' => $menuItem->text,
'type' => 'text',
'tagName' => 'span',
'icon' => isset($menuItem->icon) ? $menuItem->icon : '',
);
continue;
@@ -326,6 +327,7 @@ class navbar extends wg
{
$items[] = array(
'class' => $class,
'icon' => isset($menuItem->icon) ? $menuItem->icon : '',
'text' => $label,
'url' => commonModel::createMenuLink($menuItem, $tab),
'active' => $isActive,
@@ -338,7 +340,7 @@ class navbar extends wg
}
else
{
$items[] = array('class' => $class, 'text' => $menuItem->text, 'active' => $isActive);
$items[] = array('class' => $class, 'icon' => isset($menuItem->icon) ? $menuItem->icon : '', 'text' => $menuItem->text, 'active' => $isActive);
}
}
+129 -35
View File
@@ -41,6 +41,7 @@ window.addNewLine = function(e)
const objectClass = objectType == 'roadmap' ? '.linkRoadmap' : '.linkPlan';
newLine.find('.linkProduct').html(`<div id=product${index} class='form-group-wrapper picker-box' data-on='change' data-call='refreshPicker' data-params='event'></div>`);
newLine.find(objectClass).html(`<div id=${objectType}${index} class='form-group-wrapper picker-box'></div>`);
newLine.find('.branchBox').html(`<div id=branch${index} class='form-group-wrapper picker-box' data-call="branchChange(event)" data-on="change"></div>`, false);
$(obj).closest('.productBox').after(newLine);
@@ -50,7 +51,7 @@ window.addNewLine = function(e)
options.disabled = false;
options.placeholder = '';
new zui.Picker(`#product${index}`, options);
new zui.Picker(`#branch${index}`, {name: `branch[${index}]`, multiple: true, items: []});
new zui.Picker(`#${objectType}${index}`, {name: `${objectType}[${index}]`, items: [], multiple: true});
refreshPicker();
@@ -77,22 +78,27 @@ window.removeLine = function(e)
*/
window.loadRoadmap = function(product)
{
let productID = product.val();
let branchID = product.closest('.productBox').find('.linkBranch .pick-value').val();
let link = $.createLink('demand', 'ajaxGetRoadmaps', 'productID=' + productID + '&branch=all&param=nolaunching');
const $items = [{text: '', value: '', key: ''}];
const $roadmap = product.closest('.productBox').find('.roadmapBox .pick-value');
const objectType = product.closest('.productsBox').data('objecttype');
if(objectType != 'roadmap') return;
let productID = product.val();
let branchID = product.closest('.productBox').find('.linkBranch .pick-value').val();
if(typeof branchID == 'undefined' || branchID === '') branchID = 'all';
let link = $.createLink('demand', 'ajaxGetRoadmaps', 'productID=' + productID + '&branch=' + branchID + '&param=nolaunching');
const $items = [{text: '', value: '', key: ''}];
const $roadmap = product.closest('.productBox').find('.roadmapBox .pick-value');
const oldRoadmap = product.closest('.productBox').find('[name^=roadmap]').val();
$roadmap.closest('.picker-box').removeClass('hidden');
$roadmap.closest('.linkRoadmap').find('[name^=roadmapName]').addClass('hidden');
$roadmap.closest('.linkRoadmap').find('[name^=roadmapName]').attr('disabled', 'disabled');
$roadmap.zui('picker').render({disabled: false});
$roadmap.zui('picker').$.setValue('');
$.getJSON(link, function(data)
{
$roadmap.zui('picker').render({items: data});
if(productID == '' || productID === undefined) $roadmap.zui('picker').render({items: $items});
$roadmap.zui('picker').$.setValue(oldRoadmap);
let optionCount = $roadmap.zui('picker').options.items.length;
@@ -110,52 +116,50 @@ window.loadRoadmap = function(product)
*/
window.loadPlan = function(product)
{
const objectType = product.closest('.productsBox').data('objecttype');
if(objectType != 'plan') return;
let productID = product.val();
let link = $.createLink('charter', 'ajaxGetPlans', 'productID=' + productID);
let branchID = product.closest('.productBox').find('.linkBranch .pick-value').val();
if(typeof branchID == 'undefined' || branchID === '') branchID = 'all';
let link = $.createLink('charter', 'ajaxGetPlans', 'productID=' + productID + '&branchID=' + branchID);
const $items = [{text: '', value: '', key: ''}];
const $plan = product.closest('.productBox').find('.planBox .pick-value');
const oldPlan = product.closest('.productBox').find('[name^=plan]').val();
$plan.closest('.picker-box').removeClass('hidden');
$plan.zui('picker').render({disabled: false});
$plan.zui('picker').$.setValue('');
$.getJSON(link, function(data)
{
$plan.zui('picker').render({items: data});
if(productID == '' || productID === undefined) $plan.zui('picker').render({items: $items});
$plan.zui('picker').$.setValue(oldPlan);
});
}
window.refreshPicker = function(event)
{
let $products = $('.productsBox').find('.linkProduct .pick-value');
let selectedProducts = [];
$products.each(function()
{
if($(this).val() != '') selectedProducts.push($(this).val());
});
$products.each(function()
{
let $product = $(this);
let productID = $product.val();
let $productPicker = $product.zui('picker');
let productItems = $productPicker.options.items;
$.each(productItems, function(i, item)
{
if(item.value == '') return;
productItems[i].disabled = selectedProducts.includes(item.value) && item.value != productID;
})
$productPicker.render({items: productItems});
});
if(typeof event !== "undefined")
{
const objectType = $(event.target).closest('.productsBox').data('objecttype');
if(objectType == 'roadmap') loadRoadmap($(event.target));
if(objectType == 'plan') loadPlan($(event.target));
const $product = $(event.target);
const productID = $product.val();
if(multiBranchProducts[productID])
{
loadBranches($product);
}
else
{
updateItems();
const objectType = $product.closest('.productsBox').data('objecttype');
if(objectType == 'roadmap') loadRoadmap($product);
if(objectType == 'plan') loadPlan($product);
let $formRow = $product.closest('.productBox');
$formRow.find('.form-group').eq(0).addClass('w-1/2').removeClass('w-1/4');
$formRow.find('.form-group').eq(1).addClass('hidden');
}
}
}
@@ -168,3 +172,93 @@ window.loadRoadmapStories = function(event)
openUrl(link, {load: 'modal', size: 'lg', type: 'iframe'});
}
window.loadBranches = function(product)
{
let currentProduct = product.val();
let $formRow = product.closest('.productBox');
let charterID = $('[name="charter"]').length ? $('[name="charter"]').val() : 0;
const objectType = $('.productsBox').data('linkType');
if(typeof projectID == 'undefined') projectID = 0;
$.getJSON($.createLink('branch', 'ajaxGetBranches', "productID=" + currentProduct + "&oldBranch=&param=active&projectID=" + projectID + "&withMainBranch=true&isTwins=no&fieldID=0&multiple=&charterID=" + charterID), function(data)
{
if(data.length > 0)
{
$formRow.find('.form-group').eq(0).addClass('w-1/4').removeClass('w-1/2');
$formRow.find('.form-group').eq(1).removeClass('hidden');
const $branchPicker = $formRow.find('[name^=branch]').zui('picker');
$branchPicker.render({items: data, multiple: true});
$branchPicker.$.setValue('0').then(() =>
{
if(objectType == 'roadmap')
{
loadRoadmap(product);
}
else
{
loadPlan(product);
}
});
}
else
{
if(objectType == 'roadmap')
{
loadRoadmap(product);
}
else
{
loadPlan(product);
}
}
});
updateItems();
}
window.branchChange = function(event)
{
const objectType = $(event.target).closest('.productsBox').data('objecttype');
const $product = $(event.target).closest('.productBox').find('.linkProduct .pick-value');
if(objectType == 'roadmap')
{
loadRoadmap($product);
}
else
{
loadPlan($product);
}
updateItems();
}
window.updateItems = function()
{
let chosenProducts = [];
$(".productsBox [name^='product']").each(function()
{
const productID = $(this).val();
if(productID > 0 && chosenProducts.indexOf(productID) == -1) chosenProducts.push(productID);
});
let allItems = zui.Picker.query("[name^='product']").options.items;
let productItems = [];
for(i = 0; i < allItems.length; i++)
{
allItems[i].disabled = false;
if(chosenProducts.includes(allItems[i].value)) allItems[i].disabled = true;
productItems[allItems[i].value] = Object.assign({},allItems[i]);
productItems[allItems[i].value].i = i;
}
$(".productsBox [name^='product']").each(function()
{
const $product = $(this).zui('picker');
const productID = $(this).val();
let currentProductItems = JSON.parse(JSON.stringify(allItems));
if(productID != 0) currentProductItems[productItems[productID].i].disabled = false;
$product.render({items: currentProductItems});
});
}
+34
View File
@@ -35,6 +35,7 @@ class productCharterBox extends wg
(
setClass('productsBox'),
setData(array('objecttype' => $this->prop('objectType'))),
jsVar('multiBranchProducts', data('multiBranchProducts')),
$productsBox
);
}
@@ -89,6 +90,23 @@ class productCharterBox extends wg
)
),
formGroup
(
set::width('1/4'),
setClass('hidden linkBranch ml-px'),
set::label(''),
inputGroup
(
setClass('branchBox'),
picker
(
set::name('branch[0][]'),
set::items(array()),
set::multiple(true),
setData(array('on' => 'change', 'call' => 'branchChange', 'params' => 'event'))
)
)
),
formGroup
(
set::width('1/2'),
set::label($lang->charter->$objectType),
@@ -185,6 +203,22 @@ class productCharterBox extends wg
)
),
formGroup
(
set::width('1/4'),
setClass('hidden linkBranch ml-px'),
set::label(''),
inputGroup
(
setClass('branchBox'),
picker
(
set::name("branch[$index]"),
set::items(array()),
setData(array('on' => 'change', 'call' => 'branchChange', 'params' => 'event'))
)
)
),
formGroup
(
set::width('1/2'),
set::label($lang->charter->$objectType),
+2
View File
@@ -77,6 +77,8 @@ function isFieldRequired(?string $name, ?string $requiredFields = null): bool
global $config, $app;
$moduleName = $app->moduleName;
$methodName = $app->methodName;
if($moduleName == 'story' && $app->rawModule == 'requirement') $moduleName = 'requirement';
if($moduleName == 'story' && $app->rawModule == 'epic') $moduleName = 'epic';
if(!isset($config->$moduleName->$methodName->requiredFields)) return false;
$requiredFields = $config->$moduleName->$methodName->requiredFields;
+1 -1
View File
@@ -88,7 +88,7 @@ $config->action->majorList['doc'] = array('releaseddoc');
$config->action->needGetProjectType = 'build,task,bug,case,testcase,caselib,testtask,testsuite,testreport,doc,issue,release,risk,design,opportunity,trainplan,gapanalysis,researchplan,researchreport,';
$config->action->needGetRelateField = ',branch,story,epic,requirement,productplan,release,task,build,bug,testcase,case,testtask,testreport,doc,doclib,issue,risk,opportunity,trainplan,gapanalysis,team,whitelist,researchplan,researchreport,meeting,kanbanlane,kanbancolumn,module,review,';
$config->action->noLinkModules = ',doclib,module,webhook,gitlab,instance,gitea,gogs,sonarqube,pipeline,jenkins,kanban,kanbanspace,kanbancolumn,kanbanlane,kanbanregion,kanbancard,execution,project,traincategory,apistruct,program,product,user,entry,repo,pivot,scene,boardspace,auditplan,auditresult,productline,';
$config->action->noLinkModules = ',doclib,module,webhook,gitlab,instance,gitea,gogs,sonarqube,pipeline,jenkins,kanban,kanbanspace,kanbancolumn,kanbanlane,kanbanregion,kanbancard,execution,project,traincategory,apistruct,program,product,user,entry,repo,pivot,scene,boardspace,auditplan,auditresult,productline,chapter,';
$config->action->ignoreObjectType4Dynamic = 'kanbanregion,kanbanlane,kanbancolumn';
$config->action->ignoreActions4Dynamic = 'disconnectxuanxuan,reconnectxuanxuan,loginxuanxuan,logoutxuanxuan,editmr,removemr,syncdoingbyticket,syncdoingbystory,syncdoingbyuserstory,syncdoingbyepic,syncdoingbytask,syncdoingbybug,syncdoingbytodo,syncdoingbydemand';
+1
View File
@@ -35,6 +35,7 @@ $lang->action->old = 'Old';
$lang->action->new = 'New';
$lang->action->to = 'To';
$lang->action->superReviewer = 'Super Reviewer';
$lang->action->byRule = '(Triggered by Rule %s)';
$lang->action->trash = 'Aufräumen';
$lang->action->undelete = 'Wiederherstellen';
+1
View File
@@ -35,6 +35,7 @@ $lang->action->old = 'Old';
$lang->action->new = 'New';
$lang->action->to = 'To';
$lang->action->superReviewer = 'Super Reviewer';
$lang->action->byRule = '(Triggered by Rule %s)';
$lang->action->trash = 'Recycle';
$lang->action->undelete = 'Restore';
+1
View File
@@ -35,6 +35,7 @@ $lang->action->old = 'Old';
$lang->action->new = 'New';
$lang->action->to = 'To';
$lang->action->superReviewer = 'Super Reviewer';
$lang->action->byRule = '(Triggered by Rule %s)';
$lang->action->trash = 'Corbeille';
$lang->action->undelete = 'Restaurer';
+1
View File
@@ -35,6 +35,7 @@ $lang->action->old = '旧值为';
$lang->action->new = '新值为';
$lang->action->to = '到';
$lang->action->superReviewer = '超级评审人';
$lang->action->byRule = '(由规则引擎%s触发)';
$lang->action->trash = '回收站';
$lang->action->undelete = '还原';
+21 -3
View File
@@ -197,6 +197,7 @@ class actionModel extends model
$this->loadModel('workflow');
foreach($actions as $actionID => $action)
{
$extra = $action->extra;
$actionName = strtolower($action->action);
if($this->config->edition != 'open' && !isset($flowList[$action->objectType])) $flowList[$action->objectType] = $this->workflow->getByModule($action->objectType);
@@ -426,7 +427,7 @@ class actionModel extends model
->andWhere('vision')->eq($this->config->vision)
->orderBy($orderBy)
->page($pager)
->fetchAll();
->fetchAll('', false);
if(empty($trashes)) return array();
/* 按对象类型对已删除的对象进行分组,并获取名称字段。 */
@@ -455,6 +456,13 @@ class actionModel extends model
$objectNames['jenkins'] = $this->dao->select("id, {$field} AS name")->from($table)->where('id')->in($objectIdList)->andWhere('type')->eq('jenkins')->fetchPairs();
$objectNames['gitlab'] = $this->dao->select("id, {$field} AS name")->from($table)->where('id')->in($objectIdList)->andWhere('type')->eq('gitlab')->fetchPairs();
}
elseif($objectType == 'pivot')
{
$objectNames[$objectType] = $this->dao->select("t1.id, t2.{$field} AS name")->from($table)->alias('t1')
->leftJoin(TABLE_PIVOTSPEC)->alias('t2')->on('t1.id = t2.pivot and t1.version = t2.version')
->where('t1.id')->in($objectIdList)
->fetchPairs();
}
else
{
$objectNames[$objectType] = $this->dao->select("id, {$field} AS name")->from($table)->where('id')->in($objectIdList)->fetchPairs();
@@ -577,7 +585,7 @@ class actionModel extends model
*/
public function getHistory(array|int $actionID): array
{
return $this->dao->select()->from(TABLE_HISTORY)->where('action')->in($actionID)->fetchGroup('action');
return $this->dao->select('*')->from(TABLE_HISTORY)->where('action')->in($actionID)->fetchGroup('action');
}
/**
@@ -831,6 +839,11 @@ class actionModel extends model
list($extra) = explode('|', $extra);
if(!empty($desc['extra'][$extra])) $actionDesc = str_replace('$extra', $desc['extra'][$extra], $desc['main']);
}
if(($action->objectType == 'story' || $action->objectType == 'demand') && $action->action == 'fromboard')
{
$actionExtra = html::a(helper::createLink('board', 'view', "canvasID={$action->extra}"), $action->extra);
$actionDesc = str_replace('$extra', $actionExtra, $desc['main']);
}
if($action->objectType == 'module' && strpos(',created,moved,', $action->action) !== false)
{
@@ -849,11 +862,16 @@ class actionModel extends model
if($action->objectType == 'board' && in_array($action->action, array('importstory', 'importdemand', 'importrequirement', 'importepic', 'convertdemand', 'convertepic', 'convertrequirement', 'convertstory')))
{
if($action->action == 'importstory' || $action->action == 'importrequirement' || $action->action == 'convertstory' || $action->action == 'convertrequirement')
if($action->action == 'importstory' || $action->action == 'importrequirement' || $action->action == 'convertstory')
{
$story = $this->loadModel('story')->getById((int)$action->extra);
$link = helper::createLink('story', 'view', "storyID={$action->extra}");
}
if($action->action == 'convertrequirement')
{
$story = $this->loadModel('story')->getById((int)$action->extra);
$link = helper::createLink('requirement', 'view', "storyID={$action->extra}");
}
if($action->action == 'importdemand' || $action->action == 'convertdemand')
{
$story = $this->loadModel('demand')->getByID((int)$action->extra);
+2 -1
View File
@@ -88,7 +88,7 @@ class actionZen extends action
if($trash->objectType == 'pivot')
{
$pivotNames = json_decode($trash->objectName, true);
if(empty($pivotNames)) return;
$pivotNames = empty($pivotNames) ? array() : $pivotNames;
$trash->objectName = zget($pivotNames, $this->app->getClientLang(), '') ? : reset(array_filter($pivotNames));
}
else
@@ -116,6 +116,7 @@ class actionZen extends action
if($trash->objectType == 'doc')
{
$params = "docID={$trash->objectID}";
if($trash->comment == 'chapter') $module = 'chapter';
}
if(in_array($module, array('traincourse','traincontents')))
{
+2 -2
View File
@@ -9,7 +9,7 @@ $config->admin->menuGroup['system'] = array('custom|mode', 'backup', 'cron',
$config->admin->menuGroup['company'] = array('dept', 'company', 'user', 'group', 'tutorial');
$config->admin->menuGroup['switch'] = array('admin|setmodule');
$config->admin->menuGroup['model'] = array('auditcl', 'stage', 'design', 'cmcl', 'reviewcl', 'custom|required', 'custom|set', 'custom|flow', 'custom|code', 'custom|percent','custom|estimate', 'custom|hours', 'subject', 'process', 'activity', 'zoutput', 'classify', 'holiday', 'reviewsetting', 'custom|project');
$config->admin->menuGroup['feature'] = array('custom|set', 'custom|product', 'custom|execution', 'custom|required', 'custom|kanban', 'approvalflow', 'measurement', 'meetingroom', 'custom|browsestoryconcept', 'custom|kanban', 'sqlbuilder', 'report', 'custom|limittaskdate', 'measurement');
$config->admin->menuGroup['feature'] = array('custom|set', 'custom|product', 'custom|execution', 'custom|required', 'custom|kanban', 'measurement', 'meetingroom', 'custom|browsestoryconcept', 'custom|kanban', 'sqlbuilder', 'report', 'custom|limittaskdate', 'measurement');
$config->admin->menuGroup['template'] = array('custom|set', 'baseline');
$config->admin->menuGroup['message'] = array('mail', 'webhook', 'sms', 'message');
$config->admin->menuGroup['dev'] = array('dev', 'entry', 'editor');
@@ -61,7 +61,7 @@ $config->admin->plugins[194]->abstract = '每日生成公司级禅道应用巡
$config->admin->plugins[194]->viewLink = 'https://www.zentao.net/extension-viewExt-194.html';
$config->admin->apiRoot = 'https://www.zentao.net';
$config->admin->cdnRoot = 'https://cdn.easycorp.cn/web';
$config->admin->cdnRoot = 'https://static.zentao.net/web';
$config->admin->classURL = 'https://www.zentao.net/zentao.html';
$config->admin->dynamicURL = 'https://www.zentao.net/dynamic.html';
$config->admin->downloadURL = 'https://www.zentao.net/download.html';
+1 -1
View File
@@ -57,7 +57,7 @@ $(() =>
$('select[name="type"]').on('change', handleModelTypeChange);
$('select[name="vendor"]').on('change', handleVendorChange);
$('select[name="proxyType"]').on('change', handleProxyTypeChange);
$('button[type="submit"]]').on('click', e =>
$('button[type="submit"]').on('click', e =>
{
$('button[type="submit"]').attr('disabled', 'disabled');
$('#test-conn-btn').attr('disabled', 'disabled');
+1 -1
View File
@@ -59,7 +59,7 @@ $(() =>
$('select[name="type"]').on('change', handleModelTypeChange);
$('select[name="vendor"]').on('change', handleVendorChange);
$('select[name="proxyType"]').on('change', handleProxyTypeChange);
$('button[type="submit"]]').on('click', e =>
$('button[type="submit"]').on('click', e =>
{
$('button[type="submit"]').attr('disabled', 'disabled');
$('#test-conn-btn').attr('disabled', 'disabled');
+2
View File
@@ -282,6 +282,7 @@ class api extends control
$api->icon = "api is-$api->method";
$api->api = true;
$api->title = "$api->method $api->path " . htmlspecialchars_decode($api->title);
$api->editable = common::hasPriv('api', 'edit');
}
$data['docs'] = array_values($apis);
}
@@ -341,6 +342,7 @@ class api extends control
$api->icon = "api is-$api->method";
$api->title = "$api->method $api->path $api->title";
$api->api = true;
$api->editable = common::hasPriv('api', 'edit');
}
$this->send($api);
}
-5
View File
@@ -1,7 +1,2 @@
.table-empty-tip {padding: 80px 10px; text-align: center; background: #fff;}
.table-empty-tip .btn-group .btn {background-color: rgba(var(--color-primary-50-rgb),var(--tw-bg-opacity)); color: var(--nav-active-color); --tw-ring-color: rgba(var(--color-primary-50-rgb),var(--tw-bg-opacity));}
.picker-menu .menu-item.is-nested > .item-inner .item-content .item-title {flex-grow: 0!important; opacity: .6;}
.picker-menu .menu-item.is-nested > .item-inner .item-content .item-subtitle {--skin-pale-rgb: var(--color-gray-500-rgb); height: 1rem; min-width: 1rem; border-radius: var(--radius-full); --skin-color: rgba(var(--skin-pale-rgb), .1); --skin-text: rgba(var(--skin-pale-rgb), 1); --state-color: rgba(var(--skin-pale-rgb), .1); --skin-bg: var(--skin-color); --skin-ring: var(--skin-color); --skin-border: var(--skin-color); --skin-hover-text: var(--skin-text); --skin-hover-bg: var(--skin-bg); --tw-ring-color: var(--skin-ring); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); --color-link-hover: var(--skin-text); background-color: var(--skin-bg); border-color: var(--skin-border); box-shadow: var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000); color: var(--skin-text); display: inline-flex; font-size: .75rem; align-items: center; justify-content: center; line-height: 1rem; padding-left: .25rem; padding-right: .25rem;}
.picker-menu .menu-item.is-nested > .item-inner {background-color: inherit!important; color: inherit!important; cursor: default;}
.picker-menu .menu-item.is-nested > .item-inner::before {display: none;}
+42
View File
@@ -4760,3 +4760,45 @@ $config->bi->builtin->metrics[] = array
'desc' => '按系统统计的主机总数是指在禅道中的全部主机总数。',
'definition' => "所有主机的个数求和"
);
$config->bi->builtin->metrics[] = array
(
'name' => '按月统计的人均研发效能',
'alias' => '人均研发效能',
'code' => 'avg_of_dev_efficiency',
'purpose' => 'rate',
'scope' => 'system',
'object' => 'user',
'unit' => 'percentage',
'dateType' => 'month',
'desc' => '指团队成员在单位时间内完成的平均需求规模。它用于评估团队的生产力、资源利用效率以及工作负荷的合理性。',
'definition' => "按月统计的人均研发效能 = 当月发布的研发需求的规模总数 / 当月禅道系统中的总人数\n当月发布的研发需求,是统计当月阶段状态为已发布、已关闭关闭原因为已完成的研发需求,过滤已删除的研发需求,次月过滤已统计过的研发需求。"
);
$config->bi->builtin->metrics[] = array
(
'name' => '按月统计的已发布研发需求平均交付周期',
'alias' => '已发布研发需求平均交付周期',
'code' => 'avg_of_release_story_delivery_time',
'purpose' => 'time',
'scope' => 'system',
'object' => 'story',
'unit' => 'day',
'dateType' => 'month',
'desc' => '指从需求被提出(创建)到最终交付给客户或上线所需的平均时间,反映了团队或组织在需求实现过程中的效率。它可以用于评估需求的响应速度和交付速度。',
'definition' => "按月统计的已发布研发需求平均交付周期 = sum ( 当月发布的研发需求的发布时间 - 当月发布的研发需求的创建时间 ) / 当月发布的研发需求总数\n当月发布的研发需求,是统计当月阶段状态为已发布、已关闭关闭原因为已完成的研发需求,过滤已删除的研发需求,次月过滤已统计过的研发需求。"
);
$config->bi->builtin->metrics[] = array
(
'name' => '按月统计的已发布研发需求平均缺陷密度',
'alias' => '已发布研发需求平均缺陷密度',
'code' => 'avg_of_release_story_defect_density',
'purpose' => 'qc',
'scope' => 'system',
'object' => 'story',
'unit' => 'count',
'dateType' => 'month',
'desc' => '指在测试阶段发现的缺陷数量与需求规模的比值。它反映了代码或系统的质量状况,用于评估开发过程的质量以及测试的有效性。',
'definition' => "按月统计的已发布研发需求平均缺陷密度 = sum ( 当月发布的研发需求关联的Bug总数 ) / 当月发布的研发需求的规模总数\n当月发布的研发需求,是统计当月阶段状态为已发布、已关闭关闭原因为已完成的研发需求,过滤已删除的研发需求,次月过滤已统计过的研发需求。\n当月发布的研发需求关联的Bug总数,是统计当月发布的每个研发需求关联Bug,过滤已删除的bug。"
);
+3 -3
View File
@@ -197,12 +197,12 @@ $lang->block->default['full']['my'][] = array('title' => "Recent {$lang->project
$lang->block->default['full']['my'][] = array('title' => "Undone {$lang->projectCommon} list", 'module' => 'project', 'code' => 'project', 'width' => '2', 'params' => array('type' => 'undone', 'count' => '20', 'orderBy' => 'id_desc'));
$lang->block->default['full']['my'][] = array('title' => "{$lang->projectCommon} Statistic", 'module' => 'project', 'code' => 'statistic', 'width' => '2', 'params' => array('type' => 'undone', 'count' => '20'));
$lang->block->default['full']['my'][] = array('title' => "Undone {$lang->execution->common} Statistic", 'module' => 'execution', 'code' => 'statistic', 'width' => '2', 'params' => array('type' => 'undone', 'count' => '20'));
$lang->block->default['full']['my'][] = array('title' => "Unclosed {$lang->productCommon} Statistic", 'module' => 'product', 'code' => 'statistic', 'width' => '2', 'params' => array('type' => 'noclosed', 'count' => '20'));
$lang->block->default['full']['my'][] = array('title' => "Unclosed {$lang->productCommon} test statistics", 'module' => 'qa', 'code' => 'statistic', 'width' => '2', 'params' => array('type' => 'noclosed', 'count' => '20'));
if($config->vision != 'lite') $lang->block->default['full']['my'][] = array('title' => "Unclosed {$lang->productCommon} Statistic", 'module' => 'product', 'code' => 'statistic', 'width' => '2', 'params' => array('type' => 'noclosed', 'count' => '20'));
if($config->vision != 'lite') $lang->block->default['full']['my'][] = array('title' => "Unclosed {$lang->productCommon} test statistics", 'module' => 'qa', 'code' => 'statistic', 'width' => '2', 'params' => array('type' => 'noclosed', 'count' => '20'));
$lang->block->default['full']['my'][] = array('title' => "Zentao Dynamic", 'module' => 'zentaodynamic', 'code' => 'zentaodynamic', 'width' => '1');
$lang->block->default['full']['my'][] = array('title' => 'Dynamic', 'module' => 'dynamic', 'code' => 'dynamic', 'width' => '1');
$lang->block->default['full']['my'][] = array('title' => "Team", 'module' => 'teamachievement', 'code' => 'teamachievement', 'width' => '1');
$lang->block->default['full']['my'][] = array('title' => "{$lang->productCommon} Overview", 'module' => 'product', 'code' => 'overview', 'width' => '1');
if($config->vision != 'lite') $lang->block->default['full']['my'][] = array('title' => "{$lang->productCommon} Overview", 'module' => 'product', 'code' => 'overview', 'width' => '1');
$lang->block->default['full']['my'][] = array('title' => "{$lang->projectCommon} Overview", 'module' => 'project', 'code' => 'overview', 'width' => '1');
$lang->block->default['full']['my'][] = array('title' => "{$lang->execution->common} Overview", 'module' => 'execution', 'code' => 'overview', 'width' => '1');
+3 -3
View File
@@ -197,12 +197,12 @@ $lang->block->default['full']['my'][] = array('title' => "Recent {$lang->project
$lang->block->default['full']['my'][] = array('title' => "Undone {$lang->projectCommon} list", 'module' => 'project', 'code' => 'project', 'width' => '2', 'params' => array('type' => 'undone', 'count' => '20', 'orderBy' => 'id_desc'));
$lang->block->default['full']['my'][] = array('title' => "{$lang->projectCommon} Statistic", 'module' => 'project', 'code' => 'statistic', 'width' => '2', 'params' => array('type' => 'undone', 'count' => '20'));
$lang->block->default['full']['my'][] = array('title' => "Undone {$lang->execution->common} Statistic", 'module' => 'execution', 'code' => 'statistic', 'width' => '2', 'params' => array('type' => 'undone', 'count' => '20'));
$lang->block->default['full']['my'][] = array('title' => "Unclosed {$lang->productCommon} Statistic", 'module' => 'product', 'code' => 'statistic', 'width' => '2', 'params' => array('type' => 'noclosed', 'count' => '20'));
$lang->block->default['full']['my'][] = array('title' => "Unclosed {$lang->productCommon} test statistics", 'module' => 'qa', 'code' => 'statistic', 'width' => '2', 'params' => array('type' => 'noclosed', 'count' => '20'));
if($config->vision != 'lite') $lang->block->default['full']['my'][] = array('title' => "Unclosed {$lang->productCommon} Statistic", 'module' => 'product', 'code' => 'statistic', 'width' => '2', 'params' => array('type' => 'noclosed', 'count' => '20'));
if($config->vision != 'lite') $lang->block->default['full']['my'][] = array('title' => "Unclosed {$lang->productCommon} test statistics", 'module' => 'qa', 'code' => 'statistic', 'width' => '2', 'params' => array('type' => 'noclosed', 'count' => '20'));
$lang->block->default['full']['my'][] = array('title' => "Zentao Dynamic", 'module' => 'zentaodynamic', 'code' => 'zentaodynamic', 'width' => '1');
$lang->block->default['full']['my'][] = array('title' => 'Dynamic', 'module' => 'dynamic', 'code' => 'dynamic', 'width' => '1');
$lang->block->default['full']['my'][] = array('title' => "Team", 'module' => 'teamachievement', 'code' => 'teamachievement', 'width' => '1');
$lang->block->default['full']['my'][] = array('title' => "{$lang->productCommon} Overview", 'module' => 'product', 'code' => 'overview', 'width' => '1');
if($config->vision != 'lite') $lang->block->default['full']['my'][] = array('title' => "{$lang->productCommon} Overview", 'module' => 'product', 'code' => 'overview', 'width' => '1');
$lang->block->default['full']['my'][] = array('title' => "{$lang->projectCommon} Overview", 'module' => 'project', 'code' => 'overview', 'width' => '1');
$lang->block->default['full']['my'][] = array('title' => "{$lang->execution->common} Overview", 'module' => 'execution', 'code' => 'overview', 'width' => '1');
+3 -3
View File
@@ -197,12 +197,12 @@ $lang->block->default['full']['my'][] = array('title' => "Recent {$lang->project
$lang->block->default['full']['my'][] = array('title' => "Undone {$lang->projectCommon} list", 'module' => 'project', 'code' => 'project', 'width' => '2', 'params' => array('type' => 'undone', 'count' => '20', 'orderBy' => 'id_desc'));
$lang->block->default['full']['my'][] = array('title' => "{$lang->projectCommon} Statistic", 'module' => 'project', 'code' => 'statistic', 'width' => '2', 'params' => array('type' => 'undone', 'count' => '20'));
$lang->block->default['full']['my'][] = array('title' => "Undone {$lang->execution->common} Statistic", 'module' => 'execution', 'code' => 'statistic', 'width' => '2', 'params' => array('type' => 'undone', 'count' => '20'));
$lang->block->default['full']['my'][] = array('title' => "Unclosed {$lang->productCommon} Statistic", 'module' => 'product', 'code' => 'statistic', 'width' => '2', 'params' => array('type' => 'noclosed', 'count' => '20'));
$lang->block->default['full']['my'][] = array('title' => "Unclosed {$lang->productCommon} test statistics", 'module' => 'qa', 'code' => 'statistic', 'width' => '2', 'params' => array('type' => 'noclosed', 'count' => '20'));
if($config->vision != 'lite') $lang->block->default['full']['my'][] = array('title' => "Unclosed {$lang->productCommon} Statistic", 'module' => 'product', 'code' => 'statistic', 'width' => '2', 'params' => array('type' => 'noclosed', 'count' => '20'));
if($config->vision != 'lite') $lang->block->default['full']['my'][] = array('title' => "Unclosed {$lang->productCommon} test statistics", 'module' => 'qa', 'code' => 'statistic', 'width' => '2', 'params' => array('type' => 'noclosed', 'count' => '20'));
$lang->block->default['full']['my'][] = array('title' => "Zentao Dynamic", 'module' => 'zentaodynamic', 'code' => 'zentaodynamic', 'width' => '1');
$lang->block->default['full']['my'][] = array('title' => 'Dynamic', 'module' => 'dynamic', 'code' => 'dynamic', 'width' => '1');
$lang->block->default['full']['my'][] = array('title' => "Team", 'module' => 'teamachievement', 'code' => 'teamachievement', 'width' => '1');
$lang->block->default['full']['my'][] = array('title' => "{$lang->productCommon} Overview", 'module' => 'product', 'code' => 'overview', 'width' => '1');
if($config->vision != 'lite') $lang->block->default['full']['my'][] = array('title' => "{$lang->productCommon} Overview", 'module' => 'product', 'code' => 'overview', 'width' => '1');
$lang->block->default['full']['my'][] = array('title' => "{$lang->projectCommon} Overview", 'module' => 'project', 'code' => 'overview', 'width' => '1');
$lang->block->default['full']['my'][] = array('title' => "{$lang->execution->common} Overview", 'module' => 'execution', 'code' => 'overview', 'width' => '1');
+3 -3
View File
@@ -197,12 +197,12 @@ $lang->block->default['full']['my'][] = array('title' => "我近期参与的{$la
$lang->block->default['full']['my'][] = array('title' => "未完成的{$lang->projectCommon}列表", 'module' => 'project', 'code' => 'project', 'width' => '2', 'params' => array('type' => 'undone', 'count' => '20', 'orderBy' => 'id_desc'));
$lang->block->default['full']['my'][] = array('title' => "未完成的{$lang->execution->common}统计", 'module' => 'execution', 'code' => 'statistic', 'width' => '2', 'params' => array('type' => 'undone', 'count' => '20'));
$lang->block->default['full']['my'][] = array('title' => "未完成的{$lang->projectCommon}统计", 'module' => 'project', 'code' => 'statistic', 'width' => '2', 'params' => array('type' => 'undone', 'count' => '20'));
$lang->block->default['full']['my'][] = array('title' => "未关闭的{$lang->productCommon}统计", 'module' => 'product', 'code' => 'statistic', 'width' => '2', 'params' => array('type' => 'noclosed', 'count' => '20'));
$lang->block->default['full']['my'][] = array('title' => "未关闭{$lang->productCommon}的测试统计", 'module' => 'qa', 'code' => 'statistic', 'width' => '2', 'params' => array('type' => 'noclosed', 'count' => '20'));
if($config->vision != 'lite') $lang->block->default['full']['my'][] = array('title' => "未关闭的{$lang->productCommon}统计", 'module' => 'product', 'code' => 'statistic', 'width' => '2', 'params' => array('type' => 'noclosed', 'count' => '20'));
if($config->vision != 'lite') $lang->block->default['full']['my'][] = array('title' => "未关闭{$lang->productCommon}的测试统计", 'module' => 'qa', 'code' => 'statistic', 'width' => '2', 'params' => array('type' => 'noclosed', 'count' => '20'));
$lang->block->default['full']['my'][] = array('title' => "禅道动态", 'module' => 'zentaodynamic', 'code' => 'zentaodynamic', 'width' => '1');
$lang->block->default['full']['my'][] = array('title' => "最新动态", 'module' => 'dynamic', 'code' => 'dynamic', 'width' => '1');
$lang->block->default['full']['my'][] = array('title' => "团队成就", 'module' => 'teamachievement', 'code' => 'teamachievement', 'width' => '1');
$lang->block->default['full']['my'][] = array('title' => "{$lang->productCommon}总览", 'module' => 'product', 'code' => 'overview', 'width' => '1');
if($config->vision != 'lite') $lang->block->default['full']['my'][] = array('title' => "{$lang->productCommon}总览", 'module' => 'product', 'code' => 'overview', 'width' => '1');
$lang->block->default['full']['my'][] = array('title' => "{$lang->projectCommon}总览", 'module' => 'project', 'code' => 'overview', 'width' => '1');
$lang->block->default['full']['my'][] = array('title' => "{$lang->execution->common}总览", 'module' => 'execution', 'code' => 'overview', 'width' => '1');
+10 -8
View File
@@ -1893,7 +1893,7 @@ class blockZen extends block
/* 通过度量项获取迭代总量。 */
$executionCount = 0;
$executionCountGroup = $project ? $this->metric->getResultByCodeWithArray('count_of_execution_in_project', array('project' => $project)) : $this->metric->getResultByCodeWithArray('count_of_execution', array(), 'cron');
$executionCountGroup = $project ? $this->metric->getResultByCodeWithArray('count_of_execution_in_project', array('project' => $project)) : $this->metric->getResultByCodeWithArray('count_of_execution', array(), 'cron', null, $this->config->vision);
if(!empty($executionCountGroup))
{
$executionCountGroup = reset($executionCountGroup);
@@ -1902,7 +1902,7 @@ class blockZen extends block
/* 通过度量项获取今年完成的迭代数量。 */
$finishedExecution = 0;
$finishedExecutionGroup = $project ? $this->metric->getResultByCodeWithArray('count_of_annual_finished_execution_in_project', array('project' => $project, 'year' => date('Y')), 'cron') : $this->metric->getResultByCodeWithArray('count_of_annual_finished_execution', array('year' => date('Y')), 'cron');
$finishedExecutionGroup = $project ? $this->metric->getResultByCodeWithArray('count_of_annual_finished_execution_in_project', array('project' => $project, 'year' => date('Y')), 'cron') : $this->metric->getResultByCodeWithArray('count_of_annual_finished_execution', array('year' => date('Y')), 'cron', null, $this->config->vision);
if(!empty($finishedExecutionGroup))
{
$finishedExecutionGroup = reset($finishedExecutionGroup);
@@ -1911,7 +1911,7 @@ class blockZen extends block
/* 通过度量项获取未开始的迭代数量。 */
$waitExecution = 0;
$waitExecutionGroup = $project ? $this->metric->getResultByCodeWithArray('count_wait_execution_in_project', array('project' => $project), 'cron') : $this->metric->getResultByCodeWithArray('count_of_wait_execution', array(), 'cron');
$waitExecutionGroup = $project ? $this->metric->getResultByCodeWithArray('count_wait_execution_in_project', array('project' => $project), 'cron') : $this->metric->getResultByCodeWithArray('count_of_wait_execution', array(), 'cron', null, $this->config->vision);
if(!empty($waitExecutionGroup))
{
$waitExecutionGroup = reset($waitExecutionGroup);
@@ -1920,7 +1920,7 @@ class blockZen extends block
/* 通过度量项获取进行中的迭代数量。 */
$doingExecution = 0;
$doingExecutionGroup = $project ? $this->metric->getResultByCodeWithArray('count_of_doing_execution_in_project', array('project' => $project), 'cron') : $this->metric->getResultByCodeWithArray('count_of_doing_execution', array(), 'cron');
$doingExecutionGroup = $project ? $this->metric->getResultByCodeWithArray('count_of_doing_execution_in_project', array('project' => $project), 'cron') : $this->metric->getResultByCodeWithArray('count_of_doing_execution', array(), 'cron', null, $this->config->vision);
if(!empty($doingExecutionGroup))
{
$doingExecutionGroup = reset($doingExecutionGroup);
@@ -1929,7 +1929,7 @@ class blockZen extends block
/* 通过度量项获取已挂起的迭代数量。 */
$suspendedExecution = 0;
$suspendedExecutionGroup = $project ? $this->metric->getResultByCodeWithArray('count_of_suspended_execution_in_project', array('project' => $project), 'cron') : $this->metric->getResultByCodeWithArray('count_of_suspended_execution', array(), 'cron');
$suspendedExecutionGroup = $project ? $this->metric->getResultByCodeWithArray('count_of_suspended_execution_in_project', array('project' => $project), 'cron') : $this->metric->getResultByCodeWithArray('count_of_suspended_execution', array(), 'cron', null, $this->config->vision);
if(!empty($suspendedExecutionGroup))
{
$suspendedExecutionGroup = reset($suspendedExecutionGroup);
@@ -1937,7 +1937,7 @@ class blockZen extends block
}
/* 如果是地盘下的区块跳转到执行列表,如果是单项目下的区块跳转到项目的迭代列表。 */
$url = common::hasPriv('execution', 'all') ? helper::createLink('execution', 'all', 'status=all') : null;
$url = common::hasPriv('execution', 'all') && $this->config->vision != 'lite' ? helper::createLink('execution', 'all', 'status=all') : null;
if($project) $url = common::hasPriv('project', 'execution') ? helper::createLink('project', 'execution', "status=all&projectID=$project") : null;
/* 组装区块左侧的数据。 */
@@ -2086,7 +2086,7 @@ class blockZen extends block
}
$this->view->reviews = $reviews;
if($this->config->edition == 'max' or $this->config->edition == 'ipd')
if($this->config->edition != 'open')
{
$this->app->loadLang('approval');
$this->view->flows = $this->dao->select('module,name')->from(TABLE_WORKFLOW)->where('buildin')->eq(0)->fetchPairs('module', 'name');
@@ -2210,7 +2210,7 @@ class blockZen extends block
$this->view->{$objectList[$objectType]} = $objects;
}
if(isset($hasViewPriv['testcase'])) $this->view->testcases = $this->loadModel('testcase')->getByAssignedTo($this->app->user->account, 'skip|run', 'id_desc', $pager);
if(isset($hasViewPriv['testtask'])) $this->view->testtasks = $this->loadModel('testtask')->getByUser($this->app->user->account, $pager, 'id_desc', 'assignedTo');
if(isset($hasViewPriv['testtask'])) $this->view->testtasks = $this->loadModel('testtask')->getByUser($this->app->user->account, $pager, 'id_desc', 'wait');
if(isset($hasViewPriv['meeting']))
{
@@ -2474,6 +2474,7 @@ class blockZen extends block
$stmt = $this->dao->select('id,product,lib,title,type,addedBy,addedDate,editedDate,status,acl,`groups`,users,deleted')->from(TABLE_DOC)->alias('t1')
->where('deleted')->eq(0)
->andWhere('product')->in($productIdList)
->beginIF($this->config->doc->notArticleType)->andWhere('t1.type')->notIN($this->config->doc->notArticleType)->fi()
->orderBy('product,status,editedDate_desc')
->query();
$docGroup = array();
@@ -2549,6 +2550,7 @@ class blockZen extends block
$stmt = $this->dao->select('t1.id,t1.lib,t1.title,t1.type,t1.addedBy,t1.addedDate,t1.editedDate,t1.status,t1.acl,t1.groups,t1.users,t1.deleted,if(t1.project = 0, t2.project, t1.project) as project')->from(TABLE_DOC)->alias('t1')
->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.execution=t2.id')
->where('t1.deleted')->eq(0)
->beginIF($this->config->doc->notArticleType)->andWhere('t1.type')->notIN($this->config->doc->notArticleType)->fi()
->andWhere('t2.deleted', true)->eq(0)
->orWhere('t2.deleted is null')
->markRight(1)

Some files were not shown because too many files have changed in this diff Show More