diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000..319fd640ef
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,13 @@
+sonarqube:
+ variables:
+ SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar" # Defines the location of the analysis task cache
+ GIT_DEPTH: "0" # Tells git to fetch all the branches of the project, required by the analysis task
+ cache:
+ key: "${CI_JOB_NAME}"
+ paths:
+ - .sonar/cache
+ script:
+ - sonar-scanner -Dsonar.inclusions=$(git diff --name-only HEAD~1|tr '\n' ',')
+ allow_failure: true
+ only:
+ - master # or the name of your main branch
diff --git a/config/zentaopms.php b/config/zentaopms.php
index dd24aaf97f..86ef7cdbe9 100644
--- a/config/zentaopms.php
+++ b/config/zentaopms.php
@@ -378,3 +378,5 @@ $config->newFeatures = array('introduction', 'tutorial', 'youngBlueTheme', 'visi
$config->programPriv = new stdclass();
$config->programPriv->scrum = array('story', 'projectstory', 'projectrelease', 'project', 'execution', 'build', 'bug', 'testcase', 'testreport', 'doc', 'repo', 'meeting', 'stakeholder', 'testtask');
$config->programPriv->waterfall = array_merge($config->programPriv->scrum, array('task', 'workestimation', 'durationestimation', 'budget', 'programplan', 'review', 'reviewissue', 'weekly', 'cm', 'milestone', 'design', 'issue', 'risk', 'opportunity', 'measrecord', 'auditplan', 'trainplan', 'gapanalysis', 'pssp', 'researchplan', 'researchreport'));
+
+$config->waterfallModules = array('workestimation', 'durationestimation', 'budget', 'programplan', 'review', 'reviewissue', 'weekly', 'cm', 'milestone', 'design', 'opportunity', 'auditplan', 'trainplan', 'gapanalysis', 'pssp', 'researchplan', 'researchreport');
diff --git a/db/update16.5.sql b/db/update16.5.sql
index bb437efddd..f3e96818db 100644
--- a/db/update16.5.sql
+++ b/db/update16.5.sql
@@ -89,3 +89,327 @@ REPLACE INTO `zt_zoutput` (`id`, `activity`, `name`, `content`, `optional`, `sta
DELETE FROM `zt_config` WHERE `section` = 'customMenu';
UPDATE `zt_story` SET `plan` = '' WHERE `plan` = 0;
+
+UPDATE `zt_workflowfield` set `control` = 'datetime' WHERE `module` = 'testcase' and `field` IN ('openedDate','reviewedDate','lastEditedDate','lastRunDate');
+
+UPDATE `zt_workflowfield` set `control` = 'date' WHERE `module` = 'testtask' and `field` IN ('begin','end');
+UPDATE `zt_workflowfield` set `control` = 'richtext' WHERE `module` = 'testtask' and `field` = 'desc';
+
+UPDATE `zt_workflowfield` set `control` = 'datetime' WHERE `module` IN ('testsuite','caselib') and `field` IN ('addedDate','lastEditedDate');
+UPDATE `zt_workflowfield` set `control` = 'richtext' WHERE `module` = 'testsuite' and `field` = 'desc';
+
+UPDATE `zt_workflowfield` set `control` = 'datetime' WHERE `module` = 'feedback' and `field` IN ('openedDate','reviewedDate','processedDate','closedDate','editedDate','assignedDate');
+
+UPDATE `zt_workflowfield` SET `control` = 'datetime' WHERE `module` = 'product' and `field` = 'createdDate';
+UPDATE `zt_workflowfield` SET `control` = 'date' WHERE `module` = 'productplan' and `field` = 'begin';
+UPDATE `zt_workflowfield` SET `control` = 'date' WHERE `module` = 'productplan' and `field` = 'end';
+UPDATE `zt_workflowfield` SET `control` = 'radio' WHERE `module` = 'productplan' and `field` = 'deleted';
+UPDATE `zt_workflowfield` SET `control` = 'select', `options` = 12 WHERE `module` = 'productplan' and `field` = 'parent';
+UPDATE `zt_workflowfield` SET `control` = 'date' WHERE `module` = 'release' and `field` = 'date';
+UPDATE `zt_workflowfield` SET `control` = 'richtext' WHERE `module` = 'release' and `field` = 'desc';
+UPDATE `zt_workflowfield` SET `control` = 'radio' WHERE `module` = 'release' and `field` = 'deleted';
+UPDATE `zt_workflowfield` SET `control` = 'datetime' WHERE `module` = 'story' and `field` = 'assignedDate';
+UPDATE `zt_workflowfield` SET `control` = 'datetime' WHERE `module` = 'story' and `field` = 'lastEditedDate';
+UPDATE `zt_workflowfield` SET `control` = 'date' WHERE `module` = 'story' and `field` = 'reviewedDate';
+UPDATE `zt_workflowfield` SET `control` = 'datetime' WHERE `module` = 'story' and `field` = 'closedDate';
+UPDATE `zt_workflowfield` SET `control` = 'radio' WHERE `module` = 'story' and `field` = 'deleted';
+
+UPDATE `zt_workflowaction` SET `position` = 'browse' WHERE `module` = 'product' and `action` = 'all' and `vision` = 'rnd';
+UPDATE `zt_workflowaction` SET `type` = 'batch', `position` = 'browse' WHERE `module` = 'product' and `action` = 'batchedit' and `vision` = 'rnd';
+UPDATE `zt_workflowaction` SET `position` = 'browse' WHERE `module` = 'product' and `action` = 'browse' and `vision` = 'rnd';
+UPDATE `zt_workflowaction` SET `show` = 'direct' WHERE `module` = 'product' and `vision` = 'rnd';
+UPDATE `zt_workflowaction` SET `type` = 'batch', `position` = 'browse' WHERE `module` = 'productplan' and `action` = 'batchedit' and `vision` = 'rnd';
+UPDATE `zt_workflowaction` SET `position` = 'browse' WHERE `module` = 'productplan' and `action` = 'browse' and `vision` = 'rnd';
+UPDATE `zt_workflowaction` SET `show` = 'direct' WHERE `module` = 'productplan' and `vision` = 'rnd';
+UPDATE `zt_workflowaction` SET `position` = 'browse' WHERE `module` = 'release' and `action` = 'browse' and `vision` = 'rnd';
+UPDATE `zt_workflowaction` SET `show` = 'direct' WHERE `module` = 'release' and `vision` = 'rnd';
+UPDATE `zt_workflowaction` SET `type` = 'batch' WHERE `module` = 'story' and `action` = 'batchcreate' and `vision` = 'rnd';
+UPDATE `zt_workflowaction` SET `type` = 'batch' WHERE `module` = 'story' and `action` = 'batchedit' and `vision` = 'rnd';
+UPDATE `zt_workflowaction` SET `position` = 'browse' WHERE `module` = 'story' and `action` = 'browse' and `vision` = 'rnd';
+UPDATE `zt_workflowaction` SET `position` = 'browse' WHERE `module` = 'story' and `action` = 'exporttemplate' and `vision` = 'rnd';
+UPDATE `zt_workflowaction` SET `position` = 'browse' WHERE `module` = 'story' and `action` = 'import' and `vision` = 'rnd';
+UPDATE `zt_workflowaction` SET `position` = 'view' WHERE `module` = 'story' and `action` = 'view' and `vision` = 'rnd';
+DELETE FROM `zt_workflowfield` WHERE `module`='execution' AND `field`='stage';
+DELETE FROM `zt_workflowfield` WHERE `module`='program' AND `field`='stage';
+DELETE FROM `zt_workflowfield` WHERE `module`='project' AND `field`='stage';
+DELETE FROM `zt_workflowfield` WHERE `module`='bug' AND `field`='feedback';
+DELETE FROM `zt_workflowfield` WHERE `module`='task' AND `field`='feedback';
+
+REPLACE INTO `zt_workflowdatasource` (`type`, `name`, `code`, `buildin`, `datasource`, `view`, `keyField`, `valueField`) VALUES
+('lang', '项目模型', 'projectModel', '1', 'projectModel', '', '', ''),
+('lang', '反馈类型', 'feedbackType', '1', 'feedbackType', '', '', ''),
+('lang', '反馈处理方案', 'feedbackSolution', '1', 'feedbackSolution', '', '', ''),
+('lang', '反馈关闭原因', 'feedbackclosedReason', '1', 'feedbackclosedReason', '', '', '');
+
+REPLACE INTO `zt_workflowfield` (`module`, `field`, `type`, `length`, `name`, `control`, `expression`, `options`, `default`, `rules`, `placeholder`, `order`, `searchOrder`, `exportOrder`, `canExport`, `canSearch`, `isValue`, `readonly`, `buildin`, `desc`, `createdBy`, `createdDate`, `editedBy`, `editedDate`) VALUES
+('execution', 'type', 'char', '30', '迭代类型', 'select', '', '16', 'sprint', '', '', 3, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'begin', 'date', '', '计划开始', 'date', '', '', '', '', '', 14, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'end', 'date', '', '计划完成', 'date', '', '', '', '', '', 15, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'days', 'smallint', '5', '可用工作日', 'integer', '', '', '', '', '', 18, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'pri', 'enum', '', '优先级', 'select', '', '[1,2,3,4]', '1', '', '', 20, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'desc', 'text', '', '迭代描述', 'richtext', '', '', '', '', '', 21, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'openedDate', 'datetime', '', '创建日期', 'datetime', '', '', '', '', '', 27, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'closedDate', 'datetime', '', '关闭日期', 'datetime', '', '', '', '', '', 32, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'canceledDate', 'datetime', '', '取消日期', 'datetime', '', '', '', '', '', 34, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'acl', 'char', '30', '访问控制', 'select', '', '18', 'open', '', '', 41, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'whitelist', 'text', '', '白名单', 'multi-select', '', 'user', '', '', '', 42, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'deleted', 'enum', '', '已删除', 'radio', '', '[\"\\u672a\\u5220\\u9664\",\"\\u5df2\\u5220\\u9664\"]', '0', '', '', 47, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'type', 'char', '30', '类型', 'select', '', '16', 'sprint', '', '', 2, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'begin', 'date', '', '计划开始', 'date', '', '', '', '', '', 15, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'end', 'date', '', '计划完成', 'date', '', '', '', '', '', 16, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'days', 'smallint', '5', '可用工作日', 'integer', '', '', '', '', '', 19, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'pri', 'enum', '', '优先级', 'select', '', '[1,2,3,4]', '1', '', '', 21, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'desc', 'text', '', '项目集描述', 'richtext', '', '', '', '', '', 22, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'openedDate', 'datetime', '', '创建日期', 'datetime', '', '', '', '', '', 28, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'closedDate', 'datetime', '', '关闭日期', 'datetime', '', '', '', '', '', 33, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'canceledDate', 'datetime', '', '取消日期', 'datetime', '', '', '', '', '', 35, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'whitelist', 'text', '', '白名单', 'multi-select', '', 'user', '', '', '', 43, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'deleted', 'enum', '', '已删除', 'radio', '', '[\"\\u672a\\u5220\\u9664\",\"\\u5df2\\u5220\\u9664\"]', '0', '', '', 48, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'type', 'char', '30', '项目类型', 'select', '', '16', 'sprint', '', '', 3, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'model', 'char', '30', '项目管理方式', 'input', '', '45', '', '', '', 2, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'name', 'varchar', '90', '项目名称', 'input', '', '', '', '', '', 15, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'begin', 'date', '', '计划开始', 'date', '', '', '', '', '', 16, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'end', 'date', '', '计划完成', 'date', '', '', '', '', '', 17, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'days', 'smallint', '5', '可用工作日', 'integer', '', '', '', '', '', 20, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'pri', 'enum', '', '优先级', 'select', '', '[1,2,3,4]', '1', '', '', 22, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'desc', 'text', '', '项目描述', 'richtext', '', '', '', '', '', 23, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'openedDate', 'datetime', '', '创建日期', 'datetime', '', '', '', '', '', 29, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'closedDate', 'datetime', '', '关闭日期', 'datetime', '', '', '', '', '', 34, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'canceledDate', 'datetime', '', '取消日期', 'datetime', '', '', '', '', '', 36, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'acl', 'char', '30', '访问控制', 'select', '', '18', 'open', '', '', 43, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'whitelist', 'text', '', '项目白名单', 'multi-select', '', 'user', '', '', '', 44, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'deleted', 'enum', '', '已删除', 'radio', '', '[\"\\u672a\\u5220\\u9664\",\"\\u5df2\\u5220\\u9664\"]', '0', '', '', 49, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('build', 'project', 'mediumint', '8', '所属项目', 'select', '', '2', '0', '', '', 4, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('build', 'date', 'date', '', '打包日期', 'date', '', '', '', '', '', 9, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('build', 'desc', 'text', '', '描述', 'richtext', '', '', '', '', '', 13, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('build', 'deleted', 'enum', '', '已删除', 'radio', '', '[\"\\u672a\\u5220\\u9664\",\"\\u5df2\\u5220\\u9664\"]', '0', '', '', 14, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'project', 'mediumint', '8', '所属项目', 'select', '', '2', '0', '', '', 2, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'storyVersion', 'smallint', '6', '研发需求版本', 'integer', '', '', '1', '', '', 9, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'toTask', 'mediumint', '8', '转任务', 'select', '', '5', '0', '', '', 11, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'toStory', 'mediumint', '8', '转研发需求', 'select', '', '4', '0', '', '', 12, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'steps', 'mediumtext', '', '重现步骤', 'richtext', '', '', '', '', '', 22, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'activatedDate', 'datetime', '', '激活日期', 'datetime', '', '', '', '', '', 27, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'openedDate', 'datetime', '', '创建日期', 'datetime', '', '', '', '', '', 32, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'openedBuild', 'varchar', '255', '影响版本', 'multi-select', '', '10', '', '', '', 33, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'assignedDate', 'datetime', '', '指派日期', 'datetime', '', '', '', '', '', 35, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'deadline', 'date', '', '截止日期', 'date', '', '', '', '', '', 36, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'resolvedDate', 'datetime', '30', '解决日期', 'datetime', '', '', '', '', '', 40, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'closedDate', 'datetime', '', '关闭日期', 'datetime', '', '', '', '', '', 42, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'case', 'mediumint', '8', '相关用例', 'select', '', '41', '', '', '', 45, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'caseVersion', 'smallint', '6', '用例版本', 'integer', '', '', '', '', '', 46, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'lastEditedDate', 'datetime', '30', '修改日期', 'datetime', '', '', '', '', '', 58, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'deleted', 'enum', '', '已删除', 'radio', '', '[\"\\u672a\\u5220\\u9664\",\"\\u5df2\\u5220\\u9664\"]', '0', '', '', 59, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'project', 'mediumint', '8', '所属项目', 'select', '', '2', '0', '', '', 2, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'storyVersion', 'smallint', '6', '研发需求版本', 'integer', '', '', '1', '', '', 7, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'estimate', 'float', '', '最初预计', 'decimal', '', '', '', '', '', 13, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'consumed', 'float', '', '总计消耗', 'decimal', '', '', '', '', '', 14, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'left', 'float', '', '预计剩余', 'decimal', '', '', '', '', '', 15, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'deadline', 'date', '', '截止日期', 'date', '', '', '', '', '', 16, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'desc', 'text', '', '任务描述', 'richtext', '', '', '', '', '', 20, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'openedDate', 'datetime', '', '创建日期', 'datetime', '', '', '', '', '', 23, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'assignedDate', 'datetime', '30', '指派日期', 'datetime', '', '', '', '', '', 25, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'estStarted', 'date', '', '预计开始', 'date', '', '', '', '', '', 26, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'realStarted', 'date', '', '实际开始', 'date', '', '', '', '', '', 27, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'finishedDate', 'datetime', '', '实际完成', 'datetime', '', '', '', '', '', 29, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'canceledDate', 'datetime', '', '取消时间', 'datetime', '', '', '', '', '', 32, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'closedDate', 'datetime', '', '关闭时间', 'datetime', '', '', '', '', '', 34, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'lastEditedDate', 'datetime', '', '最后修改日期', 'datetime', '', '', '', '', '', 39, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'deleted', 'enum', '', '已删除', 'select', '', '[\"\\u672a\\u5220\\u9664\",\"\\u5df2\\u5220\\u9664\"]', '0', '', '', 48, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'lifetime', 'char', '30', 'lifetime', 'input', '', '', '', '', '', 4, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'lifetime', 'char', '30', 'lifetime', 'input', '', '', '', '', '', 3, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'designVersion', 'smallint', '6', '设计版本', 'integer', '', '', '', '', '', 8, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'attribute', 'varchar', '30', 'attribute', 'input', '', '', '0', '', '', 5, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'percent', 'float', '', 'percent', 'input', '', '', '0', '', '', 6, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'milestone', 'enum', '', 'milestone', 'radio', '', '[\"\\u672a\\u5220\\u9664\",\"\\u5df2\\u5220\\u9664\"]', '0', '', '', 7, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'output', 'text', '', 'output', 'textarea', '', '', '', '', '', 8, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'auth', 'char', '30', 'auth', 'input', '', '', '', '', '', 9, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'path', 'varchar', '255', 'path', 'input', '', '', '0', '', '', 10, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'grade', 'tinyint', '3', 'grade', 'integer', '', '', '0', '', '', 11, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'realBegan', 'date', '', '实际开始日期', 'date', '', '', '', '', '', 16, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'realEnd', 'date', '', '实际完成日期', 'date', '', '', '', '', '', 17, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'version', 'smallint', '6', 'version', 'integer', '', '', '', '', '', 22, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'parentVersion', 'smallint', '6', 'parentVersion', 'integer', '', '', '', '', '', 23, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'planDuration', 'int', '11', 'planDuration', 'integer', '', '', '', '', '', 24, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'realDuration', 'int', '11', 'realDuration', 'integer', '', '', '', '', '', 25, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'openedVersion', 'varchar', '20', 'openedVersion', 'input', '', '', '', '', '', 28, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'lastEditedBy', 'varchar', '30', 'lastEditedBy', 'select', '', 'user', '', '', '', 29, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'lastEditedDate', 'datetime', '', 'lastEditedDate', 'datetime', '', '', '', '', '', 30, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'suspendedDate', 'date', '', 'suspendedDate', 'date', '', '', '', '', '', 35, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'vision', 'varchar', '10', 'vision', 'input', '', '', 'rnd', '', '', 44, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'displayCards', 'enum', '', 'displayCards', 'radio', '', '[\"\\u672a\\u5220\\u9664\",\"\\u5df2\\u5220\\u9664\"]', '0', '', '', 45, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'fluidBoard', 'enum', '', 'fluidBoard', 'radio', '', '[\"\\u672a\\u5220\\u9664\",\"\\u5df2\\u5220\\u9664\"]', '0', '', '', 46, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'output', 'text', '', 'output', 'textarea', '', '', '', '', '', 9, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'auth', 'char', '30', 'auth', 'input', '', '', '', '', '', 10, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'path', 'varchar', '255', 'path', 'input', '', '', '0', '', '', 12, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'grade', 'tinyint', '3', 'grade', 'integer', '', '', '0', '', '', 13, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'realBegan', 'date', '', 'realBegan', 'date', '', '', '', '', '', 17, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'realEnd', 'date', '', 'realEnd', 'date', '', '', '', '', '', 18, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'version', 'smallint', '6', 'version', 'integer', '', '', '', '', '', 23, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'parentVersion', 'smallint', '6', 'parentVersion', 'integer', '', '', '', '', '', 24, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'planDuration', 'int', '11', 'planDuration', 'integer', '', '', '', '', '', 25, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'realDuration', 'int', '11', 'realDuration', 'integer', '', '', '', '', '', 26, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'openedVersion', 'varchar', '20', 'openedVersion', 'input', '', '', '', '', '', 29, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'lastEditedBy', 'varchar', '30', 'lastEditedBy', 'select', '', 'user', '', '', '', 30, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'lastEditedDate', 'datetime', '', 'lastEditedDate', 'datetime', '', '', '', '', '', 31, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'suspendedDate', 'date', '', 'suspendedDate', 'date', '', '', '', '', '', 36, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'vision', 'varchar', '10', 'vision', 'input', '', '', 'rnd', '', '', 45, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'displayCards', 'enum', '', 'displayCards', 'radio', '', '[\"\\u672a\\u5220\\u9664\",\"\\u5df2\\u5220\\u9664\"]', '0', '', '', 46, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'fluidBoard', 'enum', '', 'fluidBoard', 'radio', '', '[\"\\u672a\\u5220\\u9664\",\"\\u5df2\\u5220\\u9664\"]', '0', '', '', 47, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'lifetime', 'char', '30', '项目周期', 'input', '', '', '', '', '', 4, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'attribute', 'varchar', '30', '阶段类型', 'input', '', '', '0', '', '', 7, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'percent', 'float', '', '工作量占比', 'input', '', '', '0', '', '', 8, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'milestone', 'enum', '', '里程碑', 'radio', '', '[\"\\u672a\\u5220\\u9664\",\"\\u5df2\\u5220\\u9664\"]', '0', '', '', 9, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'output', 'text', '', '输出', 'textarea', '', '', '', '', '', 10, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'auth', 'char', '30', '权限控制', 'input', '', '', '', '', '', 11, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'path', 'varchar', '255', '路径', 'input', '', '', '0', '', '', 13, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'grade', 'tinyint', '3', '层级', 'integer', '', '', '0', '', '', 14, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'realBegan', 'date', '', '实际开始日期', 'date', '', '', '', '', '', 18, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'realEnd', 'date', '', '实际完成日期', 'date', '', '', '', '', '', 19, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'version', 'smallint', '6', '版本', 'integer', '', '', '', '', '', 24, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'parentVersion', 'smallint', '6', '父版本', 'integer', '', '', '', '', '', 25, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'planDuration', 'int', '11', '计划周期天数', 'integer', '', '', '', '', '', 26, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'realDuration', 'int', '11', '实际周期天数', 'integer', '', '', '', '', '', 27, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'openedVersion', 'varchar', '20', '创建版本', 'input', '', '', '', '', '', 30, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'lastEditedBy', 'varchar', '30', '最后编辑人', 'select', '', 'user', '', '', '', 31, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'lastEditedDate', 'datetime', '', '最后编辑日期', 'datetime', '', '', '', '', '', 32, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'suspendedDate', 'date', '', 'suspendedDate', 'date', '', '', '', '', '', 37, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'vision', 'varchar', '10', 'vision', 'input', '', '', 'rnd', '', '', 46, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'displayCards', 'enum', '', 'displayCards', 'radio', '', '[\"\\u672a\\u5220\\u9664\",\"\\u5df2\\u5220\\u9664\"]', '0', '', '', 47, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'fluidBoard', 'enum', '', 'fluidBoard', 'radio', '', '[\"\\u672a\\u5220\\u9664\",\"\\u5df2\\u5220\\u9664\"]', '0', '', '', 48, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'hardware', 'varchar', '30', '硬件', 'input', '', '', '', '', '', 20, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'feedbackBy', 'varchar', '100', '反馈者', 'input', '', '', '', '', '', 28, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'notifyEmail', 'varchar', '100', '通知邮箱', 'input', '', '', '', '', '', 29, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'result', 'mediumint', '8', '结果', 'input', '', '', '', '', '', 47, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'repo', 'mediumint', '8', '所属版本库', 'input', '', '', '', '', '', 48, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'mr', 'mediumint', '8', 'mr', 'input', '', '', '', '', '', 49, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'entry', 'text', '', 'entry', 'input', '', '', '', '', '', 50, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'lines', 'varchar', '10', '代码行', 'input', '', '', '', '', '', 51, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'v1', 'varchar', '40', '版本1', 'input', '', '', '', '', '', 52, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'v2', 'varchar', '40', '版本2', 'input', '', '', '', '', '', 53, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'repoType', 'varchar', '30', '版本库类型', 'input', '', '', '', '', '', 54, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'issueKey', 'varchar', '50', 'issueKey', 'input', '', '', '', '', '', 55, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'version', 'smallint', '6', '版本', 'integer', '', '', '', '', '', 21, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'planDuration', 'int', '11', '计划持续天数', 'integer', '', '', '', '', '', 35, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'realDuration', 'int', '11', '实际持续天数', 'integer', '', '', '', '', '', 36, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'activatedDate', 'datetime', '', '激活日期', 'datetime', '', '', '', '', '', 40, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'repo', 'mediumint', '8', 'repo', 'input', '', '', '', '', '', 41, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'mr', 'mediumint', '8', 'mr', 'input', '', '', '', '', '', 42, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'entry', 'text', '', 'entry', 'input', '', '', '', '', '', 43, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'lines', 'varchar', '10', 'lines', 'input', '', '', '', '', '', 44, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'v1', 'varchar', '40', 'v1', 'input', '', '', '', '', '', 45, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'v2', 'varchar', '40', 'v2', 'input', '', '', '', '', '', 46, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'vision', 'varchar', '10', 'vision', 'input', '', '', '', '', '', 47, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('feedback', 'type', 'char', '30', '反馈类型', 'select', '', '', '', '', '', 4, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('feedback', 'notifyEmail', 'varchar', '100', '通知邮箱', 'input', '', '', '', '', '', 9, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('feedback', 'faq', 'mediumint', '8', 'FAQ', 'input', '', '', '', '', '', 11, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('feedback', 'feedbackBy', 'varchar', '100', '反馈者', 'select', '', 'user', '', '', '', 25, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('feedback', 'solution', 'char', '30', '反馈处理方案', 'select', '', '', '', '', '', 5, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('testcase', 'auto', 'varchar', '10', '自动化测试用例', 'input', '', '', '', '', '', 15, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('testcase', 'frame', 'varchar', '10', '自动化测试框架', 'input', '', '', '', '', '', 16, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('testcase', 'howRun', 'char', '30', '测试方式', 'input', '', '', '', '', '', 18, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('testcase', 'scriptedBy', 'char', '30', '脚本由谁创建', 'select', '', 'user', '', '', '', 18, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('testcase', 'scriptedDate', 'char', '30', '脚本创建日期', 'date', '', '', '', '', '', 18, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('testcase', 'scriptStatus', 'char', '30', '脚本状态', 'input', '', '', '', '', '', 18, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('testcase', 'scriptLocation', 'char', '30', '脚本地址', 'input', '', '', '', '', '', 18, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('testcase', 'frequency', 'char', '30', '使用频率', 'input', '', '', '', '', '', 28, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('testcase', 'fromCaseID', 'char', '30', '用例来源ID', 'input', '', '', '', '', '', 28, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('testcase', 'fromCaseVersion', 'char', '30', '用例来源版本', 'input', '', '', '', '', '', 5, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('testcase', 'subStatus', 'varchar', '30', '子状态', 'select', '', '', '', '', '', 17, 0, 0, '0', '0', '0', '0', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00');
+UPDATE `zt_workflowfield` SET `options`=(SELECT id FROM `zt_workflowdatasource` WHERE `code`='projectModel' LIMIT 1) WHERE `module`='project' AND `field`='model';
+UPDATE `zt_workflowfield` SET `options`=(SELECT id FROM `zt_workflowdatasource` WHERE `code`='feedbackType' LIMIT 1) WHERE `module`='feedback' AND `field`='type';
+UPDATE `zt_workflowfield` SET `options`=(SELECT id FROM `zt_workflowdatasource` WHERE `code`='feedbackSolution' LIMIT 1) WHERE `module`='feedback' AND `field`='solution';
+
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='project' AND `action`='browse';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='project' AND `action`='create';
+UPDATE `zt_workflowaction` SET `type`='batch', `position`='browse', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='project' AND `action`='batchedit';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='project' AND `action`='edit';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='normal', `layout`='side' WHERE `module`='project' AND `action`='view';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='dropdownlist', `open`='none', `layout`='normal' WHERE `module`='project' AND `action`='delete';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='dropdownlist', `open`='modal', `layout`='normal' WHERE `module`='project' AND `action`='close';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='dropdownlist', `open`='modal', `layout`='normal' WHERE `module`='project' AND `action`='activate';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='project' AND `action`='start';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='dropdownlist', `open`='modal', `layout`='normal' WHERE `module`='project' AND `action`='suspend';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='execution' AND `action`='task';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='execution' AND `action`='create';
+UPDATE `zt_workflowaction` SET `type`='batch', `position`='browse', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='execution' AND `action`='batchedit';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='execution' AND `action`='edit';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='execution' AND `action`='view';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='none', `layout`='normal' WHERE `module`='execution' AND `action`='delete';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='execution' AND `action`='close';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='execution' AND `action`='activate';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='execution' AND `action`='start';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='execution' AND `action`='putoff';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='execution' AND `action`='suspend';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='execution' AND `action`='all';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='task' AND `action`='browse';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='task' AND `action`='create';
+UPDATE `zt_workflowaction` SET `type`='batch', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='task' AND `action`='batchcreate';
+UPDATE `zt_workflowaction` SET `type`='batch', `position`='browse', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='task' AND `action`='batchedit';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='exporttemplate';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='import';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='showimport';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='side' WHERE `module`='task' AND `action`='edit';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='normal', `layout`='side' WHERE `module`='task' AND `action`='view';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='none', `layout`='normal' WHERE `module`='task' AND `action`='delete';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='close';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='activate';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='assignTo';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='none', `layout`='normal' WHERE `module`='task' AND `action`='confirmStoryChange';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='start';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='finish';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='pause';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='restart';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='cancel';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='build' AND `action`='browse';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='build' AND `action`='create';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='build' AND `action`='edit';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='build' AND `action`='view';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='none', `layout`='normal' WHERE `module`='build' AND `action`='delete';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='bug' AND `action`='browse';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='bug' AND `action`='create';
+UPDATE `zt_workflowaction` SET `type`='batch', `position`='browse', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='bug' AND `action`='batchcreate';
+UPDATE `zt_workflowaction` SET `type`='batch', `position`='browse', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='bug' AND `action`='batchedit';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='bug' AND `action`='exporttemplate';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='bug' AND `action`='import';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='bug' AND `action`='showimport';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='side' WHERE `module`='bug' AND `action`='edit';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='normal', `layout`='side' WHERE `module`='bug' AND `action`='view';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='none', `layout`='normal' WHERE `module`='bug' AND `action`='delete';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='bug' AND `action`='close';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='bug' AND `action`='activate';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='bug' AND `action`='assignTo';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='bug' AND `action`='confirmBug';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='bug' AND `action`='resolve';
+UPDATE `zt_workflowaction` SET `type`='batch', `position`='browse', `show`='direct', `open`='none', `layout`='normal' WHERE `module`='bug' AND `action`='batchconfirm';
+UPDATE `zt_workflowaction` SET `type`='batch', `position`='browse', `show`='direct', `open`='none', `layout`='normal' WHERE `module`='bug' AND `action`='batchresolve';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='dropdownlist', `open`='modal', `layout`='normal' WHERE `module`='project' AND `action`='activate';
+UPDATE `zt_workflowaction` SET `type`='batch', `position`='browse', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='project' AND `action`='batchedit';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='project' AND `action`='browse';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='dropdownlist', `open`='modal', `layout`='normal' WHERE `module`='project' AND `action`='close';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='project' AND `action`='create';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='dropdownlist', `open`='none', `layout`='normal' WHERE `module`='project' AND `action`='delete';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='project' AND `action`='edit';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='project' AND `action`='start';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='dropdownlist', `open`='modal', `layout`='normal' WHERE `module`='project' AND `action`='suspend';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='normal', `layout`='side' WHERE `module`='project' AND `action`='view';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='activate';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='assignTo';
+UPDATE `zt_workflowaction` SET `type`='batch', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='task' AND `action`='batchcreate';
+UPDATE `zt_workflowaction` SET `type`='batch', `position`='browse', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='task' AND `action`='batchedit';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='task' AND `action`='browse';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='cancel';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='close';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='none', `layout`='normal' WHERE `module`='task' AND `action`='confirmStoryChange';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='task' AND `action`='create';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='none', `layout`='normal' WHERE `module`='task' AND `action`='delete';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='side' WHERE `module`='task' AND `action`='edit';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='exporttemplate';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='finish';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='import';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='pause';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='restart';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='showimport';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='start';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='normal', `layout`='side' WHERE `module`='task' AND `action`='view';
+UPDATE `zt_workflowaction` SET `type`='batch' WHERE `module`IN ('feedback','caselib','testsuite','testtask','testcase') AND `action` IN ('admin','browse','batchedit','batchcreate','import','showimport');
+UPDATE `zt_workflowaction` SET `position`='browse' WHERE `module`IN ('feedback','caselib','testsuite','testtask','testcase') AND `action` IN ('batchedit','batchcreate','import','showimport');
diff --git a/framework/base/control.class.php b/framework/base/control.class.php
index c4d051fadb..1576843b29 100644
--- a/framework/base/control.class.php
+++ b/framework/base/control.class.php
@@ -416,7 +416,7 @@ class baseControl
$moduleName = basename(dirname(dirname(realpath($viewFile))));
$extPath = $this->app->getModuleExtPath('', $moduleName, 'view');
- $checkedOrder = array('site', 'custom', 'vision', 'xuan', 'common');
+ $checkedOrder = array('site', 'saas', 'custom', 'vision', 'xuan', 'common');
$fileName = basename($viewFile);
foreach($checkedOrder as $checkedType)
{
@@ -805,6 +805,12 @@ class baseControl
$commonActionExtFile = $actionExtPath['custom'] . strtolower($methodName) . '.php';
if(file_exists($commonActionExtFile)) $file2Included = $commonActionExtFile;
+ if(!empty($actionExtPath['saas']))
+ {
+ $commonActionExtFile = $actionExtPath['saas'] . strtolower($methodName) . '.php';
+ if(file_exists($commonActionExtFile)) $file2Included = $commonActionExtFile;
+ }
+
if(!empty($actionExtPath['site']))
{
/**
diff --git a/framework/base/model.class.php b/framework/base/model.class.php
index 0dbb3372be..519def4741 100644
--- a/framework/base/model.class.php
+++ b/framework/base/model.class.php
@@ -260,6 +260,7 @@ class baseModel
$moduleExtPath = $this->app->getModuleExtPath($this->appName, $moduleName, 'model');
if(!empty($moduleExtPath['site'])) $extensionFile = $moduleExtPath['site'] . 'class/' . $extensionName . '.class.php';
if(!isset($extensionFile) or !file_exists($extensionFile)) $extensionFile = $moduleExtPath['custom'] . 'class/' . $extensionName . '.class.php';
+ if(!isset($extensionFile) or !file_exists($extensionFile)) $extensionFile = $moduleExtPath['saas'] . 'class/' . $extensionName . '.class.php';
if(!isset($extensionFile) or !file_exists($extensionFile)) $extensionFile = $moduleExtPath['vision'] . 'class/' . $extensionName . '.class.php';
if(!isset($extensionFile) or !file_exists($extensionFile)) $extensionFile = $moduleExtPath['xuan'] . 'class/' . $extensionName . '.class.php';
if(!isset($extensionFile) or !file_exists($extensionFile)) $extensionFile = $moduleExtPath['common'] . 'class/' . $extensionName . '.class.php';
diff --git a/framework/base/router.class.php b/framework/base/router.class.php
index 1216e2f4a6..1a44b0585d 100644
--- a/framework/base/router.class.php
+++ b/framework/base/router.class.php
@@ -1390,6 +1390,9 @@ class baseRouter
if($this->checkModuleName($moduleName))
{
+ $modulePath = $this->getExtensionRoot() . 'saas' . DS . $moduleName . DS;
+ if(is_dir($modulePath) and (file_exists($modulePath . 'control.php') or file_exists($modulePath . 'model.php'))) return $modulePath;
+
/* 1. 最后尝试在定制开发中寻找。 Finally, try to find the module in the custom dir. */
$modulePath = $this->getExtensionRoot() . 'custom' . DS . $moduleName . DS;
if(is_dir($modulePath) and (file_exists($modulePath . 'control.php') or file_exists($modulePath . 'model.php'))) return $modulePath;
@@ -1432,8 +1435,10 @@ class baseRouter
*/
public function getModuleExtPath($appName, $moduleName, $ext)
{
+ $saasExtPath = $this->getExtensionRoot() . 'saas' . DS . $moduleName . DS . 'ext' . DS . $ext . DS;
+
/* 检查失败或者extensionLevel为0,直接返回空。If check failed or extensionLevel == 0, return empty array. */
- if(!$this->checkModuleName($moduleName) or $this->config->framework->extensionLevel == 0) return array();
+ if(!$this->checkModuleName($moduleName) or $this->config->framework->extensionLevel == 0) return array('saas' => $saasExtPath);
$paths = array();
@@ -1442,10 +1447,16 @@ class baseRouter
$paths['xuan'] = $this->getExtensionRoot() . 'xuan' . DS . $moduleName . DS . 'ext' . DS . $ext . DS;
$paths['vision'] = $this->config->vision == 'rnd' ? '' : $this->basePath . 'extension' . DS . $this->config->vision . DS . $moduleName . DS . 'ext' . DS . $ext . DS;
$paths['custom'] = $this->getExtensionRoot() . 'custom' . DS . $moduleName . DS . 'ext' . DS . $ext . DS;
- if($this->config->framework->extensionLevel == 1) return $paths;
+ if($this->config->framework->extensionLevel == 1)
+ {
+ $paths['saas'] = $saasExtPath;
+ return $paths;
+ }
/* When extensionLevel == 2. */
$paths['site'] = empty($this->siteCode) ? '' : $this->getExtensionRoot() . $this->config->edition . DS . $moduleName . DS . 'ext' . DS . '_' . $this->siteCode . DS . $ext . DS;
+ $paths['saas'] = $saasExtPath;
+
return $paths;
}
@@ -1494,6 +1505,12 @@ class baseRouter
/* 如果扩展目录为空,不包含任何扩展文件。If there's no ext paths return false.*/
if(empty($moduleExtPaths)) return false;
+ if(!empty( $moduleExtPaths['saas']))
+ {
+ $this->extActionFile = $moduleExtPaths['saas'] . $this->methodName . '.php';
+ if(file_exists($this->extActionFile)) return true;
+ }
+
/* 1. 如果extensionLevel == 2,且扩展文件存在,返回该站点扩展文件。 If extensionLevel == 2 and site extensionFile exists, return it. */
if($this->config->framework->extensionLevel == 2 and !empty( $moduleExtPaths['site']))
{
@@ -2322,6 +2339,7 @@ class baseRouter
if(!empty($extConfigPath['common'])) $commonExtConfigFiles = helper::ls($extConfigPath['common'], '.php');
if(!empty($extConfigPath['xuan'])) $commonExtConfigFiles = array_merge($commonExtConfigFiles, helper::ls($extConfigPath['xuan'], '.php'));
if(!empty($extConfigPath['vision'])) $commonExtConfigFiles = array_merge($commonExtConfigFiles, helper::ls($extConfigPath['vision'], '.php'));
+ if(!empty($extConfigPath['saas'])) $commonExtConfigFiles = array_merge($commonExtConfigFiles, helper::ls($extConfigPath['saas'], '.php'));
if(!empty($extConfigPath['custom'])) $commonExtConfigFiles = array_merge($commonExtConfigFiles, helper::ls($extConfigPath['custom'], '.php'));
}
if($config->framework->extensionLevel == 2 and !empty($extConfigPath['site'])) $siteExtConfigFiles = helper::ls($extConfigPath['site'], '.php');
@@ -2427,6 +2445,9 @@ class baseRouter
{
$path = $moduleName . DS . 'lang' . DS . $this->clientLang . '.php';
+ $modulePath = $this->getExtensionRoot() . 'saas' . DS;
+ if(file_exists($modulePath . $path)) return $modulePath . $path;
+
/* 1. 如果通用版本里有此模块,优先使用。 If module is in the open edition, use it. */
$modulePath = $this->getModuleRoot($appName);
if(file_exists($modulePath . $path)) return $modulePath . $path;
@@ -2489,6 +2510,7 @@ class baseRouter
if(!empty($extLangPath['xuan'])) $commonExtLangFiles = array_merge($commonExtLangFiles, helper::ls($extLangPath['xuan'] . $this->clientLang, '.php'));
if(!empty($extLangPath['vision'])) $commonExtLangFiles = array_merge($commonExtLangFiles, helper::ls($extLangPath['vision'] . $this->clientLang, '.php'));
if(!empty($extLangPath['custom'])) $commonExtLangFiles = array_merge($commonExtLangFiles, helper::ls($extLangPath['custom'] . $this->clientLang, '.php'));
+ if(!empty($extLangPath['saas'])) $commonExtLangFiles = array_merge($commonExtLangFiles, helper::ls($extLangPath['saas'] . $this->clientLang, '.php'));
}
if($this->config->framework->extensionLevel == 2 and !empty($extLangPath['site'])) $siteExtLangFiles = helper::ls($extLangPath['site'] . $this->clientLang, '.php');
$extLangFiles = array_merge($commonExtLangFiles, $siteExtLangFiles);
diff --git a/framework/control.class.php b/framework/control.class.php
index c7666ca0db..c153bb29f0 100644
--- a/framework/control.class.php
+++ b/framework/control.class.php
@@ -86,7 +86,7 @@ class control extends baseControl
/**
* Det default priv by workflow.
- *
+ *
* @access public
* @return bool
*/
@@ -197,6 +197,7 @@ class control extends baseControl
$commonExtViewFile = $viewExtPath['common'] . $this->devicePrefix . $methodName . ".{$viewType}.php";
$xuanExtViewFile = $viewExtPath['xuan'] . $this->devicePrefix . $methodName . ".{$viewType}.php";
$visionExtViewFile = $viewExtPath['vision'] . $this->devicePrefix . $methodName . ".{$viewType}.php";
+ $saasExtViewFile = $viewExtPath['saas'] . $this->devicePrefix . $methodName . ".{$viewType}.php";
$customExtViewFile = $viewExtPath['custom'] . $this->devicePrefix . $methodName . ".{$viewType}.php";
$siteExtViewFile = empty($viewExtPath['site']) ? '' : $viewExtPath['site'] . $this->devicePrefix . $methodName . ".{$viewType}.php";
@@ -217,6 +218,10 @@ class control extends baseControl
{
$viewFile = $xuanExtViewFile;
}
+ else if(file_exists($saasExtViewFile))
+ {
+ $viewFile = $saasExtViewFile;
+ }
else if(file_exists($commonExtViewFile))
{
$viewFile = $commonExtViewFile;
@@ -235,10 +240,11 @@ class control extends baseControl
$commonExtHookFiles = glob($viewExtPath['vision'] . $this->devicePrefix . $methodName . ".*.{$viewType}.hook.php");
}
$xuanExtHookFiles = glob($viewExtPath['xuan'] . $this->devicePrefix . $methodName . ".*.{$viewType}.hook.php");
+ $saasExtHookFiles = glob($viewExtPath['saas'] . $this->devicePrefix . $methodName . ".*.{$viewType}.hook.php");
$customExtHookFiles = glob($viewExtPath['custom'] . $this->devicePrefix . $methodName . ".*.{$viewType}.hook.php");
$siteExtHookFiles = empty($viewExtPath['site']) ? '' : glob($viewExtPath['site'] . $this->devicePrefix . $methodName . ".*.{$viewType}.hook.php");
- $extHookFiles = array_merge((array)$commonExtHookFiles, (array)$xuanExtHookFiles, (array)$customExtHookFiles, (array)$siteExtHookFiles);
+ $extHookFiles = array_merge((array)$commonExtHookFiles, (array)$xuanExtHookFiles, (array)$saasExtHookFiles, (array)$customExtHookFiles, (array)$siteExtHookFiles);
}
if(!empty($extHookFiles)) return array('viewFile' => $viewFile, 'hookFiles' => $extHookFiles);
@@ -320,10 +326,7 @@ class control extends baseControl
if(!isset($this->config->bizVersion)) return false;
$moduleName = $this->moduleName;
- if($moduleName == 'bug' || $moduleName == 'feedback') return $this->$moduleName->buildOperateMenu($object, $type);
-
- $flow = $this->loadModel('workflow')->getByModule($moduleName);
- return $this->loadModel('flow')->buildOperateMenu($flow, $object, $type);
+ return $this->$moduleName->buildOperateMenu($object, $type);
}
/**
@@ -342,9 +345,9 @@ class control extends baseControl
}
/**
- * Set workflow export fields
- *
- * @param array $fields
+ * Set workflow export fields
+ *
+ * @param array $fields
* @access public
* @return array
*/
diff --git a/framework/model.class.php b/framework/model.class.php
index 267fbc1edf..be9480d01f 100644
--- a/framework/model.class.php
+++ b/framework/model.class.php
@@ -83,7 +83,15 @@ class model extends baseModel
if(isset($this->config->bizVersion))
{
static $actions;
- if(empty($actions)) $actions = $this->dao->select('*')->from(TABLE_WORKFLOWACTION)->where('module')->eq($moduleName)->andWhere('buildin')->eq('1')->andWhere('status')->eq('enable')->fetchAll('action');
+ if(empty($actions))
+ {
+ $actions = $this->dao->select('*')->from(TABLE_WORKFLOWACTION)
+ ->where('module')->eq($moduleName)
+ ->andWhere('buildin')->eq('1')
+ ->andWhere('status')->eq('enable')
+ ->beginIF(!empty($this->config->vision))->andWhere('vision')->eq($this->config->vision)->fi()
+ ->fetchAll('action');
+ }
}
$enabled = true;
@@ -133,7 +141,16 @@ class model extends baseModel
if(strpos($module, '.') !== false) list($appName, $moduleName) = explode('.', $module);
static $actions;
- if(empty($actions)) $actions = $this->dao->select('*')->from(TABLE_WORKFLOWACTION)->where('module')->eq($moduleName)->andWhere('buildin')->eq('0')->andWhere('status')->eq('enable')->orderBy('order_asc')->fetchAll();
+ if(empty($actions))
+ {
+ $actions = $this->dao->select('*')->from(TABLE_WORKFLOWACTION)
+ ->where('module')->eq($moduleName)
+ ->andWhere('buildin')->eq('0')
+ ->andWhere('status')->eq('enable')
+ ->beginIF(!empty($this->config->vision))->andWhere('vision')->eq($this->config->vision)->fi()
+ ->orderBy('order_asc')
+ ->fetchAll();
+ }
$menu = '';
if($show)
diff --git a/framework/router.class.php b/framework/router.class.php
index 500761517b..3fb5136cfa 100755
--- a/framework/router.class.php
+++ b/framework/router.class.php
@@ -327,6 +327,7 @@ class router extends baseRouter
if(!empty($extConfigPath['common'])) $commonExtConfigFiles = helper::ls($extConfigPath['common'], '.php');
if(!empty($extConfigPath['xuan'])) $commonExtConfigFiles = array_merge($commonExtConfigFiles, helper::ls($extConfigPath['xuan'], '.php'));
if(!empty($extConfigPath['vision'])) $commonExtConfigFiles = array_merge($commonExtConfigFiles, helper::ls($extConfigPath['vision'], '.php'));
+ if(!empty($extConfigPath['saas'])) $commonExtConfigFiles = array_merge($commonExtConfigFiles, helper::ls($extConfigPath['saas'], '.php'));
if(!empty($extConfigPath['custom'])) $commonExtConfigFiles = array_merge($commonExtConfigFiles, helper::ls($extConfigPath['custom'], '.php'));
}
if($config->framework->extensionLevel == 2 and !empty($extConfigPath['site'])) $siteExtConfigFiles = helper::ls($extConfigPath['site'], '.php');
diff --git a/lib/date/date.class.php b/lib/date/date.class.php
index efdc37eb02..5737e68cb2 100644
--- a/lib/date/date.class.php
+++ b/lib/date/date.class.php
@@ -9,14 +9,14 @@
* @version $Id: date.class.php 2605 2013-01-09 07:22:58Z wwccss $
* @link http://www.zentao.net
*/
-class date
+class date
{
/**
* Build hour time list.
- *
- * @param int $begin
- * @param int $end
- * @param int $delta
+ *
+ * @param int $begin
+ * @param int $end
+ * @param int $delta
* @access public
* @return array
*/
@@ -37,7 +37,7 @@ class date
/**
* Get today.
- *
+ *
* @access public
* @return date
*/
@@ -47,8 +47,8 @@ class date
}
/**
- * Get yesterday
- *
+ * Get yesterday
+ *
* @access public
* @return date
*/
@@ -59,7 +59,7 @@ class date
/**
* Get tomorrow.
- *
+ *
* @access public
* @return date
*/
@@ -70,7 +70,7 @@ class date
/**
* Get the day before yesterday.
- *
+ *
* @access public
* @return date
*/
@@ -81,8 +81,8 @@ class date
/**
* Get now time period.
- *
- * @param int $delta
+ *
+ * @param int $delta
* @access public
* @return string the current time period, like 0915
*/
@@ -114,8 +114,8 @@ class date
/**
* Format time 0915 to 09:15
- *
- * @param string $time
+ *
+ * @param string $time
* @access public
* @return string
*/
@@ -127,7 +127,7 @@ class date
/**
* Get the begin and end date of this week.
- *
+ *
* @access public
* @return array
*/
@@ -141,7 +141,7 @@ class date
/**
* Get the begin and end date of last week.
- *
+ *
* @access public
* @return array
*/
@@ -155,7 +155,7 @@ class date
/**
* Get the time at the middle of this week.
- *
+ *
* If today in week is 1, move it one day in future. Else is 7, move it back one day. To keep the time geted in this week.
*
* @access public
@@ -172,7 +172,7 @@ class date
/**
* Get middle of last week.
- *
+ *
* @access public
* @return time
*/
@@ -188,7 +188,7 @@ class date
/**
* Get begin and end time of this month.
- *
+ *
* @access public
* @return array
*/
@@ -201,7 +201,7 @@ class date
/**
* Get begin and end time of last month.
- *
+ *
* @access public
* @return array
*/
@@ -213,11 +213,11 @@ class date
}
/**
- * Get begin and end time of this season.
- *
+ * Get begin and end time of this season.
+ *
* @static
* @access public
- * @return array
+ * @return array
*/
public static function getThisSeason()
{
@@ -230,11 +230,11 @@ class date
}
/**
- * Get begin and end time of last season.
- *
+ * Get begin and end time of last season.
+ *
* @static
* @access public
- * @return array
+ * @return array
*/
public static function getLastSeason()
{
@@ -242,43 +242,43 @@ class date
$begin = date('Y-m-d H:i:s', mktime(0, 0, 0, $season * 3 - 2, 1, date('Y')));
$endDay = date('t', mktime(0, 0 , 0, $season * 3, 1, date("Y"))); // Get end day.
$end = date('Y-m-d H:i:s', mktime(23, 59, 59, $season * 3, $endDay, date('Y')));
-
+
return array('begin' => $begin, 'end' => $end);
}
/**
* Get begin and end time of this year.
- *
+ *
* @static
* @access public
- * @return array
+ * @return array
*/
public static function getThisYear()
{
$begin = date(DT_DATE1, strtotime('1/1 this year')) . ' 00:00:00';
- $end = date(DT_DATE1, strtotime('1/1 next year -1 day')) . ' 23:59:59';
+ $end = date(DT_DATE1, strtotime('1/1 next year -1 day')) . ' 23:59:59';
return array('begin' => $begin, 'end' => $end);
}
/**
* Get begin and end time of last year.
- *
+ *
* @static
* @access public
- * @return array
+ * @return array
*/
public static function getLastYear()
{
$begin = date(DT_DATE1, strtotime('1/1 last year')) . ' 00:00:00';
- $end = date(DT_DATE1, strtotime('1/1 this year -1 day')) . ' 23:59:59';
+ $end = date(DT_DATE1, strtotime('1/1 this year -1 day')) . ' 23:59:59';
return array('begin' => $begin, 'end' => $end);
}
/**
- * Get date list
- *
- * @param string $begin
- * @param string $end
+ * Get date list
+ *
+ * @param string $begin
+ * @param string $end
* @param string $format m/d/Y|Y-m-d
* @param string $type noweekend|withweekend
* @param int $weekend 2|1
@@ -295,7 +295,7 @@ class date
for($date = $begin; $date <= $end; $date += 24 * 3600)
{
$weekDay = date('w', $date);
- if($type == 'noweekend' and (($weekend == 2 and $weekDay == 6) or $weekDay == 0)) continue;
+ if(strpos($type, 'noweekend') !== false and (($weekend == 2 and $weekDay == 6) or $weekDay == 0)) continue;
$dateList[] = date($format, $date);
}
diff --git a/module/action/config.php b/module/action/config.php
index caa2de9145..b5c5f0c8c0 100755
--- a/module/action/config.php
+++ b/module/action/config.php
@@ -60,3 +60,9 @@ $config->action->majorList['execution'] = array('opened', 'edited');
$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 = ',story,productplan,release,task,build,bug,testcase,case,testtask,testreport,doc,doclib,issue,risk,opportunity,trainplan,gapanalysis,team,whitelist,researchplan,researchreport,meeting,kanbanlane,kanbancolumn,';
$config->action->noLinkModules = ',doclib,module,webhook,gitlab,sonarqube,pipeline,jenkins,kanban,kanbanspace,kanbancolumn,kanbanlane,kanbanregion,kanbancard,execution,project,traincategory,apistruct,program,product,';
+
+$config->action->preferredTypeNum = 10;
+
+$config->action->preferredType = new stdclass();
+$config->action->preferredType->new = array('user', 'story', 'task', 'bug', 'case', 'doc', 'program', 'product', 'project', 'execution');
+$config->action->preferredType->classic = array('user', 'story', 'task', 'bug', 'case', 'doc', 'product', 'execution', 'productplan', 'build');
diff --git a/module/action/control.php b/module/action/control.php
index 7a143d26ba..1057f4fc8a 100755
--- a/module/action/control.php
+++ b/module/action/control.php
@@ -14,6 +14,7 @@ class action extends control
/**
* Trash.
*
+ * @param string $browseType
* @param string $type all|hidden
* @param string $orderBy
* @param int $recTotal
@@ -22,7 +23,7 @@ class action extends control
* @access public
* @return void
*/
- public function trash($type = 'all', $orderBy = 'id_desc', $recTotal = 0, $recPerPage = 20, $pageID = 1)
+ public function trash($browseType = 'all', $type = 'all', $orderBy = 'id_desc', $recTotal = 0, $recPerPage = 20, $pageID = 1)
{
$this->loadModel('backup');
@@ -60,18 +61,37 @@ class action extends control
$pager = pager::init($recTotal, $recPerPage, $pageID);
/* Append id for secend sort. */
- $sort = common::appendOrder($orderBy);
- $trashes = $this->action->getTrashes($type, $sort, $pager);
+ $sort = common::appendOrder($orderBy);
+ $trashes = $this->action->getTrashes($browseType, $type, $sort, $pager);
+ $objectTypeList = $this->action->getTrashObjectTypes($type);
+ $objectTypeList = array_keys($objectTypeList);
+
+ $preferredType = array();
+ $moreType = array();
+ $preferredTypeConfig = $this->config->systemMode == 'new' ? $this->config->action->preferredType->new : $this->config->action->preferredType->classic;
+ foreach($objectTypeList as $objectType)
+ {
+ in_array($objectType, $preferredTypeConfig) ? $preferredType[$objectType] = $objectType : $moreType[$objectType] = $objectType;
+ }
+ if(count($preferredType) < $this->config->action->preferredTypeNum)
+ {
+ $toPreferredType = array_splice($moreType, 0, $this->config->action->preferredTypeNum - count($preferredType));
+ $preferredType = $preferredType + $toPreferredType;
+ }
/* Title and position. */
$this->view->title = $this->lang->action->trash;
$this->view->position[] = $this->lang->action->trash;
- $this->view->trashes = $trashes;
- $this->view->type = $type;
- $this->view->orderBy = $orderBy;
- $this->view->pager = $pager;
- $this->view->users = $this->loadModel('user')->getPairs('noletter');
+ $this->view->trashes = $trashes;
+ $this->view->type = $type;
+ $this->view->currentObjectType = $browseType;
+ $this->view->orderBy = $orderBy;
+ $this->view->pager = $pager;
+ $this->view->users = $this->loadModel('user')->getPairs('noletter');
+ $this->view->preferredType = $preferredType;
+ $this->view->moreType = $moreType;
+ $this->view->preferredTypeConfig = $preferredTypeConfig;
$this->display();
}
diff --git a/module/action/css/trash.css b/module/action/css/trash.css
index afbfc580cc..2f9190111c 100755
--- a/module/action/css/trash.css
+++ b/module/action/css/trash.css
@@ -1,3 +1,7 @@
.table-footer .table-actions {width: auto; visibility: visible; opacity: 1;}
.table-footer .table-actions .text {line-height: 28px;}
table tbody tr td {overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}
+
+#mainMenu .btn-toolbar .btn-group .dropdown-menu .btn-active-text:hover .text {color: #fff}
+
+.dropdown-menu .btn-active-text:hover .text:after {border-bottom: 0px;}
diff --git a/module/action/model.php b/module/action/model.php
index bbea80d598..2791f3ee01 100755
--- a/module/action/model.php
+++ b/module/action/model.php
@@ -610,17 +610,19 @@ class actionModel extends model
/**
* Get deleted objects.
*
+ * @param string $objectType
* @param string $type all|hidden
* @param string $orderBy
* @param object $pager
* @access public
* @return array
*/
- public function getTrashes($type, $orderBy, $pager)
+ public function getTrashes($objectType, $type, $orderBy, $pager)
{
- $extra = $type == 'hidden' ? self::BE_HIDDEN : self::CAN_UNDELETED;
+ $extra = $type == 'hidden' ? self::BE_HIDDEN : self::CAN_UNDELETED;
$trashes = $this->dao->select('*')->from(TABLE_ACTION)
->where('action')->eq('deleted')
+ ->beginIF($objectType != 'all')->andWhere('objectType')->eq($objectType)->fi()
->andWhere('extra')->eq($extra)
->andWhere('vision')->eq($this->config->vision)
->orderBy($orderBy)->page($pager)->fetchAll();
@@ -665,9 +667,23 @@ class actionModel extends model
$trash->objectName = isset($objectNames[$objectType][$trash->objectID]) ? $objectNames[$objectType][$trash->objectID] : '';
}
+
return $trashes;
}
+ /**
+ * Get object type list of trashes.
+ *
+ * @param string $type
+ * @access public
+ * @return array
+ */
+ public function getTrashObjectTypes($type)
+ {
+ $extra = $type == 'hidden' ? self::BE_HIDDEN : self::CAN_UNDELETED;
+ return $this->dao->select('objectType')->from(TABLE_ACTION)->where('action')->eq('deleted')->andWhere('extra')->eq($extra)->andWhere('vision')->eq($this->config->vision)->fetchAll('objectType');
+ }
+
/**
* Get histories of an action.
*
diff --git a/module/action/view/trash.html.php b/module/action/view/trash.html.php
index 2651f5754a..16edede05c 100755
--- a/module/action/view/trash.html.php
+++ b/module/action/view/trash.html.php
@@ -12,9 +12,53 @@
?>
+
- goback, '', "class='btn'");?>
- " . $lang->action->dynamic->hidden, '', "class='btn btn-danger'");?>
+ goback, '', "class='btn'");?>
+ " . $lang->action->dynamic->hidden, '', "class='btn btn-danger'");?>
@@ -31,7 +75,7 @@
- recTotal}&recPerPage={$pager->recPerPage}"; ?>
+ recTotal}&recPerPage={$pager->recPerPage}"; ?>
| action->objectType);?> |
diff --git a/module/api/model.php b/module/api/model.php
index 4296aa73d6..0cfde0ab69 100644
--- a/module/api/model.php
+++ b/module/api/model.php
@@ -764,6 +764,7 @@ class apiModel extends model
public function createDemoData($name, $baseUrl, $version = '16.0')
{
/* Replace the doc lib name to api lib name. */
+ $this->app->loadLang('doc');
$this->lang->doclib->name = $this->lang->doclib->apiLibName;
$firstAccount = $this->dao->select('account')->from(TABLE_USER)->orderBy('id_asc')->limit(1)->fetch('account');
diff --git a/module/bug/config.php b/module/bug/config.php
index 23ccd42efb..a2a00b49c6 100644
--- a/module/bug/config.php
+++ b/module/bug/config.php
@@ -71,6 +71,7 @@ $config->bug->search['fields']['resolvedBy'] = $lang->bug->resolvedBy;
$config->bug->search['fields']['status'] = $lang->bug->status;
$config->bug->search['fields']['confirmed'] = $lang->bug->confirmed;
+$config->bug->search['fields']['story'] = $lang->bug->story;
if($config->systemMode == 'new') $config->bug->search['fields']['project'] = $lang->bug->project;
$config->bug->search['fields']['product'] = $lang->bug->product;
@@ -116,6 +117,7 @@ $config->bug->search['params']['resolvedBy'] = array('operator' => '=',
$config->bug->search['params']['status'] = array('operator' => '=', 'control' => 'select', 'values' => $lang->bug->statusList);
$config->bug->search['params']['confirmed'] = array('operator' => '=', 'control' => 'select', 'values' => $lang->bug->confirmedList);
+$config->bug->search['params']['story'] = array('operator' => 'include', 'control' => 'input', 'values' => '');
if($config->systemMode == 'new') $config->bug->search['params']['project'] = array('operator' => '=', 'control' => 'select', 'values' => '');
$config->bug->search['params']['product'] = array('operator' => '=', 'control' => 'select', 'values' => '');
diff --git a/module/bug/model.php b/module/bug/model.php
index a534aded89..9ff6b81d69 100644
--- a/module/bug/model.php
+++ b/module/bug/model.php
@@ -1679,12 +1679,7 @@ class bugModel extends model
}
}
- $allProduct = "`product` = 'all'";
- $bugQuery = $this->session->projectBugQuery;
- if(strpos($this->session->projectBugQuery, $allProduct) !== false)
- {
- $bugQuery = str_replace($allProduct, '1', $this->session->projectBugQuery);
- }
+ $bugQuery = $this->getBugQuery($this->session->projectBugQuery);
$bugs = $this->dao->select('*')->from(TABLE_BUG)
->where($bugQuery)
@@ -1746,12 +1741,7 @@ class bugModel extends model
}
}
- $allProduct = "`product` = 'all'";
- $bugQuery = $this->session->executionBugQuery;
- if(strpos($this->session->executionBugQuery, $allProduct) !== false)
- {
- $bugQuery = str_replace($allProduct, '1', $this->session->executionBugQuery);
- }
+ $bugQuery = $this->getBugQuery($this->session->executionBugQuery);
$bugs = $this->dao->select('*')->from(TABLE_BUG)
->where($bugQuery)
@@ -2703,34 +2693,15 @@ class bugModel extends model
if($this->session->bugQuery == false) $this->session->set('bugQuery', ' 1 = 1');
}
- $allProduct = "`product` = 'all'";
- $bugQuery = $this->session->bugQuery;
+ $bugQuery = $this->getBugQuery($this->session->bugQuery);
+
if(is_array($productIDList)) $productIDList = implode(',', $productIDList);
if(strpos($bugQuery, '`product` =') === false) $bugQuery .= ' AND `product` in (' . $productIDList . ')';
- if(strpos($bugQuery, $allProduct) !== false)
- {
- $products = $this->app->user->view->products;
- $bugQuery = str_replace($allProduct, '1', $bugQuery);
- $bugQuery = $bugQuery . ' AND `product` ' . helper::dbIN($products);
- }
$allBranch = "`branch` = 'all'";
if($branch !== 'all' and strpos($bugQuery, '`branch` =') === false) $bugQuery .= " AND `branch` in('0','$branch')";
if(strpos($bugQuery, $allBranch) !== false) $bugQuery = str_replace($allBranch, '1', $bugQuery);
- $allProject = "`project` = 'all'";
- if(strpos($bugQuery, $allProject) !== false)
- {
- $projectIDList = $this->getAllProjectIds();
- if(is_array($projectIDList)) $projectIDList = implode(',', $projectIDList);
- $bugQuery = str_replace($allProject, '1', $bugQuery);
- $bugQuery = $bugQuery . ' AND `project` in (' . $projectIDList . ')';
- }
-
- /* Fix bug #2878. */
- if(strpos($bugQuery, ' `resolvedDate` ') !== false) $bugQuery = str_replace(' `resolvedDate` ', " `resolvedDate` != '0000-00-00 00:00:00' AND `resolvedDate` ", $bugQuery);
- if(strpos($bugQuery, ' `closedDate` ') !== false) $bugQuery = str_replace(' `closedDate` ', " `closedDate` != '0000-00-00 00:00:00' AND `closedDate` ", $bugQuery);
-
$bugs = $this->dao->select('*')->from(TABLE_BUG)->where($bugQuery)
->beginIF(!$this->app->user->admin)->andWhere('execution')->in('0,' . $this->app->user->view->sprints)->fi()
->beginIF($excludeBugs)->andWhere('id')->notIN($excludeBugs)->fi()
@@ -2747,6 +2718,58 @@ class bugModel extends model
return $bugs;
}
+ /**
+ * Get bug query.
+ *
+ * @param string $bugQuery
+ * @access public
+ * @return string
+ */
+ public function getBugQuery($bugQuery)
+ {
+ $allProduct = "`product` = 'all'";
+ if(strpos($bugQuery, $allProduct) !== false)
+ {
+ $products = $this->app->user->view->products;
+ $bugQuery = str_replace($allProduct, '1', $bugQuery);
+ $bugQuery = $bugQuery . ' AND `product` ' . helper::dbIN($products);
+ }
+
+ $allProject = "`project` = 'all'";
+ if(strpos($bugQuery, $allProject) !== false)
+ {
+ $projectIDList = $this->getAllProjectIds();
+ if(is_array($projectIDList)) $projectIDList = implode(',', $projectIDList);
+ $bugQuery = str_replace($allProject, '1', $bugQuery);
+ $bugQuery = $bugQuery . ' AND `project` in (' . $projectIDList . ')';
+ }
+
+ /* Fix bug #2878. */
+ if(strpos($bugQuery, ' `resolvedDate` ') !== false) $bugQuery = str_replace(' `resolvedDate` ', " `resolvedDate` != '0000-00-00 00:00:00' AND `resolvedDate` ", $bugQuery);
+ if(strpos($bugQuery, ' `closedDate` ') !== false) $bugQuery = str_replace(' `closedDate` ', " `closedDate` != '0000-00-00 00:00:00' AND `closedDate` ", $bugQuery);
+ if(strpos($bugQuery, ' `story` ') !== false)
+ {
+ preg_match_all("/`story`[ ]+(NOT *)?LIKE[ ]+'%([^%]*)%'/Ui", $bugQuery, $out);
+ if(!empty($out[2]))
+ {
+ foreach($out[2] as $searchValue)
+ {
+ $story = $this->dao->select('id')->from(TABLE_STORY)->alias('t1')
+ ->leftJoin(TABLE_STORYSPEC)->alias('t2')->on('t1.id=t2.story')
+ ->where('t1.title')->like("%$searchValue%")
+ ->orWhere('t1.keywords')->like("%$searchValue%")
+ ->orWhere('t2.spec')->like("%$searchValue%")
+ ->orWhere('t2.verify')->like("%$searchValue%")
+ ->fetchPairs('id');
+
+ $bugQuery = preg_replace("/`story`[ ]+(NOT[ ]*)?LIKE[ ]+'%$searchValue%'/Ui", '`story` $1 IN (' . implode(',', $story) .')', $bugQuery);
+ }
+ }
+ $bugQuery .= ' AND `story` != 0';
+ }
+ return $bugQuery;
+ }
+
/**
* Form customed bugs.
*
@@ -3234,9 +3257,9 @@ class bugModel extends model
/**
* Build bug menu.
- *
- * @param object $bug
- * @param string $type
+ *
+ * @param object $bug
+ * @param string $type
* @access public
* @return string
*/
diff --git a/module/bug/view/view.html.php b/module/bug/view/view.html.php
index f80245adc1..b7f0629533 100644
--- a/module/bug/view/view.html.php
+++ b/module/bug/view/view.html.php
@@ -96,7 +96,7 @@
deleted):?>
- buildOperateMenu($bug, 'view');?>
+ bug->buildOperateMenu($bug, 'view');?>
diff --git a/module/build/model.php b/module/build/model.php
index b72d747ec6..06a61c1dc0 100644
--- a/module/build/model.php
+++ b/module/build/model.php
@@ -505,7 +505,6 @@ class buildModel extends model
*/
public function batchUnlinkBug($buildID)
{
-
$bugList = $this->post->unlinkBugs;
if(empty($bugList)) return true;
@@ -518,4 +517,27 @@ class buildModel extends model
$this->loadModel('action');
foreach($this->post->unlinkBugs as $unlinkBugID) $this->action->create('bug', $unlinkBugID, 'unlinkedfrombuild', '', $buildID);
}
+
+ /**
+ * Build action menu.
+ *
+ * @param object $build
+ * @param string $type
+ * @access public
+ * @return string
+ */
+ public function buildOperateMenu($build, $type = 'view')
+ {
+ $canBeChanged = common::canBeChanged('build', $build);
+ if($build->deleted || !$canBeChanged) return '';
+
+ $menu = '';
+ $params = "buildID=$build->id";
+
+ $menu .= $this->buildFlowMenu('build', $build, 'view', 'direct');
+ $menu .= $this->buildMenu('build', 'edit', $params, $build, $type);
+ $menu .= $this->buildMenu('build', 'delete', $params, $build, $type, 'trash', 'hiddenwin');
+
+ return $menu;
+ }
}
diff --git a/module/build/view/view.html.php b/module/build/view/view.html.php
index 101bd6b0fb..b2dad81742 100644
--- a/module/build/view/view.html.php
+++ b/module/build/view/view.html.php
@@ -48,15 +48,7 @@ tbody tr td:first-child input {display: none;}
- deleted and $canBeChanged)
- {
- echo $this->buildOperateMenu($build, 'view');
-
- if(common::hasPriv('build', 'edit')) echo html::a($this->createLink('build', 'edit', "buildID=$build->id"), " " . $this->lang->edit, '', "class='btn btn-link' title='{$this->lang->edit}' data-app='{$app->tab}'");
- if(common::hasPriv('build', 'delete')) echo html::a($this->createLink('build', 'delete', "buildID=$build->id"), " " . $this->lang->delete, '', "class='btn btn-link' title='{$this->lang->delete}' target='hiddenwin' data-app='{$app->tab}'");
- }
- ?>
+ build->buildOperateMenu($build, 'view');?>
diff --git a/module/caselib/lang/zh-cn.php b/module/caselib/lang/zh-cn.php
index 90cc8b069e..114092a784 100644
--- a/module/caselib/lang/zh-cn.php
+++ b/module/caselib/lang/zh-cn.php
@@ -25,7 +25,8 @@ $lang->caselib->exportTemplet = '导出模板';
$lang->caselib->batchCreateCase = '批量创建用例';
$lang->caselib->import = '导入';
$lang->caselib->showImport = '显示导入数据';
-
+$lang->caselib->type = '类型';
+$lang->caselib->product = '所属' . $lang->productCommon;
$lang->caselib->browseAction = '用例库列表';
$lang->caselib->deleteAction = "删除用例库";
$lang->caselib->importAction = '导入用例';
diff --git a/module/caselib/model.php b/module/caselib/model.php
index 19e11a3c29..bbdd5e2ee2 100644
--- a/module/caselib/model.php
+++ b/module/caselib/model.php
@@ -103,6 +103,7 @@ class caselibModel extends model
$this->dao->update(TABLE_TESTSUITE)->data($lib)
->autoCheck()
->batchcheck($this->config->caselib->edit->requiredFields, 'notempty')
+ ->checkFlow()
->where('id')->eq($libID)
->checkFlow()
->exec();
@@ -594,4 +595,65 @@ class caselibModel extends model
}
}
}
+
+ /**
+ * Build case lib menu.
+ *
+ * @param object $object
+ * @param string $type
+ * @access public
+ * @return string
+ */
+ public function buildOperateMenu($object, $type = 'view')
+ {
+ $function = 'buildOperate' . ucfirst($type) . 'Menu';
+ return $this->$function($object);
+ }
+
+ /**
+ * Build case lib view menu.
+ *
+ * @param object $lib
+ * @access public
+ * @return string
+ */
+ public function buildOperateViewMenu($lib)
+ {
+ if($lib->deleted) return '';
+
+ $menu = '';
+ $params = "libID=$lib->id";
+ $menu .= $this->buildFlowMenu('caselib', $lib, 'view', 'direct');
+ $menu .= "";
+ $menu .= $this->buildMenu('caselib', 'edit', $params, $lib, 'view');
+ $menu .= $this->buildMenu('caselib', 'delete', $params, $lib, 'view', 'trash', 'hiddenwin');
+
+ return $menu;
+ }
+
+ /**
+ * Build case lib browse menu.
+ *
+ * @param object $case
+ * @access public
+ * @return string
+ */
+ public function buildOperateBrowseMenu($case)
+ {
+ $menu = '';
+ $params = "caseID=$case->id";
+
+ if($this->config->testcase->needReview || !empty($this->config->testcase->forceReview))
+ {
+ $menu .= $this->buildMenu('testcase', 'review', $params, $case, 'browse', 'glasses', '', 'iframe');
+ }
+ $menu .= $this->buildMenu('testcase', 'edit', $params, $case, 'browse');
+ if(common::hasPriv('testcase', 'delete'))
+ {
+ $deleteURL = helper::createLink('testcase', 'delete', "$params&confirm=yes");
+ $menu .= html::a("javascript:ajaxDelete(\"$deleteURL\", \"caseList\", confirmDelete)", '', '', "title='{$this->lang->testcase->delete}' class='btn'");
+ }
+
+ return $menu;
+ }
}
diff --git a/module/caselib/view/browse.html.php b/module/caselib/view/browse.html.php
index 5fc8f8174d..980fcde929 100644
--- a/module/caselib/view/browse.html.php
+++ b/module/caselib/view/browse.html.php
@@ -157,15 +157,7 @@ js::set('flow', $config->global->flow);
processStatus('testcase', $case);?> |
" . $this->loadModel('flow')->getFieldValue($extendField, $case) . "";?>
- testcase->needReview or !empty($config->testcase->forceReview)) common::printIcon('testcase', 'review', "caseID=$case->id", $case, 'list', 'glasses', '', 'iframe');
- common::printIcon('testcase', 'edit', "caseID=$case->id", $case, 'list');
- if(common::hasPriv('testcase', 'delete'))
- {
- $deleteURL = $this->createLink('testcase', 'delete', "caseID=$case->id&confirm=yes");
- echo html::a("javascript:ajaxDelete(\"$deleteURL\", \"caseList\", confirmDelete)", '', '', "title='{$lang->testcase->delete}' class='btn'");
- }
- ?>
+ caselib->buildOperateMenu($case, 'browse');?>
|
diff --git a/module/caselib/view/view.html.php b/module/caselib/view/view.html.php
index 1d3b1dc160..0bc7645556 100644
--- a/module/caselib/view/view.html.php
+++ b/module/caselib/view/view.html.php
@@ -37,15 +37,9 @@
";?>
+ caselib->buildOperateMenu($lib, 'view');?>
diff --git a/module/custom/config.php b/module/custom/config.php
index ff9462b546..9b42284642 100644
--- a/module/custom/config.php
+++ b/module/custom/config.php
@@ -48,8 +48,8 @@ $config->custom->fieldList['release']['create'] = 'desc';
$config->custom->fieldList['release']['edit'] = 'desc';
$config->custom->fieldList['execution']['create'] = 'days,desc,PO,PM,QD,RD';
$config->custom->fieldList['execution']['edit'] = 'days,desc,PO,PM,QD,RD';
-$config->custom->fieldList['task']['create'] = 'story,pri,estimate,desc,estStarted,deadline';
-$config->custom->fieldList['task']['edit'] = 'pri,estimate,estStarted,deadline';
+$config->custom->fieldList['task']['create'] = 'module,story,pri,estimate,desc,estStarted,deadline';
+$config->custom->fieldList['task']['edit'] = 'module,pri,estimate,estStarted,deadline';
$config->custom->fieldList['task']['finish'] = 'comment';
$config->custom->fieldList['task']['activate'] = 'assignedTo,comment';
$config->custom->fieldList['build'] = 'scmPath,filePath,desc';
diff --git a/module/custom/model.php b/module/custom/model.php
index aa725021d4..a8ffc88a32 100644
--- a/module/custom/model.php
+++ b/module/custom/model.php
@@ -251,7 +251,9 @@ class customModel extends model
foreach($item['subMenu'] as $subMenu)
{
if(!isset($subMenu['link']) or strpos($subMenu['link'], '|') === false) continue;
- list($subLabel, $module, $method, $vars) = explode('|', $subMenu['link']);
+ list($subLabel, $module, $method) = explode('|', $subMenu['link']);
+ if(count(explode('|', $subMenu['link'])) > 3) list($subLabel, $module, $method, $vars) = explode('|', $subMenu['link']);
+
$hasPriv = commonModel::hasPriv($module, $method);
if($hasPriv) break;
}
diff --git a/module/doc/control.php b/module/doc/control.php
index 362d8e7ef4..a8a29389c1 100644
--- a/module/doc/control.php
+++ b/module/doc/control.php
@@ -695,6 +695,26 @@ class doc extends control
}
}
+ /**
+ * Update order.
+ *
+ * @access public
+ * @return void
+ */
+ public function updateOrder()
+ {
+ if($_SERVER['REQUEST_METHOD'] == 'POST')
+ {
+ foreach($_POST['orders'] as $id => $order)
+ {
+ $this->dao->update(TABLE_DOC)->set('`order`')->eq($order)->where('id')->eq($id)->exec();
+ }
+
+ if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
+ return $this->send(array('result' => 'success'));
+ }
+ }
+
/**
* Ajax get modules by libID.
*
diff --git a/module/doc/css/tablecontents.css b/module/doc/css/tablecontents.css
index a864562332..9cd6f9101e 100644
--- a/module/doc/css/tablecontents.css
+++ b/module/doc/css/tablecontents.css
@@ -20,3 +20,21 @@ span.dotted-line+a {display: block;}
.tree li.has-list.open:before {content: unset;}
#modules i.icon-file-text {color: #D0D2D6; font-size: 14px;}
.tree li>a {max-width: 80%;}
+
+.sortable-sorting .module-name > a {cursor: move;}
+.sortable-sorting li >.tree-group {opacity: .5;}
+.sortable-sorting .drop-here .tree-group {background-color: #fff3e0;}
+.sortable-sorting .drop-here .tree-group > * {opacity: .1;}
+.sortable-sorting .drag-shadow .tree-group {opacity: 1!important;}
+.sortable-sorting .drag-shadow .tree-actions {visibility: hidden;}
+.is-sorting > li > .tree-group {opacity: 1; border-radius: 4px;}
+.is-sorting > li ul {display: none!important;}
+
+.tree-group {position: relative;}
+.tree-group > .module-name {white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%; display: block;}
+.tree-group .tree-actions {display: none; position: absolute; right: 0; top: 0; background-color: #fff; white-space: nowrap;}
+.tree-group:hover > .module-name {width: calc(100% - 20px);}
+.tree-group:hover .tree-actions {display: block}
+.tree li.has-list.open:before {left: 6px;}
+.sortDoc>.tail-info{right: 20px;}
+.sortDoc>.tree-actions{position: absolute; right: 0px; background: #fff;}
diff --git a/module/doc/js/tablecontents.js b/module/doc/js/tablecontents.js
index a3e3e75859..d11c77d92a 100644
--- a/module/doc/js/tablecontents.js
+++ b/module/doc/js/tablecontents.js
@@ -18,4 +18,81 @@ $(function()
})
$('.menu-actions > a').blur(function() {$(this).css('background', 'none');})
+
+ /* Make modules tree sortable. */
+ $('#modules').sortable(
+ {
+ trigger: '.module-name>a.sort-module, .tree-actions>.sortModule>.icon-move, a.sortDoc, .tree-actions>.sortDoc>.icon-move',
+ dropToClass: 'sort-to',
+ stopPropagation: true,
+ nested: true,
+ selector: 'li',
+ dragCssClass: 'drop-here',
+ canMoveHere: function($ele, $target)
+ {
+ var maxTop = $('.side-col > .cell > ul').height() - $ele.height();
+ if(parseFloat($('.drag-shadow').css('top')) < 0) $('.drag-shadow').css('top', '0');
+ if(parseFloat($('.drag-shadow').css('left')) != 0) $('.drag-shadow').css('left', '0');
+ if(parseFloat($('.drag-shadow').css('top')) > maxTop) $('.drag-shadow').css('top', maxTop + 'px');
+ return true;
+ },
+ targetSelector: function($ele, $root)
+ {
+ var $ul = $ele.closest('ul');
+ setTimeout(function()
+ {
+ if($('#modules').hasClass('sortable-sorting')) $ul.addClass('is-sorting');
+ }, 100);
+
+ if($ele.hasClass('sortDoc'))
+ {
+ return $ul.children('li.sortDoc');
+ }
+ else
+ {
+ return $ul.children('li.catalog');
+ }
+ },
+ always: function()
+ {
+ $('#modules,#modules .is-sorting').removeClass('is-sorting');
+ },
+ finish: function(e)
+ {
+ if(!e.changed) return;
+
+ var orders = {};
+ var link = '';
+ var elementClass = e.list.context.className;
+ if(elementClass.indexOf('sortDoc') >= 0)
+ {
+ $('#modules').find('li.sortDoc').each(function()
+ {
+ var $li = $(this);
+
+ var item = $li.data();
+ orders['orders[' + item.id + ']'] = $li.attr('data-order') || item.order;
+ });
+
+ link = createLink('doc', 'updateOrder');
+ }
+ else
+ {
+ $('#modules').find('li.can-sort').each(function()
+ {
+ var $li = $(this);
+
+ var item = $li.data();
+ '' == 'book' ? orders['sort[' + item.id + ']'] = $li.attr('data-order') || item.order : orders['orders[' + item.id + ']'] = $li.attr('data-order') || item.order;
+ });
+
+ link = createLink('tree', 'updateOrder');
+ }
+
+ $.post(link, orders, function(data){}).error(function()
+ {
+ bootbox.alert(lang.timeout);
+ });
+ }
+ });
})
diff --git a/module/doc/lang/en.php b/module/doc/lang/en.php
index bd888eb70d..ac26a6ddbb 100644
--- a/module/doc/lang/en.php
+++ b/module/doc/lang/en.php
@@ -93,6 +93,7 @@ $lang->doc->collector = 'Collector';
$lang->doc->main = 'Main Document Library';
$lang->doc->order = 'Order';
$lang->doc->doc = 'Document';
+$lang->doc->updateOrder = 'Update Order';
$lang->doc->moduleDoc = 'By Module';
$lang->doc->searchDoc = 'Search';
diff --git a/module/doc/lang/zh-cn.php b/module/doc/lang/zh-cn.php
index ff69f8c514..1fd47b0c4d 100644
--- a/module/doc/lang/zh-cn.php
+++ b/module/doc/lang/zh-cn.php
@@ -93,6 +93,7 @@ $lang->doc->collector = '收藏者';
$lang->doc->main = '文档主库';
$lang->doc->order = '排序';
$lang->doc->doc = '文档';
+$lang->doc->updateOrder = '更新排序';
$lang->doc->moduleDoc = '按模块浏览';
$lang->doc->searchDoc = '搜索';
diff --git a/module/doc/model.php b/module/doc/model.php
index bd6c10c81c..87f9a94b0c 100644
--- a/module/doc/model.php
+++ b/module/doc/model.php
@@ -2379,6 +2379,7 @@ EOT;
$docs = $this->dao->select('*')->from(TABLE_DOC)
->where('lib')->eq($rootID)
->andWhere('deleted')->eq(0)
+ ->orderBy('`order` asc')
->fetchAll();
$moduleDocs = array();
foreach($docs as $doc)
@@ -2410,25 +2411,36 @@ EOT;
{
if(!$docID and $currentMethod != 'tablecontents') $docID = $doc->id;
- $treeMenu[0] .= 'id == $docID ? ' class="active"' : ' class="independent"') . " data-id=$doc->id>";
+ $class = common::hasPriv('doc', 'updateOrder') ? ' sortDoc' : '';
+ $treeMenu[0] .= 'id>";
if($currentMethod == 'tablecontents')
{
- $treeMenu[0] .= '' . zget($users, $doc->editedBy) . ' ' . $doc->editedDate . '';
+ $treeMenu[0] .= '' . zget($users, $doc->editedBy) . ' ' . $doc->editedDate . '';
+ if(common::hasPriv('doc', 'updateOrder'))
+ {
+ $treeMenu[0] .= "
";
+ $treeMenu[0] .= html::a('javascript:;', "", '', "title='{$this->lang->doc->updateOrder}' class='sortDoc'");
+ $treeMenu[0] .= '
';
+ }
}
if($currentMethod == 'objectlibs')
{
- $treeMenu[0] .= "
" . html::a(inlink('objectLibs', "type=$type&objectID=$objectID&libID=$rootID&docID={$doc->id}"), " " . $doc->title, '', "data-app='{$this->app->tab}' class='doc-title' title='{$doc->title}'") . '';
- if(common::hasPriv('doc', 'edit'))
+ $class = common::hasPriv('doc', 'updateOrder') ? 'sortDoc' : '';
+ $treeMenu[0] .= "
" . html::a(inlink('objectLibs', "type=$type&objectID=$objectID&libID=$rootID&docID={$doc->id}"), " " . $doc->title, '', "data-app='{$this->app->tab}' class='doc-title $class' title='{$doc->title}'") . '';
+ if(common::hasPriv('doc', 'edit') or common::hasPriv('doc', 'updateOrder'))
{
$treeMenu[0] .= "
";
- $treeMenu[0] .= html::a(helper::createLink('doc', 'edit', "docID={$doc->id}&comment=false&objectType=$type&objectID=$objectID&libID=$rootID"), "", '', "title={$this->lang->doc->edit} data-app='{$this->app->tab}'");
+ if(common::hasPriv('doc', 'edit')) $treeMenu[0] .= html::a(helper::createLink('doc', 'edit', "docID={$doc->id}&comment=false&objectType=$type&objectID=$objectID&libID=$rootID"), "", '', "title={$this->lang->doc->edit} data-app='{$this->app->tab}'");
+ if(common::hasPriv('doc', 'updateOrder')) $treeMenu[0] .= html::a('javascript:;', "", '', "title='{$this->lang->doc->updateOrder}' class='sortDoc'");
$treeMenu[0] .= '
';
}
}
else
{
- $treeMenu[0] .= html::a(inlink('objectLibs', "type=$type&objectID=$objectID&libID=$rootID&docID={$doc->id}"), "
" . $doc->title, '', "data-app='{$this->app->tab}' class='doc-title' title='{$doc->title}'");
+ $class = common::hasPriv('doc', 'updateOrder') ? 'sortDoc' : '';
+ $treeMenu[0] .= html::a(inlink('objectLibs', "type=$type&objectID=$objectID&libID=$rootID&docID={$doc->id}"), "
" . $doc->title, '', "data-app='{$this->app->tab}' class='doc-title $class' title='{$doc->title}'");
+ $treeMenu[0] .= '
';
}
$treeMenu[0] .= '';
@@ -2477,27 +2489,38 @@ EOT;
else
{
if(!$docID and $currentMethod != 'tablecontents') $docID = $doc->id;
- $treeMenu[$module->id] .= '
id == $docID ? ' class="active"' : ' class="doc"') . " data-id=$doc->id>";
+ $class = common::hasPriv('doc', 'updateOrder') ? ' sortDoc' : '';
+ $treeMenu[$module->id] .= 'id>";
if($currentMethod == 'tablecontents')
{
- $treeMenu[$module->id] .= '' . zget($users, $doc->editedBy) . ' ' . $doc->editedDate . '';
+ $treeMenu[$module->id] .= '' . zget($users, $doc->editedBy) . ' ' . $doc->editedDate . '';
+ if(common::hasPriv('doc', 'updateOrder'))
+ {
+ $treeMenu[$module->id] .= "
";
+ $treeMenu[$module->id] .= html::a('javascript:;', "", '', "title='{$this->lang->doc->updateOrder}' class='sortDoc'");
+ $treeMenu[$module->id] .= '
';
+ }
}
if($currentMethod == 'objectlibs')
{
- $treeMenu[$module->id] .= "
" . html::a(inlink('objectLibs', "type=$type&objectID=$objectID&libID=$libID&docID={$doc->id}"), " " . $doc->title, '', "data-app='{$this->app->tab}' class='doc-title' title='{$doc->title}'") . '';
- if(common::hasPriv('doc', 'edit'))
+ $class = common::hasPriv('doc', 'updateOrder') ? 'sortDoc' : '';
+ $treeMenu[$module->id] .= "
" . html::a(inlink('objectLibs', "type=$type&objectID=$objectID&libID=$libID&docID={$doc->id}"), " " . $doc->title, '', "data-app='{$this->app->tab}' class='doc-title $class' title='{$doc->title}'") . '';
+ if(common::hasPriv('doc', 'edit') or common::hasPriv('doc', 'updateOrder'))
{
$treeMenu[$module->id] .= "
";
- $treeMenu[$module->id] .= html::a(helper::createLink('doc', 'edit', "docID={$doc->id}&comment=false&objectType=$type&objectID=$objectID&libID=$libID"), "", '', "title={$this->lang->doc->edit} data-app={$this->app->tab}");
+ if(common::hasPriv('doc', 'edit')) $treeMenu[$module->id] .= html::a(helper::createLink('doc', 'edit', "docID={$doc->id}&comment=false&objectType=$type&objectID=$objectID&libID=$libID"), "", '', "title={$this->lang->doc->edit} data-app={$this->app->tab}");
+ if(common::hasPriv('doc', 'updateOrder')) $treeMenu[$module->id] .= html::a('javascript:;', "", '', "title='{$this->lang->doc->updateOrder}' class='sortDoc'");
$treeMenu[$module->id] .= '
';
}
$treeMenu[$module->id] .= '
';
}
elseif($currentMethod == 'tablecontents')
{
- $treeMenu[$module->id] .= html::a(inlink('objectLibs', "type=$type&objectID=$objectID&libID=$libID&docID={$doc->id}"), "
" . $doc->title, '', "data-app='{$this->app->tab}' class='doc-title' title='{$doc->title}'");
+ $class = common::hasPriv('doc', 'updateOrder') ? 'sortDoc' : '';
+ $treeMenu[$module->id] .= html::a(inlink('objectLibs', "type=$type&objectID=$objectID&libID=$libID&docID={$doc->id}"), "
" . $doc->title, '', "data-app='{$this->app->tab}' class='doc-title $class' title='{$doc->title}'");
+ $treeMenu[$module->id] .= '
';
}
$treeMenu[$module->id] .= '';
@@ -2511,22 +2534,25 @@ EOT;
}
else
{
- if($currentMethod == 'tablecontents')
+ $li = "
" . $module->name . '';
+ if($currentMethod == 'tablecontents' and common::hasPriv('tree', 'updateOrder'))
{
- $li = "
" . $module->name . '';
+ $li .= "
";
+ $li .= html::a('javascript:;', "", '', "title='{$this->lang->doc->updateOrder}' class='sortModule'");
+ $li .= '
';
}
else
{
- $li = "
" . $module->name . '';
- if(common::hasPriv('tree', 'edit') or common::hasPriv('tree', 'browse'))
+ if(common::hasPriv('tree', 'edit') or common::hasPriv('tree', 'browse') or common::hasPriv('tree', 'browse') or common::hasPriv('tree', 'updateOrder'))
{
$li .= "
";
- if(common::hasPriv('tree', 'edit')) $li .= html::a(helper::createLink('tree', 'edit', "module=$module->id&type=doc"), "", '', "data-toggle='modal' title={$this->lang->doc->editType}");
- if(common::hasPriv('tree', 'browse')) $li .= html::a(helper::createLink('tree', 'browse', "rootID=$libID&type=doc&module=$module->id", '', 1), "", '', "class='iframe' title={$this->lang->doc->editChildType}");
+ if(common::hasPriv('tree', 'edit')) $li .= html::a(helper::createLink('tree', 'edit', "module=$module->id&type=doc"), "", '', "data-toggle='modal' title='{$this->lang->doc->editType}'");
+ if(common::hasPriv('tree', 'browse')) $li .= html::a(helper::createLink('tree', 'browse', "rootID=$libID&type=doc&module=$module->id", '', 1), "", '', "class='iframe' title='{$this->lang->doc->editChildType}'");
+ if(common::hasPriv('tree', 'updateOrder')) $li .= html::a('javascript:;', "", '', "title='{$this->lang->doc->updateOrder}' class='sortModule'");
$li .= '
';
}
- $li .= '
';
}
+ $li .= '
';
}
if($treeMenu[$module->id])
{
diff --git a/module/doc/view/side.html.php b/module/doc/view/side.html.php
index 32cbbc41b9..701393f393 100644
--- a/module/doc/view/side.html.php
+++ b/module/doc/view/side.html.php
@@ -79,7 +79,7 @@ $sideWidth = common::checkNotCN() ? '270' : '238';
/* Make modules tree sortable */
$('#modules').sortable(
{
- trigger: '.module-name>a.sort-module',
+ trigger: '.module-name>a.sort-module, .tree-actions>.sortModule>.icon-move, .module-name>a.sortDoc, .tree-actions>.sortDoc>.icon-move',
dropToClass: 'sort-to',
stopPropagation: true,
nested: true,
@@ -100,7 +100,15 @@ $sideWidth = common::checkNotCN() ? '270' : '238';
{
if($('#modules').hasClass('sortable-sorting')) $ul.addClass('is-sorting');
}, 100);
- return $ul.children('li');
+
+ if($ele.hasClass('sortDoc'))
+ {
+ return $ul.children('li.sortDoc');
+ }
+ else
+ {
+ return $ul.children('li.catalog');
+ }
},
always: function()
{
@@ -110,16 +118,34 @@ $sideWidth = common::checkNotCN() ? '270' : '238';
{
if(!e.changed) return;
- var orders = {};
- $('#modules').find('li.can-sort').each(function()
+ var orders = {};
+ var link = '';
+ var elementClass = e.list.context.className;
+ if(elementClass.indexOf('sortDoc') >= 0)
{
- var $li = $(this);
+ $('#modules').find('li.sortDoc').each(function()
+ {
+ var $li = $(this);
- var item = $li.data();
- '' == 'book' ? orders['sort[' + item.id + ']'] = $li.attr('data-order') || item.order : orders['orders[' + item.id + ']'] = $li.attr('data-order') || item.order;
- });
+ var item = $li.data();
+ orders['orders[' + item.id + ']'] = $li.attr('data-order') || item.order;
+ });
+
+ link = createLink('doc', 'updateOrder');
+ }
+ else
+ {
+ $('#modules').find('li.can-sort').each(function()
+ {
+ var $li = $(this);
+
+ var item = $li.data();
+ '' == 'book' ? orders['sort[' + item.id + ']'] = $li.attr('data-order') || item.order : orders['orders[' + item.id + ']'] = $li.attr('data-order') || item.order;
+ });
+
+ link = '' == 'book' ? createLink('doc', 'sortBookOrder') : createLink('tree', 'updateOrder');
+ }
- var link = '' == 'book' ? createLink('doc', 'sortBookOrder') : createLink('tree', 'updateOrder');
$.post(link, orders, function(data){}).error(function()
{
bootbox.alert(lang.timeout);
diff --git a/module/execution/control.php b/module/execution/control.php
index ba411988dd..0f3f306bda 100644
--- a/module/execution/control.php
+++ b/module/execution/control.php
@@ -1149,12 +1149,12 @@ class execution extends control
* @access public
* @return void
*/
- public function burn($executionID = 0, $type = 'noweekend', $interval = 0, $burnBy = 'left')
+ public function burn($executionID = 0, $type = 'noweekend,nodelay', $interval = 0, $burnBy = 'left')
{
$this->loadModel('report');
$execution = $this->commonAction($executionID);
$executionID = $execution->id;
- $burnBy = $this->cookie->burnBy ? $this->cookie->burnBy : $burnBy;
+ $burnBy = $this->cookie->burnBy ? $this->cookie->burnBy : $burnBy;
/* Header and position. */
$title = $execution->name . $this->lang->colon . $this->lang->execution->burn;
@@ -1163,7 +1163,10 @@ class execution extends control
/* Get date list. */
$executionInfo = $this->execution->getByID($executionID);
- list($dateList, $interval) = $this->execution->getDateList($executionInfo->begin, $executionInfo->end, $type, $interval, 'Y-m-d');
+ $deadline = $execution->status == 'closed' ? substr($execution->closedDate, 0, 10) : $execution->suspendedDate;
+ $deadline = strpos('closed,suspended', $execution->status) === false ? helper::today() : $deadline;
+ $endDate = strpos($type, 'withdelay') !== false ? $deadline : $executionInfo->end;
+ list($dateList, $interval) = $this->execution->getDateList($executionInfo->begin, $endDate, $type, $interval, 'Y-m-d');
$chartData = $this->execution->buildBurnData($executionID, $dateList, $type, $burnBy);
$dayList = array_fill(1, floor((int)$execution->days / $this->config->execution->maxBurnDay) + 5, '');
@@ -1677,11 +1680,16 @@ class execution extends control
if($this->app->tab == 'project')
{
- $projectID = $this->session->project;
- $project = $this->project->getById($projectID);
+ $projectID = $this->session->project;
+ $project = $this->project->getById($projectID);
+ $allProjects = $this->project->getPairsByModel($project->model, 0, 'noclosed', isset($projectID) ? $projectID : 0);
$this->project->setMenu($projectID);
$this->view->project = $project;
}
+ else
+ {
+ $allProjects = $this->project->getPairsByModel('all', 0, 'noclosed', isset($projectID) ? $projectID : 0);
+ }
if(!$this->post->executionIDList) return print(js::locate($this->session->executionList, 'parent'));
$executionIDList = $this->post->executionIDList;
@@ -1718,7 +1726,7 @@ class execution extends control
$this->view->position[] = $this->lang->execution->batchEdit;
$this->view->executionIDList = $executionIDList;
$this->view->executions = $executions;
- $this->view->allProjects = $this->project->getPairsByModel($project->model, 0, 'noclosed', isset($projectID) ? $projectID : 0);
+ $this->view->allProjects = $allProjects;
$this->view->pmUsers = $pmUsers;
$this->view->poUsers = $poUsers;
$this->view->qdUsers = $qdUsers;
@@ -1833,6 +1841,7 @@ class execution extends control
if(!empty($_POST))
{
$this->loadModel('action');
+ $this->execution->computeBurn($executionID);
$changes = $this->execution->suspend($executionID);
if(dao::isError()) return print(js::error(dao::getError()));
@@ -1928,6 +1937,7 @@ class execution extends control
if(!empty($_POST))
{
$this->loadModel('action');
+ $this->execution->computeBurn($executionID);
$changes = $this->execution->close($executionID);
if(dao::isError()) return print(js::error(dao::getError()));
@@ -3149,7 +3159,16 @@ class execution extends control
}
$projectExecutions = array();
- foreach($orderedExecutions as $execution) $projectExecutions[$execution->project][] = $execution;
+ $parentIdList = array();
+ foreach($orderedExecutions as $execution)
+ {
+ $projectExecutions[$execution->project][] = $execution;
+ if($execution->type != 'stage') continue;
+ if($execution->grade == 2 and $execution->project != $execution->parent) $parentIdList[$execution->parent] = $execution->parent;
+ }
+
+ $parents = array();
+ if($parentIdList) $parents = $this->execution->getByIdList($parentIdList);
$this->view->link = $this->execution->getLink($module, $method, $extra);
$this->view->module = $module;
@@ -3158,6 +3177,7 @@ class execution extends control
$this->view->extra = $extra;
$this->view->projects = $projectPairs;
$this->view->executions = $projectExecutions;
+ $this->view->parents = $parents;
$this->display();
}
diff --git a/module/execution/css/burn.css b/module/execution/css/burn.css
index b954f140ad..71e7d4b506 100644
--- a/module/execution/css/burn.css
+++ b/module/execution/css/burn.css
@@ -6,5 +6,6 @@
#burnLegend > div {position: relative; padding-left: 30px;}
#burnLegend > div > .barline {position: absolute; width: 20px; left: 0; top: 13px; height: 3px; background: #EEEEEE;}
#burnLegend .line-real .bg-primary {background: #1183fb;}
+#burnLegend .line-real .bg-delay {background: red;}
.pull-left .input-control {margin-right: 10px;}
diff --git a/module/execution/js/burn.js b/module/execution/js/burn.js
index 85e78025cf..192151808a 100644
--- a/module/execution/js/burn.js
+++ b/module/execution/js/burn.js
@@ -5,6 +5,18 @@ $(function()
{
location.href = createLink('execution', 'burn', 'executionID=' + executionID + '&type=' + type + '&interval=' + $(this).val());
});
+
+ $('#weekend').click(function()
+ {
+ var browseType = type.match('noweekend') ? type.replace('noweekend', 'withweekend') : type.replace('withweekend', 'noweekend');
+ location.href = createLink('execution', 'burn', 'executionID=' + executionID + '&type=' + browseType + '&interval=' + interval);
+ });
+
+ $('#delay').click(function()
+ {
+ var browseType = type.match('nodelay') ? type.replace('nodelay', 'withdelay') : type.replace('withdelay', 'nodelay');
+ location.href = createLink('execution', 'burn', 'executionID=' + executionID + '&type=' + browseType + '&interval=' + interval);
+ });
})
/* Save burn as image.*/
diff --git a/module/execution/js/kanban.js b/module/execution/js/kanban.js
index ff3a64f56c..b125445210 100644
--- a/module/execution/js/kanban.js
+++ b/module/execution/js/kanban.js
@@ -328,6 +328,10 @@ if(!window.kanbanDropRules)
{
backlog: ['ready', 'backlog'],
ready: ['backlog', 'ready'],
+ tested: ['verified'],
+ verified: ['tested', 'released'],
+ released: ['verified', 'closed'],
+ closed: ['released'],
},
bug:
{
@@ -957,7 +961,12 @@ function changeCardColType(cardID, fromColID, toColID, fromLaneID, toLaneID, car
/* Story lane. */
if(cardType == 'story')
{
- if(toColType == 'ready' || toColType == 'backlog')
+ if(toColType == 'closed' && priv.canCloseStory)
+ {
+ var link = createLink('story', 'close', 'storyID=' + objectID, '', true);
+ showIframe = true;
+ }
+ else
{
if(toColType == 'ready' && typeof(reviewStoryParis[objectID]) != 'undefined')
{
diff --git a/module/execution/js/taskkanban.js b/module/execution/js/taskkanban.js
index 8df8beb996..56a6cf5af4 100644
--- a/module/execution/js/taskkanban.js
+++ b/module/execution/js/taskkanban.js
@@ -480,6 +480,10 @@ if(!window.kanbanDropRules)
{
backlog: ['ready'],
ready: ['backlog'],
+ tested: ['verified'],
+ verified: ['tested', 'released'],
+ released: ['verified', 'closed'],
+ closed: ['released'],
},
bug:
{
@@ -495,7 +499,7 @@ if(!window.kanbanDropRules)
{
'wait': ['developing', 'developed', 'canceled', 'closed'],
'developing': ['developed', 'pause', 'canceled'],
- 'developed': ['closed'],
+ 'developed': ['developing', 'closed'],
'pause': ['developing'],
'canceled': ['developing'],
'closed': ['developing'],
@@ -683,7 +687,12 @@ function changeCardColType(cardID, fromColID, toColID, fromLaneID, toLaneID, car
/* Story lane. */
if(cardType == 'story')
{
- if(toColType == 'ready' || toColType == 'backlog')
+ if(toColType == 'closed' && priv.canCloseStory)
+ {
+ var link = createLink('story', 'close', 'storyID=' + objectID, '', true);
+ showIframe = true;
+ }
+ else
{
if(toColType == 'ready' && typeof(reviewStoryParis[objectID]) != 'undefined')
{
@@ -706,7 +715,7 @@ function changeCardColType(cardID, fromColID, toColID, fromLaneID, toLaneID, car
updateKanban(browseType, kanbanGroup[groupBy]);
}
}
- })
+ });
}
}
diff --git a/module/execution/lang/en.php b/module/execution/lang/en.php
index 0c7530ff03..8b2a97e1ac 100644
--- a/module/execution/lang/en.php
+++ b/module/execution/lang/en.php
@@ -359,7 +359,9 @@ $lang->execution->linkStory = 'Link Story';
$lang->execution->createTask = 'Create Task';
$lang->execution->goback = "Go Back";
$lang->execution->noweekend = 'Exclude Weekend';
+$lang->execution->nowdelay = 'Exclude Delay Date';
$lang->execution->withweekend = 'Include Weekend';
+$lang->execution->withdelay = 'Include Delay Date';
$lang->execution->interval = 'Intervals ';
$lang->execution->fixFirstWithLeft = 'Update hours left too';
$lang->execution->unfinishedExecution = "This {$lang->executionCommon} has ";
@@ -384,6 +386,7 @@ $lang->execution->charts->burn->graph->rotateNames = 1;
$lang->execution->charts->burn->graph->showValues = 0;
$lang->execution->charts->burn->graph->reference = 'Ideal';
$lang->execution->charts->burn->graph->actuality = 'Actual';
+$lang->execution->charts->burn->graph->delay = 'Delay';
$lang->execution->placeholder = new stdclass();
$lang->execution->placeholder->code = "Abbreviation of {$lang->executionCommon} name";
diff --git a/module/execution/lang/zh-cn.php b/module/execution/lang/zh-cn.php
index b3cd85caf9..060c63b27f 100644
--- a/module/execution/lang/zh-cn.php
+++ b/module/execution/lang/zh-cn.php
@@ -359,7 +359,9 @@ $lang->execution->linkStory = "关联{$lang->SRCommon}";
$lang->execution->createTask = '创建任务';
$lang->execution->goback = "返回任务列表";
$lang->execution->noweekend = '去除周末';
+$lang->execution->nodelay = '去除延期日期';
$lang->execution->withweekend = '显示周末';
+$lang->execution->withdelay = '显示延期日期';
$lang->execution->interval = '间隔';
$lang->execution->fixFirstWithLeft = '修改剩余工时';
$lang->execution->unfinishedExecution = "该{$lang->executionCommon}下还有";
@@ -384,6 +386,7 @@ $lang->execution->charts->burn->graph->rotateNames = 1;
$lang->execution->charts->burn->graph->showValues = 0;
$lang->execution->charts->burn->graph->reference = '参考';
$lang->execution->charts->burn->graph->actuality = '实际';
+$lang->execution->charts->burn->graph->delay = '延期';
$lang->execution->placeholder = new stdclass();
$lang->execution->placeholder->code = '团队内部的简称';
diff --git a/module/execution/model.php b/module/execution/model.php
index 854121d41c..9973811363 100644
--- a/module/execution/model.php
+++ b/module/execution/model.php
@@ -481,6 +481,8 @@ class executionModel extends model
->remove('products, branch, uid, plans, syncStories, contactListMenu, teamMembers')
->get();
+ if(in_array($execution->status, array('closed', 'suspended'))) $this->computeBurn($executionID);
+
if($this->config->systemMode == 'new' and (empty($execution->project) or $execution->project == $oldExecution->project)) $this->checkBeginAndEndDate($oldExecution->project, $execution->begin, $execution->end);
if(dao::isError()) return false;
@@ -516,6 +518,7 @@ class executionModel extends model
->checkIF($execution->end != '', 'end', 'ge', $execution->begin)
->checkIF((!empty($execution->name) and $this->config->systemMode == 'new'), 'name', 'unique', "id != $executionID and type in ('sprint','stage') and `project` = $executionProject")
->checkIF(!empty($execution->code), 'code', 'unique', "id != $executionID and type in ('sprint','stage')")
+ ->checkFlow()
->where('id')->eq($executionID)
->checkFlow()
->limit(1)
@@ -694,13 +697,6 @@ class executionModel extends model
if(is_array($executions[$executionID]->{$extendField->field})) $executions[$executionID]->{$extendField->field} = join(',', $executions[$executionID]->{$extendField->field});
$executions[$executionID]->{$extendField->field} = htmlSpecialString($executions[$executionID]->{$extendField->field});
- $message = $this->checkFlowRule($extendField, $executions[$executionID]->{$extendField->field});
-
- if($message)
- {
- dao::$errors['message'][] = $message;
- return false;
- }
}
}
@@ -751,6 +747,7 @@ class executionModel extends model
->checkIF($execution->end != '', 'end', 'ge', $execution->begin)
->checkIF((!empty($execution->name) and $this->config->systemMode == 'new'), 'name', 'unique', "id != $executionID and type in ('sprint','stage') and `project` = $projectID")
->checkIF(!empty($execution->code), 'code', 'unique', "id != $executionID and type in ('sprint','stage')")
+ ->checkFlow()
->where('id')->eq($executionID)
->limit(1)
->exec();
@@ -814,6 +811,7 @@ class executionModel extends model
->autoCheck()
->check($this->config->execution->start->requiredFields, 'notempty')
->checkIF($execution->realBegan != '', 'realBegan', 'le', helper::today())
+ ->checkFlow()
->where('id')->eq((int)$executionID)
->exec();
@@ -847,6 +845,7 @@ class executionModel extends model
$this->dao->update(TABLE_EXECUTION)->data($execution)
->autoCheck()
+ ->checkFlow()
->where('id')->eq((int)$executionID)
->exec();
@@ -870,10 +869,12 @@ class executionModel extends model
->setDefault('status', 'suspended')
->setDefault('lastEditedBy', $this->app->user->account)
->setDefault('lastEditedDate', $now)
+ ->setDefault('suspendedDate', helper::today())
->remove('comment')->get();
$this->dao->update(TABLE_EXECUTION)->data($execution)
->autoCheck()
+ ->checkFlow()
->where('id')->eq((int)$executionID)
->exec();
@@ -909,6 +910,7 @@ class executionModel extends model
$this->dao->update(TABLE_EXECUTION)->data($execution)
->autoCheck()
+ ->checkFlow()
->where('id')->eq((int)$executionID)
->exec();
@@ -979,6 +981,7 @@ class executionModel extends model
->check($this->config->execution->close->requiredFields,'notempty')
->checkIF($execution->realEnd != '', 'realEnd', 'le', helper::today())
->checkIF($execution->realEnd != '', 'realEnd', 'ge', $oldExecution->realBegan)
+ ->checkFlow()
->where('id')->eq((int)$executionID)
->exec();
@@ -2827,21 +2830,77 @@ class executionModel extends model
/**
* Compute burn of a execution.
*
+ * @param int $executionID
* @access public
* @return array
*/
- public function computeBurn()
+ public function computeBurn($executionID = 0)
{
$today = helper::today();
- $burns = array();
-
$executions = $this->dao->select('id, code')->from(TABLE_EXECUTION)
- ->where('end')->ge($today)
- ->andWhere('type')->ne('ops')
+ ->where('type')->in('sprint,stage')
+ ->andWhere('lifetime')->ne('ops')
->andWhere('status')->notin('done,closed,suspended')
+ ->beginIF($executionID)->andWhere('id')->eq($executionID)->fi()
->fetchPairs();
- if(!$executions) return $burns;
+ if(!$executions) return array();
+ /* Update historical data of burn. */
+ $table = $this->config->edition == 'open' ? TABLE_TASKESTIMATE : TABLE_EFFORT;
+ $field = $this->config->edition == 'open' ? 'task' : 'objectID';
+ $totalConsumed = $this->dao->select("t2.execution as execution, t1.$field as task, t1.date as date, sum(t1.consumed) as totalConsumed")->from($table)->alias('t1')
+ ->leftJoin(TABLE_TASK)->alias('t2')->on("t1.$field=t2.id")
+ ->where('t2.execution')->in(array_keys($executions))
+ ->beginIF($this->config->edition != 'open')->andWhere('t1.objectType')->eq('task')->fi()
+ ->andWhere('t2.deleted')->eq('0')
+ ->andWhere('t2.parent')->ge('0')
+ ->andWhere('t2.status')->ne('cancel')
+ ->andWhere('t1.date')->ne($today)
+ ->groupBy("t1.$field, t1.date")
+ ->orderBy('t1.id_desc')
+ ->fetchGroup('task', 'date');
+
+ $latestIdList = $this->dao->select('max(id) as id')->from($table)
+ ->where($field)->in(array_keys($totalConsumed))
+ ->beginIF($this->config->edition != 'open')->andWhere('objectType')->eq('task')->fi()
+ ->groupBy("$field,date")->fetchAll('id');
+ $latestLefts = $this->dao->select("$field, date, `left`")->from($table)
+ ->where('id')->in(array_keys($latestIdList))
+ ->fetchGroup($field, 'date');
+
+ $burnList = array();
+ foreach($totalConsumed as $taskID => $consumedList)
+ {
+ foreach($consumedList as $date => $consumed)
+ {
+ $executionID = $consumed->execution;
+ if(!isset($burnList[$executionID])) $burnList[$executionID] = array();
+
+ if(!isset($burnList[$executionID][$date]))
+ {
+ $burnList[$executionID][$date] = new stdclass();
+ $burnList[$executionID][$date]->consumed = 0;
+ $burnList[$executionID][$date]->left = 0;
+ }
+
+ $burnList[$executionID][$date]->consumed += $consumed->totalConsumed;
+ $burnList[$executionID][$date]->left += $latestLefts[$taskID][$date]->left;
+ }
+ }
+
+ foreach($burnList as $executionID => $burns)
+ {
+ foreach($burns as $date => $burn)
+ {
+ $burn->execution = $executionID;
+ $burn->date = $date;
+
+ $this->dao->replace(TABLE_BURN)->data($burn)->exec();
+ $burn->executionName = $executions[$burn->execution];
+ }
+ }
+
+ /* Update today's data of burn. */
$burns = $this->dao->select("execution, '$today' AS date, sum(estimate) AS `estimate`, sum(`left`) AS `left`, SUM(consumed) AS `consumed`")
->from(TABLE_TASK)
->where('execution')->in(array_keys($executions))
@@ -2861,8 +2920,9 @@ class executionModel extends model
->where('execution')->in(array_keys($executions))
->andWhere('deleted')->eq('0')
->andWhere('parent')->ge('0')
- ->andWhere('status')->eq('done')
+ ->andWhere('status', true)->eq('done')
->orWhere('status')->eq('closed')
+ ->markRight(1)
->groupBy('execution')
->fetchAll('execution');
$storyPoints = $this->dao->select('t1.project, sum(t2.estimate) AS `storyPoint`')->from(TABLE_PROJECTSTORY)->alias('t1')
@@ -2893,8 +2953,10 @@ class executionModel extends model
$this->dao->replace(TABLE_BURN)->data($burn)->exec();
$burn->executionName = $executions[$burn->execution];
+ $burnList[$executionID][$today] = $burn;
}
- return $burns;
+
+ return $burnList;
}
/**
@@ -2927,10 +2989,12 @@ class executionModel extends model
*
* @param int $executionID
* @param string $burnBy
+ * @param bool $showDelay
+ * @param array $dateList
* @access public
* @return array
*/
- public function getBurnDataFlot($executionID = 0, $burnBy = '')
+ public function getBurnDataFlot($executionID = 0, $burnBy = '', $showDelay = false, $dateList = array())
{
/* Get execution and burn counts. */
$execution = $this->getById($executionID);
@@ -2943,11 +3007,29 @@ class executionModel extends model
foreach($sets as $date => $set)
{
if($date < $execution->begin) continue;
- if($date > $execution->end) continue;
+ if(!$showDelay and $date > $execution->end) $set->value = 'null';
+ if($showDelay and $date < $execution->end) $set->value = 'null';
$burnData[$date] = $set;
$count++;
}
+
+ if($showDelay)
+ {
+ foreach($dateList as $date)
+ {
+ if(!isset($burnData[$date]))
+ {
+ $set = new stdClass();
+ $set->name = $date;
+ $set->left = 0;
+ $set->value = 'null';
+
+ $burnData[$date] = $set;
+ }
+ }
+ }
+
$burnData = array_reverse($burnData);
return $burnData;
@@ -3632,6 +3714,15 @@ class executionModel extends model
$chartData['burnLine'] = $this->report->createSingleJSON($sets, $dateList);
$chartData['baseLine'] = $baselineJSON;
+ $execution = $this->getById($executionID);
+ if((strpos('closed,suspended', $execution->status) === false and helper::today() > $execution->end)
+ or ($execution->status == 'closed' and substr($execution->closedDate, 0, 10) > $execution->end)
+ or ($execution->status == 'suspended' and $execution->suspendedDate > $execution->end))
+ {
+ $delaySets = $this->getBurnDataFlot($executionID, $burnBy, true, $dateList);
+ $chartData['delayLine'] = $this->report->createSingleJSON($delaySets, $dateList);
+ }
+
return $chartData;
}
@@ -3996,4 +4087,36 @@ class executionModel extends model
}
$this->dao->update(TABLE_PROJECT)->set('path')->eq($path['path'])->set('grade')->eq($path['grade'])->where('id')->eq($execution->id)->exec();
}
+
+ /**
+ * Build execution action menu.
+ *
+ * @param object $execution
+ * @param string $type
+ * @access public
+ * @return string
+ */
+ public function buildOperateMenu($execution, $type = 'view')
+ {
+ if($execution->deleted) return '';
+
+ $menu = '';
+ $params = "executionID=$execution->id";
+
+ $menu .= "
";
+ $menu .= $this->buildMenu('execution', 'start', $params, $execution, $type, '', '', 'iframe', true);
+ $menu .= $this->buildMenu('execution', 'activate', $params, $execution, $type, '', '', 'iframe', true);
+ $menu .= $this->buildMenu('execution', 'putoff', $params, $execution, $type, '', '', 'iframe', true);
+ $menu .= $this->buildMenu('execution', 'suspend', $params, $execution, $type, '', '', 'iframe', true);
+ $menu .= $this->buildMenu('execution', 'close', $params, $execution, $type, '', '', 'iframe', true);
+
+ $menu .= "
";
+ $menu .= $this->buildFlowMenu('execution', $execution, 'view', 'direct');
+ $menu .= "
";
+
+ $menu .= $this->buildMenu('execution', 'edit', "execution=$execution->id", $execution);
+ $menu .= $this->buildMenu('execution', 'delete', "execution=$execution->id", $execution, 'button', 'trash', 'hiddenwin');
+
+ return $menu;
+ }
}
diff --git a/module/execution/view/ajaxgetdropmenu.html.php b/module/execution/view/ajaxgetdropmenu.html.php
index 5d014e6ce1..f5c2d77aa6 100644
--- a/module/execution/view/ajaxgetdropmenu.html.php
+++ b/module/execution/view/ajaxgetdropmenu.html.php
@@ -46,7 +46,12 @@ foreach($executions as $projectID => $projectExecutions)
if($execution->status != 'done' and $execution->status != 'closed' and ($execution->PM == $this->app->user->account or isset($execution->teams[$this->app->user->account]))) $executionCounts[$projectID]['myExecution'] ++;
if($execution->status != 'done' and $execution->status != 'closed' and $execution->PM != $this->app->user->account and !isset($execution->teams[$this->app->user->account])) $executionCounts[$projectID]['others'] ++;
if($execution->status == 'done' or $execution->status == 'closed') $executionCounts[$projectID]['closed'] ++;
- $executionNames[] = $execution->name;
+ $onlyChildStage = $execution->grade == 2 and $execution->project != $execution->parent;
+ $executionNames[$execution->id] = $execution->name;
+ if($onlyChildStage and isset($parents[$execution->parent]))
+ {
+ $executionNames[$execution->id] = $parents[$execution->parent]->name . '/' . $execution->name;
+ }
}
}
$executionsPinYin = common::convert2Pinyin($executionNames);
@@ -77,7 +82,7 @@ foreach($executions as $projectID => $projectExecutions)
$selected = $execution->id == $executionID ? 'selected' : '';
if($execution->status != 'done' and $execution->status != 'closed' and ($execution->PM == $this->app->user->account or isset($execution->teams[$this->app->user->account])))
{
- $myExecutionsHtml .= '
' . html::a(sprintf($link, $execution->id), $execution->name, '', "class='$selected clickable' title='{$execution->name}' data-key='" . zget($executionsPinYin, $execution->name, '') . "' data-app='{$this->app->tab}'") . '';
+ $myExecutionsHtml .= '
' . html::a(sprintf($link, $execution->id), $executionNames[$execution->id], '', "class='$selected clickable' title='{$execution->name}' data-key='" . zget($executionsPinYin, $execution->name, '') . "' data-app='{$this->app->tab}'") . '';
if($selected == 'selected') $tabActive = 'myExecution';
@@ -85,7 +90,7 @@ foreach($executions as $projectID => $projectExecutions)
}
else if($execution->status != 'done' and $execution->status != 'closed' and $execution->PM != $this->app->user->account and !isset($execution->teams[$this->app->user->account]))
{
- $normalExecutionsHtml .= '
' . html::a(sprintf($link, $execution->id), $execution->name, '', "class='$selected clickable' title='{$execution->name}' data-key='" . zget($executionsPinYin, $execution->name, '') . "' data-app='{$this->app->tab}'") . '';
+ $normalExecutionsHtml .= '
' . html::a(sprintf($link, $execution->id), $executionNames[$execution->id], '', "class='$selected clickable' title='{$execution->name}' data-key='" . zget($executionsPinYin, $execution->name, '') . "' data-app='{$this->app->tab}'") . '';
if($selected == 'selected') $tabActive = 'other';
@@ -93,7 +98,7 @@ foreach($executions as $projectID => $projectExecutions)
}
else if($execution->status == 'done' or $execution->status == 'closed')
{
- $closedExecutionsHtml .= '
' . html::a(sprintf($link, $execution->id), $execution->name, '', "class='$selected clickable' title='$execution->name' data-key='" . zget($executionsPinYin, $execution->name, '') . "' data-app='{$this->app->tab}'") . '';
+ $closedExecutionsHtml .= '
' . html::a(sprintf($link, $execution->id), $executionNames[$execution->id], '', "class='$selected clickable' title='$execution->name' data-key='" . zget($executionsPinYin, $execution->name, '') . "' data-app='{$this->app->tab}'") . '';
if($selected == 'selected') $tabActive = 'closed';
}
@@ -171,7 +176,7 @@ $(function()
$('li.has-list div.hide-in-search').removeClass('hidden');
$('.nav-tabs li.active').find('span').show();
}
-
+
if($('.form-control.search-input').val().length > 0)
{
$('#closed').attr("hidden", false);
@@ -191,7 +196,7 @@ $(function()
$('#closed').attr("hidden", true);
$('#gray-line').attr("hidden", true);
}
-
+
var listItem = $(this).find('.has-list');
listItem.each(function ()
{
diff --git a/module/execution/view/burn.html.php b/module/execution/view/burn.html.php
index 71da19f850..81ea61c552 100644
--- a/module/execution/view/burn.html.php
+++ b/module/execution/view/burn.html.php
@@ -18,13 +18,24 @@
execution->burnXUnit);?>
execution->burnYUnit);?>
+
' . $lang->execution->computeBurn, 'hiddenwin', "title='{$lang->execution->computeBurn}{$lang->execution->burn}' class='btn btn-primary' id='computeBurn'");
- echo '
';
- echo html::a($this->createLink('execution', 'burn', "executionID=$executionID&type=$weekend&interval=$interval"), $lang->execution->$weekend, '', "class='btn btn-link'");
+ if(strpos('wait,doing', $execution->status) !== false)
+ {
+ common::printLink('execution', 'computeBurn', 'reload=yes', '
' . $lang->execution->computeBurn, 'hiddenwin', "title='{$lang->execution->computeBurn}' class='btn btn-primary' id='computeBurn'");
+ echo '
';
+ }
+
+ $weekend = strpos($type, 'noweekend') !== false ? 'withweekend' : 'noweekend';
+ $delay = strpos($type, 'withdelay') !== false ? 'nodelay' : 'withdelay';
+ echo html::a('#', $lang->execution->$weekend, '', "class='btn btn-link' id='weekend'");
+ if((strpos('closed,suspended', $execution->status) === false and helper::today() > $execution->end)
+ or ($execution->status == 'closed' and substr($execution->closedDate, 0, 10) > $execution->end)
+ or ($execution->status == 'suspended' and $execution->suspendedDate > $execution->end))
+ echo html::a('#', $lang->execution->$delay, '', "class='btn btn-link' id='delay'");
+
if(common::canModify('execution', $execution)) common::printLink('execution', 'fixFirst', "execution=$execution->id", $lang->execution->fixFirst, '', "class='btn btn-link iframe' data-width='700'");
echo $lang->execution->howToUpdateBurn;
?>
@@ -54,6 +65,9 @@
execution->charts->burn->graph->reference;?>
execution->charts->burn->graph->actuality;?>
+
+
execution->charts->burn->graph->delay;?>
+
@@ -94,6 +108,19 @@ function initBurnChar()
}]
};
+ var delaySets =
+ {
+ label: "execution->charts->burn->graph->delay;?>",
+ color: 'red',
+ pointStrokeColor: 'red',
+ pointHighlightStroke: 'red',
+ pointColor: 'red',
+ fillColor: 'rgba(0,106,241, .07)',
+ pointHighlightFill: '#fff',
+ data:
+ }
+ if(type.match('withdelay')) data.datasets.push(delaySets);
+
var burnChart = $("#burnCanvas").lineChart(data,
{
pointDotStrokeWidth: 2,
diff --git a/module/execution/view/computeburn.html.php b/module/execution/view/computeburn.html.php
index ea9ea7d339..8cbbbff589 100644
--- a/module/execution/view/computeburn.html.php
+++ b/module/execution/view/computeburn.html.php
@@ -11,8 +11,11 @@
*/
?>
$burnList)
{
- echo $burn->execution . "\t" . $burn->executionName . "\t" . $burn->date . "\t" . $burn->left . "\n";
+ foreach($burnList as $date => $burn)
+ {
+ echo $burn->execution . "\t" . $burn->executionName . "\t" . $burn->date . "\t" . $burn->left . "\n";
+ }
}
?>
diff --git a/module/execution/view/kanban.html.php b/module/execution/view/kanban.html.php
index cfddc793bd..ec45f38420 100644
--- a/module/execution/view/kanban.html.php
+++ b/module/execution/view/kanban.html.php
@@ -105,6 +105,7 @@ js::set('priv',
'canEditStory' => common::hasPriv('story', 'edit'),
'canDeleteStory' => common::hasPriv('story', 'delete'),
'canChangeStory' => common::hasPriv('story', 'change'),
+ 'canCloseStory' => common::hasPriv('story', 'close'),
'canUnlinkStory' => common::hasPriv('execution', 'unlinkStory'),
)
);
diff --git a/module/execution/view/taskkanban.html.php b/module/execution/view/taskkanban.html.php
index 4d86ea58cd..7ec69b0041 100644
--- a/module/execution/view/taskkanban.html.php
+++ b/module/execution/view/taskkanban.html.php
@@ -193,7 +193,8 @@ js::set('priv',
'canStartTask' => common::hasPriv('task', 'start'),
'canAssignBug' => common::hasPriv('bug', 'assignto'),
'canConfirmBug' => common::hasPriv('bug', 'confirmBug'),
- 'canActivateBug' => common::hasPriv('bug', 'activate')
+ 'canActivateBug' => common::hasPriv('bug', 'activate'),
+ 'canCloseStory' => common::hasPriv('story', 'close')
)
);
?>
diff --git a/module/execution/view/view.html.php b/module/execution/view/view.html.php
index fabcf9761c..90ec222e0e 100644
--- a/module/execution/view/view.html.php
+++ b/module/execution/view/view.html.php
@@ -146,26 +146,9 @@
- id";
- $browseLink = $this->session->executionList ? $this->session->executionList : inlink('browse', "executionID=$execution->id");
- common::printBack($browseLink);
- if(!$execution->deleted)
- {
- echo "
";
- common::printIcon('execution', 'start', "executionID=$execution->id", $execution, 'button', '', '', 'iframe', true);
- common::printIcon('execution', 'activate', "executionID=$execution->id", $execution, 'button', '', '', 'iframe', true);
- common::printIcon('execution', 'putoff', "executionID=$execution->id", $execution, 'button', '', '', 'iframe', true);
- common::printIcon('execution', 'suspend', "executionID=$execution->id", $execution, 'button', '', '', 'iframe', true);
- common::printIcon('execution', 'close', "executionID=$execution->id", $execution, 'button', '', '', 'iframe', true);
-
- echo $this->buildOperateMenu($execution, 'view');
-
- echo "
";
- common::printIcon('execution', 'edit', $params, $execution);
- common::printIcon('execution', 'delete', $params, $execution, 'button', 'trash', 'hiddenwin');
- }
- ?>
+ session->executionList ? $this->session->executionList : inlink('browse', "executionID=$execution->id");?>
+
+ execution->buildOperateMenu($execution, 'view');?>
diff --git a/module/gitlab/control.php b/module/gitlab/control.php
index dcf9627853..e3faaf7ada 100644
--- a/module/gitlab/control.php
+++ b/module/gitlab/control.php
@@ -259,7 +259,7 @@ class gitlab extends control
$user = $this->gitlab->apiGetCurrentUser($gitlab->url, $gitlab->token, true);
if(!is_object($user)) return $this->send(array('result' => 'fail', 'message' => array('url' => array(sprintf($this->lang->gitlab->hostError, $this->config->gitlab->minCompatibleVersion)))));
- if(isset($user->error)) return $this->send(array('result' => 'fail', 'message' => array('token' => array($this->lang->gitlab->tokenError))));
+ if(!isset($user->id)) return $this->send(array('result' => 'fail', 'message' => array('token' => array($this->lang->gitlab->tokenError))));
/* Verify version compatibility. */
$result = $this->gitlab->getVersion($gitlab->url, $gitlab->token);
diff --git a/module/group/lang/resource.php b/module/group/lang/resource.php
index 60aa69d0a4..9e7c18a9bd 100644
--- a/module/group/lang/resource.php
+++ b/module/group/lang/resource.php
@@ -1215,6 +1215,7 @@ $lang->resource->doc->objectLibs = 'objectLibs';
$lang->resource->doc->collect = 'collectAction';
$lang->resource->doc->tableContents = 'tableContents';
$lang->resource->doc->showFiles = 'showFiles';
+$lang->resource->doc->updateOrder = 'updateOrder';
$lang->doc->methodOrder[0] = 'index';
$lang->doc->methodOrder[5] = 'browse';
diff --git a/module/kanban/control.php b/module/kanban/control.php
index eeb922a87d..72fc0b3a02 100644
--- a/module/kanban/control.php
+++ b/module/kanban/control.php
@@ -1320,8 +1320,14 @@ class kanban extends control
{
$region = $this->kanban->getRegionByID($regionID);
+ $cards = $this->kanban->getCardsByObject('region', $regionID, 1);
+ foreach($this->config->kanban->fromType as $fromType)
+ {
+ $cards = $this->kanban->getImportedCards($region->kanban, $cards, $fromType, 1);
+ }
+
$this->view->kanban = $this->kanban->getByID($region->kanban);
- $this->view->cards = $this->kanban->getCardsByObject('region', $regionID, 1);
+ $this->view->cards = $cards;
$this->view->users = $this->loadModel('user')->getPairs('noletter|nodeleted');
$this->view->userIdPairs = $this->user->getPairs('noletter|nodeleted|showid');
$this->view->usersAvatar = $this->user->getAvatarPairs();
@@ -1374,13 +1380,20 @@ class kanban extends control
*/
public function deleteCard($cardID, $confirm = 'no')
{
+ $card = $this->kanban->getCardByID($cardID);
if($confirm == 'no')
{
- return print(js::confirm($this->lang->kanbancard->confirmDelete, $this->createLink('kanban', 'deleteCard', "cardID=$cardID&confirm=yes")));
+ $confirmTip = $card->fromType == '' ? $this->lang->kanbancard->confirmDelete : $this->lang->kanbancard->confirmRemove;
+ return print(js::confirm($confirmTip, $this->createLink('kanban', 'deleteCard', "cardID=$cardID&confirm=yes")));
}
else
{
- $this->kanban->delete(TABLE_KANBANCARD, $cardID);
+ if($card->fromType == '') $this->kanban->delete(TABLE_KANBANCARD, $cardID);
+ if($card->fromType != '')
+ {
+ $this->dao->delete()->from(TABLE_KANBANCARD)->where('id')->eq($cardID)->exec();
+ $this->loadModel('action')->create('kanbancard', $cardID, 'Deleted', '', $cardID);
+ }
if(isonlybody()) return print(js::reload('parent.parent'));
return print(js::reload('parent'));
@@ -1663,6 +1676,20 @@ class kanban extends control
->andWhere('`column`')->eq($toColID)
->exec();
+ $toColumn = $this->kanban->getColumnByID($toColID);
+ if($toColumn->laneType == 'story' and in_array($toColumn->type, array('tested', 'verified', 'released', 'closed')))
+ {
+ $data = new stdclass();
+ $data->stage = $toColumn->type;
+ if($toColumn->type == 'released')
+ {
+ $fromColumn = $this->kanban->getColumnByID($fromColID);
+ if($fromColumn->type == 'closed') $data->status = 'active';
+ }
+ $this->dao->update(TABLE_STORY)->data($data)->where('id')->eq($cardID)->exec();
+ $this->dao->update(TABLE_STORYSTAGE)->set('stage')->eq($toColumn->type)->where('story')->eq($cardID)->exec();
+ }
+
$kanbanGroup = $regionID == 0 ? $this->kanban->getExecutionKanban($executionID, $browseType, $groupBy) : $this->kanban->getRDKanban($executionID, $browseType, $orderBy, $regionID, $groupBy);
echo json_encode($kanbanGroup);
}
diff --git a/module/kanban/css/view.css b/module/kanban/css/view.css
index 52ff8d0e5b..313bd267e2 100644
--- a/module/kanban/css/view.css
+++ b/module/kanban/css/view.css
@@ -121,13 +121,13 @@
.label-closed, .label-terminate {background: #e5e5e5 !important; color: #b8b8b8;}
.label-deleted {background: #ff5d5d !important; color: #fff;}
.label-normal {background: #e1e8d9 !important; color: #9abe76;}
-.kanban-item > .kanban-card > .execInfo > .user, .kanban-item > .kanban-card > .releaseInfo > .user, .kanban-item > .kanban-card > .productplanInfo > .user {position: absolute; right: 13px; bottom: 1px}
+.kanban-item > .kanban-card > .execInfo > .user, .kanban-item > .kanban-card > .releaseInfo > .user, .kanban-item > .kanban-card > .productplanInfo > .user {float:right}
.kanban-item > .kanban-card > .execInfo > .user > .avatar, .kanban-item > .kanban-card > .releaseInfo > .user > .avatar, .kanban-item > .kanban-card > .productplanInfo > .user > .avatar {display: inline-block; width: 24px; height: 24px; line-height: 24px; margin-right: 0px; margin-left: 2px}
.kanban-card .releaseTitle .icon, .kanban-card .title .icon, .kanban-card .productplanTitle .icon, .kanban-card .title .icon, .kanban-card .buildTitle .icon{padding-right: 5px;}
.kanban-card .label-finish {margin-right: 7px; margin-top: -1px; padding: 3px 5px; float: left; background-color:#2a5f29;}
.kanban-card .releaseTitle, .kanban-card .productplanTitle {width: 100%; float: left;}
-.progress-box {display: flex; flex-direction: row; margin-top: 10px;}
+.progress-box {width:100%; display: flex; flex-direction: row; margin-top: 10px;}
.progress {flex: auto; margin: 5px auto 5px;}
.progress-number {padding-left: 5px;}
diff --git a/module/kanban/js/view.js b/module/kanban/js/view.js
index 0d0b723b39..1ba790454f 100644
--- a/module/kanban/js/view.js
+++ b/module/kanban/js/view.js
@@ -278,6 +278,22 @@ function renderKanbanItem(item, $item)
var whiteStyle = null;
if(item.color == '#2a5f29' || item.color == '#b10b0b' || item.color == '#cfa227') whiteStyle = 'style="color:#FFFFFF"';
+ var privs = item.actions;
+ var printMoreBtn = (privs.includes('editCard') || privs.includes('archiveCard') || privs.includes('copyCard') || privs.includes('deleteCard') || privs.includes('moveCard') || privs.includes('setCardColor'));
+ var $actions = $item.children('.actions');
+ var $title = $item.children('.title');
+ if(printMoreBtn && !$actions.length)
+ {
+ $(
+ [
+ ''
+ ].join('')).appendTo($item);
+ }
+
if(item.fromType == 'execution')
{
renderExecutionItem(item, $item);
@@ -296,53 +312,13 @@ function renderKanbanItem(item, $item)
}
else
{
- var $title = $item.children('.title');
- var privs = item.actions;
- var printMoreBtn = (privs.includes('editCard') || privs.includes('archiveCard') || privs.includes('copyCard') || privs.includes('deleteCard') || privs.includes('moveCard') || privs.includes('setCardColor'));
-
- if(privs.includes('sortCard')) $item.parent().addClass('sort');
if(!$title.length)
{
- $('' + kanbanLang.finished + '
').appendTo($item);
if(privs.includes('viewCard')) $title = $('').appendTo($item).attr('href', createLink('kanban', 'viewCard', 'cardID=' + item.id, '', true));
if(!privs.includes('viewCard')) $title = $('').appendTo($item);
}
$title.text(item.name).attr('title', item.name);
- if(kanban.performable == 1 && item.status == 'done' )
- {
- var finishColor = '#2a5f29';
- if(item.color == '#2a5f29') finishColor = '#FFFFFF';
- $title.text(item.name).css('color', finishColor);
- $item.children('.label-finish').show();
- if(item.color == '#2a5f29')
- {
- $item.children('.label-finish').css({'background-color':'#FFFFFF','color':'#2a5f29'});
- }
- else
- {
- $item.children('.label-finish').css({'background-color':'','color':'#FFFFFF'});
- }
- }
- else
- {
- $title.text(item.name).css('color', '');
- $item.children('.label-finish').hide();
- }
-
- var $actions = $item.children('.actions');
- if(printMoreBtn && !$actions.length)
- {
- $(
- [
- ''
- ].join('')).appendTo($item);
- }
-
var $info = $item.children('.info');
if(!$info.length) $info = $(
[
@@ -353,11 +329,6 @@ function renderKanbanItem(item, $item)
'',
''
].join('')).appendTo($item);
- if(kanban.performable == 1)
- {
- var $progress = $item.children('.progress-box');
- if(!$progress.length) $progress = $('').appendTo($item);
- }
$item.data('card', item);
@@ -367,15 +338,6 @@ function renderKanbanItem(item, $item)
$info.children('.pri')
.attr('class', 'pri label-pri label-pri-' + item.pri)
.text(item.pri);
- if(kanban.performable == 1)
- {
- $progress.find('.progress-bar').css('width', item.progress + '%');
- $progress.find('.progress-number').html(item.progress + '%');
- }
-
- $item.css('background-color', item.color);
- $item.toggleClass('has-color', item.color != '#fff' && item.color != '');
- $item.find('.info > .label-light').css('background-color', item.color);
var $time = $info.children('.time');
if(item.end == '0000-00-00' && item.begin == '0000-00-00')
@@ -413,6 +375,41 @@ function renderKanbanItem(item, $item)
for(i = 0; i < assignedTo.length; i++) title.push(users[assignedTo[i]]);
$user.html(renderUsersAvatar(assignedTo, item.id)).attr('title', title);
}
+
+ if(kanban.performable == 1)
+ {
+ var $progress = $item.children('.progress-box');
+ if(!$progress.length) $progress = $('').appendTo($item);
+ $progress.find('.progress-bar').css('width', item.progress + '%');
+ $progress.find('.progress-number').html(item.progress + '%');
+ }
+ $item.css('background-color', item.color);
+ $item.toggleClass('has-color', item.color != '#fff' && item.color != '');
+ $item.find('.info > .label-light').css('background-color', item.color);
+
+ $title = item.fromType == 'execution' ? $item.children('.header').children('.executionName').children('.title') : $item.children('.title');
+ if(!$title.children('.label-finish').length) $title.prepend('' + kanbanLang.finished + '
');
+ var name = item.title ? item.title : item.name;
+ if(kanban.performable == 1 && item.status == 'done' )
+ {
+ var finishColor = '#2a5f29';
+ if(item.color == '#2a5f29') finishColor = '#FFFFFF';
+ $title.css('color', finishColor);
+ $title.children('.label-finish').show();
+ if(item.color == '#2a5f29')
+ {
+ $item.children('.label-finish').css({'background-color':'#FFFFFF','color':'#2a5f29'});
+ }
+ else
+ {
+ $item.children('.label-finish').css({'background-color':'','color':'#FFFFFF'});
+ }
+ }
+ else
+ {
+ $title.css('color', '');
+ $title.children('.label-finish').hide();
+ }
}
/**
@@ -445,14 +442,19 @@ function renderExecutionItem(item, $item)
/* Print execution name. */
var $title = $titleBox.children('.title');
+ var name = item.title ? item.title : item.name;
if(!$title.length)
{
var icon = mode == 'new' ? 'run' : 'project';
var viewMethod = item.execType == 'kanban' ? 'kanban' : 'view';
- if(privs.includes('viewExecution') && item.deleted == '0') $title = $('' + item.name + '').appendTo($titleBox).attr('href', createLink('execution', viewMethod, 'executionID=' + item.fromID));
- if(!privs.includes('viewExecution') || item.deleted == '1') $title = $('' + item.name + '
').appendTo($titleBox);
+ if(privs.includes('viewExecution') && item.deleted == '0') $title = $('' + name + '').appendTo($titleBox).attr('href', createLink('execution', viewMethod, 'executionID=' + item.fromID));
+ if(!privs.includes('viewExecution') || item.deleted == '1') $title = $('' + name + '
').appendTo($titleBox);
}
- $title.attr('title', item.name);
+ if(!$title.children('i').length)
+ {
+ $title.append('' + item.title);
+ }
+ $title.attr('title', name);
if(item.delay)
{
@@ -477,7 +479,7 @@ function renderExecutionItem(item, $item)
{
if(item.deleted == '0')
{
- $statusBox = $('' + executionLang.statusList[item.status] + '').appendTo($info);
+ $statusBox = $('' + executionLang.statusList[item.objectStatus] + '').appendTo($info);
}
else
{
@@ -519,12 +521,17 @@ function renderReleaseItem(item, $item)
/* Print name. */
var $title = $item.children('.releaseTitle');
+ var name = item.title ? item.title : item.name;
if(!$title.length)
{
- if(privs.includes('viewRelease') && item.deleted == '0') $title = $('' + item.name + '').appendTo($item).attr('href', createLink('release', 'view', 'releaseID=' + item.fromID));
- if(!privs.includes('viewRelease') || item.deleted == '1') $title = $('' + item.name + '
').appendTo($item);
+ if(privs.includes('viewRelease') && item.deleted == '0') $title = $('' + name + '').appendTo($item).attr('href', createLink('release', 'view', 'releaseID=' + item.fromID));
+ if(!privs.includes('viewRelease') || item.deleted == '1') $title = $('' + name + '
').appendTo($item);
}
- $title.attr('title', item.name);
+ if(!$title.children('i').length)
+ {
+ $title.prepend('');
+ }
+ $title.attr('title', name);
var $info = $item.children('.releaseInfo');
if(!$info.length) $info = $(
@@ -538,7 +545,7 @@ function renderReleaseItem(item, $item)
{
if(item.deleted == '0')
{
- $statusBox = $('' + releaseLang.statusList[item.status] + '').appendTo($info);
+ $statusBox = $('' + releaseLang.statusList[item.objectStatus] + '').appendTo($info);
}
else
{
@@ -578,10 +585,15 @@ function renderProductplanItem(item, $item)
/* Print name. */
var $title = $item.children('.productplanTitle');
+ var name = item.title ? item.title : item.name;
if(!$title.length)
{
- if(privs.includes('viewPlan') && item.deleted == '0') $title = $('' + item.title + '').appendTo($item).attr('href', createLink('productplan', 'view', 'productplanID=' + item.fromID));
- if(!privs.includes('viewPlan') || item.deleted == '1') $title = $('' + item.title + '
').appendTo($item);
+ if(privs.includes('viewPlan') && item.deleted == '0') $title = $('' + name + '').appendTo($item).attr('href', createLink('productplan', 'view', 'productplanID=' + item.fromID));
+ if(!privs.includes('viewPlan') || item.deleted == '1') $title = $('' + name + '
').appendTo($item);
+ }
+ if(!$title.children('i').length)
+ {
+ $title.append('' + name);
}
$title.attr('title', item.title);
@@ -597,7 +609,7 @@ function renderProductplanItem(item, $item)
{
if(item.deleted == '0')
{
- $statusBox = $('' + productplanLang.statusList[item.status] + '').appendTo($info);
+ $statusBox = $('' + productplanLang.statusList[item.objectStatus] + '').appendTo($info);
}
else
{
@@ -646,12 +658,17 @@ function renderBuildItem(item, $item)
/* Print name. */
var $title = $item.children('.buildTitle');
+ var name = item.title ? item.title : item.name;
if(!$title.length)
{
- if(privs.includes('viewBuild') && item.deleted == '0') $title = $('' + item.name + '').appendTo($item).attr('href', createLink('build', 'view', 'buildID=' + item.fromID));
- if(!privs.includes('viewBuild') || item.deleted == '1') $title = $('' + item.name + '
').appendTo($item);
+ if(privs.includes('viewBuild') && item.deleted == '0') $title = $('' + name + '').appendTo($item).attr('href', createLink('build', 'view', 'buildID=' + item.fromID));
+ if(!privs.includes('viewBuild') || item.deleted == '1') $title = $('' + name + '
').appendTo($item);
}
- $title.attr('title', item.name);
+ if(!$title.children('i').length)
+ {
+ $title.append('' + item.title);
+ }
+ $title.attr('title', name);
var $info = $item.children('.info');
if(!$info.length) $info = $(
@@ -1069,7 +1086,7 @@ function createCardMenu(options)
if(!privs.length) return [];
var items = [];
- if(privs.includes('editCard')) items.push({label: kanbanLang.editCard, icon: 'edit', url: createLink('kanban', 'editCard', 'cardID=' + card.id, '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '80%'}});
+ if(privs.includes('editCard') && card.fromType == '') items.push({label: kanbanLang.editCard, icon: 'edit', url: createLink('kanban', 'editCard', 'cardID=' + card.id, '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '80%'}});
if(privs.includes('performable') && kanban.performable == 1)
{
if(card.status == 'done')
@@ -1081,9 +1098,6 @@ function createCardMenu(options)
items.push({label: kanbanLang.finishCard, icon: 'checked', onClick: function(){finishCard(card.id, card.kanban, card.region);}});
}
}
- if(privs.includes('archiveCard') && kanban.archived == '1') items.push({label: kanbanLang.archiveCard, icon: 'card-archive', url: createLink('kanban', 'archiveCard', 'cardID=' + card.id), attrs: {'target': 'hiddenwin'}});
- if(privs.includes('copyCard')) items.push({label: kanbanLang.copyCard, icon: 'copy', url: createLink('kanban', 'copyCard', 'cardID=' + card.id, '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal'}});
- if(privs.includes('deleteCard')) items.push({label: kanbanLang.deleteCard, icon: 'trash', url: createLink('kanban', 'deleteCard', 'cardID=' + card.id), attrs: {'target': 'hiddenwin'}});
if(privs.includes('moveCard'))
{
var moveCardItems = [];
@@ -1107,10 +1121,11 @@ function createCardMenu(options)
moveCardItems = moveCardItems.reverse();
items.push({label: kanbanLang.moveCard, icon: 'move', items: moveCardItems});
}
+ if(privs.includes('archiveCard') && kanban.archived == '1') items.push({label: kanbanLang.archiveCard, icon: 'card-archive', url: createLink('kanban', 'archiveCard', 'cardID=' + card.id), attrs: {'target': 'hiddenwin'}});
if(privs.includes('setCardColor'))
{
var cardColoritems = [];
- if(!card.color) color = "#fff";
+ if(!card.color) card.color = "#fff";
for(let i = 0 ; i < colorList.length ; i ++ )
{
var attr = card.color == colorList[i] ? '' : '';
@@ -1120,6 +1135,7 @@ function createCardMenu(options)
};
items.push({label: kanbanLang.cardColor, icon: 'color', items: cardColoritems});
}
+ if(privs.includes('deleteCard')) items.push({label: card.fromType == '' ? kanbanLang.deleteCard : kanbanLang.removeCard, icon: card.fromType == '' ? 'trash' : 'unlink', url: createLink('kanban', 'deleteCard', 'cardID=' + card.id), attrs: {'target': 'hiddenwin'}});
var bounds = options.$trigger[0].getBoundingClientRect();
items.$options = {x: bounds.right, y: bounds.top};
diff --git a/module/kanban/lang/en.php b/module/kanban/lang/en.php
index b6d8efb4a2..cce018447c 100644
--- a/module/kanban/lang/en.php
+++ b/module/kanban/lang/en.php
@@ -40,6 +40,7 @@ $lang->kanban->moveCard = 'Move Card';
$lang->kanban->cardColor = 'Card Color';
$lang->kanban->setCardColor = 'Set Card Color';
$lang->kanban->deleteCard = 'Delete Card';
+$lang->kanban->removeCard = 'Remove Card';
$lang->kanban->assigntoCard = 'Assign';
$lang->kanban->setting = 'Setting';
$lang->kanban->enableArchived = 'Enable Archived';
@@ -365,6 +366,7 @@ $lang->kanbancard->confirmArchive = 'Are you sure to archive this card? After
$lang->kanbancard->confirmDelete = 'Are you sure to delete this card? After deleting the card, it will be deleted from the Kanban. You can only view it in the system recycle bin.';
$lang->kanbancard->confirmRestore = "Are you sure you want to restore this card? After the card is restored, the card will be restored to the '%s' Kanban column.";
$lang->kanbancard->confirmRestoreTip = "The card's column has been archived or deleted, please restore '%s' column first.";
+$lang->kanbancard->confirmRemove = 'Are you sure to remove this card? After removing the card, the original data would not be affected.The card would be removed from the Kanban and counld not be restored.';
$lang->kanbancard->priList[1] = 1;
$lang->kanbancard->priList[2] = 2;
diff --git a/module/kanban/lang/zh-cn.php b/module/kanban/lang/zh-cn.php
index 3b30ccdcdf..00a8305975 100644
--- a/module/kanban/lang/zh-cn.php
+++ b/module/kanban/lang/zh-cn.php
@@ -40,6 +40,7 @@ $lang->kanban->moveCard = '移动卡片';
$lang->kanban->cardColor = '卡片颜色';
$lang->kanban->setCardColor = '设置卡片颜色';
$lang->kanban->deleteCard = '删除卡片';
+$lang->kanban->removeCard = '移除卡片';
$lang->kanban->assigntoCard = '指派';
$lang->kanban->setting = '设置';
$lang->kanban->enableArchived = '设置归档功能';
@@ -365,6 +366,7 @@ $lang->kanbancard->confirmArchive = '您确认归档该卡片吗?归档卡
$lang->kanbancard->confirmDelete = '您确认删除该卡片吗?删除卡片后,该卡片将从看板中删除,您只能通过系统回收站查看。';
$lang->kanbancard->confirmRestore = '您确定要还原该卡片吗?还原卡片后,该卡片将还原到“%s”看板列中。';
$lang->kanbancard->confirmRestoreTip = '该卡片所属的看板列已被归档或删除,请先还原“%s”看板列。';
+$lang->kanbancard->confirmRemove = '您确认移除该卡片吗?移除卡片后,源数据不会受到影响,卡片将从看板中移除且无法恢复。';
$lang->kanbancard->priList[1] = 1;
$lang->kanbancard->priList[2] = 2;
diff --git a/module/kanban/model.php b/module/kanban/model.php
index 78ad667080..b3f439b886 100644
--- a/module/kanban/model.php
+++ b/module/kanban/model.php
@@ -1113,16 +1113,17 @@ class kanbanModel extends model
* @param int $kanbanID
* @param object $cards
* @param string $fromType
+ * @param int $archived
* @access public
* @return array
*/
- public function getImportedCards($kanbanID, $cards, $fromType)
+ public function getImportedCards($kanbanID, $cards, $fromType, $archived = 0)
{
/* Get imported cards based on imported object type. */
$objectCards = $this->dao->select('*')->from(TABLE_KANBANCARD)
->where('deleted')->eq(0)
->andWhere('kanban')->eq($kanbanID)
- ->andWhere('archived')->eq(0)
+ ->andWhere('archived')->eq($archived)
->andWhere('fromType')->eq($fromType)
->fetchGroup('fromID', 'id');
@@ -1168,6 +1169,8 @@ class kanbanModel extends model
$objectCard->execType = $object->type;
}
+ $objectCard->objectStatus = $objectCard->status;
+ $objectCard->status = $objectCard->progress == 100 ? 'done' : 'doing';
$cards[$cardID] = $objectCard;
}
}
diff --git a/module/kanban/view/viewarchivedcard.html.php b/module/kanban/view/viewarchivedcard.html.php
index 551ca80854..1a18aed718 100644
--- a/module/kanban/view/viewarchivedcard.html.php
+++ b/module/kanban/view/viewarchivedcard.html.php
@@ -19,6 +19,7 @@
#archivedCards .kanban-item > .info {margin-top: 5px; position: relative;}
#archivedCards .kanban-item > .info > .pri {height: 14px; border-width: 1px; font-size: 12px; line-height: 12px; min-width: 14px; padding: 0 1px;}
#archivedCards .kanban-item > .info > .time {margin-left: 10px; font-size: 12px}
+#archivedCards .kanban-item > .info.build > .time {margin-left:0px; font-size: 12px}
#archivedCards .kanban-item > .info > .user {position: absolute; right: 0; top: 0}
#archivedCards .card-actions {position: relative; padding: 15px 10px; padding-top: 0px;}
#archivedCards .card-actions > .btn {display: block;}
@@ -31,6 +32,7 @@
#archivedCards .info > .users > span:after {right: -4px; content: ''; display: block; position: absolute; width: 2px; height: 2px; background-color: #8990a2; top: 0px; border-radius: 50%;}
#archivedCards .info > .users .avatar {display: inline-block; position: relative; border-radius: 50%; top: -5px; margin: 5px; right: -7px; margin-left: -4px;}
#archivedCards .cardName {word-wrap: break-word;}
+#archivedCards .card-item .icon {margin-right:2px;}
#archivedCards .card-item .red {background-color: #b10b0b;}
#archivedCards .card-item .yellow {background-color: #cfa227;}
#archivedCards .card-item .green {background-color: #2a5f29;}
@@ -40,10 +42,18 @@
#archivedCards .card-item .has-color .info > .label-pri,
#archivedCards .card-item .has-color .info > .estimate,
#archivedCards .card-item .has-color .info > .label-light {color: #FFFFFF;}
+#archivedCards .info.execution .label-light, #archivedCards .info.productplan .label-light, #archivedCards .info.build .label-light, #archivedCards .info.release .label-light {background: #EFEFEF !important; color: #838A9D !important;}
#archivedCards .card-item .has-color .info > .label-pri {border-color: #FFFFFF;}
+#archivedCards .card-item .has-color .progress-box > .progress-number {color: #FFFFFF;}
#archivedCards .progress-box {width: 97%;}
#performable {padding: 25px 10px !important;}
+loadLang('execution');
+$app->loadLang('release');
+$app->loadLang('build');
+$app->loadLang('productplan');
+?>
kanban->archivedCard;?>
@@ -76,48 +86,78 @@
status == 'done'):?>
kanban->finished;?>
+ fromType)):?>
createLink('kanban', 'viewCard', "cardID=$card->id", '', true), $card->name, '', "class='cardName iframe' data-toggle='modal' data-width='80%' title='$card->name'");?>
pri;?>
estimate and $card->estimate != 0) echo "
{$card->estimate}h";?>
- end) and !helper::isZeroDate($card->begin)):?>
+
+ title) ? $card->title : $card->name;
+ if(common::hasPriv($card->fromType, 'view')) echo html::a($this->createLink($card->fromType, 'view', "id=$card->fromID"), $name, '', "class='cardName' title='$name'");
+ if(!common::hasPriv($card->fromType, 'view')) echo "
$name
";
+ echo "
";
+ if(isset($lang->{$card->fromType}->statusList[$card->objectStatus])) echo "
" . $lang->{$card->fromType}->statusList[$card->objectStatus] . '';
+ if(isset($card->date) and !helper::isZeroDate($card->date)) echo "
" . date("Y-m-d", strtotime($card->date)) . ""
+ ?>
+
+ end) and !helper::isZeroDate($card->begin) and $card->begin != '2030-01-01' and $card->end != '2030-01-01'):?>
begin)) . $lang->kanbancard->beginAB;?>
- begin) and !helper::isZeroDate($card->end)):?>
+ begin) and !helper::isZeroDate($card->end) and $card->begin != '2030-01-01' and $card->end != '2030-01-01'):?>
end)) . $lang->kanbancard->deadlineAB;?>
- begin) and !helper::isZeroDate($card->end)):?>
+ begin) and !helper::isZeroDate($card->end) and $card->begin != '2030-01-01' and $card->end != '2030-01-01' and $card->fromType == ''):?>
begin)) . ' ~ ' . date("m/d", strtotime($card->end));?>
+ begin) and !helper::isZeroDate($card->end) and $card->begin != '2030-01-01' and $card->end != '2030-01-01' and $card->fromType != ''):?>
+
begin)) . ' ' . $lang->{$card->fromType}->to . ' ' . date("m-d", strtotime($card->end));?>
+
assignedTo);
+ if($card->begin == '2030-01-01' or $card->end == '2030-01-01') echo '
' . $lang->{$card->fromType}->future . '';
+
+ if($card->fromType == '') $assignedToList = explode(',', $card->assignedTo);
+ if($card->fromType == 'execution') $assignedToList = $card->PM;
+ if($card->fromType == 'build') $assignedToList = $card->builder;
+ if($card->fromType == 'release' or $card->fromType == 'productplan') $assignedToList = $card->createdBy;
$count = 0;
$members = '';
?>
- $account)
- {
- if(!isset($users[$account]) or !isset($usersAvatar[$account]))
- {
- unset($assignedToList[$index]);
- continue;
- }
- $members .= $users[$account] . ',';
- }
- $userCount = count($assignedToList);
- ?>
- 0):?>
-
- 2) continue;
- echo html::smallAvatar(array('avatar' => $usersAvatar[$account], 'account' => $account, 'name' => $users[$account]), 'avatar-circle');
- $count ++;
- }
- ?>
- 2) echo "...";?>
-
+
+ $account)
+ {
+ if(!isset($users[$account]) or !isset($usersAvatar[$account]))
+ {
+ unset($assignedToList[$index]);
+ continue;
+ }
+ $members .= $users[$account] . ',';
+ }
+ $userCount = count($assignedToList);
+ ?>
+ 0):?>
+
+ 2) continue;
+ echo html::smallAvatar(array('avatar' => $usersAvatar[$account], 'account' => $account, 'name' => $users[$account]), 'avatar-circle');
+ $count ++;
+ }
+ ?>
+ 2) echo "...";?>
+
+
+
+ ";
+ echo html::smallAvatar(array('avatar' => $usersAvatar[$assignedToList], 'account' => $assignedToList, 'name' => $users[$assignedToList]), 'avatar-circle');
+ echo "
";
+ }
+ ?>
performable):?>
@@ -137,7 +177,7 @@
if($canRestore) echo html::a(inlink('restoreCard', "cardID={$card->id}"), $lang->kanban->restore, '', "class='btn btn-xs btn-primary' target='hiddenwin'");
- if($canDelete) echo html::a($this->createLink('kanban', 'deleteCard', "cardID=$card->id"), $lang->delete, '', "class='btn btn-xs delete-card' target='hiddenwin'");
+ if($canDelete) echo html::a($this->createLink('kanban', 'deleteCard', "cardID=$card->id"), $card->fromType == '' ? $lang->delete : $lang->unlink, '', "class='btn btn-xs delete-card' target='hiddenwin'");
?>
@@ -152,5 +192,17 @@ $(function()
{
$('#archivedCards').animate({right: -400}, 500);
});
+ $('.card-item').each(function()
+ {
+ var $item = $(this).children('.col-xs-10').children('.kanban-item');
+ var icon = '';
+
+ if($item.children('.build').length) icon = '';
+ if($item.children('.execution').length) icon = '';
+ if($item.children('.productplan').length) icon = '';
+ if($item.children('.release').length) icon = '';
+
+ $item.children('.cardName').prepend(icon);
+ });
})
diff --git a/module/product/config.php b/module/product/config.php
index 15b950ff39..ba557eda1f 100644
--- a/module/product/config.php
+++ b/module/product/config.php
@@ -87,6 +87,36 @@ $config->product->search['params']['closedDate'] = array('operator' => '=',
$config->product->search['params']['lastEditedDate'] = array('operator' => '=', 'control' => 'input', 'values' => '', 'class' => 'date');
$config->product->search['params']['activatedDate'] = array('operator' => '=', 'control' => 'input', 'values' => '', 'class' => 'date');
+$config->product->all = new stdclass();
+$config->product->all->search['module'] = 'product';
+$config->product->all->search['fields']['name'] = $lang->product->name;
+$config->product->all->search['fields']['code'] = $lang->product->code;
+$config->product->all->search['fields']['id'] = $lang->product->id;
+if($config->systemMode == 'new') $config->product->all->search['fields']['program'] = $lang->product->program;
+$config->product->all->search['fields']['line'] = $lang->product->line;
+$config->product->all->search['fields']['desc'] = $lang->product->desc;
+$config->product->all->search['fields']['PO'] = $lang->product->PO;
+$config->product->all->search['fields']['QD'] = $lang->product->QD;
+$config->product->all->search['fields']['RD'] = $lang->product->RD;
+$config->product->all->search['fields']['reviewer'] = $lang->product->reviewer;
+$config->product->all->search['fields']['type'] = $lang->product->type;
+$config->product->all->search['fields']['createdDate'] = $lang->product->createdDate;
+$config->product->all->search['fields']['createdBy'] = $lang->product->createdBy;
+
+$config->product->all->search['params']['name'] = array('operator' => 'include', 'control' => 'input', 'values' => '');
+$config->product->all->search['params']['code'] = array('operator' => 'include', 'control' => 'input', 'values' => '');
+$config->product->all->search['params']['id'] = array('operator' => '=', 'control' => 'input', 'values' => '');
+if($config->systemMode == 'new') $config->product->all->search['params']['program'] = array('operator' => '=', 'control' => 'select', 'values' => '');
+$config->product->all->search['params']['line'] = array('operator' => '=', 'control' => 'select', 'values' => '');
+$config->product->all->search['params']['desc'] = array('operator' => 'include', 'control' => 'input', 'values' => '');
+$config->product->all->search['params']['PO'] = array('operator' => '=', 'control' => 'select', 'values' => 'users');
+$config->product->all->search['params']['QD'] = array('operator' => '=', 'control' => 'select', 'values' => 'users');
+$config->product->all->search['params']['RD'] = array('operator' => '=', 'control' => 'select', 'values' => 'users');
+$config->product->all->search['params']['reviewer'] = array('operator' => 'include', 'control' => 'select', 'values' => 'users');
+$config->product->all->search['params']['type'] = array('operator' => '=', 'control' => 'select', 'values' => $lang->product->typeList);
+$config->product->all->search['params']['createdDate'] = array('operator' => '=', 'control' => 'input', 'values' => '', 'class' => 'date');
+$config->product->all->search['params']['createdBy'] = array('operator' => '=', 'control' => 'select', 'values' => 'users');
+
$config->product->create = new stdclass();
$config->product->edit = new stdclass();
$config->product->create->requiredFields = 'name,code';
diff --git a/module/product/control.php b/module/product/control.php
index 17e5520626..401edefb9b 100644
--- a/module/product/control.php
+++ b/module/product/control.php
@@ -301,7 +301,7 @@ class product extends control
$actionURL = $this->createLink($rawModule, $rawMethod, $params . "productID=$productID&branch=$branch&browseType=bySearch&queryID=myQueryID&storyType=$storyType");
$this->config->product->search['onMenuBar'] = 'yes';
- $this->product->buildSearchForm($productID, $this->products, $queryID, $actionURL, $branch);
+ $this->product->buildSearchForm($productID, $this->products, $queryID, $actionURL, $branch, $projectID);
$showModule = !empty($this->config->datatable->productBrowse->showModule) ? $this->config->datatable->productBrowse->showModule : '';
@@ -1197,22 +1197,30 @@ class product extends control
* @access public
* @return void
*/
- public function all($browseType = 'noclosed', $orderBy = 'order_asc')
+ public function all($browseType = 'noclosed', $orderBy = 'program_asc', $param = 0, $recTotal = 0, $recPerPage = 20, $pageID = 1)
{
/* Load module and set session. */
$this->loadModel('program');
$this->session->set('productList', $this->app->getURI(true), 'product');
+ $queryID = ($browseType == 'bySearch') ? (int)$param : 0;
+
if($this->app->viewType == 'mhtml')
{
$productID = $this->product->saveState(0, $this->products);
$this->product->setMenu($productID);
}
+ $this->app->loadClass('pager', $static = true);
+ $pager = new pager($recTotal, $recPerPage, $pageID);
+
/* Process product structure. */
- $productStats = $this->product->getStats($orderBy, '', $browseType, '', 'story');
+ $productStats = $this->product->getStats($orderBy, $pager, $browseType, '', 'story', '', $queryID);
$productStructure = $this->product->statisticProgram($productStats);
+ $actionURL = $this->createLink('product', 'all', "browseType=bySearch&orderBy=order_asc&queryID=myQueryID");
+ $this->product->buildProductSearchForm($param, $actionURL);
+
$this->view->title = $this->lang->product->common;
$this->view->position[] = $this->lang->product->common;
@@ -1221,6 +1229,7 @@ class product extends control
$this->view->productStructure = $productStructure;
$this->view->orderBy = $orderBy;
$this->view->browseType = $browseType;
+ $this->view->pager = $pager;
$this->display();
}
diff --git a/module/product/model.php b/module/product/model.php
index 4de525f5e5..d92ec088e7 100644
--- a/module/product/model.php
+++ b/module/product/model.php
@@ -275,6 +275,48 @@ class productModel extends model
->fetchAll('id');
}
+ /**
+ * Get list by search.
+ *
+ * @param int $queryID
+ * @access public
+ * @return array
+ */
+ public function getListBySearch($queryID = 0)
+ {
+ if($queryID)
+ {
+ $query = $this->loadModel('search')->getQuery($queryID);
+ if($query)
+ {
+ $this->session->set('productQuery', $query->sql);
+ $this->session->set('productForm', $query->form);
+ }
+ else
+ {
+ $this->session->set('productQuery', ' 1 = 1');
+ }
+ }
+ else
+ {
+ if($this->session->productQuery == false) $this->session->set('productQuery', ' 1 = 1');
+ }
+
+ $productQuery = $this->session->productQuery;
+ $productQuery = preg_replace('/`(\w+)`/', 't1.`$1`', $productQuery);
+
+ $products = $this->dao->select('t1.id as id,t1.*')->from(TABLE_PRODUCT)->alias('t1')
+ ->leftJoin(TABLE_PROGRAM)->alias('t2')->on('t1.program = t2.id')
+ ->where($productQuery)
+ ->andWhere('t1.deleted')->eq(0)
+ ->beginIF(!$this->app->user->admin)->andWhere('t1.id')->in($this->app->user->view->products)->fi()
+ ->andWhere('t1.vision')->eq($this->config->vision)->fi()
+ ->orderBy('t1.order_asc')
+ ->fetchAll('id');
+
+ return $products;
+ }
+
/**
* Get product pairs.
*
@@ -609,6 +651,7 @@ class productModel extends model
->batchCheck($this->config->product->create->requiredFields, 'notempty')
->checkIF((!empty($product->name) and $this->config->systemMode == 'new'), 'name', 'unique', "`program` = $programID")
->checkIF(!empty($product->code), 'code', 'unique')
+ ->checkFlow()
->exec();
if(!dao::isError())
@@ -718,6 +761,7 @@ class productModel extends model
->batchCheck($this->config->product->edit->requiredFields, 'notempty')
->checkIF((!empty($product->name) and $this->config->systemMode == 'new'), 'name', 'unique', "id != $productID and `program` = $programID")
->checkIF(!empty($product->code), 'code', 'unique', "id != $productID")
+ ->checkFlow()
->where('id')->eq($productID)
->exec();
@@ -773,8 +817,6 @@ class productModel extends model
if(is_array($products[$productID]->{$extendField->field})) $products[$productID]->{$extendField->field} = join(',', $products[$productID]->{$extendField->field});
$products[$productID]->{$extendField->field} = htmlSpecialString($products[$productID]->{$extendField->field});
- $message = $this->checkFlowRule($extendField, $products[$productID]->{$extendField->field});
- if($message) return print(js::alert($message));
}
}
if(dao::isError()) return print(js::error(dao::getError()));
@@ -791,6 +833,7 @@ class productModel extends model
->autoCheck()
->batchCheck($this->config->product->edit->requiredFields , 'notempty')
->checkIF((!empty($product->name) and $this->config->systemMode == 'new'), 'name', 'unique', "id != $productID and `program` = $programID")
+ ->checkFlow()
->where('id')->eq($productID)
->exec();
if(dao::isError()) return print(js::error('product#' . $productID . dao::getError(true)));
@@ -828,6 +871,7 @@ class productModel extends model
$this->dao->update(TABLE_PRODUCT)->data($product)
->autoCheck()
+ ->checkFlow()
->where('id')->eq((int)$productID)
->exec();
@@ -973,13 +1017,15 @@ class productModel extends model
* @param int $queryID
* @param int $actionURL
* @param int $branch
+ * @param int $projectID
* @access public
* @return void
*/
- public function buildSearchForm($productID, $products, $queryID, $actionURL, $branch = 0)
+ public function buildSearchForm($productID, $products, $queryID, $actionURL, $branch = 0, $projectID = 0)
{
$productIdList = ($this->app->tab == 'project' and empty($productID)) ? array_keys($products) : $productID;
$branchParam = ($this->app->tab == 'project' and empty($productID)) ? '' : $branch;
+ $projectID = ($this->app->tab == 'project' and empty($projectID)) ? $this->session->project : $projectID;
$this->config->product->search['actionURL'] = $actionURL;
$this->config->product->search['queryID'] = $queryID;
@@ -995,7 +1041,7 @@ class productModel extends model
if($productID)
{
$modules = array();
- $branchList = $this->loadModel('branch')->getPairs($productID, '', $this->session->project);
+ $branchList = $this->loadModel('branch')->getPairs($productID, '', $projectID);
$branchModuleList = $this->tree->getOptionMenu($productID, 'story', 0, array_keys($branchList));
foreach($branchModuleList as $branchID => $branchModules) $modules[] = $branchModules;
}
@@ -1003,7 +1049,7 @@ class productModel extends model
{
$moduleList = array();
$modules = array('/');
- $branchGroup = $this->loadModel('execution')->getBranchByProduct(array_keys($products), $this->session->project, '');
+ $branchGroup = $this->loadModel('execution')->getBranchByProduct(array_keys($products), $projectID, '');
foreach($products as $productID => $productName)
{
if(isset($branchGroup[$productID]))
@@ -1045,6 +1091,31 @@ class productModel extends model
$this->loadModel('search')->setSearchParams($this->config->product->search);
}
+ /**
+ * Build search form for all method of product module.
+ *
+ * @param int $queryID
+ * @param string $actionURL
+ * @access public
+ * @return void
+ */
+ public function buildProductSearchForm($queryID, $actionURL)
+ {
+ $this->config->product->all->search['queryID'] = $queryID;
+ $this->config->product->all->search['actionURL'] = $actionURL;
+
+ $linePairs = $this->getLinePairs();
+ $this->config->product->all->search['params']['line']['values'] = array('' => '') + $linePairs;
+
+ if($this->config->systemMode == 'new')
+ {
+ $programPairs = $this->loadModel('program')->getTopPairs('', 'noclosed');
+ $this->config->product->all->search['params']['program']['values'] = array('' => '') + $programPairs;
+ }
+
+ $this->loadModel('search')->setSearchParams($this->config->product->all->search);
+ }
+
/**
* Get project pairs by product.
*
@@ -1565,13 +1636,13 @@ class productModel extends model
* @access public
* @return array
*/
- public function getStats($orderBy = 'order_asc', $pager = null, $status = 'noclosed', $line = 0, $storyType = 'story', $programID = 0)
+ public function getStats($orderBy = 'order_asc', $pager = null, $status = 'noclosed', $line = 0, $storyType = 'story', $programID = 0, $param = 0)
{
$this->loadModel('report');
$this->loadModel('story');
$this->loadModel('bug');
- $products = $this->getList($programID, $status, $limit = 0, $line);
+ $products = $status == 'bySearch' ? $this->getListBySearch($param) : $this->getList($programID, $status, $limit = 0, $line);
if(empty($products)) return array();
$productKeys = array_keys($products);
@@ -2020,6 +2091,37 @@ class productModel extends model
return true;
}
+ /**
+ * Build operate menu.
+ *
+ * @param object $product
+ * @param string $type
+ * @access public
+ * @return string
+ */
+ public function buildOperateMenu($product, $type = 'view')
+ {
+ $menu = '';
+ $params = "product=$product->id";
+
+ if($type == 'view')
+ {
+ $menu .= "";
+ $menu .= $this->buildFlowMenu('product', $product, $type, 'direct');
+ $menu .= "";
+
+ $menu .= $this->buildMenu('product', 'close', $params, $product, $type, '', '', 'iframe', true, "data-app='product'");
+ $menu .= "";
+ }
+
+ $menu .= $this->buildMenu('product', 'edit', $params, $product, $type);
+
+ if($type == 'view') $menu .= $this->buildMenu('product', 'delete', $params, $product, $type, 'trash', 'hiddenwin');
+ if($type == 'browse' && common::hasPriv('product', 'updateOrder')) $menu .= "";
+
+ return $menu;
+ }
+
/**
* Create the link from module,method,extra,branch.
*
diff --git a/module/product/view/all.html.php b/module/product/view/all.html.php
index 1365dd5449..7b29353309 100644
--- a/module/product/view/all.html.php
+++ b/module/product/view/all.html.php
@@ -17,6 +17,7 @@
{$recTotal}" : '';?>
{$label}" . $recTotalLabel, '', "class='btn btn-link' id='{$key}Tab'");?>
+ product->searchStory;?>
" . $lang->export, '', "class='btn btn-link export'", true, true)?>
@@ -26,16 +27,15 @@
+
diff --git a/module/product/view/view.html.php b/module/product/view/view.html.php
index e05d64fdba..dd670efb83 100644
--- a/module/product/view/view.html.php
+++ b/module/product/view/view.html.php
@@ -158,24 +158,10 @@
diff --git a/module/productplan/model.php b/module/productplan/model.php
index 7eefd49a79..7ec779299b 100644
--- a/module/productplan/model.php
+++ b/module/productplan/model.php
@@ -432,6 +432,7 @@ class productplanModel extends model
->autoCheck()
->batchCheck($this->config->productplan->create->requiredFields, 'notempty')
->checkIF(!$this->post->future && !empty($_POST['begin']) && !empty($_POST['end']), 'end', 'ge', $plan->begin)
+ ->checkFlow()
->exec();
if(!dao::isError())
{
@@ -513,6 +514,7 @@ class productplanModel extends model
->autoCheck()
->batchCheck($this->config->productplan->edit->requiredFields, 'notempty')
->checkIF(!$this->post->future && !empty($_POST['begin']) && !empty($_POST['end']), 'end', 'ge', $plan->begin)
+ ->checkFlow()
->where('id')->eq((int)$planID)
->exec();
if(dao::isError()) return false;
@@ -653,8 +655,6 @@ class productplanModel extends model
if(is_array($plan->{$extendField->field})) $plan->{$extendField->field} = join(',', $plan->{$extendField->field});
$plan->{$extendField->field} = htmlSpecialString($plan->{$extendField->field});
- $message = $this->checkFlowRule($extendField, $plan->{$extendField->field});
- if($message) return print(js::alert($message));
}
$plans[$planID] = $plan;
@@ -697,7 +697,7 @@ class productplanModel extends model
if($change)
{
if($parentID > 0 and !isset($parents[$parentID])) $parents[$parentID] = $parentID;
- $this->dao->update(TABLE_PRODUCTPLAN)->data($plan)->autoCheck()->where('id')->eq($planID)->exec();
+ $this->dao->update(TABLE_PRODUCTPLAN)->data($plan)->autoCheck()->checkFlow()->where('id')->eq($planID)->exec();
if(dao::isError()) return print(js::error(dao::getError()));
$changes[$planID] = $change;
}
@@ -1002,17 +1002,21 @@ class productplanModel extends model
*
* @param object $plan
* @param string $action
+ * @param string $module
* @access public
* @return void
*/
- public static function isClickable($plan, $action)
+ public static function isClickable($plan, $action, $module = 'productplan')
{
$action = strtolower($action);
- $clickable = commonModel::hasPriv('productplan', $action);
+ $clickable = commonModel::hasPriv($module, $action);
if(!$clickable) return false;
switch($action)
{
+ case 'create' :
+ if($plan->parent > 0 or strpos('done,closed', $plan->status) !== false) return false;
+ break;
case 'start' :
if($plan->status != 'wait' or $plan->parent < 0) return false;
break;
@@ -1025,8 +1029,100 @@ class productplanModel extends model
case 'activate' :
if($plan->status == 'wait' or $plan->status == 'doing' or $plan->parent < 0) return false;
break;
+ case 'delete' :
+ if($plan->parent < 0) return false;
+ break;
+ }
+
+ if($module == 'execution' && $action == 'create')
+ {
+ if($plan->parent < 0 || $plan->expired || in_array($plan->status, array('done', 'closed'))) return false;
+
+ $product = $this->loadModel('product')->getById($plan->product);
+ $branchList = $this->loadModel('branch')->getList($plan->product, 0, 'all');
+
+ $branchStatusList = array();
+ foreach($branchList as $productBranch) $branchStatusList[$productBranch->id] = $productBranch->status;
+
+ if($product->type != 'normal')
+ {
+ $branchStatus = isset($branchStatusList[$plan->branch]) ? $branchStatusList[$plan->branch] : '';
+ if($branchStatus == 'closed') return false;
+ }
}
return true;
}
+
+ /**
+ * Build operate menu.
+ *
+ * @param object $plan
+ * @param string $type
+ * @access public
+ * @return string
+ */
+ public function buildOperateMenu($plan, $type = 'view')
+ {
+ $params = "planID=$plan->id";
+
+ $menu = '';
+ $menu .= $this->buildMenu('productplan', 'start', $params, $plan, $type, 'play', 'hiddenwin', '', false, '', $this->lang->productplan->startAB);
+ $menu .= $this->buildMenu('productplan', 'finish', $params, $plan, $type, 'checked', 'hiddenwin', '', false, '', $this->lang->productplan->finishAB);
+ $menu .= $this->buildMenu('productplan', 'close', $params, $plan, $type, 'off', 'hiddenwin', 'iframe', true, '', $this->lang->productplan->closeAB);
+
+ if($type == 'view') $menu .= $this->buildMenu('productplan', 'activate', $params, $plan, $type, 'magic', 'hiddenwin', '', false, '', $this->lang->productplan->activateAB);
+
+ if($type == 'browse')
+ {
+ if($this->isClickable($plan, 'create', 'execution'))
+ {
+ $executionLink = $this->config->systemMode == 'new' ? '#projects' : helper::createLink('execution', 'create', "projectID=0&executionID=0©ExecutionID=0&plan=$plan->id&confirm=no&productID=$plan->product");
+ if($this->config->systemMode == 'new')
+ {
+ $menu .= html::a($executionLink, '
', '', "data-toggle='modal' data-id='$plan->id' onclick='getPlanID(this, $plan->branch)' class='btn' title='{$this->lang->productplan->createExecution}'");
+ }
+ else
+ {
+ $menu .= html::a($executionLink, '
', '', "class='btn' title='{$this->lang->productplan->createExecution}'");
+ }
+ }
+ else
+ {
+ $menu .= "
";
+ }
+
+ if(common::hasPriv('productplan', 'linkStory', $plan) and $plan->parent >= 0)
+ {
+ $menu .= $this->buildMenu('productplan', 'view', "{$params}&type=story&orderBy=id_desc&link=true", $plan, $type, 'link', '', '', '', '', $this->lang->productplan->linkStory);
+ }
+ else
+ {
+ $menu .= "
";
+ }
+
+ if(common::hasPriv('productplan', 'linkBug', $plan) and $plan->parent >= 0)
+ {
+ $menu .= $this->buildMenu('productplan', 'view', "{$params}&type=bug&orderBy=id_desc&link=true", $plan, $type, 'bug', '', '', '', '', $this->lang->productplan->linkBug);
+ }
+ else
+ {
+ $menu .= "
";
+ }
+
+ $menu .= $this->buildMenu('productplan', 'edit', $params, $plan, $type);
+ }
+
+ $menu .= $this->buildMenu('productplan', 'create', "product={$plan->product}&branch={$plan->branch}&parent={$plan->id}", $plan, $type, 'split', '', '', '', '', $this->lang->productplan->children);
+
+ if($type == 'browse') $menu .= $this->buildMenu('productplan', 'delete', "{$params}&confirm=no", $plan, $type, 'trash', 'hiddenwin', '', '', $this->lang->productplan->delete);
+
+ if($type == 'view')
+ {
+ if(common::hasPriv('productplan', 'edit', $plan)) $menu .= html::a(helper::createLink('productplan', 'edit', $params), "
" . $this->lang->edit, '', "class='btn btn-link' title='{$this->lang->edit}'");
+ if($plan->parent >= 0 && common::hasPriv('productplan', 'delete', $plan)) $menu .= html::a(helper::createLink('productplan', 'delete', $params), "
" . $this->lang->delete, '', "class='btn btn-link' title='{$this->lang->delete}' target='hiddenwin'");
+ }
+
+ return $menu;
+ }
}
diff --git a/module/productplan/view/browsebylist.html.php b/module/productplan/view/browsebylist.html.php
index 776abaaf9a..b4bbe598f4 100644
--- a/module/productplan/view/browsebylist.html.php
+++ b/module/productplan/view/browsebylist.html.php
@@ -144,67 +144,7 @@
" . $this->loadModel('flow')->getFieldValue($extendField, $plan) . "";?>
-
- parent >= 0 )
- {
- $attr = "target='hiddenwin'";
- common::printIcon('productplan', 'start', "planID=$plan->id", $plan, 'list', 'play', '', '', false, $attr);
- common::printIcon('productplan', 'finish', "planID=$plan->id", $plan, 'list', 'checked', '', '', false, $attr);
- common::printIcon('productplan', 'close', "planID=$plan->id", $plan, 'list', 'off', '', 'iframe', true);
- }
-
- $attr = $plan->expired ? "disabled='disabled'" : '';
- $class = '';
- if($product->type != 'normal')
- {
- $branchStatus = isset($branchStatusList[$plan->branch]) ? $branchStatusList[$plan->branch] : '';
- if($branchStatus == 'closed') $class = 'disabled';
- }
- if(common::hasPriv('execution', 'create', $plan) and $plan->parent >= 0)
- {
- $disabled = '';
- $executionLink = $config->systemMode == 'new' ? '#projects' : $this->createLink('execution', 'create', "projectID=0&executionID=0©ExecutionID=0&plan=$plan->id&confirm=no&productID=$productID");
-
- if(in_array($plan->status, array('done', 'closed'))) $disabled = 'disabled';
-
- if($config->systemMode == 'new')
- {
- echo html::a($executionLink, '', '', "data-toggle='modal' data-id='$plan->id' onclick='getPlanID(this, $plan->branch)' class='btn {$disabled} {$class}' title='{$lang->productplan->createExecution}' $attr");
- }
- else
- {
- echo html::a($executionLink, '', '', "class='btn {$disabled}' title='{$lang->productplan->createExecution}' $attr");
- }
- }
- if(common::hasPriv('productplan', 'linkStory', $plan) and $plan->parent >= 0) echo html::a(inlink('view', "planID=$plan->id&type=story&orderBy=id_desc&link=true"), '', '', "class='btn' title='{$lang->productplan->linkStory}'");
- if(common::hasPriv('productplan', 'linkBug', $plan) and $plan->parent >= 0) echo html::a(inlink('view', "planID=$plan->id&type=bug&orderBy=id_desc&link=true"), '', '', "class='btn' title='{$lang->productplan->linkBug}'");
- common::printIcon('productplan', 'edit', "planID=$plan->id", $plan, 'list');
- if(common::hasPriv('productplan', 'create', $plan))
- {
- if($plan->parent > 0 or strpos('done,closed', $plan->status) !== false)
- {
- echo " ";
- }
- else
- {
- echo html::a($this->createLink('productplan', 'create', "product=$productID&branch=$branch&parent={$plan->id}"), "", '', "class='btn {$class}' title='{$this->lang->productplan->children}'");
- }
- }
-
- if(common::hasPriv('productplan', 'delete', $plan))
- {
- $deleteURL = '###';
- $disabled = 'disabled';
- if($plan->parent >= 0)
- {
- $deleteURL = $this->createLink('productplan', 'delete', "planID=$plan->id&confirm=no");
- $disabled = '';
- }
- echo html::a($deleteURL, '', 'hiddenwin', "class='btn {$disabled}' title='{$lang->productplan->delete}'");
- }
- ?>
- |
+
productplan->buildOperateMenu($plan, 'browse'); ?> |
diff --git a/module/productplan/view/view.html.php b/module/productplan/view/view.html.php
index 0ffd20a298..76002e70b1 100644
--- a/module/productplan/view/view.html.php
+++ b/module/productplan/view/view.html.php
@@ -38,31 +38,7 @@
- deleted && !isonlybody())
- {
- echo $this->buildOperateMenu($plan, 'view');
-
- if($plan->parent >= 0)
- {
- $attr = "target='hiddenwin'";
- $class = $plan->status == 'wait' ? '' : 'disabled';
-
- common::printLink('productplan', 'start', "planID=$plan->id", "{$lang->productplan->startAB}", '', "class='btn btn-link {$class}'{$attr} title='{$lang->productplan->start}'", '', false, $plan);
- $class = $plan->status == 'doing' ? '' : 'disabled';
- common::printLink('productplan', 'finish', "planID=$plan->id", "{$lang->productplan->finishAB}", '', "class='btn btn-link {$class}' target='hiddenwin' title='{$lang->productplan->finish}'", '', false, $plan);
- $class = $plan->status !== 'closed' ? 'iframe' : 'disabled';
- common::printLink('productplan', 'close', "planID=$plan->id", "{$lang->productplan->closeAB}", '', "class='btn btn-link {$class}' title='{$lang->productplan->close}'", '', true, $plan);
- $class = in_array($plan->status, array('closed', 'done')) ? '' : 'disabled';
- common::printLink('productplan', 'activate', "planID=$plan->id", "{$lang->productplan->activateAB}", '', "class='btn btn-link {$class}' target='hiddenwin' title='{$lang->productplan->activate}'", '', false, $plan);
- }
-
- $class = (isset($branchStatus) and $branchStatus == 'closed') ? 'disabled' : '';
- if(common::hasPriv('productplan', 'create', $plan) and $plan->parent <= 0 and strpos('wait,doing', $plan->status) !== false) echo html::a($this->createLink('productplan', 'create', "product={$plan->product}&branch={$plan->branch}&parent={$plan->id}"), " " . $this->lang->productplan->children , '', "class='btn btn-link {$class}' title='{$this->lang->productplan->children}'");
- if(common::hasPriv('productplan', 'edit', $plan)) echo html::a($this->createLink('productplan', 'edit', "planID=$plan->id"), " " . $this->lang->edit, '', "class='btn btn-link' title='{$this->lang->edit}'");
- if(common::hasPriv('productplan', 'delete', $plan) and $plan->parent >= 0) echo html::a($this->createLink('productplan', 'delete', "planID=$plan->id"), " " . $this->lang->delete, '', "class='btn btn-link' title='{$this->lang->delete}' target='hiddenwin'");
- }
- ?>
+ deleted && !isonlybody()) echo $this->productplan->buildOperateMenu($plan, 'view'); ?>
diff --git a/module/program/config.php b/module/program/config.php
index d93159b907..15afcb8278 100644
--- a/module/program/config.php
+++ b/module/program/config.php
@@ -22,3 +22,31 @@ $config->program->sortFields = new stdclass();
$config->program->sortFields->id = 'id';
$config->program->sortFields->begin = 'begin';
$config->program->sortFields->end = 'end';
+
+global $lang;
+$config->program->search['module'] = 'program';
+$config->program->search['fields']['name'] = $lang->program->name;
+$config->program->search['fields']['status'] = $lang->program->status;
+$config->program->search['fields']['desc'] = $lang->program->desc;
+$config->program->search['fields']['PM'] = $lang->program->PM;
+$config->program->search['fields']['openedDate'] = $lang->program->openedDate;
+$config->program->search['fields']['begin'] = $lang->program->begin;
+$config->program->search['fields']['end'] = $lang->program->end;
+$config->program->search['fields']['openedBy'] = $lang->program->openedBy;
+$config->program->search['fields']['lastEditedDate'] = $lang->program->lastEditedDate;
+$config->program->search['fields']['realBegan'] = $lang->program->realBegin;
+$config->program->search['fields']['realEnd'] = $lang->program->realEnd;
+$config->program->search['fields']['closedDate'] = $lang->program->closedDate;
+
+$config->program->search['params']['name'] = array('operator' => 'include', 'control' => 'input', 'values' => '');
+$config->program->search['params']['status'] = array('operator' => '=', 'control' => 'select', 'values' => array('' => '') + $lang->program->statusList);
+$config->program->search['params']['desc'] = array('operator' => 'include', 'control' => 'input', 'values' => '');
+$config->program->search['params']['PM'] = array('operator' => '=', 'control' => 'select', 'values' => 'users');
+$config->program->search['params']['openedDate'] = array('operator' => '=', 'control' => 'input', 'values' => '', 'class' => 'date');
+$config->program->search['params']['begin'] = array('operator' => '=', 'control' => 'input', 'values' => '', 'class' => 'date');
+$config->program->search['params']['end'] = array('operator' => '=', 'control' => 'input', 'values' => '', 'class' => 'date');
+$config->program->search['params']['openedBy'] = array('operator' => '=', 'control' => 'select', 'values' => 'users');
+$config->program->search['params']['lastEditedDate'] = array('operator' => '=', 'control' => 'input', 'values' => '', 'class' => 'date');
+$config->program->search['params']['realBegan'] = array('operator' => '=', 'control' => 'input', 'values' => '', 'class' => 'date');
+$config->program->search['params']['realEnd'] = array('operator' => '=', 'control' => 'input', 'values' => '', 'class' => 'date');
+$config->program->search['params']['closedDate'] = array('operator' => '=', 'control' => 'input', 'values' => '', 'class' => 'date');
diff --git a/module/program/control.php b/module/program/control.php
index 54e34c9fa2..3c8d716d36 100644
--- a/module/program/control.php
+++ b/module/program/control.php
@@ -26,13 +26,16 @@ class program extends control
* @access public
* @return void
*/
- public function browse($status = 'all', $orderBy = 'order_asc')
+ public function browse($status = 'all', $orderBy = 'order_asc', $recTotal = 0, $recPerPage = 10, $pageID = 1, $param = 0)
{
if(common::hasPriv('program', 'create')) $this->lang->pageActions = html::a($this->createLink('program', 'create'), "
" . $this->lang->program->create, '', "class='btn btn-primary create-program-btn'");
$this->session->set('programList', $this->app->getURI(true), 'program');
$this->session->set('projectList', $this->app->getURI(true), 'program');
+ $this->app->loadClass('pager', $static = true);
+ $pager = new pager($recTotal, $recPerPage, $pageID);
+
$programType = $this->cookie->programType ? $this->cookie->programType : 'bylist';
if($programType === 'bygrid')
@@ -41,7 +44,18 @@ class program extends control
}
else
{
- $programs = $this->program->getList($status, $orderBy, null, true);
+ if(strtolower($status) == 'bysearch')
+ {
+ $queryID = (int)$param;
+ $programs = $this->program->getListBySearch($orderBy, $pager, $queryID);
+ }
+ else
+ {
+ $topPrograms = $this->program->getList($status, $orderBy, $pager, 'top');
+ $programIds = array();
+ foreach($topPrograms as $programID => $program) $programIds[] = $programID;
+ $programs = $this->program->getList($status, $orderBy, null, 'child', $programIds);
+ }
}
/* Get PM id list. */
@@ -52,12 +66,19 @@ class program extends control
}
$PMList = $this->loadModel('user')->getListByAccounts($accounts, 'account');
+ /* Build the search form. */
+ $actionURL = $this->createLink('program', 'browse', "status=bySearch&orderBy={$orderBy}&recTotal={$recTotal}&recPerPage={$recPerPage}&pageID={$pageID}¶m=myQueryID");
+ $this->config->program->search['onMenuBar'] = 'yes';
+ $this->config->program->search['actionURL'] = $actionURL;
+ $this->loadModel('search')->setSearchParams($this->config->program->search);
+
$this->view->title = $this->lang->program->browse;
$this->view->position[] = $this->lang->program->browse;
$this->view->programs = $programs;
$this->view->status = $status;
$this->view->orderBy = $orderBy;
+ $this->view->pager = $pager;
$this->view->users = $this->user->getPairs('noletter');
$this->view->userIdPairs = $this->user->getPairs('noletter|showid');
$this->view->usersAvatar = $this->user->getAvatarPairs('');
diff --git a/module/program/js/browse.js b/module/program/js/browse.js
index e84d284444..dc14dca89b 100644
--- a/module/program/js/browse.js
+++ b/module/program/js/browse.js
@@ -43,6 +43,13 @@ $(function()
$('#programForm').removeClass('has-row-checked');
}
});
+
+ var myPager = $('.pager').data('zui.pager');
+ if(status != 'bySearch' && myPager)
+ {
+ myPager.lang.pageSize = pageSize;
+ myPager.set();
+ }
});
function showEditCheckbox(show)
diff --git a/module/program/lang/en.php b/module/program/lang/en.php
index 31568de285..754323b538 100644
--- a/module/program/lang/en.php
+++ b/module/program/lang/en.php
@@ -1,33 +1,36 @@
program->id = 'ID';
-$lang->program->name = 'Name';
-$lang->program->template = 'Template';
-$lang->program->category = 'Type';
-$lang->program->desc = 'Description';
-$lang->program->status = 'Status';
-$lang->program->PM = 'Manager';
-$lang->program->budget = 'Budget';
-$lang->program->budgetUnit = 'Budget Unit';
-$lang->program->begin = 'Begin';
-$lang->program->end = 'End';
-$lang->program->stage = 'Stage';
-$lang->program->type = 'Type';
-$lang->program->pri = 'Priority';
-$lang->program->parent = 'Parent';
-$lang->program->exchangeRate = 'Exchange Rate';
-$lang->program->openedBy = 'OpenedBy';
-$lang->program->openedDate = 'OpenedDate';
-$lang->program->closedBy = 'ClosedBy';
-$lang->program->closedDate = 'ClosedDate';
-$lang->program->canceledBy = 'CanceledBy';
-$lang->program->canceledDate = 'CanceledDate';
-$lang->program->team = 'Team';
-$lang->program->order = 'Rank';
-$lang->program->days = 'Days';
-$lang->program->acl = 'Access Control';
-$lang->program->whitelist = 'WhiteList';
-$lang->program->deleted = 'Deleted';
+$lang->program->id = 'ID';
+$lang->program->name = 'Name';
+$lang->program->template = 'Template';
+$lang->program->category = 'Type';
+$lang->program->desc = 'Description';
+$lang->program->status = 'Status';
+$lang->program->PM = 'Manager';
+$lang->program->budget = 'Budget';
+$lang->program->budgetUnit = 'Budget Unit';
+$lang->program->begin = 'Begin';
+$lang->program->end = 'End';
+$lang->program->realBegin = 'Actual Begin';
+$lang->program->realEnd = 'Actual End';
+$lang->program->stage = 'Stage';
+$lang->program->type = 'Type';
+$lang->program->pri = 'Priority';
+$lang->program->parent = 'Parent';
+$lang->program->exchangeRate = 'Exchange Rate';
+$lang->program->openedBy = 'OpenedBy';
+$lang->program->openedDate = 'OpenedDate';
+$lang->program->closedBy = 'ClosedBy';
+$lang->program->closedDate = 'ClosedDate';
+$lang->program->canceledBy = 'CanceledBy';
+$lang->program->canceledDate = 'CanceledDate';
+$lang->program->lastEditedDate = 'LastEditedDate';
+$lang->program->team = 'Team';
+$lang->program->order = 'Rank';
+$lang->program->days = 'Days';
+$lang->program->acl = 'Access Control';
+$lang->program->whitelist = 'WhiteList';
+$lang->program->deleted = 'Deleted';
/* Actions. */
$lang->program->common = 'Program';
@@ -71,6 +74,7 @@ $lang->program->moreProgram = 'More program';
$lang->program->stakeholderType = 'Stakeholder type';
$lang->program->parentBudget = 'Parent program surplus budget:';
$lang->program->isStakeholderKey = 'Key stakeholder';
+$lang->program->pageSize = '
{recPerPage} Top Programs per page';
$lang->program->stakeholderTypeList['inside'] = 'Inside';
$lang->program->stakeholderTypeList['outside'] = 'Outside';
diff --git a/module/program/lang/zh-cn.php b/module/program/lang/zh-cn.php
index c38753ce88..f4279b673c 100644
--- a/module/program/lang/zh-cn.php
+++ b/module/program/lang/zh-cn.php
@@ -1,33 +1,36 @@
program->id = '编号';
-$lang->program->name = '项目集名称';
-$lang->program->template = '项目集模板';
-$lang->program->category = '项目集类型';
-$lang->program->desc = '项目集描述';
-$lang->program->status = '状态';
-$lang->program->PM = '负责人';
-$lang->program->budget = '预算';
-$lang->program->budgetUnit = '预算单位';
-$lang->program->begin = '计划开始';
-$lang->program->end = '计划完成';
-$lang->program->stage = '阶段';
-$lang->program->type = '类型';
-$lang->program->pri = '优先级';
-$lang->program->parent = '父项目集';
-$lang->program->exchangeRate = '汇率';
-$lang->program->openedBy = '由谁创建';
-$lang->program->openedDate = '创建日期';
-$lang->program->closedBy = '由谁关闭';
-$lang->program->closedDate = '关闭日期';
-$lang->program->canceledBy = '由谁取消';
-$lang->program->canceledDate = '取消日期';
-$lang->program->team = '团队';
-$lang->program->order = '排序';
-$lang->program->days = '可用工作日';
-$lang->program->acl = '访问控制';
-$lang->program->whitelist = '白名单';
-$lang->program->deleted = '已删除';
+$lang->program->id = '编号';
+$lang->program->name = '项目集名称';
+$lang->program->template = '项目集模板';
+$lang->program->category = '项目集类型';
+$lang->program->desc = '项目集描述';
+$lang->program->status = '状态';
+$lang->program->PM = '负责人';
+$lang->program->budget = '预算';
+$lang->program->budgetUnit = '预算单位';
+$lang->program->begin = '计划开始';
+$lang->program->end = '计划完成';
+$lang->program->realBegin = '实际开始';
+$lang->program->realEnd = '实际完成';
+$lang->program->stage = '阶段';
+$lang->program->type = '类型';
+$lang->program->pri = '优先级';
+$lang->program->parent = '父项目集';
+$lang->program->exchangeRate = '汇率';
+$lang->program->openedBy = '由谁创建';
+$lang->program->openedDate = '创建日期';
+$lang->program->closedBy = '由谁关闭';
+$lang->program->closedDate = '关闭日期';
+$lang->program->canceledBy = '由谁取消';
+$lang->program->canceledDate = '取消日期';
+$lang->program->lastEditedDate = '最后编辑';
+$lang->program->team = '团队';
+$lang->program->order = '排序';
+$lang->program->days = '可用工作日';
+$lang->program->acl = '访问控制';
+$lang->program->whitelist = '白名单';
+$lang->program->deleted = '已删除';
/* Actions. */
$lang->program->common = '项目集';
@@ -71,6 +74,7 @@ $lang->program->moreProgram = '更多项目集';
$lang->program->stakeholderType = '干系人类型';
$lang->program->parentBudget = '所属项目集剩余预算:';
$lang->program->isStakeholderKey = '关键干系人';
+$lang->program->pageSize = '每页
{recPerPage} 项顶级项目集';
$lang->program->stakeholderTypeList['inside'] = '内部';
$lang->program->stakeholderTypeList['outside'] = '外部';
diff --git a/module/program/model.php b/module/program/model.php
index f1868c9348..01c47c2219 100644
--- a/module/program/model.php
+++ b/module/program/model.php
@@ -130,11 +130,22 @@ class programModel extends model
* @param string $status
* @param string $orderBy
* @param object $pager
+ * @param string $type top|child
+ * @param mixed $idList
* @access public
* @return array
*/
- public function getList($status = 'all', $orderBy = 'id_asc', $pager = NULL)
+ public function getList($status = 'all', $orderBy = 'id_asc', $pager = NULL, $type = '', $idList = '')
{
+ $projectIdList = array();
+ if($type === 'child')
+ {
+ foreach($idList as $topID)
+ {
+ $projectIdList += $this->dao->select('id')->from(TABLE_PROGRAM)->Where('path')->like(",$topID,%")->fetchPairs('id');
+ }
+ }
+
return $this->dao->select('*')->from(TABLE_PROGRAM)
->where('deleted')->eq(0)
->andWhere('vision')->eq($this->config->vision)
@@ -146,6 +157,54 @@ class programModel extends model
->markRight(2)
->beginIF($status != 'all')->andWhere('status')->eq($status)->fi()
->beginIF(!$this->cookie->showClosed)->andWhere('status')->ne('closed')->fi()
+ ->beginIF($type === 'top')->andWhere('parent')->eq(0)->fi()
+ ->beginIF(!empty($projectIdList))->andWhere('id')->in($projectIdList)->fi()
+ ->orderBy($orderBy)
+ ->page($pager)
+ ->fetchAll('id');
+ }
+
+ /**
+ * Get program list by search.
+ *
+ * @param string $orderBy
+ * @param object $pager
+ * @param int $queryID
+ * @access public
+ * @return void
+ */
+ public function getListBySearch($orderBy = 'id_asc', $pager = NULL, $queryID = 0)
+ {
+ if($queryID)
+ {
+ $query = $this->loadModel('search')->getQuery($queryID);
+ if($query)
+ {
+ $this->session->set('programQuery', $query->sql);
+ $this->session->set('programForm', $query->form);
+ }
+ else
+ {
+ $this->session->set('programQuery', ' 1 = 1');
+ }
+ }
+ else
+ {
+ if($this->session->programQuery == false) $this->session->set('programQuery', ' 1 = 1');
+ }
+ $query = $this->session->programQuery;
+
+ return $this->dao->select('*')->from(TABLE_PROGRAM)
+ ->where('deleted')->eq(0)
+ ->andWhere('vision')->eq($this->config->vision)
+ ->andWhere('((type')->eq('program')
+ ->beginIF(!$this->app->user->admin and $this->app->rawMethod != 'browse')->andWhere('id')->in($this->app->user->view->programs)->fi()
+ ->markRight(1)
+ ->orWhere('(type')->eq('project')
+ ->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->projects)->fi()
+ ->markRight(2)
+ ->beginIF(!$this->cookie->showClosed)->andWhere('status')->ne('closed')->fi()
+ ->beginIF($query)->andWhere($query)->fi()
->orderBy($orderBy)
->page($pager)
->fetchAll('id');
@@ -240,15 +299,15 @@ class programModel extends model
foreach($products as $product)
{
$product->plans = zget($plans, $product->id, array());
-
+
/* Convert predefined HTML entities to characters. */
!empty($product->plans) && array_map(function($planVal)
{
return $planVal->title = htmlspecialchars_decode($planVal->title, ENT_QUOTES);
- },
+ },
$product->plans);
$product->name = htmlspecialchars_decode($product->name, ENT_QUOTES);
-
+
$product->releases = zget($releases, $product->id, array());
$projects = zget($projectGroup, $product->id, array());
foreach($projects as $project)
@@ -657,6 +716,7 @@ class programModel extends model
->checkIF($program->end != '', 'end', 'date')
->checkIF($program->end != '', 'end', 'gt', $program->begin)
->checkIF(!empty($program->name), 'name', 'unique', "`type`='program' and `parent` = $program->parent")
+ ->checkFlow()
->exec();
if(!dao::isError())
@@ -756,6 +816,7 @@ class programModel extends model
->checkIF($program->end != '', 'end', 'date')
->checkIF($program->end != '', 'end', 'gt', $program->begin)
->checkIF(!empty($program->name), 'name', 'unique', "id!=$programID and `type`='program' and `parent` = $program->parent")
+ ->checkFlow()
->where('id')->eq($programID)
->limit(1)
->exec();
diff --git a/module/program/view/browse.html.php b/module/program/view/browse.html.php
index 6b13491250..abbf959b56 100644
--- a/module/program/view/browse.html.php
+++ b/module/program/view/browse.html.php
@@ -15,6 +15,7 @@
edit);?>
selectAll);?>
+program->pageSize);?>
@@ -27,6 +28,7 @@
$lang->program->showClosed), '', $this->cookie->showClosed ? 'checked=checked' : '');?>
$lang->project->edit), '', $this->cookie->editProject ? 'checked=checked' : '');?>
+
user->search;?>
' . $lang->project->create, '', 'class="btn btn-secondary" data-toggle="modal" data-target="#guideDialog"');?>
@@ -35,6 +37,7 @@
+
program->noProgram;?>
@@ -43,6 +46,7 @@
+
model == 'kanban' ? 'disabled' : '';?>
- id", $program, 'list', 'edit', '', 'iframe', true);?>
- id", $program, 'list', 'group', '', "$class", '', 'data-app="project"');?>
- id", $program, 'list', 'lock', '', "$class", '', 'data-app="project"');?>
+ id", $program, 'list', 'edit', '', 'iframe', true);?>
+ id", $program, 'list', 'group', '', "$class", '', 'data-app="project"');?>
+ id", $program, 'list', 'lock', '', "$class", '', 'data-app="project"');?>
@@ -138,6 +138,7 @@
+