diff --git a/api/v1/entries/bug.php b/api/v1/entries/bug.php index 2e79f9889a..46deccc7d1 100644 --- a/api/v1/entries/bug.php +++ b/api/v1/entries/bug.php @@ -91,6 +91,11 @@ class bugEntry extends entry $bug->actions = $this->loadModel('action')->processActionForAPI($data->data->actions, $data->data->users, $this->lang->bug); + $preAndNext = $data->data->preAndNext; + $bug->preAndNext = array(); + $bug->preAndNext['pre'] = $preAndNext->pre ? $preAndNext->pre->id : ''; + $bug->preAndNext['next'] = $preAndNext->next ? $preAndNext->next->id : ''; + $this->send(200, $this->format($bug, 'activatedDate:time,openedDate:time,assignedDate:time,resolvedDate:time,closedDate:time,lastEditedDate:time,deadline:date,deleted:bool')); } diff --git a/api/v1/entries/doc.php b/api/v1/entries/doc.php index 7be2c0d40b..6d8e2e57ed 100644 --- a/api/v1/entries/doc.php +++ b/api/v1/entries/doc.php @@ -44,6 +44,11 @@ class docEntry extends entry $doc->addedBy = zget($usersWithAvatar, $doc->addedBy); } + $preAndNext = $data->data->preAndNext; + $doc->preAndNext = array(); + $doc->preAndNext['pre'] = $preAndNext->pre ? $preAndNext->pre->id : ''; + $doc->preAndNext['next'] = $preAndNext->next ? $preAndNext->next->id : ''; + $this->send(200, $this->format($doc, 'addedDate:time,assignedDate:date,editedDate:time')); } diff --git a/api/v1/entries/ping.php b/api/v1/entries/ping.php new file mode 100644 index 0000000000..1b27d54262 --- /dev/null +++ b/api/v1/entries/ping.php @@ -0,0 +1,24 @@ + + * @package entries + * @version 1 + * @link http://www.zentao.net + */ +class pingEntry extends entry +{ + /** + * GET method. + * + * @access public + * @return void + */ + public function get() + { + $this->send(200, array('token' => session_id(), 'tokenLife' => ini_get('session.gc_maxlifetime'))); + } +} diff --git a/api/v1/entries/story.php b/api/v1/entries/story.php index 87b51ba6aa..f2f04d0723 100644 --- a/api/v1/entries/story.php +++ b/api/v1/entries/story.php @@ -105,6 +105,11 @@ class storyEntry extends Entry $story->actions = $this->loadModel('action')->processActionForAPI($data->data->actions, $data->data->users, $this->lang->story); + $preAndNext = $data->data->preAndNext; + $story->preAndNext = array(); + $story->preAndNext['pre'] = $preAndNext->pre ? $preAndNext->pre->id : ''; + $story->preAndNext['next'] = $preAndNext->next ? $preAndNext->next->id : ''; + $this->send(200, $this->format($story, 'openedDate:time,assignedDate:time,reviewedDate:time,lastEditedDate:time,closedDate:time')); } diff --git a/api/v1/entries/task.php b/api/v1/entries/task.php index 9bc6433089..2728fc895a 100644 --- a/api/v1/entries/task.php +++ b/api/v1/entries/task.php @@ -85,6 +85,9 @@ class taskEntry extends Entry $user = zget($usersWithAvatar, $account, ''); $team->realname = $user ? $user->realname : $account; $team->avatar = $user ? $user->avatar : ''; + $team->estimate = round($team->estimate, 1); + $team->consumed = round($team->consumed, 1); + $team->left = round($team->left, 1); $allHours = $team->consumed + $team->left; $team->progress = empty($allHours) ? 0 : round($team->consumed / $allHours * 100, 1); @@ -96,6 +99,11 @@ class taskEntry extends Entry $task->actions = $this->loadModel('action')->processActionForAPI($data->data->actions, $data->data->users, $this->lang->task); + $preAndNext = $data->data->preAndNext; + $task->preAndNext = array(); + $task->preAndNext['pre'] = $preAndNext->pre ? $preAndNext->pre->id : ''; + $task->preAndNext['next'] = $preAndNext->next ? $preAndNext->next->id : ''; + $this->send(200, $this->format($task, 'openedDate:time,assignedDate:time,realStarted:time,finishedDate:time,canceledDate:time,closedDate:time,lastEditedDate:time,deleted:bool')); } diff --git a/config/routes.php b/config/routes.php index 089e444419..a4b9b92b46 100644 --- a/config/routes.php +++ b/config/routes.php @@ -6,6 +6,7 @@ $routes = array(); $routes['/tokens'] = 'tokens'; $routes['/langs'] = 'langs'; +$routes['/ping'] = 'ping'; $routes['/comments'] = 'comments'; $routes['/tabs/:module'] = 'tabs'; diff --git a/config/zentaopms.php b/config/zentaopms.php index 47748473ca..4f29e4d7d0 100644 --- a/config/zentaopms.php +++ b/config/zentaopms.php @@ -170,6 +170,7 @@ define('TABLE_USERTPL', '`' . $config->db->prefix . 'usertpl`'); define('TABLE_PRODUCT', '`' . $config->db->prefix . 'product`'); define('TABLE_BRANCH', '`' . $config->db->prefix . 'branch`'); define('TABLE_EXPECT', '`' . $config->db->prefix . 'expect`'); +define('TABLE_STAGE', '`' . $config->db->prefix . 'stage`'); define('TABLE_STAKEHOLDER', '`' . $config->db->prefix . 'stakeholder`'); define('TABLE_STORY', '`' . $config->db->prefix . 'story`'); define('TABLE_STORYSPEC', '`' . $config->db->prefix . 'storyspec`'); @@ -195,6 +196,8 @@ define('TABLE_BURN', '`' . $config->db->prefix . 'burn`'); define('TABLE_BUILD', '`' . $config->db->prefix . 'build`'); define('TABLE_ACL', '`' . $config->db->prefix . 'acl`'); +define('TABLE_DESIGN', '`' . $config->db->prefix . 'design`'); +define('TABLE_DESIGNSPEC', '`' . $config->db->prefix . 'designspec`'); define('TABLE_DOCLIB', '`' . $config->db->prefix . 'doclib`'); define('TABLE_DOC', '`' . $config->db->prefix . 'doc`'); define('TABLE_API', '`' . $config->db->prefix . 'api`'); @@ -206,6 +209,7 @@ define('TABLE_API_LIB_RELEASE', '`' . $config->db->prefix . 'api_lib_release`'); define('TABLE_MODULE', '`' . $config->db->prefix . 'module`'); define('TABLE_ACTION', '`' . $config->db->prefix . 'action`'); define('TABLE_FILE', '`' . $config->db->prefix . 'file`'); +define('TABLE_HOLIDAY', '`' . $config->db->prefix . 'holiday`'); define('TABLE_HISTORY', '`' . $config->db->prefix . 'history`'); define('TABLE_EXTENSION', '`' . $config->db->prefix . 'extension`'); define('TABLE_CRON', '`' . $config->db->prefix . 'cron`'); @@ -216,6 +220,7 @@ define('TABLE_SUITECASE', '`' . $config->db->prefix . 'suitecase`'); define('TABLE_TESTREPORT', '`' . $config->db->prefix . 'testreport`'); define('TABLE_ENTRY', '`' . $config->db->prefix . 'entry`'); +define('TABLE_WEEKLYREPORT', '`' . $config->db->prefix . 'weeklyreport`'); define('TABLE_WEBHOOK', '`' . $config->db->prefix . 'webhook`'); define('TABLE_LOG', '`' . $config->db->prefix . 'log`'); define('TABLE_SCORE', '`' . $config->db->prefix . 'score`'); @@ -226,53 +231,57 @@ define('TABLE_JOB', '`' . $config->db->prefix . 'job`'); define('TABLE_COMPILE', '`' . $config->db->prefix . 'compile`'); define('TABLE_MR', '`' . $config->db->prefix . 'mr`'); -define('TABLE_REPO', '`' . $config->db->prefix . 'repo`'); -define('TABLE_RELATION', '`' . $config->db->prefix . 'relation`'); -define('TABLE_REPOHISTORY', '`' . $config->db->prefix . 'repohistory`'); -define('TABLE_REPOFILES', '`' . $config->db->prefix . 'repofiles`'); -define('TABLE_REPOBRANCH', '`' . $config->db->prefix . 'repobranch`'); +define('TABLE_REPO', '`' . $config->db->prefix . 'repo`'); +define('TABLE_RELATION', '`' . $config->db->prefix . 'relation`'); +define('TABLE_REPOHISTORY', '`' . $config->db->prefix . 'repohistory`'); +define('TABLE_REPOFILES', '`' . $config->db->prefix . 'repofiles`'); +define('TABLE_REPOBRANCH', '`' . $config->db->prefix . 'repobranch`'); +define('TABLE_KANBANLANE', '`' . $config->db->prefix . 'kanbanlane`'); +define('TABLE_KANBANCOLUMN', '`' . $config->db->prefix . 'kanbancolumn`'); if(!defined('TABLE_LANG')) define('TABLE_LANG', '`' . $config->db->prefix . 'lang`'); if(!defined('TABLE_PROJECTSPEC')) define('TABLE_PROJECTSPEC', '`' . $config->db->prefix . 'projectspec`'); if(!defined('TABLE_SEARCHINDEX')) define('TABLE_SEARCHINDEX', $config->db->prefix . 'searchindex'); if(!defined('TABLE_SEARCHDICT')) define('TABLE_SEARCHDICT', $config->db->prefix . 'searchdict'); -$config->objectTables['product'] = TABLE_PRODUCT; -$config->objectTables['productplan'] = TABLE_PRODUCTPLAN; -$config->objectTables['story'] = TABLE_STORY; -$config->objectTables['requirement'] = TABLE_STORY; -$config->objectTables['release'] = TABLE_RELEASE; -$config->objectTables['program'] = TABLE_PROJECT; -$config->objectTables['project'] = TABLE_PROJECT; -$config->objectTables['execution'] = TABLE_PROJECT; -$config->objectTables['task'] = TABLE_TASK; -$config->objectTables['build'] = TABLE_BUILD; -$config->objectTables['bug'] = TABLE_BUG; -$config->objectTables['case'] = TABLE_CASE; -$config->objectTables['testcase'] = TABLE_CASE; -$config->objectTables['testtask'] = TABLE_TESTTASK; -$config->objectTables['testsuite'] = TABLE_TESTSUITE; -$config->objectTables['testreport'] = TABLE_TESTREPORT; -$config->objectTables['user'] = TABLE_USER; -$config->objectTables['api'] = TABLE_API; -$config->objectTables['doc'] = TABLE_DOC; -$config->objectTables['doclib'] = TABLE_DOCLIB; -$config->objectTables['todo'] = TABLE_TODO; -$config->objectTables['custom'] = TABLE_LANG; -$config->objectTables['branch'] = TABLE_BRANCH; -$config->objectTables['module'] = TABLE_MODULE; -$config->objectTables['caselib'] = TABLE_TESTSUITE; -$config->objectTables['entry'] = TABLE_ENTRY; -$config->objectTables['webhook'] = TABLE_WEBHOOK; -$config->objectTables['stakeholder'] = TABLE_STAKEHOLDER; -$config->objectTables['job'] = TABLE_JOB; -$config->objectTables['team'] = TABLE_TEAM; -$config->objectTables['pipeline'] = TABLE_PIPELINE; -$config->objectTables['mr'] = TABLE_MR; +$config->objectTables['product'] = TABLE_PRODUCT; +$config->objectTables['productplan'] = TABLE_PRODUCTPLAN; +$config->objectTables['story'] = TABLE_STORY; +$config->objectTables['requirement'] = TABLE_STORY; +$config->objectTables['release'] = TABLE_RELEASE; +$config->objectTables['program'] = TABLE_PROJECT; +$config->objectTables['project'] = TABLE_PROJECT; +$config->objectTables['execution'] = TABLE_PROJECT; +$config->objectTables['task'] = TABLE_TASK; +$config->objectTables['build'] = TABLE_BUILD; +$config->objectTables['bug'] = TABLE_BUG; +$config->objectTables['case'] = TABLE_CASE; +$config->objectTables['testcase'] = TABLE_CASE; +$config->objectTables['testtask'] = TABLE_TESTTASK; +$config->objectTables['testsuite'] = TABLE_TESTSUITE; +$config->objectTables['testreport'] = TABLE_TESTREPORT; +$config->objectTables['user'] = TABLE_USER; +$config->objectTables['api'] = TABLE_API; +$config->objectTables['doc'] = TABLE_DOC; +$config->objectTables['doclib'] = TABLE_DOCLIB; +$config->objectTables['todo'] = TABLE_TODO; +$config->objectTables['custom'] = TABLE_LANG; +$config->objectTables['branch'] = TABLE_BRANCH; +$config->objectTables['module'] = TABLE_MODULE; +$config->objectTables['caselib'] = TABLE_TESTSUITE; +$config->objectTables['entry'] = TABLE_ENTRY; +$config->objectTables['webhook'] = TABLE_WEBHOOK; +$config->objectTables['stakeholder'] = TABLE_STAKEHOLDER; +$config->objectTables['job'] = TABLE_JOB; +$config->objectTables['team'] = TABLE_TEAM; +$config->objectTables['pipeline'] = TABLE_PIPELINE; +$config->objectTables['mr'] = TABLE_MR; +$config->objectTables['kanbancolumn'] = TABLE_KANBANCOLUMN; +$config->objectTables['kanbanlane'] = TABLE_KANBANLANE; $config->newFeatures = array('introduction', 'tutorial', 'youngBlueTheme'); /* Program privs.*/ $config->programPriv = new stdclass(); -$config->programPriv->scrum = array('product', 'story', 'productplan', 'release', 'projectrelease', 'project', 'task', 'build', 'qa', 'bug', 'testcase', 'testsuite', 'testreport', 'caselib', 'doc', 'report', 'repo', 'svn', 'git', 'search', 'tree', 'file', 'jenkins', 'gitlab', 'job', 'ci', 'branch'); -$config->programPriv->waterfall = $config->programPriv->scrum + array('workestimation', 'durationestimation', 'budget', 'programplan', 'review', 'reviewissue', 'weekly', 'milestone', 'design', 'issue', 'risk', 'auditplan', 'nc', 'cm', 'pssp'); +$config->programPriv->scrum = array('projectstory', 'projectrelease', 'project', 'build', 'bug', 'testcase', 'testreport', 'caselib', 'doc', 'repo', 'meeting', 'stakeholder'); +$config->programPriv->waterfall = array_merge($config->programPriv->scrum, array('workestimation', 'durationestimation', 'budget', 'programplan', 'review', 'reviewissue', 'weekly', 'milestone', 'design', 'issue', 'risk', 'opportunity', 'measrecord', 'auditplan', 'trainplan', 'gapanalysis', 'pssp')); diff --git a/db/update15.7.1.sql b/db/update15.7.1.sql index 93a929f290..f1bb243e51 100644 --- a/db/update15.7.1.sql +++ b/db/update15.7.1.sql @@ -5,3 +5,140 @@ ALTER TABLE `zt_branch` ADD `createdDate` date NOT NULL AFTER `desc`; ALTER TABLE `zt_branch` ADD `closedDate` date NOT NULL AFTER `createdDate`; ALTER TABLE `zt_projectstory` ADD `branch` mediumint(8) NOT NULL AFTER `product`; ALTER TABLE `zt_projectproduct` ADD PRIMARY KEY `project_product_branch` (`project`, `product`, `branch`), DROP INDEX `PRIMARY`; + +-- DROP TABLE IF EXISTS `zt_kanbanlane`; +CREATE TABLE IF NOT EXISTS `zt_kanbanlane` ( + `id` int(8) NOT NULL AUTO_INCREMENT, + `execution` mediumint(8) NOT NULL DEFAULT '0', + `type` char(30) NOT NULL, + `groupby` char(30) NOT NULL, + `extra` char(30) NOT NULL, + `name` varchar(255) NOT NULL DEFAULT '', + `color` char(30) NOT NULL, + `order` smallint(6) NOT NULL DEFAULT '0', + `lastEditedTime` datetime NOT NULL, + `deleted` enum('0','1') NOT NULL default '0', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +-- DROP TABLE IF EXISTS `zt_kanbancolumn`; +CREATE TABLE IF NOT EXISTS `zt_kanbancolumn` ( + `id` int(8) NOT NULL AUTO_INCREMENT, + `lane` mediumint(8) NOT NULL DEFAULT '0', + `parent` mediumint(8) NOT NULL DEFAULT '0', + `type` char(30) NOT NULL, + `name` varchar(255) NOT NULL DEFAULT '', + `color` char(30) NOT NULL, + `limit` smallint(6) NOT NULL DEFAULT '-1', + `order` mediumint(8) NOT NULL DEFAULT '0', + `cards` text NULL, + `deleted` enum('0','1') NOT NULL default '0', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +-- DROP TABLE IF EXISTS `zt_stage`; +CREATE TABLE IF NOT EXISTS `zt_stage` ( + `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `percent` varchar(255) NOT NULL, + `type` varchar(255) NOT NULL, + `createdBy` varchar(30) NOT NULL, + `createdDate` datetime NOT NULL, + `editedBy` varchar(30) NOT NULL, + `editedDate` datetime NOT NULL, + `deleted` enum('0','1') NOT NULL DEFAULT '0', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +-- DROP TABLE IF EXISTS `zt_design`; +CREATE TABLE IF NOT EXISTS `zt_design` ( + `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, + `project` varchar(255) NOT NULL, + `product` varchar(255) NOT NULL, + `commit` text NOT NULL, + `commitedBy` varchar(30) NOT NULL, + `execution` mediumint(8) unsigned NOT NULL DEFAULT '0', + `name` varchar(255) NOT NULL, + `status` varchar(30) NOT NULL, + `createdBy` varchar(30) NOT NULL, + `createdDate` datetime NOT NULL, + `editedBy` varchar(30) NOT NULL, + `editedDate` datetime NOT NULL, + `assignedTo` varchar(30) NOT NULL, + `assignedBy` varchar(30) NOT NULL, + `assignedDate` datetime NOT NULL, + `deleted` enum('0','1') NOT NULL DEFAULT '0', + `story` char(30) NOT NULL, + `desc` text NOT NULL, + `version` smallint(6) NOT NULL, + `type` char(30) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +-- DROP TABLE IF EXISTS `zt_designspec`; +CREATE TABLE IF NOT EXISTS `zt_designspec` ( + `design` mediumint(8) NOT NULL, + `version` smallint(6) NOT NULL, + `name` varchar(255) NOT NULL, + `desc` text NOT NULL, + `files` varchar(255) NOT NULL, + UNIQUE KEY `design` (`design`,`version`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +-- DROP TABLE IF EXISTS `zt_weeklyreport`; +CREATE TABLE IF NOT EXISTS `zt_weeklyreport`( + `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, + `project` mediumint(8) unsigned NOT NULL, + `weekStart` date NOT NULL, + `pv` float(9,2) NOT NULL, + `ev` float(9,2) NOT NULL, + `ac` float(9,2) NOT NULL, + `sv` float(9,2) NOT NULL, + `cv` float(9,2) NOT NULL, + `staff` smallint(5) unsigned NOT NULL, + `progress` varchar(255) NOT NULL, + `workload` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `week` (`project`,`weekStart`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +-- DROP TABLE IF EXISTS `zt_holiday`; +CREATE TABLE IF NOT EXISTS `zt_holiday` ( + `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(30) NOT NULL DEFAULT '', + `type` enum('holiday', 'working') NOT NULL DEFAULT 'holiday', + `desc` text NOT NULL, + `year` char(4) NOT NULL, + `begin` date NOT NULL, + `end` date NOT NULL, + PRIMARY KEY (`id`), + KEY `year` (`year`), + KEY `name` (`name`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +REPLACE INTO `zt_stage` (`name`,`percent`,`type`,`createdBy`,`createdDate`,`editedBy`,`editedDate`,`deleted`) VALUES +('需求','10','request','admin','2020-02-08 21:08:30','admin','2020-02-12 13:50:27','0'), +('设计','10','design','admin','2020-02-08 21:08:30','admin','2020-02-12 13:50:27','0'), +('开发','50','dev','admin','2020-02-08 21:08:30','admin','2020-02-12 13:50:27','0'), +('测试','15','qa','admin','2020-02-08 21:08:30','admin','2020-02-12 13:50:27','0'), +('发布','10','release','admin','2020-02-08 21:08:30','admin','2020-02-12 13:50:27','0'), +('总结评审','5','review','admin','2020-02-08 21:08:45','admin','2020-02-12 13:50:27','0'); + +INSERT INTO `zt_lang` (`lang`, `module`, `section`, `key`, `value`, `system`) VALUES +('all','stage','typeList','request','需求', '1'), +('all','stage','typeList','design','设计', '1'), +('all','stage','typeList','dev','开发', '1'), +('all','stage','typeList','qa','测试', '1'), +('all','stage','typeList','release','发布', '1'), +('all','stage','typeList','review','总结评审','1'), +('all','stage','typeList','other','其他','1'); + +ALTER TABLE `zt_bug` +ADD `feedbackBy` varchar(100) NOT NULL AFTER `activatedDate`, +ADD `notifyEmail` varchar(100) NOT NULL AFTER `feedbackBy`; + +ALTER TABLE `zt_story` +ADD `feedbackBy` varchar(100) NOT NULL AFTER `version`, +ADD `notifyEmail` varchar(100) NOT NULL AFTER `feedbackBy`; + +ALTER TABLE `zt_product` ADD `reviewer` varchar(255) NOT NULL AFTER `whitelist`; diff --git a/db/zentao.sql b/db/zentao.sql index e113ddc7dc..a25b303d5c 100644 --- a/db/zentao.sql +++ b/db/zentao.sql @@ -30,99 +30,94 @@ CREATE TABLE IF NOT EXISTS `zt_action` ( KEY `objectID` (`objectID`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -- DROP TABLE IF EXISTS `zt_api_lib_release`; -CREATE TABLE `zt_api_lib_release` -( - `id` int UNSIGNED NOT NULL AUTO_INCREMENT, - `lib` int UNSIGNED NOT NULL DEFAULT 0, - `desc` varchar(255) NOT NULL DEFAULT '', - `version` varchar(255) NOT NULL DEFAULT '', - `snap` mediumtext NOT NULL, - `addedBy` varchar(30) NOT NULL DEFAULT 0, - `addedDate` datetime NOT NULL, - PRIMARY KEY (`id`) +CREATE TABLE `zt_api_lib_release` ( + `id` int UNSIGNED NOT NULL AUTO_INCREMENT, + `lib` int UNSIGNED NOT NULL DEFAULT 0, + `desc` varchar(255) NOT NULL DEFAULT '', + `version` varchar(255) NOT NULL DEFAULT '', + `snap` mediumtext NOT NULL, + `addedBy` varchar(30) NOT NULL DEFAULT 0, + `addedDate` datetime NOT NULL, + PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -- DROP TABLE IF EXISTS `zt_api`; -CREATE TABLE `zt_api` -( - `id` int UNSIGNED NOT NULL AUTO_INCREMENT, - `product` varchar(255) NOT NULL DEFAULT '', - `lib` int UNSIGNED NOT NULL DEFAULT 0, - `module` int UNSIGNED NOT NULL DEFAULT 0, - `title` varchar(100) NOT NULL DEFAULT '', - `path` varchar(255) NOT NULL DEFAULT '', - `protocol` varchar(10) NOT NULL DEFAULT '', - `method` varchar(10) NOT NULL DEFAULT '', - `requestType` varchar(100) NOT NULL DEFAULT '', - `responseType` varchar(100) NOT NULL DEFAULT '', - `status` varchar(20) NOT NULL DEFAULT '', - `owner` varchar(30) NOT NULl DEFAULT 0, - `desc` text NULL, - `version` smallint UNSIGNED NOT NULL DEFAULT 0, - `params` text NULL, - `paramsExample` text NUll, - `responseExample` text NUll, - `response` text NULL, - `commonParams` text NULL, - `addedBy` varchar(30) NOT NULL DEFAULT 0, - `addedDate` datetime NOT NULL, - `editedBy` varchar(30) NOT NULL DEFAULT 0, - `editedDate` datetime NOT NULL, - `deleted` enum ('0', '1') NOT NULL DEFAULT '0', - PRIMARY KEY (`id`) +CREATE TABLE `zt_api` ( + `id` int UNSIGNED NOT NULL AUTO_INCREMENT, + `product` varchar(255) NOT NULL DEFAULT '', + `lib` int UNSIGNED NOT NULL DEFAULT 0, + `module` int UNSIGNED NOT NULL DEFAULT 0, + `title` varchar(100) NOT NULL DEFAULT '', + `path` varchar(255) NOT NULL DEFAULT '', + `protocol` varchar(10) NOT NULL DEFAULT '', + `method` varchar(10) NOT NULL DEFAULT '', + `requestType` varchar(100) NOT NULL DEFAULT '', + `responseType` varchar(100) NOT NULL DEFAULT '', + `status` varchar(20) NOT NULL DEFAULT '', + `owner` varchar(30) NOT NULl DEFAULT 0, + `desc` text NULL, + `version` smallint UNSIGNED NOT NULL DEFAULT 0, + `params` text NULL, + `paramsExample` text NUll, + `responseExample` text NUll, + `response` text NULL, + `commonParams` text NULL, + `addedBy` varchar(30) NOT NULL DEFAULT 0, + `addedDate` datetime NOT NULL, + `editedBy` varchar(30) NOT NULL DEFAULT 0, + `editedDate` datetime NOT NULL, + `deleted` enum ('0', '1') NOT NULL DEFAULT '0', + PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -- DROP TABLE IF EXISTS `zt_apispec`; -CREATE TABLE `zt_apispec` -( - `id` int UNSIGNED NOT NULL AUTO_INCREMENT, - `doc` int UNSIGNED NOT NULL DEFAULT 0, - `module` int UNSIGNED NOT NULL DEFAULT 0, - `title` varchar(100) NOT NULL DEFAULT '', - `path` varchar(255) NOT NULL DEFAULT '', - `protocol` varchar(10) NOT NULL DEFAULT '', - `method` varchar(10) NOT NULL DEFAULT '', - `requestType` varchar(100) NOT NULL DEFAULT '', - `responseType` varchar(100) NOT NULL DEFAULT '', - `status` varchar(20) NOT NULL DEFAULT '', - `owner` varchar(255) NOT NULl DEFAULT 0, - `desc` text NULL, - `version` smallint UNSIGNED NOT NULL DEFAULT 0, - `params` text NULL, - `paramsExample` text NUll, - `responseExample` text NUll, - `response` text NULL, - `addedBy` varchar(30) NOT NULL DEFAULT 0, - `addedDate` datetime NULL, - PRIMARY KEY (`id`) +CREATE TABLE `zt_apispec` ( + `id` int UNSIGNED NOT NULL AUTO_INCREMENT, + `doc` int UNSIGNED NOT NULL DEFAULT 0, + `module` int UNSIGNED NOT NULL DEFAULT 0, + `title` varchar(100) NOT NULL DEFAULT '', + `path` varchar(255) NOT NULL DEFAULT '', + `protocol` varchar(10) NOT NULL DEFAULT '', + `method` varchar(10) NOT NULL DEFAULT '', + `requestType` varchar(100) NOT NULL DEFAULT '', + `responseType` varchar(100) NOT NULL DEFAULT '', + `status` varchar(20) NOT NULL DEFAULT '', + `owner` varchar(255) NOT NULl DEFAULT 0, + `desc` text NULL, + `version` smallint UNSIGNED NOT NULL DEFAULT 0, + `params` text NULL, + `paramsExample` text NUll, + `responseExample` text NUll, + `response` text NULL, + `addedBy` varchar(30) NOT NULL DEFAULT 0, + `addedDate` datetime NULL, + PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -- DROP TABLE IF EXISTS `zt_apistruct`; -CREATE TABLE `zt_apistruct` -( - `id` int unsigned NOT NULL AUTO_INCREMENT, - `lib` int UNSIGNED NOT NULL DEFAULT 0, - `name` varchar(30) NOT NULL DEFAULT '', - `type` varchar(50) NOT NULL DEFAULT '', - `desc` text NOT NULL DEFAULT '', - `version` smallint unsigned NOT NULL DEFAULT 0, - `attribute` text NULL, - `addedBy` varchar(30) NOT NULL DEFAULT 0, - `addedDate` datetime NOT NULL, - `editEdBy` varchar(30) NOT NULL DEFAULT 0, - `editedDate` datetime NOT NULL, - `deleted` enum ('0', '1') NOT NULL DEFAULT '0', - primary key (`id`) +CREATE TABLE `zt_apistruct` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `lib` int UNSIGNED NOT NULL DEFAULT 0, + `name` varchar(30) NOT NULL DEFAULT '', + `type` varchar(50) NOT NULL DEFAULT '', + `desc` varchar(255) NOT NULL DEFAULT '', + `version` smallint unsigned NOT NULL DEFAULT 0, + `attribute` text NULL, + `addedBy` varchar(30) NOT NULL DEFAULT 0, + `addedDate` datetime NOT NULL, + `editEdBy` varchar(30) NOT NULL DEFAULT 0, + `editedDate` datetime NOT NULL, + `deleted` enum ('0', '1') NOT NULL DEFAULT '0', + primary key (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -- DROP TABLE IF EXISTS `zt_apistruct_spec`; -CREATE TABLE `zt_apistruct_spec` -( - `id` int UNSIGNED NOT NULL AUTO_INCREMENT, - `name` varchar(255) NOT NULL DEFAULT '', - `type` varchar(50) NOT NULL DEFAULT '', - `desc` varchar(255) NOT NULL DEFAULT '', - `attribute` text NULL, - `version` smallint unsigned NOT NULL DEFAULT 0, - `addedBy` varchar(30) NOT NULL DEFAULT 0, - `addedDate` datetime NOT NULL, - primary key (`id`) +CREATE TABLE `zt_apistruct_spec` ( + `id` int UNSIGNED NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL DEFAULT '', + `type` varchar(50) NOT NULL DEFAULT '', + `desc` varchar(255) NOT NULL DEFAULT '', + `attribute` text NULL, + `version` smallint unsigned NOT NULL DEFAULT 0, + `addedBy` varchar(30) NOT NULL DEFAULT 0, + `addedDate` datetime NOT NULL, + primary key (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -- DROP TABLE IF EXISTS `zt_block`; CREATE TABLE IF NOT EXISTS `zt_block` ( @@ -187,6 +182,8 @@ CREATE TABLE IF NOT EXISTS `zt_bug` ( `confirmed` tinyint(1) NOT NULL default '0', `activatedCount` smallint(6) NOT NULL, `activatedDate` datetime NOT NULL, + `feedbackBy` varchar(100) NOT NULL, + `notifyEmail` varchar(100) NOT NULL, `mailto` text, `openedBy` varchar(30) NOT NULL default '', `openedDate` datetime NOT NULL, @@ -398,6 +395,39 @@ CREATE TABLE IF NOT EXISTS `zt_dept` ( KEY `parent` (`parent`), KEY `path` (`path`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; +-- DROP TABLE IF EXISTS `zt_design`; +CREATE TABLE IF NOT EXISTS `zt_design` ( + `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, + `project` varchar(255) NOT NULL, + `product` varchar(255) NOT NULL, + `commit` text NOT NULL, + `commitedBy` varchar(30) NOT NULL, + `execution` mediumint(8) unsigned NOT NULL DEFAULT '0', + `name` varchar(255) NOT NULL, + `status` varchar(30) NOT NULL, + `createdBy` varchar(30) NOT NULL, + `createdDate` datetime NOT NULL, + `editedBy` varchar(30) NOT NULL, + `editedDate` datetime NOT NULL, + `assignedTo` varchar(30) NOT NULL, + `assignedBy` varchar(30) NOT NULL, + `assignedDate` datetime NOT NULL, + `deleted` enum('0','1') NOT NULL DEFAULT '0', + `story` char(30) NOT NULL, + `desc` text NOT NULL, + `version` smallint(6) NOT NULL, + `type` char(30) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +-- DROP TABLE IF EXISTS `zt_designspec`; +CREATE TABLE IF NOT EXISTS `zt_designspec` ( + `design` mediumint(8) NOT NULL, + `version` smallint(6) NOT NULL, + `name` varchar(255) NOT NULL, + `desc` text NOT NULL, + `files` varchar(255) NOT NULL, + UNIQUE KEY `design` (`design`,`version`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; -- DROP TABLE IF EXISTS `zt_doc`; CREATE TABLE IF NOT EXISTS `zt_doc` ( `id` mediumint(8) unsigned NOT NULL auto_increment, @@ -563,6 +593,19 @@ CREATE TABLE IF NOT EXISTS `zt_grouppriv` ( `method` char(30) NOT NULL default '', UNIQUE KEY `group` (`group`,`module`,`method`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; +-- DROP TABLE IF EXISTS `zt_holiday`; +CREATE TABLE IF NOT EXISTS `zt_holiday` ( + `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(30) NOT NULL DEFAULT '', + `type` enum('holiday', 'working') NOT NULL DEFAULT 'holiday', + `desc` text NOT NULL, + `year` char(4) NOT NULL, + `begin` date NOT NULL, + `end` date NOT NULL, + PRIMARY KEY (`id`), + KEY `year` (`year`), + KEY `name` (`name`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; -- DROP TABLE IF EXISTS `zt_history`; CREATE TABLE IF NOT EXISTS `zt_history` ( `id` mediumint(8) unsigned NOT NULL auto_increment, @@ -600,6 +643,34 @@ CREATE TABLE IF NOT EXISTS `zt_job` ( `deleted` enum('0','1') NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; +-- DROP TABLE IF EXISTS `zt_kanbanlane`; +CREATE TABLE IF NOT EXISTS `zt_kanbanlane` ( + `id` int(8) NOT NULL AUTO_INCREMENT, + `execution` mediumint(8) NOT NULL DEFAULT '0', + `type` char(30) NOT NULL, + `groupby` char(30) NOT NULL, + `extra` char(30) NOT NULL, + `name` varchar(255) NOT NULL DEFAULT '', + `color` char(30) NOT NULL, + `order` smallint(6) NOT NULL DEFAULT '0', + `lastEditedDate` datetime NOT NULL, + `deleted` enum('0','1') NOT NULL default '0', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +-- DROP TABLE IF EXISTS `zt_kanbancolumn`; +CREATE TABLE IF NOT EXISTS `zt_kanbancolumn` ( + `id` int(8) NOT NULL AUTO_INCREMENT, + `lane` mediumint(8) NOT NULL DEFAULT '0', + `parent` mediumint(8) NOT NULL DEFAULT '0', + `type` char(30) NOT NULL, + `name` varchar(255) NOT NULL DEFAULT '', + `color` char(30) NOT NULL, + `limit` smallint(6) NOT NULL DEFAULT '-1', + `order` mediumint(8) NOT NULL DEFAULT '0', + `cards` text NULL, + `deleted` enum('0','1') NOT NULL default '0', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; -- DROP TABLE IF EXISTS `zt_lang`; CREATE TABLE IF NOT EXISTS `zt_lang` ( `id` mediumint(8) unsigned NOT NULL auto_increment, @@ -738,6 +809,7 @@ CREATE TABLE IF NOT EXISTS `zt_product` ( `RD` varchar(30) NOT NULL, `acl` enum('open','private','custom') NOT NULL default 'open', `whitelist` text NOT NULL, + `reviewer` varchar(255) NOT NULL, `createdBy` varchar(30) NOT NULL, `createdDate` datetime NOT NULL, `createdVersion` varchar(20) NOT NULL, @@ -995,6 +1067,19 @@ CREATE TABLE IF NOT EXISTS `zt_searchindex` ( FULLTEXT KEY `content` (`content`), FULLTEXT KEY `title` (`title`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; +-- DROP TABLE IF EXISTS `zt_stage`; +CREATE TABLE IF NOT EXISTS `zt_stage` ( + `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `percent` varchar(255) NOT NULL, + `type` varchar(255) NOT NULL, + `createdBy` varchar(30) NOT NULL, + `createdDate` datetime NOT NULL, + `editedBy` varchar(30) NOT NULL, + `editedDate` datetime NOT NULL, + `deleted` enum('0','1') NOT NULL DEFAULT '0', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; -- DROP TABLE IF EXISTS `zt_stakeholder`; CREATE TABLE IF NOT EXISTS `zt_stakeholder` ( `id` mediumint(8) NOT NULL AUTO_INCREMENT PRIMARY KEY, @@ -1049,6 +1134,8 @@ CREATE TABLE IF NOT EXISTS `zt_story` ( `linkStories` varchar(255) NOT NULL, `duplicateStory` mediumint(8) unsigned NOT NULL, `version` smallint(6) NOT NULL default '1', + `feedbackBy` varchar(100) NOT NULL, + `notifyEmail` varchar(100) NOT NULL, `URChanged` enum('0','1') NOT NULL DEFAULT '0', `deleted` enum('0','1') NOT NULL default '0', PRIMARY KEY (`id`), @@ -1415,6 +1502,22 @@ CREATE TABLE IF NOT EXISTS `zt_userview` ( `sprints` mediumtext NOT NULL, UNIQUE KEY `account` (`account`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; +-- DROP TABLE IF EXISTS `zt_weeklyreport`; +CREATE TABLE IF NOT EXISTS `zt_weeklyreport`( + `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, + `project` mediumint(8) unsigned NOT NULL, + `weekStart` date NOT NULL, + `pv` float(9,2) NOT NULL, + `ev` float(9,2) NOT NULL, + `ac` float(9,2) NOT NULL, + `sv` float(9,2) NOT NULL, + `cv` float(9,2) NOT NULL, + `staff` smallint(5) unsigned NOT NULL, + `progress` varchar(255) NOT NULL, + `workload` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `week` (`project`,`weekStart`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; -- DROP TABLE IF EXISTS `zt_webhook`; CREATE TABLE IF NOT EXISTS `zt_webhook` ( `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, @@ -5701,7 +5804,22 @@ REPLACE INTO `zt_lang` (`lang`, `module`, `section`, `key`, `value`, `system`) V ('zh-cn', 'custom', 'URSRList', '3', '{\"SRName\":\"\\u8f6f\\u9700\",\"URName\":\"\\u7528\\u9700\"}', '1'),('zh-cn', 'custom', 'URSRList', '4', '{\"SRName\":\"\\u6545\\u4e8b\",\"URName\":\"\\u53f2\\u8bd7\"}', '1'), ('zh-cn', 'custom', 'URSRList', '5', '{\"SRName\":\"\\u9700\\u6c42\",\"URName\":\"\\u7528\\u6237\\u9700\\u6c42\"}', '1'), ('en', 'custom', 'URSRList', '1', '{\"SRName\":\"Story\",\"URName\":\"Epic\"}', '0'), -('en', 'custom', 'URSRList', '2', '{\"SRName\":\"Software Requirement\",\"URName\":\"User Requirement\"}', '0'); +('en', 'custom', 'URSRList', '2', '{\"SRName\":\"Software Requirement\",\"URName\":\"User Requirement\"}', '0'), +('all','stage','typeList','request','需求', '1'), +('all','stage','typeList','design','设计', '1'), +('all','stage','typeList','dev','开发', '1'), +('all','stage','typeList','qa','测试', '1'), +('all','stage','typeList','release','发布', '1'), +('all','stage','typeList','review','总结评审','1'), +('all','stage','typeList','other','其他','1'); + +REPLACE INTO `zt_stage` (`name`,`percent`,`type`,`createdBy`,`createdDate`,`editedBy`,`editedDate`,`deleted`) VALUES +('需求','10','request','admin','2020-02-08 21:08:30','admin','2020-02-12 13:50:27','0'), +('设计','10','design','admin','2020-02-08 21:08:30','admin','2020-02-12 13:50:27','0'), +('开发','50','dev','admin','2020-02-08 21:08:30','admin','2020-02-12 13:50:27','0'), +('测试','15','qa','admin','2020-02-08 21:08:30','admin','2020-02-12 13:50:27','0'), +('发布','10','release','admin','2020-02-08 21:08:30','admin','2020-02-12 13:50:27','0'), +('总结评审','5','review','admin','2020-02-08 21:08:45','admin','2020-02-12 13:50:27','0'); INSERT INTO `zt_config` (`owner`, `module`, `section`, `key`, `value`) VALUES ('system', 'custom', '', 'hourPoint', '0'); INSERT INTO `zt_config` (`owner`, `module`, `section`, `key`, `value`) VALUES ('system', 'common', '', 'CRProduct', '1'); diff --git a/framework/api/router.class.php b/framework/api/router.class.php index c643a2f50d..baf63f2ea1 100644 --- a/framework/api/router.class.php +++ b/framework/api/router.class.php @@ -82,13 +82,13 @@ class api extends router $this->httpMethod = strtolower($_SERVER['REQUEST_METHOD']); - $fileName = substr($_SERVER['SCRIPT_FILENAME'], strlen($_SERVER['DOCUMENT_ROOT'])); - $this->path = substr($_SERVER['REQUEST_URI'], strlen($fileName) + 1); + $fileName = ltrim(substr($_SERVER['SCRIPT_FILENAME'], strlen($_SERVER['DOCUMENT_ROOT'])), '/'); + $this->path = substr(ltrim($_SERVER['REQUEST_URI'], '/'), strlen($fileName) + 1); if(strpos($this->path, '?') > 0) $this->path = strstr($this->path, '?', true); - $subPos = $this->path ? strpos($this->path, '/') : 0; - $this->version = $subPos ? substr($this->path, 0, $subPos) : ''; - $this->path = $subPos ? substr($this->path, $subPos) : ''; + $subPos = $this->path ? strpos($this->path, '/') : false; + $this->version = $subPos !== false ? substr($this->path, 0, $subPos) : ''; + $this->path = $subPos !== false ? substr($this->path, $subPos) : ''; $this->loadApiLang(); } diff --git a/framework/base/control.class.php b/framework/base/control.class.php index 2c0a0eabba..1623500402 100644 --- a/framework/base/control.class.php +++ b/framework/base/control.class.php @@ -945,10 +945,7 @@ class baseControl */ public function sendError($error) { - $this->send(array( - 'result' => 'fail', - 'message' => $error, - )); + $this->send(array('result' => 'fail', 'message' => $error)); } /** diff --git a/lib/front/front.class.php b/lib/front/front.class.php index 716a533b74..9ff779b05a 100644 --- a/lib/front/front.class.php +++ b/lib/front/front.class.php @@ -229,7 +229,7 @@ class html extends baseHTML /** * Create user avatar. * - * @param string|object|array $user User object or user avatar url or user account + * @param string|object|array $user User object or user account * @param string|int $size Avatar size, can be a number or preset sizes: "xs", "sm", "", "lg", "xl", default is "" * @param string $className Avatar element class name, default is "avatar-circle" * @param string $attrib Extra attributes on avatar element @@ -248,7 +248,6 @@ class html extends baseHTML if(is_string($user)) { $userObj->account = $user; - if(strlen($user) > 1) $userObj->avatar = $user; $user = $userObj; } elseif(is_array($user)) diff --git a/lib/hyperdown/LICENSE b/lib/hyperdown/LICENSE deleted file mode 100644 index 08cc28bc27..0000000000 --- a/lib/hyperdown/LICENSE +++ /dev/null @@ -1,29 +0,0 @@ -Software License Agreement (BSD License) - -Copyright (c) 2015, SegmentFault -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, this - list of conditions and the following disclaimer in the documentation and/or - other materials provided with the distribution. - -* Neither the name of schillmania.com nor the names of its contributors may be - used to endorse or promote products derived from this software without - specific prior written permission from schillmania.com. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/hyperdown/hyperdown.class.php b/lib/hyperdown/hyperdown.class.php deleted file mode 100644 index f9eea6bff8..0000000000 --- a/lib/hyperdown/hyperdown.class.php +++ /dev/null @@ -1,1293 +0,0 @@ - - * @license BSD License - */ -class hyperdown -{ - /** - * _whiteList - * - * @var string - */ - public $_commonWhiteList = 'kbd|b|i|strong|em|sup|sub|br|code|del|a|hr|small'; - - /** - * _specialWhiteList - * - * @var mixed - * @access public - */ - public $_specialWhiteList = array( - 'table' => 'table|tbody|thead|tfoot|tr|td|th' - ); - - /** - * _footnotes - * - * @var array - */ - public $_footnotes; - - /** - * _blocks - * - * @var array - */ - public $_blocks; - - /** - * _current - * - * @var string - */ - public $_current; - - /** - * _pos - * - * @var int - */ - public $_pos; - - /** - * _definitions - * - * @var array - */ - public $_definitions; - - /** - * @var array - */ - public $_hooks = array(); - - /** - * @var array - */ - public $_holders; - - /** - * @var string - */ - public $_uniqid; - - /** - * @var int - */ - public $_id; - - /** - * makeHtml - * - * @param mixed $text - * @return string - */ - public function makeHtml($text) - { - $this->_footnotes = array(); - $this->_definitions = array(); - $this->_holders = array(); - $this->_uniqid = md5(uniqid()); - $this->_id = 0; - - $text = $this->initText($text); - $html = $this->parse($text); - $html = $this->makeFootnotes($html); - - return $this->call('makeHtml', $html); - } - - /** - * @param $type - * @param $callback - */ - public function hook($type, $callback) - { - $this->_hooks[$type][] = $callback; - } - - /** - * @param $str - * @return string - */ - public function makeHolder($str) - { - $key = "\r" . $this->_uniqid . $this->_id . "\r"; - $this->_id ++; - $this->_holders[$key] = $str; - - return $key; - } - - /** - * @param $text - * @return mixed - */ - public function initText($text) - { - $text = str_replace(array("\t", "\r"), array(' ', ''), $text); - return $text; - } - - /** - * @param $html - * @return string - */ - public function makeFootnotes($html) - { - if (count($this->_footnotes) > 0) { - $html .= '

    '; - $index = 1; - - while ($val = array_shift($this->_footnotes)) { - if (is_string($val)) { - $val .= " ↩"; - } else { - $val[count($val) - 1] .= " ↩"; - $val = count($val) > 1 ? $this->parse(implode("\n", $val)) : $this->parseInline($val[0]); - } - - $html .= "
  1. {$val}
  2. "; - $index ++; - } - - $html .= '
'; - } - - return $html; - } - - /** - * parse - * - * @param string $text - * @return string - */ - public function parse($text) - { - $blocks = $this->parseBlock($text, $lines); - $html = ''; - - foreach ($blocks as $block) { - list ($type, $start, $end, $value) = $block; - $extract = array_slice($lines, $start, $end - $start + 1); - $method = 'parse' . ucfirst($type); - - $extract = $this->call('before' . ucfirst($method), $extract, $value); - $result = $this->{$method}($extract, $value); - $result = $this->call('after' . ucfirst($method), $result, $value); - - $html .= $result; - } - - return $html; - } - - /** - * @param $type - * @param $value - * @return mixed - */ - public function call($type, $value) - { - if (empty($this->_hooks[$type])) { - return $value; - } - - $args = func_get_args(); - $args = array_slice($args, 1); - - foreach ($this->_hooks[$type] as $callback) { - $value = call_user_func_array($callback, $args); - $args[0] = $value; - } - - return $value; - } - - /** - * @param $text - * @param $clearHolders - * @return string - */ - public function releaseHolder($text, $clearHolders = true) - { - $deep = 0; - while (strpos($text, "\r") !== false && $deep < 10) { - $text = str_replace(array_keys($this->_holders), array_values($this->_holders), $text); - $deep ++; - } - - if ($clearHolders) { - $this->_holders = array(); - } - - return $text; - } - - /** - * parseInline - * - * @param string $text - * @param string $whiteList - * @param bool $clearHolders - * @param bool $enableAutoLink - * @return string - */ - public function parseInline($text, $whiteList = '', $clearHolders = true, $enableAutoLink = true) - { - $self = $this; - $text = $this->call('beforeParseInline', $text); - - // escape - $text = preg_replace_callback( - "/\\\(.)/u", - function ($matches) use ($self) { - $escaped = htmlspecialchars($matches[1]); - $escaped = str_replace('$', '$', $escaped); - return $self->makeHolder($escaped); - }, - $text - ); - - // code - $text = preg_replace_callback( - "/(^|[^\\\])(`+)(.+?)\\2/", - function ($matches) use ($self) { - return $matches[1] . $self->makeHolder( - '' . htmlspecialchars($matches[3]) . '' - ); - }, - $text - ); - - // link - $text = preg_replace_callback( - "/<(https?:\/\/.+)>/i", - function ($matches) use ($self) { - $url = $self->cleanUrl($matches[1]); - $link = $self->call('parseLink', $matches[1]); - - return $self->makeHolder( - "{$link}" - ); - }, - $text - ); - - // encode unsafe tags - $text = preg_replace_callback( - "/<(\/?)([a-z0-9-]+)(\s+[^>]*)?>/i", - function ($matches) use ($self, $whiteList) { - if (false !== stripos( - '|' . $self->_commonWhiteList . '|' . $whiteList . '|', '|' . $matches[2] . '|' - )) { - return $self->makeHolder($matches[0]); - } else { - return htmlspecialchars($matches[0]); - } - }, - $text - ); - - $text = str_replace(array('<', '>'), array('<', '>'), $text); - - // footnote - $text = preg_replace_callback( - "/\[\^((?:[^\]]|\\\\\]|\\\\\[)+?)\]/", - function ($matches) use ($self) { - $id = array_search($matches[1], $self->_footnotes); - - if (false === $id) { - $id = count($self->_footnotes) + 1; - $self->_footnotes[$id] = $self->parseInline($matches[1], '', false); - } - - return $self->makeHolder( - "{$id}" - ); - }, - $text - ); - - // image - $text = preg_replace_callback( - "/!\[((?:[^\]]|\\\\\]|\\\\\[)*?)\]\(((?:[^\)]|\\\\\)|\\\\\()+?)\)/", - function ($matches) use ($self) { - $escaped = $self->escapeBracket($matches[1]); - $url = $self->escapeBracket($matches[2]); - $url = $self->cleanUrl($url); - return $self->makeHolder( - "\"{$escaped}\"" - ); - }, - $text - ); - - $text = preg_replace_callback( - "/!\[((?:[^\]]|\\\\\]|\\\\\[)*?)\]\[((?:[^\]]|\\\\\]|\\\\\[)+?)\]/", - function ($matches) use ($self) { - $escaped = $self->escapeBracket($matches[1]); - - $result = isset( $self->_definitions[$matches[2]] ) ? - "_definitions[$matches[2]]}\" alt=\"{$escaped}\" title=\"{$escaped}\">" - : $escaped; - - return $self->makeHolder($result); - }, - $text - ); - - // link - $text = preg_replace_callback( - "/\[((?:[^\]]|\\\\\]|\\\\\[)+?)\]\(((?:[^\)]|\\\\\)|\\\\\()+?)\)/", - function ($matches) use ($self) { - $escaped = $self->parseInline( - $self->escapeBracket($matches[1]), '', false, false - ); - $url = $self->escapeBracket($matches[2]); - $url = $self->cleanUrl($url); - return $self->makeHolder("{$escaped}"); - }, - $text - ); - - $text = preg_replace_callback( - "/\[((?:[^\]]|\\\\\]|\\\\\[)+?)\]\[((?:[^\]]|\\\\\]|\\\\\[)+?)\]/", - function ($matches) use ($self) { - $escaped = $self->parseInline( - $self->escapeBracket($matches[1]), '', false - ); - $result = isset( $self->_definitions[$matches[2]] ) ? - "_definitions[$matches[2]]}\">{$escaped}" - : $escaped; - - return $self->makeHolder($result); - }, - $text - ); - - // strong and em and some fuck - $text = $this->parseInlineCallback($text); - $text = preg_replace( - "/<([_a-z0-9-\.\+]+@[^@]+\.[a-z]{2,})>/i", - "\\1", - $text - ); - - // autolink url - if ($enableAutoLink) { - $text = preg_replace_callback( - "/(^|[^\"])((https?):[x80-xff_a-z0-9-\.\/%#@\?\+=~\|\,&\(\)]+)($|[^\"])/i", - function ($matches) use ($self) { - $link = $self->call('parseLink', $matches[2]); - return "{$matches[1]}{$link}{$matches[4]}"; - }, - $text - ); - } - - $text = $this->call('afterParseInlineBeforeRelease', $text); - $text = $this->releaseHolder($text, $clearHolders); - - $text = $this->call('afterParseInline', $text); - - return $text; - } - - /** - * @param $text - * @return mixed - */ - public function parseInlineCallback($text) - { - $self = $this; - - $text = preg_replace_callback( - "/(\*{3})(.+?)\\1/", - function ($matches) use ($self) { - return '' . - $self->parseInlineCallback($matches[2]) . - ''; - }, - $text - ); - - $text = preg_replace_callback( - "/(\*{2})(.+?)\\1/", - function ($matches) use ($self) { - return '' . - $self->parseInlineCallback($matches[2]) . - ''; - }, - $text - ); - - $text = preg_replace_callback( - "/(\*)(.+?)\\1/", - function ($matches) use ($self) { - return '' . - $self->parseInlineCallback($matches[2]) . - ''; - }, - $text - ); - - $text = preg_replace_callback( - "/(\s+|^)(_{3})(.+?)\\2(\s+|$)/", - function ($matches) use ($self) { - return $matches[1] . '' . - $self->parseInlineCallback($matches[3]) . - '' . $matches[4]; - }, - $text - ); - - $text = preg_replace_callback( - "/(\s+|^)(_{2})(.+?)\\2(\s+|$)/", - function ($matches) use ($self) { - return $matches[1] . '' . - $self->parseInlineCallback($matches[3]) . - '' . $matches[4]; - }, - $text - ); - - $text = preg_replace_callback( - "/(\s+|^)(_)(.+?)\\2(\s+|$)/", - function ($matches) use ($self) { - return $matches[1] . '' . - $self->parseInlineCallback($matches[3]) . - '' . $matches[4]; - }, - $text - ); - - $text = preg_replace_callback( - "/(~{2})(.+?)\\1/", - function ($matches) use ($self) { - return '' . - $self->parseInlineCallback($matches[2]) . - ''; - }, - $text - ); - - return $text; - } - - /** - * parseBlock - * - * @param string $text - * @param array $lines - * @return array - */ - public function parseBlock($text, &$lines) - { - $lines = explode("\n", $text); - $this->_blocks = array(); - $this->_current = 'normal'; - $this->_pos = -1; - $special = implode("|", array_keys($this->_specialWhiteList)); - $emptyCount = 0; - - // analyze by line - foreach ($lines as $key => $line) { - $block = $this->getBlock(); - - // code block is special - if (preg_match("/^(\s*)(~|`){3,}([^`~]*)$/i", $line, $matches)) { - if ($this->isBlock('code')) { - $isAfterList = $block[3][2]; - - if ($isAfterList) { - $this->combineBlock() - ->setBlock($key); - } else { - $this->setBlock($key) - ->endBlock(); - } - } else { - $isAfterList = false; - - if ($this->isBlock('list')) { - $space = $block[3]; - - $isAfterList = ($space > 0 && strlen($matches[1]) >= $space) - || strlen($matches[1]) > $space; - } - - $this->startBlock('code', $key, array( - $matches[1], $matches[3], $isAfterList - )); - } - - continue; - } else if ($this->isBlock('code')) { - $this->setBlock($key); - continue; - } - - // html block is special too - if (preg_match("/^\s*<({$special})(\s+[^>]*)?>/i", $line, $matches)) { - $tag = strtolower($matches[1]); - if (!$this->isBlock('html', $tag) && !$this->isBlock('pre')) { - $this->startBlock('html', $key, $tag); - } - - continue; - } else if (preg_match("/<\/({$special})>\s*$/i", $line, $matches)) { - $tag = strtolower($matches[1]); - - if ($this->isBlock('html', $tag)) { - $this->setBlock($key) - ->endBlock(); - } - - continue; - } else if ($this->isBlock('html')) { - $this->setBlock($key); - continue; - } - - switch (true) { - // pre block - case preg_match("/^ {4}/", $line): - $emptyCount = 0; - - if ($this->isBlock('pre') || $this->isBlock('list')) { - $this->setBlock($key); - } else if ($this->isBlock('normal')) { - $this->startBlock('pre', $key); - } - break; - - // list - case preg_match("/^(\s*)((?:[0-9a-z]+\.)|\-|\+|\*)\s+/", $line, $matches): - $space = strlen($matches[1]); - $emptyCount = 0; - - // opened - if ($this->isBlock('list')) { - $this->setBlock($key, $space); - } else { - $this->startBlock('list', $key, $space); - } - break; - - // footnote - case preg_match("/^\[\^((?:[^\]]|\\]|\\[)+?)\]:/", $line, $matches): - $space = strlen($matches[0]) - 1; - $this->startBlock('footnote', $key, array( - $space, $matches[1] - )); - break; - - // definition - case preg_match("/^\s*\[((?:[^\]]|\\]|\\[)+?)\]:\s*(.+)$/", $line, $matches): - $this->_definitions[$matches[1]] = $this->cleanUrl($matches[2]); - $this->startBlock('definition', $key) - ->endBlock(); - break; - - // block quote - case preg_match("/^\s*>/", $line): - if ($this->isBlock('quote')) { - $this->setBlock($key); - } else { - $this->startBlock('quote', $key); - } - break; - - // table - case preg_match("/^ *((?:(?:(?:[ :]*\-[ :]*)+(?:\||\+))|(?:(?:\||\+)(?:[ :]*\-[ :]*)+)|(?:(?:[ :]*\-[ :]*)+(?:\||\+)(?:[ :]*\-[ :]*)+))+)\s*$/", $line, $matches): - if ($this->isBlock('table')) { - $block[3][0][] = $block[3][2]; - $block[3][2] ++; - $this->setBlock($key, $block[3]); - } else { - $head = 0; - - if (empty($block) || - $block[0] != 'normal' || - preg_match("/^\s*$/", $lines[$block[2]])) { - $this->startBlock('table', $key); - } else { - $head = 1; - $this->backBlock(1, 'table'); - } - - if ($matches[1][0] == '|') { - $matches[1] = substr($matches[1], 1); - - if ($matches[1][strlen($matches[1]) - 1] == '|') { - $matches[1] = substr($matches[1], 0, -1); - } - } - - $rows = preg_split("/(\+|\|)/", $matches[1]); - $aligns = array(); - foreach ($rows as $row) { - $align = 'none'; - - if (preg_match("/^\s*(:?)\-+(:?)\s*$/", $row, $matches)) { - if (!empty($matches[1]) && !empty($matches[2])) { - $align = 'center'; - } else if (!empty($matches[1])) { - $align = 'left'; - } else if (!empty($matches[2])) { - $align = 'right'; - } - } - - $aligns[] = $align; - } - - $this->setBlock($key, array(array($head), $aligns, $head + 1)); - } - break; - - // single heading - case preg_match("/^(#+)(.*)$/", $line, $matches): - $num = min(strlen($matches[1]), 6); - $this->startBlock('sh', $key, $num) - ->endBlock(); - break; - - // multi heading - case preg_match("/^\s*((=|-){2,})\s*$/", $line, $matches) - && ($block && $block[0] == "normal" && !preg_match("/^\s*$/", $lines[$block[2]])): // check if last line isn't empty - if ($this->isBlock('normal')) { - $this->backBlock(1, 'mh', $matches[1][0] == '=' ? 1 : 2) - ->setBlock($key) - ->endBlock(); - } else { - $this->startBlock('normal', $key); - } - break; - - // hr - case preg_match("/^[-\*]{3,}\s*$/", $line): - $this->startBlock('hr', $key) - ->endBlock(); - break; - - // normal - default: - if ($this->isBlock('list')) { - if (preg_match("/^(\s*)/", $line)) { // empty line - if ($emptyCount > 0) { - $this->startBlock('normal', $key); - } else { - $this->setBlock($key); - } - - $emptyCount ++; - } else if ($emptyCount == 0) { - $this->setBlock($key); - } else { - $this->startBlock('normal', $key); - } - } else if ($this->isBlock('footnote')) { - preg_match("/^(\s*)/", $line, $matches); - if (strlen($matches[1]) >= $block[3][0]) { - $this->setBlock($key); - } else { - $this->startBlock('normal', $key); - } - } else if ($this->isBlock('table')) { - if (false !== strpos($line, '|')) { - $block[3][2] ++; - $this->setBlock($key, $block[3]); - } else { - $this->startBlock('normal', $key); - } - } else if ($this->isBlock('pre')) { - if (preg_match("/^\s*$/", $line)) { - if ($emptyCount > 0) { - $this->startBlock('normal', $key); - } else { - $this->setBlock($key); - } - - $emptyCount ++; - } else { - $this->startBlock('normal', $key); - } - } else if ($this->isBlock('quote')) { - if (preg_match("/^(\s*)/", $line)) { // empty line - if ($emptyCount > 0) { - $this->startBlock('normal', $key); - } else { - $this->setBlock($key); - } - - $emptyCount ++; - } else if ($emptyCount == 0) { - $this->setBlock($key); - } else { - $this->startBlock('normal', $key); - } - } else { - if (empty($block) || $block[0] != 'normal') { - $this->startBlock('normal', $key); - } else { - $this->setBlock($key); - } - } - break; - } - } - - return $this->optimizeBlocks($this->_blocks, $lines); - } - - /** - * @param array $blocks - * @param array $lines - * @return array - */ - public function optimizeBlocks(array $blocks, array $lines) - { - $blocks = $this->call('beforeOptimizeBlocks', $blocks, $lines); - - $key = 0; - while (isset($blocks[$key])) { - $moved = false; - - $block = &$blocks[$key]; - $prevBlock = isset($blocks[$key - 1]) ? $blocks[$key - 1] : NULL; - $nextBlock = isset($blocks[$key + 1]) ? $blocks[$key + 1] : NULL; - - list ($type, $from, $to) = $block; - - if ('pre' == $type) { - $isEmpty = array_reduce($lines, function ($result, $line) { - return preg_match("/^\s*$/", $line) && $result; - }, true); - - if ($isEmpty) { - $block[0] = $type = 'normal'; - } - } - - if ('normal' == $type) { - // combine two blocks - $types = array('list', 'quote'); - - if ($from == $to && preg_match("/^\s*$/", $lines[$from]) - && !empty($prevBlock) && !empty($nextBlock)) { - if ($prevBlock[0] == $nextBlock[0] && in_array($prevBlock[0], $types)) { - // combine 3 blocks - $blocks[$key - 1] = array( - $prevBlock[0], $prevBlock[1], $nextBlock[2], NULL - ); - array_splice($blocks, $key, 2); - - // do not move - $moved = true; - } - } - } - - if (!$moved) { - $key ++; - } - } - - return $this->call('afterOptimizeBlocks', $blocks, $lines); - } - - /** - * parseCode - * - * @param array $lines - * @param array $parts - * @return string - */ - public function parseCode(array $lines, array $parts) - { - list ($blank, $lang) = $parts; - $lang = trim($lang); - $count = strlen($blank); - - if (! preg_match("/^[_a-z0-9-\+\#\:\.]+$/i", $lang)) { - $lang = NULL; - } else { - $parts = explode(':', $lang); - if (count($parts) > 1) { - list ($lang, $rel) = $parts; - $lang = trim($lang); - $rel = trim($rel); - } - } - - $lines = array_map(function ($line) use ($count) { - return preg_replace("/^[ ]{{$count}}/", '', $line); - }, array_slice($lines, 1, -1)); - $str = implode("\n", $lines); - - return preg_match("/^\s*$/", $str) ? '' : - '
'
-            . htmlspecialchars($str) . '
'; - } - - /** - * parsePre - * - * @param array $lines - * @return string - */ - public function parsePre(array $lines) - { - foreach ($lines as &$line) { - $line = htmlspecialchars(substr($line, 4)); - } - $str = implode("\n", $lines); - - return preg_match("/^\s*$/", $str) ? '' : '
' . $str . '
'; - } - - /** - * parseSh - * - * @param array $lines - * @param int $num - * @return string - */ - public function parseSh(array $lines, $num) - { - $line = $this->parseInline(trim($lines[0], '# ')); - return preg_match("/^\s*$/", $line) ? '' : "{$line}"; - } - - /** - * parseMh - * - * @param array $lines - * @param int $num - * @return string - */ - public function parseMh(array $lines, $num) - { - return $this->parseSh($lines, $num); - } - - /** - * parseQuote - * - * @param array $lines - * @return string - */ - public function parseQuote(array $lines) - { - foreach ($lines as &$line) { - $line = preg_replace("/^\s*> ?/", '', $line); - } - $str = implode("\n", $lines); - - return preg_match("/^\s*$/", $str) ? '' : '
' . $this->parse($str) . '
'; - } - - /** - * parseList - * - * @param array $lines - * @return string - */ - public function parseList(array $lines) - { - $html = ''; - $minSpace = 99999; - $rows = array(); - - // count levels - foreach ($lines as $key => $line) { - if (preg_match("/^(\s*)((?:[0-9a-z]+\.?)|\-|\+|\*)(\s+)(.*)$/", $line, $matches)) { - $space = strlen($matches[1]); - $type = false !== strpos('+-*', $matches[2]) ? 'ul' : 'ol'; - $minSpace = min($space, $minSpace); - - $rows[] = array($space, $type, $line, $matches[4]); - } else { - $rows[] = $line; - } - } - - $found = false; - $secondMinSpace = 99999; - foreach ($rows as $row) { - if (is_array($row) && $row[0] != $minSpace) { - $secondMinSpace = min($secondMinSpace, $row[0]); - $found = true; - } - } - $secondMinSpace = $found ? $secondMinSpace : $minSpace; - - $lastType = ''; - $leftLines = array(); - - foreach ($rows as $row) { - if (is_array($row)) { - list ($space, $type, $line, $text) = $row; - - if ($space != $minSpace) { - $leftLines[] = preg_replace("/^\s{" . $secondMinSpace . "}/", '', $line); - } else { - if (!empty($leftLines)) { - $html .= "
  • " . $this->parse(implode("\n", $leftLines)) . "
  • "; - } - - if ($lastType != $type) { - if (!empty($lastType)) { - $html .= ""; - } - - $html .= "<{$type}>"; - } - - $leftLines = array($text); - $lastType = $type; - } - } else { - $leftLines[] = preg_replace("/^\s{" . $secondMinSpace . "}/", '', $row); - } - } - - if (!empty($leftLines)) { - $html .= "
  • " . $this->parse(implode("\n", $leftLines)) . "
  • "; - } - - return $html; - } - - /** - * @param array $lines - * @param array $value - * @return string - */ - public function parseTable(array $lines, array $value) - { - list ($ignores, $aligns) = $value; - $head = count($ignores) > 0 && array_sum($ignores) > 0; - - $html = ''; - $body = $head ? NULL : true; - $output = false; - - foreach ($lines as $key => $line) { - if (in_array($key, $ignores)) { - if ($head && $output) { - $head = false; - $body = true; - } - continue; - } - - $line = trim($line); - $output = true; - - if ($line[0] == '|') { - $line = substr($line, 1); - - if ($line[strlen($line) - 1] == '|') { - $line = substr($line, 0, -1); - } - } - - - $rows = array_map(function ($row) { - if (preg_match("/^\s+$/", $row)) { - return ' '; - } else { - return trim($row); - } - }, explode('|', $line)); - $columns = array(); - $last = -1; - - foreach ($rows as $row) { - if (strlen($row) > 0) { - $last ++; - $columns[$last] = array( - isset($columns[$last]) ? $columns[$last][0] + 1 : 1, $row - ); - } else if (isset($columns[$last])) { - $columns[$last][0] ++; - } else { - $columns[0] = array(1, $row); - } - } - - if ($head) { - $html .= ''; - } else if ($body) { - $html .= ''; - } - - $html .= ''; - - foreach ($columns as $key => $column) { - list ($num, $text) = $column; - $tag = $head ? 'th' : 'td'; - - $html .= "<{$tag}"; - if ($num > 1) { - $html .= " colspan=\"{$num}\""; - } - - if (isset($aligns[$key]) && $aligns[$key] != 'none') { - $html .= " align=\"{$aligns[$key]}\""; - } - - $html .= '>' . $this->parseInline($text) . ""; - } - - $html .= ''; - - if ($head) { - $html .= ''; - } else if ($body) { - $body = false; - } - } - - if ($body !== NULL) { - $html .= ''; - } - - $html .= '
    '; - return $html; - } - - /** - * parseHr - * - * @return string - */ - public function parseHr() - { - return '
    '; - } - - /** - * parseNormal - * - * @param array $lines - * @return string - */ - public function parseNormal(array $lines) - { - foreach ($lines as &$line) { - $line = $this->parseInline($line); - } - - $str = trim(implode("\n", $lines)); - $str = preg_replace("/(\n\s*){2,}/", "

    ", $str); - $str = preg_replace("/\n/", "
    ", $str); - - return preg_match("/^\s*$/", $str) ? '' : "

    {$str}

    "; - } - - /** - * parseFootnote - * - * @param array $lines - * @param array $value - * @return string - */ - public function parseFootnote(array $lines, array $value) - { - list($space, $note) = $value; - $index = array_search($note, $this->_footnotes); - - if (false !== $index) { - $lines[0] = preg_replace("/^\[\^((?:[^\]]|\\]|\\[)+?)\]:/", '', $lines[0]); - $this->_footnotes[$index] = $lines; - } - - return ''; - } - - /** - * parseDefine - * - * @return string - */ - public function parseDefinition() - { - return ''; - } - - /** - * parseHtml - * - * @param array $lines - * @param string $type - * @return string - */ - public function parseHtml(array $lines, $type) - { - foreach ($lines as &$line) { - $line = $this->parseInline($line, - isset($this->_specialWhiteList[$type]) ? $this->_specialWhiteList[$type] : ''); - } - - return implode("\n", $lines); - } - - /** - * @param $url - * @return string - */ - public function cleanUrl($url) - { - return $url; - if (preg_match("/^\s*((http|https|ftp|mailto):[x80-xff_a-z0-9-\.\/%#@\?\+=~\|\,&\(\)]+)/i", $url, $matches)) { - return $matches[1]; - } else if (preg_match("/^\s*([x80-xff_a-z0-9-\.\/%#@\?\+=~\|\,&]+)/i", $url, $matches)) { - return $matches[1]; - } else { - return '#'; - } - } - - /** - * @param $str - * @return mixed - */ - public function escapeBracket($str) - { - return str_replace( - array('\[', '\]', '\(', '\)'), array('[', ']', '(', ')'), $str - ); - } - - /** - * startBlock - * - * @param mixed $type - * @param mixed $start - * @param mixed $value - * @return $this - */ - public function startBlock($type, $start, $value = NULL) - { - $this->_pos ++; - $this->_current = $type; - - $this->_blocks[$this->_pos] = array($type, $start, $start, $value); - - return $this; - } - - /** - * endBlock - * - * @return $this - */ - public function endBlock() - { - $this->_current = 'normal'; - return $this; - } - - /** - * isBlock - * - * @param mixed $type - * @param mixed $value - * @return bool - */ - public function isBlock($type, $value = NULL) - { - return $this->_current == $type - && (NULL === $value ? true : $this->_blocks[$this->_pos][3] == $value); - } - - /** - * getBlock - * - * @return array - */ - public function getBlock() - { - return isset($this->_blocks[$this->_pos]) ? $this->_blocks[$this->_pos] : NULL; - } - - /** - * setBlock - * - * @param mixed $to - * @param mixed $value - * @return $this - */ - public function setBlock($to = NULL, $value = NULL) - { - if (NULL !== $to) { - $this->_blocks[$this->_pos][2] = $to; - } - - if (NULL !== $value) { - $this->_blocks[$this->_pos][3] = $value; - } - - return $this; - } - - /** - * backBlock - * - * @param mixed $step - * @param mixed $type - * @param mixed $value - * @return $this - */ - public function backBlock($step, $type, $value = NULL) - { - if ($this->_pos < 0) { - return $this->startBlock($type, 0, $value); - } - - $last = $this->_blocks[$this->_pos][2]; - $this->_blocks[$this->_pos][2] = $last - $step; - - if ($this->_blocks[$this->_pos][1] <= $this->_blocks[$this->_pos][2]) { - $this->_pos ++; - } - - $this->_current = $type; - $this->_blocks[$this->_pos] = array( - $type, $last - $step + 1, $last, $value - ); - - return $this; - } - - /** - * @return $this - */ - public function combineBlock() - { - if ($this->_pos < 1) { - return $this; - } - - $prev = $this->_blocks[$this->_pos - 1]; - $current = $this->_blocks[$this->_pos]; - - $prev[2] = $current[2]; - $this->_blocks[$this->_pos - 1] = $prev; - $this->_current = $prev[0]; - unset($this->_blocks[$this->_pos]); - $this->_pos --; - - return $this; - } -} diff --git a/lib/parsedown/LICENSE.txt b/lib/parsedown/LICENSE.txt new file mode 100644 index 0000000000..8e7c764d16 --- /dev/null +++ b/lib/parsedown/LICENSE.txt @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2013-2018 Emanuil Rusev, erusev.com + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/parsedown/parsedown.class.php b/lib/parsedown/parsedown.class.php new file mode 100644 index 0000000000..69147dc2ac --- /dev/null +++ b/lib/parsedown/parsedown.class.php @@ -0,0 +1,1992 @@ +textElements($text); + + # convert to markup + $markup = $this->elements($Elements); + + # trim line breaks + $markup = trim($markup, "\n"); + + return $markup; + } + + protected function textElements($text) + { + # make sure no definitions are set + $this->DefinitionData = array(); + + # standardize line breaks + $text = str_replace(array("\r\n", "\r"), "\n", $text); + + # remove surrounding line breaks + $text = trim($text, "\n"); + + # split text into lines + $lines = explode("\n", $text); + + # iterate through lines to identify blocks + return $this->linesElements($lines); + } + + # + # Setters + # + + function setBreaksEnabled($breaksEnabled) + { + $this->breaksEnabled = $breaksEnabled; + + return $this; + } + + protected $breaksEnabled; + + function setMarkupEscaped($markupEscaped) + { + $this->markupEscaped = $markupEscaped; + + return $this; + } + + protected $markupEscaped; + + function setUrlsLinked($urlsLinked) + { + $this->urlsLinked = $urlsLinked; + + return $this; + } + + protected $urlsLinked = true; + + function setSafeMode($safeMode) + { + $this->safeMode = (bool) $safeMode; + + return $this; + } + + protected $safeMode; + + function setStrictMode($strictMode) + { + $this->strictMode = (bool) $strictMode; + + return $this; + } + + protected $strictMode; + + protected $safeLinksWhitelist = array( + 'http://', + 'https://', + 'ftp://', + 'ftps://', + 'mailto:', + 'tel:', + 'data:image/png;base64,', + 'data:image/gif;base64,', + 'data:image/jpeg;base64,', + 'irc:', + 'ircs:', + 'git:', + 'ssh:', + 'news:', + 'steam:', + ); + + # + # Lines + # + + protected $BlockTypes = array( + '#' => array('Header'), + '*' => array('Rule', 'List'), + '+' => array('List'), + '-' => array('SetextHeader', 'Table', 'Rule', 'List'), + '0' => array('List'), + '1' => array('List'), + '2' => array('List'), + '3' => array('List'), + '4' => array('List'), + '5' => array('List'), + '6' => array('List'), + '7' => array('List'), + '8' => array('List'), + '9' => array('List'), + ':' => array('Table'), + '<' => array('Comment', 'Markup'), + '=' => array('SetextHeader'), + '>' => array('Quote'), + '[' => array('Reference'), + '_' => array('Rule'), + '`' => array('FencedCode'), + '|' => array('Table'), + '~' => array('FencedCode'), + ); + + # ~ + + protected $unmarkedBlockTypes = array( + 'Code', + ); + + # + # Blocks + # + + protected function lines(array $lines) + { + return $this->elements($this->linesElements($lines)); + } + + protected function linesElements(array $lines) + { + $Elements = array(); + $CurrentBlock = null; + + foreach ($lines as $line) + { + if (chop($line) === '') + { + if (isset($CurrentBlock)) + { + $CurrentBlock['interrupted'] = (isset($CurrentBlock['interrupted']) + ? $CurrentBlock['interrupted'] + 1 : 1 + ); + } + + continue; + } + + while (($beforeTab = strstr($line, "\t", true)) !== false) + { + $shortage = 4 - mb_strlen($beforeTab, 'utf-8') % 4; + + $line = $beforeTab + . str_repeat(' ', $shortage) + . substr($line, strlen($beforeTab) + 1) + ; + } + + $indent = strspn($line, ' '); + + $text = $indent > 0 ? substr($line, $indent) : $line; + + # ~ + + $Line = array('body' => $line, 'indent' => $indent, 'text' => $text); + + # ~ + + if (isset($CurrentBlock['continuable'])) + { + $methodName = 'block' . $CurrentBlock['type'] . 'Continue'; + $Block = $this->$methodName($Line, $CurrentBlock); + + if (isset($Block)) + { + $CurrentBlock = $Block; + + continue; + } + else + { + if ($this->isBlockCompletable($CurrentBlock['type'])) + { + $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; + $CurrentBlock = $this->$methodName($CurrentBlock); + } + } + } + + # ~ + + $marker = $text[0]; + + # ~ + + $blockTypes = $this->unmarkedBlockTypes; + + if (isset($this->BlockTypes[$marker])) + { + foreach ($this->BlockTypes[$marker] as $blockType) + { + $blockTypes []= $blockType; + } + } + + # + # ~ + + foreach ($blockTypes as $blockType) + { + $Block = $this->{"block$blockType"}($Line, $CurrentBlock); + + if (isset($Block)) + { + $Block['type'] = $blockType; + + if ( ! isset($Block['identified'])) + { + if (isset($CurrentBlock)) + { + $Elements[] = $this->extractElement($CurrentBlock); + } + + $Block['identified'] = true; + } + + if ($this->isBlockContinuable($blockType)) + { + $Block['continuable'] = true; + } + + $CurrentBlock = $Block; + + continue 2; + } + } + + # ~ + + if (isset($CurrentBlock) and $CurrentBlock['type'] === 'Paragraph') + { + $Block = $this->paragraphContinue($Line, $CurrentBlock); + } + + if (isset($Block)) + { + $CurrentBlock = $Block; + } + else + { + if (isset($CurrentBlock)) + { + $Elements[] = $this->extractElement($CurrentBlock); + } + + $CurrentBlock = $this->paragraph($Line); + + $CurrentBlock['identified'] = true; + } + } + + # ~ + + if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) + { + $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; + $CurrentBlock = $this->$methodName($CurrentBlock); + } + + # ~ + + if (isset($CurrentBlock)) + { + $Elements[] = $this->extractElement($CurrentBlock); + } + + # ~ + + return $Elements; + } + + protected function extractElement(array $Component) + { + if ( ! isset($Component['element'])) + { + if (isset($Component['markup'])) + { + $Component['element'] = array('rawHtml' => $Component['markup']); + } + elseif (isset($Component['hidden'])) + { + $Component['element'] = array(); + } + } + + return $Component['element']; + } + + protected function isBlockContinuable($Type) + { + return method_exists($this, 'block' . $Type . 'Continue'); + } + + protected function isBlockCompletable($Type) + { + return method_exists($this, 'block' . $Type . 'Complete'); + } + + # + # Code + + protected function blockCode($Line, $Block = null) + { + if (isset($Block) and $Block['type'] === 'Paragraph' and ! isset($Block['interrupted'])) + { + return; + } + + if ($Line['indent'] >= 4) + { + $text = substr($Line['body'], 4); + + $Block = array( + 'element' => array( + 'name' => 'pre', + 'element' => array( + 'name' => 'code', + 'text' => $text, + ), + ), + ); + + return $Block; + } + } + + protected function blockCodeContinue($Line, $Block) + { + if ($Line['indent'] >= 4) + { + if (isset($Block['interrupted'])) + { + $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); + + unset($Block['interrupted']); + } + + $Block['element']['element']['text'] .= "\n"; + + $text = substr($Line['body'], 4); + + $Block['element']['element']['text'] .= $text; + + return $Block; + } + } + + protected function blockCodeComplete($Block) + { + return $Block; + } + + # + # Comment + + protected function blockComment($Line) + { + if ($this->markupEscaped or $this->safeMode) + { + return; + } + + if (strpos($Line['text'], '') !== false) + { + $Block['closed'] = true; + } + + return $Block; + } + } + + protected function blockCommentContinue($Line, array $Block) + { + if (isset($Block['closed'])) + { + return; + } + + $Block['element']['rawHtml'] .= "\n" . $Line['body']; + + if (strpos($Line['text'], '-->') !== false) + { + $Block['closed'] = true; + } + + return $Block; + } + + # + # Fenced Code + + protected function blockFencedCode($Line) + { + $marker = $Line['text'][0]; + + $openerLength = strspn($Line['text'], $marker); + + if ($openerLength < 3) + { + return; + } + + $infostring = trim(substr($Line['text'], $openerLength), "\t "); + + if (strpos($infostring, '`') !== false) + { + return; + } + + $Element = array( + 'name' => 'code', + 'text' => '', + ); + + if ($infostring !== '') + { + /** + * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes + * Every HTML element may have a class attribute specified. + * The attribute, if specified, must have a value that is a set + * of space-separated tokens representing the various classes + * that the element belongs to. + * [...] + * The space characters, for the purposes of this specification, + * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab), + * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and + * U+000D CARRIAGE RETURN (CR). + */ + $language = substr($infostring, 0, strcspn($infostring, " \t\n\f\r")); + + $Element['attributes'] = array('class' => "language-$language"); + } + + $Block = array( + 'char' => $marker, + 'openerLength' => $openerLength, + 'element' => array( + 'name' => 'pre', + 'element' => $Element, + ), + ); + + return $Block; + } + + protected function blockFencedCodeContinue($Line, $Block) + { + if (isset($Block['complete'])) + { + return; + } + + if (isset($Block['interrupted'])) + { + $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); + + unset($Block['interrupted']); + } + + if (($len = strspn($Line['text'], $Block['char'])) >= $Block['openerLength'] + and chop(substr($Line['text'], $len), ' ') === '' + ) { + $Block['element']['element']['text'] = substr($Block['element']['element']['text'], 1); + + $Block['complete'] = true; + + return $Block; + } + + $Block['element']['element']['text'] .= "\n" . $Line['body']; + + return $Block; + } + + protected function blockFencedCodeComplete($Block) + { + return $Block; + } + + # + # Header + + protected function blockHeader($Line) + { + $level = strspn($Line['text'], '#'); + + if ($level > 6) + { + return; + } + + $text = trim($Line['text'], '#'); + + if ($this->strictMode and isset($text[0]) and $text[0] !== ' ') + { + return; + } + + $text = trim($text, ' '); + + $Block = array( + 'element' => array( + 'name' => 'h' . $level, + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $text, + 'destination' => 'elements', + ) + ), + ); + + return $Block; + } + + # + # List + + protected function blockList($Line, array $CurrentBlock = null) + { + list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]{1,9}+[.\)]'); + + if (preg_match('/^('.$pattern.'([ ]++|$))(.*+)/', $Line['text'], $matches)) + { + $contentIndent = strlen($matches[2]); + + if ($contentIndent >= 5) + { + $contentIndent -= 1; + $matches[1] = substr($matches[1], 0, -$contentIndent); + $matches[3] = str_repeat(' ', $contentIndent) . $matches[3]; + } + elseif ($contentIndent === 0) + { + $matches[1] .= ' '; + } + + $markerWithoutWhitespace = strstr($matches[1], ' ', true); + + $Block = array( + 'indent' => $Line['indent'], + 'pattern' => $pattern, + 'data' => array( + 'type' => $name, + 'marker' => $matches[1], + 'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : substr($markerWithoutWhitespace, -1)), + ), + 'element' => array( + 'name' => $name, + 'elements' => array(), + ), + ); + $Block['data']['markerTypeRegex'] = preg_quote($Block['data']['markerType'], '/'); + + if ($name === 'ol') + { + $listStart = ltrim(strstr($matches[1], $Block['data']['markerType'], true), '0') ?: '0'; + + if ($listStart !== '1') + { + if ( + isset($CurrentBlock) + and $CurrentBlock['type'] === 'Paragraph' + and ! isset($CurrentBlock['interrupted']) + ) { + return; + } + + $Block['element']['attributes'] = array('start' => $listStart); + } + } + + $Block['li'] = array( + 'name' => 'li', + 'handler' => array( + 'function' => 'li', + 'argument' => !empty($matches[3]) ? array($matches[3]) : array(), + 'destination' => 'elements' + ) + ); + + $Block['element']['elements'] []= & $Block['li']; + + return $Block; + } + } + + protected function blockListContinue($Line, array $Block) + { + if (isset($Block['interrupted']) and empty($Block['li']['handler']['argument'])) + { + return null; + } + + $requiredIndent = ($Block['indent'] + strlen($Block['data']['marker'])); + + if ($Line['indent'] < $requiredIndent + and ( + ( + $Block['data']['type'] === 'ol' + and preg_match('/^[0-9]++'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) + ) or ( + $Block['data']['type'] === 'ul' + and preg_match('/^'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) + ) + ) + ) { + if (isset($Block['interrupted'])) + { + $Block['li']['handler']['argument'] []= ''; + + $Block['loose'] = true; + + unset($Block['interrupted']); + } + + unset($Block['li']); + + $text = isset($matches[1]) ? $matches[1] : ''; + + $Block['indent'] = $Line['indent']; + + $Block['li'] = array( + 'name' => 'li', + 'handler' => array( + 'function' => 'li', + 'argument' => array($text), + 'destination' => 'elements' + ) + ); + + $Block['element']['elements'] []= & $Block['li']; + + return $Block; + } + elseif ($Line['indent'] < $requiredIndent and $this->blockList($Line)) + { + return null; + } + + if ($Line['text'][0] === '[' and $this->blockReference($Line)) + { + return $Block; + } + + if ($Line['indent'] >= $requiredIndent) + { + if (isset($Block['interrupted'])) + { + $Block['li']['handler']['argument'] []= ''; + + $Block['loose'] = true; + + unset($Block['interrupted']); + } + + $text = substr($Line['body'], $requiredIndent); + + $Block['li']['handler']['argument'] []= $text; + + return $Block; + } + + if ( ! isset($Block['interrupted'])) + { + $text = preg_replace('/^[ ]{0,'.$requiredIndent.'}+/', '', $Line['body']); + + $Block['li']['handler']['argument'] []= $text; + + return $Block; + } + } + + protected function blockListComplete(array $Block) + { + if (isset($Block['loose'])) + { + foreach ($Block['element']['elements'] as &$li) + { + if (end($li['handler']['argument']) !== '') + { + $li['handler']['argument'] []= ''; + } + } + } + + return $Block; + } + + # + # Quote + + protected function blockQuote($Line) + { + if (preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) + { + $Block = array( + 'element' => array( + 'name' => 'blockquote', + 'handler' => array( + 'function' => 'linesElements', + 'argument' => (array) $matches[1], + 'destination' => 'elements', + ) + ), + ); + + return $Block; + } + } + + protected function blockQuoteContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) + { + return; + } + + if ($Line['text'][0] === '>' and preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) + { + $Block['element']['handler']['argument'] []= $matches[1]; + + return $Block; + } + + if ( ! isset($Block['interrupted'])) + { + $Block['element']['handler']['argument'] []= $Line['text']; + + return $Block; + } + } + + # + # Rule + + protected function blockRule($Line) + { + $marker = $Line['text'][0]; + + if (substr_count($Line['text'], $marker) >= 3 and chop($Line['text'], " $marker") === '') + { + $Block = array( + 'element' => array( + 'name' => 'hr', + ), + ); + + return $Block; + } + } + + # + # Setext + + protected function blockSetextHeader($Line, array $Block = null) + { + if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) + { + return; + } + + if ($Line['indent'] < 4 and chop(chop($Line['text'], ' '), $Line['text'][0]) === '') + { + $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; + + return $Block; + } + } + + # + # Markup + + protected function blockMarkup($Line) + { + if ($this->markupEscaped or $this->safeMode) + { + return; + } + + if (preg_match('/^<[\/]?+(\w*)(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+(\/)?>/', $Line['text'], $matches)) + { + $element = strtolower($matches[1]); + + if (in_array($element, $this->textLevelElements)) + { + return; + } + + $Block = array( + 'name' => $matches[1], + 'element' => array( + 'rawHtml' => $Line['text'], + 'autobreak' => true, + ), + ); + + return $Block; + } + } + + protected function blockMarkupContinue($Line, array $Block) + { + if (isset($Block['closed']) or isset($Block['interrupted'])) + { + return; + } + + $Block['element']['rawHtml'] .= "\n" . $Line['body']; + + return $Block; + } + + # + # Reference + + protected function blockReference($Line) + { + if (strpos($Line['text'], ']') !== false + and preg_match('/^\[(.+?)\]:[ ]*+?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $Line['text'], $matches) + ) { + $id = strtolower($matches[1]); + + $Data = array( + 'url' => $matches[2], + 'title' => isset($matches[3]) ? $matches[3] : null, + ); + + $this->DefinitionData['Reference'][$id] = $Data; + + $Block = array( + 'element' => array(), + ); + + return $Block; + } + } + + # + # Table + + protected function blockTable($Line, array $Block = null) + { + if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) + { + return; + } + + if ( + strpos($Block['element']['handler']['argument'], '|') === false + and strpos($Line['text'], '|') === false + and strpos($Line['text'], ':') === false + or strpos($Block['element']['handler']['argument'], "\n") !== false + ) { + return; + } + + if (chop($Line['text'], ' -:|') !== '') + { + return; + } + + $alignments = array(); + + $divider = $Line['text']; + + $divider = trim($divider); + $divider = trim($divider, '|'); + + $dividerCells = explode('|', $divider); + + foreach ($dividerCells as $dividerCell) + { + $dividerCell = trim($dividerCell); + + if ($dividerCell === '') + { + return; + } + + $alignment = null; + + if ($dividerCell[0] === ':') + { + $alignment = 'left'; + } + + if (substr($dividerCell, - 1) === ':') + { + $alignment = $alignment === 'left' ? 'center' : 'right'; + } + + $alignments []= $alignment; + } + + # ~ + + $HeaderElements = array(); + + $header = $Block['element']['handler']['argument']; + + $header = trim($header); + $header = trim($header, '|'); + + $headerCells = explode('|', $header); + + if (count($headerCells) !== count($alignments)) + { + return; + } + + foreach ($headerCells as $index => $headerCell) + { + $headerCell = trim($headerCell); + + $HeaderElement = array( + 'name' => 'th', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $headerCell, + 'destination' => 'elements', + ) + ); + + if (isset($alignments[$index])) + { + $alignment = $alignments[$index]; + + $HeaderElement['attributes'] = array( + 'style' => "text-align: $alignment;", + ); + } + + $HeaderElements []= $HeaderElement; + } + + # ~ + + $Block = array( + 'alignments' => $alignments, + 'identified' => true, + 'element' => array( + 'name' => 'table', + 'elements' => array(), + ), + ); + + $Block['element']['elements'] []= array( + 'name' => 'thead', + ); + + $Block['element']['elements'] []= array( + 'name' => 'tbody', + 'elements' => array(), + ); + + $Block['element']['elements'][0]['elements'] []= array( + 'name' => 'tr', + 'elements' => $HeaderElements, + ); + + return $Block; + } + + protected function blockTableContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) + { + return; + } + + if (count($Block['alignments']) === 1 or $Line['text'][0] === '|' or strpos($Line['text'], '|')) + { + $Elements = array(); + + $row = $Line['text']; + + $row = trim($row); + $row = trim($row, '|'); + + preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches); + + $cells = array_slice($matches[0], 0, count($Block['alignments'])); + + foreach ($cells as $index => $cell) + { + $cell = trim($cell); + + $Element = array( + 'name' => 'td', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $cell, + 'destination' => 'elements', + ) + ); + + if (isset($Block['alignments'][$index])) + { + $Element['attributes'] = array( + 'style' => 'text-align: ' . $Block['alignments'][$index] . ';', + ); + } + + $Elements []= $Element; + } + + $Element = array( + 'name' => 'tr', + 'elements' => $Elements, + ); + + $Block['element']['elements'][1]['elements'] []= $Element; + + return $Block; + } + } + + # + # ~ + # + + protected function paragraph($Line) + { + return array( + 'type' => 'Paragraph', + 'element' => array( + 'name' => 'p', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $Line['text'], + 'destination' => 'elements', + ), + ), + ); + } + + protected function paragraphContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) + { + return; + } + + $Block['element']['handler']['argument'] .= "\n".$Line['text']; + + return $Block; + } + + # + # Inline Elements + # + + protected $InlineTypes = array( + '!' => array('Image'), + '&' => array('SpecialCharacter'), + '*' => array('Emphasis'), + ':' => array('Url'), + '<' => array('UrlTag', 'EmailTag', 'Markup'), + '[' => array('Link'), + '_' => array('Emphasis'), + '`' => array('Code'), + '~' => array('Strikethrough'), + '\\' => array('EscapeSequence'), + ); + + # ~ + + protected $inlineMarkerList = '!*_&[:<`~\\'; + + # + # ~ + # + + public function line($text, $nonNestables = array()) + { + return $this->elements($this->lineElements($text, $nonNestables)); + } + + protected function lineElements($text, $nonNestables = array()) + { + # standardize line breaks + $text = str_replace(array("\r\n", "\r"), "\n", $text); + + $Elements = array(); + + $nonNestables = (empty($nonNestables) + ? array() + : array_combine($nonNestables, $nonNestables) + ); + + # $excerpt is based on the first occurrence of a marker + + while ($excerpt = strpbrk($text, $this->inlineMarkerList)) + { + $marker = $excerpt[0]; + + $markerPosition = strlen($text) - strlen($excerpt); + + $Excerpt = array('text' => $excerpt, 'context' => $text); + + foreach ($this->InlineTypes[$marker] as $inlineType) + { + # check to see if the current inline type is nestable in the current context + + if (isset($nonNestables[$inlineType])) + { + continue; + } + + $Inline = $this->{"inline$inlineType"}($Excerpt); + + if ( ! isset($Inline)) + { + continue; + } + + # makes sure that the inline belongs to "our" marker + + if (isset($Inline['position']) and $Inline['position'] > $markerPosition) + { + continue; + } + + # sets a default inline position + + if ( ! isset($Inline['position'])) + { + $Inline['position'] = $markerPosition; + } + + # cause the new element to 'inherit' our non nestables + + + $Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables']) + ? array_merge($Inline['element']['nonNestables'], $nonNestables) + : $nonNestables + ; + + # the text that comes before the inline + $unmarkedText = substr($text, 0, $Inline['position']); + + # compile the unmarked text + $InlineText = $this->inlineText($unmarkedText); + $Elements[] = $InlineText['element']; + + # compile the inline + $Elements[] = $this->extractElement($Inline); + + # remove the examined text + $text = substr($text, $Inline['position'] + $Inline['extent']); + + continue 2; + } + + # the marker does not belong to an inline + + $unmarkedText = substr($text, 0, $markerPosition + 1); + + $InlineText = $this->inlineText($unmarkedText); + $Elements[] = $InlineText['element']; + + $text = substr($text, $markerPosition + 1); + } + + $InlineText = $this->inlineText($text); + $Elements[] = $InlineText['element']; + + foreach ($Elements as &$Element) + { + if ( ! isset($Element['autobreak'])) + { + $Element['autobreak'] = false; + } + } + + return $Elements; + } + + # + # ~ + # + + protected function inlineText($text) + { + $Inline = array( + 'extent' => strlen($text), + 'element' => array(), + ); + + $Inline['element']['elements'] = self::pregReplaceElements( + $this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/', + array( + array('name' => 'br'), + array('text' => "\n"), + ), + $text + ); + + return $Inline; + } + + protected function inlineCode($Excerpt) + { + $marker = $Excerpt['text'][0]; + + if (preg_match('/^(['.$marker.']++)[ ]*+(.+?)[ ]*+(? strlen($matches[0]), + 'element' => array( + 'name' => 'code', + 'text' => $text, + ), + ); + } + } + + protected function inlineEmailTag($Excerpt) + { + $hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?'; + + $commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@' + . $hostnameLabel . '(?:\.' . $hostnameLabel . ')*'; + + if (strpos($Excerpt['text'], '>') !== false + and preg_match("/^<((mailto:)?$commonMarkEmail)>/i", $Excerpt['text'], $matches) + ){ + $url = $matches[1]; + + if ( ! isset($matches[2])) + { + $url = "mailto:$url"; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $matches[1], + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + protected function inlineEmphasis($Excerpt) + { + if ( ! isset($Excerpt['text'][1])) + { + return; + } + + $marker = $Excerpt['text'][0]; + + if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) + { + $emphasis = 'strong'; + } + elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) + { + $emphasis = 'em'; + } + else + { + return; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => $emphasis, + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $matches[1], + 'destination' => 'elements', + ) + ), + ); + } + + protected function inlineEscapeSequence($Excerpt) + { + if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) + { + return array( + 'element' => array('rawHtml' => $Excerpt['text'][1]), + 'extent' => 2, + ); + } + } + + protected function inlineImage($Excerpt) + { + if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') + { + return; + } + + $Excerpt['text']= substr($Excerpt['text'], 1); + + $Link = $this->inlineLink($Excerpt); + + if ($Link === null) + { + return; + } + + $Inline = array( + 'extent' => $Link['extent'] + 1, + 'element' => array( + 'name' => 'img', + 'attributes' => array( + 'src' => $Link['element']['attributes']['href'], + 'alt' => $Link['element']['handler']['argument'], + ), + 'autobreak' => true, + ), + ); + + $Inline['element']['attributes'] += $Link['element']['attributes']; + + unset($Inline['element']['attributes']['href']); + + return $Inline; + } + + protected function inlineLink($Excerpt) + { + $Element = array( + 'name' => 'a', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => null, + 'destination' => 'elements', + ), + 'nonNestables' => array('Url', 'Link'), + 'attributes' => array( + 'href' => null, + 'title' => null, + ), + ); + + $extent = 0; + + $remainder = $Excerpt['text']; + + if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) + { + $Element['handler']['argument'] = $matches[1]; + + $extent += strlen($matches[0]); + + $remainder = substr($remainder, $extent); + } + else + { + return; + } + + if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+[)]/', $remainder, $matches)) + { + $Element['attributes']['href'] = $matches[1]; + + if (isset($matches[2])) + { + $Element['attributes']['title'] = substr($matches[2], 1, - 1); + } + + $extent += strlen($matches[0]); + } + else + { + if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) + { + $definition = strlen($matches[1]) ? $matches[1] : $Element['handler']['argument']; + $definition = strtolower($definition); + + $extent += strlen($matches[0]); + } + else + { + $definition = strtolower($Element['handler']['argument']); + } + + if ( ! isset($this->DefinitionData['Reference'][$definition])) + { + return; + } + + $Definition = $this->DefinitionData['Reference'][$definition]; + + $Element['attributes']['href'] = $Definition['url']; + $Element['attributes']['title'] = $Definition['title']; + } + + return array( + 'extent' => $extent, + 'element' => $Element, + ); + } + + protected function inlineMarkup($Excerpt) + { + if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) + { + return; + } + + if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt['text'], $matches)) + { + return array( + 'element' => array('rawHtml' => $matches[0]), + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches)) + { + return array( + 'element' => array('rawHtml' => $matches[0]), + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*+(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+\/?>/s', $Excerpt['text'], $matches)) + { + return array( + 'element' => array('rawHtml' => $matches[0]), + 'extent' => strlen($matches[0]), + ); + } + } + + protected function inlineSpecialCharacter($Excerpt) + { + if (substr($Excerpt['text'], 1, 1) !== ' ' and strpos($Excerpt['text'], ';') !== false + and preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt['text'], $matches) + ) { + return array( + 'element' => array('rawHtml' => '&' . $matches[1] . ';'), + 'extent' => strlen($matches[0]), + ); + } + + return; + } + + protected function inlineStrikethrough($Excerpt) + { + if ( ! isset($Excerpt['text'][1])) + { + return; + } + + if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) + { + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'del', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $matches[1], + 'destination' => 'elements', + ) + ), + ); + } + } + + protected function inlineUrl($Excerpt) + { + if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') + { + return; + } + + if (strpos($Excerpt['context'], 'http') !== false + and preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE) + ) { + $url = $matches[0][0]; + + $Inline = array( + 'extent' => strlen($matches[0][0]), + 'position' => $matches[0][1], + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + + return $Inline; + } + } + + protected function inlineUrlTag($Excerpt) + { + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches)) + { + $url = $matches[1]; + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + # ~ + + protected function unmarkedText($text) + { + $Inline = $this->inlineText($text); + return $this->element($Inline['element']); + } + + # + # Handlers + # + + protected function handle(array $Element) + { + if (isset($Element['handler'])) + { + if (!isset($Element['nonNestables'])) + { + $Element['nonNestables'] = array(); + } + + if (is_string($Element['handler'])) + { + $function = $Element['handler']; + $argument = $Element['text']; + unset($Element['text']); + $destination = 'rawHtml'; + } + else + { + $function = $Element['handler']['function']; + $argument = $Element['handler']['argument']; + $destination = $Element['handler']['destination']; + } + + $Element[$destination] = $this->{$function}($argument, $Element['nonNestables']); + + if ($destination === 'handler') + { + $Element = $this->handle($Element); + } + + unset($Element['handler']); + } + + return $Element; + } + + protected function handleElementRecursive(array $Element) + { + return $this->elementApplyRecursive(array($this, 'handle'), $Element); + } + + protected function handleElementsRecursive(array $Elements) + { + return $this->elementsApplyRecursive(array($this, 'handle'), $Elements); + } + + protected function elementApplyRecursive($closure, array $Element) + { + $Element = call_user_func($closure, $Element); + + if (isset($Element['elements'])) + { + $Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']); + } + elseif (isset($Element['element'])) + { + $Element['element'] = $this->elementApplyRecursive($closure, $Element['element']); + } + + return $Element; + } + + protected function elementApplyRecursiveDepthFirst($closure, array $Element) + { + if (isset($Element['elements'])) + { + $Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']); + } + elseif (isset($Element['element'])) + { + $Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']); + } + + $Element = call_user_func($closure, $Element); + + return $Element; + } + + protected function elementsApplyRecursive($closure, array $Elements) + { + foreach ($Elements as &$Element) + { + $Element = $this->elementApplyRecursive($closure, $Element); + } + + return $Elements; + } + + protected function elementsApplyRecursiveDepthFirst($closure, array $Elements) + { + foreach ($Elements as &$Element) + { + $Element = $this->elementApplyRecursiveDepthFirst($closure, $Element); + } + + return $Elements; + } + + protected function element(array $Element) + { + if ($this->safeMode) + { + $Element = $this->sanitiseElement($Element); + } + + # identity map if element has no handler + $Element = $this->handle($Element); + + $hasName = isset($Element['name']); + + $markup = ''; + + if ($hasName) + { + $markup .= '<' . $Element['name']; + + if (isset($Element['attributes'])) + { + foreach ($Element['attributes'] as $name => $value) + { + if ($value === null) + { + continue; + } + + $markup .= " $name=\"".self::escape($value).'"'; + } + } + } + + $permitRawHtml = false; + + if (isset($Element['text'])) + { + $text = $Element['text']; + } + // very strongly consider an alternative if you're writing an + // extension + elseif (isset($Element['rawHtml'])) + { + $text = $Element['rawHtml']; + + $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode']; + $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode; + } + + $hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']); + + if ($hasContent) + { + $markup .= $hasName ? '>' : ''; + + if (isset($Element['elements'])) + { + $markup .= $this->elements($Element['elements']); + } + elseif (isset($Element['element'])) + { + $markup .= $this->element($Element['element']); + } + else + { + if (!$permitRawHtml) + { + $markup .= self::escape($text, true); + } + else + { + $markup .= $text; + } + } + + $markup .= $hasName ? '' : ''; + } + elseif ($hasName) + { + $markup .= ' />'; + } + + return $markup; + } + + protected function elements(array $Elements) + { + $markup = ''; + + $autoBreak = true; + + foreach ($Elements as $Element) + { + if (empty($Element)) + { + continue; + } + + $autoBreakNext = (isset($Element['autobreak']) + ? $Element['autobreak'] : isset($Element['name']) + ); + // (autobreak === false) covers both sides of an element + $autoBreak = !$autoBreak ? $autoBreak : $autoBreakNext; + + $markup .= ($autoBreak ? "\n" : '') . $this->element($Element); + $autoBreak = $autoBreakNext; + } + + $markup .= $autoBreak ? "\n" : ''; + + return $markup; + } + + # ~ + + protected function li($lines) + { + $Elements = $this->linesElements($lines); + + if ( ! in_array('', $lines) + and isset($Elements[0]) and isset($Elements[0]['name']) + and $Elements[0]['name'] === 'p' + ) { + unset($Elements[0]['name']); + } + + return $Elements; + } + + # + # AST Convenience + # + + /** + * Replace occurrences $regexp with $Elements in $text. Return an array of + * elements representing the replacement. + */ + protected static function pregReplaceElements($regexp, $Elements, $text) + { + $newElements = array(); + + while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE)) + { + $offset = $matches[0][1]; + $before = substr($text, 0, $offset); + $after = substr($text, $offset + strlen($matches[0][0])); + + $newElements[] = array('text' => $before); + + foreach ($Elements as $Element) + { + $newElements[] = $Element; + } + + $text = $after; + } + + $newElements[] = array('text' => $text); + + return $newElements; + } + + # + # Deprecated Methods + # + + function parse($text) + { + $markup = $this->text($text); + + return $markup; + } + + protected function sanitiseElement(array $Element) + { + static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/'; + static $safeUrlNameToAtt = array( + 'a' => 'href', + 'img' => 'src', + ); + + if ( ! isset($Element['name'])) + { + unset($Element['attributes']); + return $Element; + } + + if (isset($safeUrlNameToAtt[$Element['name']])) + { + $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]); + } + + if ( ! empty($Element['attributes'])) + { + foreach ($Element['attributes'] as $att => $val) + { + # filter out badly parsed attribute + if ( ! preg_match($goodAttribute, $att)) + { + unset($Element['attributes'][$att]); + } + # dump onevent attribute + elseif (self::striAtStart($att, 'on')) + { + unset($Element['attributes'][$att]); + } + } + } + + return $Element; + } + + protected function filterUnsafeUrlInAttribute(array $Element, $attribute) + { + foreach ($this->safeLinksWhitelist as $scheme) + { + if (self::striAtStart($Element['attributes'][$attribute], $scheme)) + { + return $Element; + } + } + + $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]); + + return $Element; + } + + # + # Static Methods + # + + protected static function escape($text, $allowQuotes = false) + { + return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8'); + } + + protected static function striAtStart($string, $needle) + { + $len = strlen($needle); + + if ($len > strlen($string)) + { + return false; + } + else + { + return strtolower(substr($string, 0, $len)) === strtolower($needle); + } + } + + static function instance($name = 'default') + { + if (isset(self::$instances[$name])) + { + return self::$instances[$name]; + } + + $instance = new static(); + + self::$instances[$name] = $instance; + + return $instance; + } + + private static $instances = array(); + + # + # Fields + # + + protected $DefinitionData; + + # + # Read-Only + + protected $specialCharacters = array( + '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '~' + ); + + protected $StrongRegex = array( + '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s', + '_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us', + ); + + protected $EmRegex = array( + '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', + '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', + ); + + protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+'; + + protected $voidElements = array( + 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', + ); + + protected $textLevelElements = array( + 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', + 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', + 'i', 'rp', 'del', 'code', 'strike', 'marquee', + 'q', 'rt', 'ins', 'font', 'strong', + 's', 'tt', 'kbd', 'mark', + 'u', 'xm', 'sub', 'nobr', + 'sup', 'ruby', + 'var', 'span', + 'wbr', 'time', + ); +} diff --git a/lib/scm/gitlab.class.php b/lib/scm/gitlab.class.php index c0bab8b334..7aea29306d 100644 --- a/lib/scm/gitlab.class.php +++ b/lib/scm/gitlab.class.php @@ -90,14 +90,10 @@ class gitlab /** * Get files info. * - * The API path requested is: "GET /projects/:id/repository/files/:file_path". - * Known issue of GitLab API: if a '%' in 'file_path', GitLab API will show a error 'file_path should be a valid file path'. - * * @param string $path * @param string $ref * @access public * @return array - * @doc https://docs.gitlab.com/ee/api/repository_files.html */ public function files($path, $ref = 'master') { diff --git a/module/action/config.php b/module/action/config.php index a032f196ab..54d57c603e 100755 --- a/module/action/config.php +++ b/module/action/config.php @@ -1,37 +1,39 @@ action->objectNameFields['product'] = 'name'; -$config->action->objectNameFields['story'] = 'title'; -$config->action->objectNameFields['productplan'] = 'title'; -$config->action->objectNameFields['release'] = 'name'; -$config->action->objectNameFields['program'] = 'name'; -$config->action->objectNameFields['project'] = 'name'; -$config->action->objectNameFields['execution'] = 'name'; -$config->action->objectNameFields['task'] = 'name'; -$config->action->objectNameFields['build'] = 'name'; -$config->action->objectNameFields['bug'] = 'title'; -$config->action->objectNameFields['testcase'] = 'title'; -$config->action->objectNameFields['case'] = 'title'; -$config->action->objectNameFields['testtask'] = 'name'; -$config->action->objectNameFields['user'] = 'account'; -$config->action->objectNameFields['api'] = 'title'; -$config->action->objectNameFields['doc'] = 'title'; -$config->action->objectNameFields['doclib'] = 'name'; -$config->action->objectNameFields['todo'] = 'name'; -$config->action->objectNameFields['branch'] = 'name'; -$config->action->objectNameFields['module'] = 'name'; -$config->action->objectNameFields['testsuite'] = 'name'; -$config->action->objectNameFields['caselib'] = 'name'; -$config->action->objectNameFields['testreport'] = 'title'; -$config->action->objectNameFields['entry'] = 'name'; -$config->action->objectNameFields['webhook'] = 'name'; -$config->action->objectNameFields['risk'] = 'name'; -$config->action->objectNameFields['issue'] = 'title'; -$config->action->objectNameFields['design'] = 'name'; -$config->action->objectNameFields['stakeholder'] = 'user'; -$config->action->objectNameFields['budget'] = 'name'; -$config->action->objectNameFields['job'] = 'name'; -$config->action->objectNameFields['team'] = 'name'; -$config->action->objectNameFields['pipeline'] = 'name'; +$config->action->objectNameFields['product'] = 'name'; +$config->action->objectNameFields['story'] = 'title'; +$config->action->objectNameFields['productplan'] = 'title'; +$config->action->objectNameFields['release'] = 'name'; +$config->action->objectNameFields['program'] = 'name'; +$config->action->objectNameFields['project'] = 'name'; +$config->action->objectNameFields['execution'] = 'name'; +$config->action->objectNameFields['task'] = 'name'; +$config->action->objectNameFields['build'] = 'name'; +$config->action->objectNameFields['bug'] = 'title'; +$config->action->objectNameFields['testcase'] = 'title'; +$config->action->objectNameFields['case'] = 'title'; +$config->action->objectNameFields['testtask'] = 'name'; +$config->action->objectNameFields['user'] = 'account'; +$config->action->objectNameFields['api'] = 'title'; +$config->action->objectNameFields['doc'] = 'title'; +$config->action->objectNameFields['doclib'] = 'name'; +$config->action->objectNameFields['todo'] = 'name'; +$config->action->objectNameFields['branch'] = 'name'; +$config->action->objectNameFields['module'] = 'name'; +$config->action->objectNameFields['testsuite'] = 'name'; +$config->action->objectNameFields['caselib'] = 'name'; +$config->action->objectNameFields['testreport'] = 'title'; +$config->action->objectNameFields['entry'] = 'name'; +$config->action->objectNameFields['webhook'] = 'name'; +$config->action->objectNameFields['risk'] = 'name'; +$config->action->objectNameFields['issue'] = 'title'; +$config->action->objectNameFields['design'] = 'name'; +$config->action->objectNameFields['stakeholder'] = 'user'; +$config->action->objectNameFields['budget'] = 'name'; +$config->action->objectNameFields['job'] = 'name'; +$config->action->objectNameFields['team'] = 'name'; +$config->action->objectNameFields['pipeline'] = 'name'; +$config->action->objectNameFields['kanbancolumn'] = 'name'; +$config->action->objectNameFields['kanbanlane'] = 'name'; $config->action->commonImgSize = 870; @@ -46,5 +48,5 @@ $config->action->majorList['project'] = array('opened', 'edited'); $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,case,testtask,testreport,doc,doclib,issue,risk,opportunity,trainplan,gapanalysis,team,whitelist,researchplan,researchreport,meeting,branch,'; +$config->action->needGetRelateField = ',story,productplan,release,task,build,bug,case,testtask,testreport,doc,doclib,issue,risk,opportunity,trainplan,gapanalysis,team,whitelist,researchplan,researchreport,meeting,kanbanlane,kanbancolumn,'; $config->action->noLinkModules = ',doclib,module,webhook,gitlab,pipeline,jenkins,'; diff --git a/module/action/lang/en.php b/module/action/lang/en.php index 226eed0e12..3c6653965a 100755 --- a/module/action/lang/en.php +++ b/module/action/lang/en.php @@ -369,6 +369,10 @@ $lang->action->dynamicAction->execution['undeleted'] = 'Restore ' . $lang->execu $lang->action->dynamicAction->execution['hidden'] = 'Hide ' . $lang->executionCommon; $lang->action->dynamicAction->execution['moved'] = 'Improt Task'; +$lang->action->dynamicAction->kanbancolumn['edited'] = 'Column Settings'; +$lang->action->dynamicAction->kanbanlane['edited'] = 'Swimlane Settings'; +$lang->action->dynamicAction->kanbanlane['moved'] = 'Move Swimlane'; + $lang->action->dynamicAction->team['managedTeam'] = 'Manage Team'; $lang->action->dynamicAction->task['opened'] = 'Create Task'; @@ -500,27 +504,28 @@ else { $lang->action->label->execution = "$lang->executionCommon|execution|task|executionID=%s"; } -$lang->action->label->task = 'Task|task|view|taskID=%s'; -$lang->action->label->build = 'Build|build|view|buildID=%s'; -$lang->action->label->bug = 'Bug|bug|view|bugID=%s'; -$lang->action->label->case = 'Case|testcase|view|caseID=%s'; -$lang->action->label->testtask = 'Request|testtask|view|caseID=%s'; -$lang->action->label->testsuite = 'Test Suite|testsuite|view|suiteID=%s'; -$lang->action->label->caselib = 'Case Library|caselib|view|libID=%s'; -$lang->action->label->todo = 'Todo|todo|view|todoID=%s'; -$lang->action->label->doclib = 'Doc Library|doc|objectLibs|type=%s&objectID=%s&libID=%s&docID=&version=&appendLib=%s'; -$lang->action->label->doc = 'Doc|doc|view|docID=%s'; -$lang->action->label->user = 'User|user|view|account=%s'; -$lang->action->label->testreport = 'Report|testreport|view|report=%s'; -$lang->action->label->entry = 'Application|entry|browse|'; -$lang->action->label->webhook = 'Webhook|webhook|browse|'; -$lang->action->label->space = ' '; -$lang->action->label->risk = 'Risk|risk|view|riskID=%s'; -$lang->action->label->issue = 'Issue|issue|view|issueID=%s'; -$lang->action->label->design = 'Design|design|view|designID=%s'; -$lang->action->label->stakeholder = 'Stakeholder|stakeholder|view|userID=%s'; -$lang->action->label->api = 'Interface|api|index|libID=%s&moduleID=%s&apiID=%s'; -$lang->action->label->branch = 'Branch|branch|manage|prouctID=%s&browseType=all'; +$lang->action->label->task = 'Task|task|view|taskID=%s'; +$lang->action->label->build = 'Build|build|view|buildID=%s'; +$lang->action->label->bug = 'Bug|bug|view|bugID=%s'; +$lang->action->label->case = 'Case|testcase|view|caseID=%s'; +$lang->action->label->testtask = 'Request|testtask|view|caseID=%s'; +$lang->action->label->testsuite = 'Test Suite|testsuite|view|suiteID=%s'; +$lang->action->label->caselib = 'Case Library|caselib|view|libID=%s'; +$lang->action->label->todo = 'Todo|todo|view|todoID=%s'; +$lang->action->label->doclib = 'Doc Library|doc|objectLibs|type=%s&objectID=%s&libID=%s'; +$lang->action->label->doc = 'Doc|doc|view|docID=%s'; +$lang->action->label->user = 'User|user|view|account=%s'; +$lang->action->label->testreport = 'Report|testreport|view|report=%s'; +$lang->action->label->entry = 'Application|entry|browse|'; +$lang->action->label->webhook = 'Webhook|webhook|browse|'; +$lang->action->label->space = ' '; +$lang->action->label->risk = 'Risk|risk|view|riskID=%s'; +$lang->action->label->issue = 'Issue|issue|view|issueID=%s'; +$lang->action->label->design = 'Design|design|view|designID=%s'; +$lang->action->label->stakeholder = 'Stakeholder|stakeholder|view|userID=%s'; +$lang->action->label->api = 'Interface|api|index|libID=%s&moduleID=%s&apiID=%s'; +$lang->action->label->kanbancolumn = 'Kanban column|execution|kanban|execution=%s'; +$lang->action->label->kanbanlane = 'Kanban Lane|execution|kanban|execution=%s&type=all'; /* Object type. */ $lang->action->search = new stdclass(); diff --git a/module/action/lang/zh-cn.php b/module/action/lang/zh-cn.php index 11744bc835..1b67311e4f 100755 --- a/module/action/lang/zh-cn.php +++ b/module/action/lang/zh-cn.php @@ -29,6 +29,9 @@ $lang->action->url = '网址'; $lang->action->contentType = '内容类型'; $lang->action->data = '数据'; $lang->action->result = '结果'; +$lang->action->modified = '修改了'; +$lang->action->old = '旧值为'; +$lang->action->new = '新值为'; $lang->action->trash = '回收站'; $lang->action->undelete = '还原'; @@ -369,6 +372,10 @@ $lang->action->dynamicAction->execution['undeleted'] = '还原' . $lang->executi $lang->action->dynamicAction->execution['hidden'] = '隐藏' . $lang->executionCommon; $lang->action->dynamicAction->execution['moved'] = '导入任务'; +$lang->action->dynamicAction->kanbancolumn['edited'] = '设置看板列'; +$lang->action->dynamicAction->kanbanlane['edited'] = '设置泳道'; +$lang->action->dynamicAction->kanbanlane['moved'] = '移动泳道'; + $lang->action->dynamicAction->team['managedTeam'] = '维护团队'; $lang->action->dynamicAction->task['opened'] = '创建任务'; @@ -500,27 +507,28 @@ else { $lang->action->label->execution = "$lang->executionCommon|execution|task|executionID=%s"; } -$lang->action->label->task = '任务|task|view|taskID=%s'; -$lang->action->label->build = '版本|build|view|buildID=%s'; -$lang->action->label->bug = 'Bug|bug|view|bugID=%s'; -$lang->action->label->case = '用例|testcase|view|caseID=%s'; -$lang->action->label->testtask = '测试单|testtask|view|caseID=%s'; -$lang->action->label->testsuite = '测试套件|testsuite|view|suiteID=%s'; -$lang->action->label->caselib = '用例库|caselib|view|libID=%s'; -$lang->action->label->todo = '待办|todo|view|todoID=%s'; -$lang->action->label->doclib = '文档库|doc|tablecontents|type=%s&objectID=%s&libID=%s'; -$lang->action->label->doc = '文档|doc|view|docID=%s'; -$lang->action->label->user = '用户|user|view|account=%s'; -$lang->action->label->testreport = '报告|testreport|view|report=%s'; -$lang->action->label->entry = '应用|entry|browse|'; -$lang->action->label->webhook = 'Webhook|webhook|browse|'; -$lang->action->label->space = ' '; -$lang->action->label->risk = '风险|risk|view|riskID=%s'; -$lang->action->label->issue = '问题|issue|view|issueID=%s'; -$lang->action->label->design = '设计|design|view|designID=%s'; -$lang->action->label->stakeholder = '干系人|stakeholder|view|userID=%s'; -$lang->action->label->api = '接口|api|index|libID=%s&moduleID=%s&apiID=%s'; -$lang->action->label->branch = '分支|branch|manage|productID=%s&browseType=all'; +$lang->action->label->task = '任务|task|view|taskID=%s'; +$lang->action->label->build = '版本|build|view|buildID=%s'; +$lang->action->label->bug = 'Bug|bug|view|bugID=%s'; +$lang->action->label->case = '用例|testcase|view|caseID=%s'; +$lang->action->label->testtask = '测试单|testtask|view|caseID=%s'; +$lang->action->label->testsuite = '测试套件|testsuite|view|suiteID=%s'; +$lang->action->label->caselib = '用例库|caselib|view|libID=%s'; +$lang->action->label->todo = '待办|todo|view|todoID=%s'; +$lang->action->label->doclib = '文档库|doc|tablecontents|type=%s&objectID=%s&libID=%s'; +$lang->action->label->doc = '文档|doc|view|docID=%s'; +$lang->action->label->user = '用户|user|view|account=%s'; +$lang->action->label->testreport = '报告|testreport|view|report=%s'; +$lang->action->label->entry = '应用|entry|browse|'; +$lang->action->label->webhook = 'Webhook|webhook|browse|'; +$lang->action->label->space = ' '; +$lang->action->label->risk = '风险|risk|view|riskID=%s'; +$lang->action->label->issue = '问题|issue|view|issueID=%s'; +$lang->action->label->design = '设计|design|view|designID=%s'; +$lang->action->label->stakeholder = '干系人|stakeholder|view|userID=%s'; +$lang->action->label->api = '接口|api|index|libID=%s&moduleID=%s&apiID=%s'; +$lang->action->label->kanbancolumn = '看板列|execution|kanban|execution=%s'; +$lang->action->label->kanbanlane = '看板泳道|execution|kanban|execution=%s&type=all'; /* Object type. */ $lang->action->search = new stdclass(); diff --git a/module/action/model.php b/module/action/model.php index 98a4eca807..ad7a2819ac 100755 --- a/module/action/model.php +++ b/module/action/model.php @@ -66,8 +66,6 @@ class actionModel extends model $action->product = $relation['product']; $action->project = (int)$relation['project']; $action->execution = (int)$relation['execution']; - - $this->dao->insert(TABLE_ACTION)->data($action)->autoCheck()->exec(); $actionID = $this->dao->lastInsertID(); @@ -190,7 +188,7 @@ class actionModel extends model $fields = '*'; if(strpos('story, productplan, case, branch', $objectType) !== false) $fields = 'product'; if(strpos('build, bug, testtask, doc', $objectType) !== false) $fields = 'product, project, execution'; - if(strpos('case, repo', $objectType) !== false) $fields = 'execution'; + if(strpos('case, repo, kanbanlane', $objectType) !== false) $fields = 'execution'; if($objectType == 'release') $fields = 'product, build'; if($objectType == 'task') $fields = 'project, execution, story'; @@ -199,6 +197,8 @@ class actionModel extends model /* Process story, release and task. */ if($objectType == 'story') $record->project = $this->dao->select('project')->from(TABLE_PROJECTSTORY)->where('story')->eq($objectID)->orderBy('project_desc')->limit(1)->fetch('project'); if($objectType == 'release') $record->project = $this->dao->select('project')->from(TABLE_BUILD)->where('id')->eq($record->build)->fetch('project'); + if($objectType == 'kanbanlane') $record->execution = $this->dao->select($fields)->from(TABLE_KANBANLANE)->where('id')->eq($objectID)->fetch('execution'); + if($objectType == 'kanbancolumn') $record->execution = $extra; if($objectType == 'team') { $team = $this->dao->select('type')->from(TABLE_PROJECT)->where('id')->eq($objectID)->fetch(); @@ -818,7 +818,7 @@ class actionModel extends model if(!$actions) return array(); $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'action'); - return $this->transformActions($actions);; + return $this->transformActions($actions); } /** @@ -1199,6 +1199,10 @@ class actionModel extends model { $params = sprintf($vars, trim($action->product, ',')); } + elseif($action->objectType == 'kanbancolumn' or $action->objectType == 'kanbanlane') + { + $params = sprintf($vars, $action->extra); + } else { $params = sprintf($vars, $action->objectID); @@ -1233,6 +1237,11 @@ class actionModel extends model if($action->objectType == 'stakeholder' and $action->project == 0) $action->objectLink = ''; + if($action->objectType == 'story' and $action->action == 'import2storylib') + { + $action->objectLink = helper::createLink('assetlib', 'storyView', "storyID=$action->objectID"); + } + return $action; } @@ -1436,10 +1445,11 @@ class actionModel extends model * @param array $actions * @param string $direction * @param string $type all|today|yesterday|thisweek|lastweek|thismonth|lastmonth + * @param string $orderBy date_desc|date_asc * @access public * @return array */ - public function buildDateGroup($actions, $direction = 'next', $type = 'today') + public function buildDateGroup($actions, $direction = 'next', $type = 'today', $orderBy = 'date_desc') { $dateGroup = array(); foreach($actions as $action) @@ -1467,7 +1477,17 @@ class actionModel extends model } } - if($direction != 'next') $dateGroup = array_reverse($dateGroup); + /* Modify date to the corrret order. */ + if($this->app->rawModule != 'company' and $direction != 'next') + { + $dateGroup = array_reverse($dateGroup); + } + elseif($this->app->rawModule == 'company' and (($direction == 'next' and $orderBy == 'date_asc') or ($direction == 'pre' and $orderBy == 'date_desc'))) + { + $dateGroup = array_reverse($dateGroup); + foreach($dateGroup as $key => $dateItem) $dateGroup[$key] = array_reverse($dateItem); + } + return $dateGroup; } diff --git a/module/api/control.php b/module/api/control.php index d419fa54db..2a6d71a760 100755 --- a/module/api/control.php +++ b/module/api/control.php @@ -67,7 +67,7 @@ class api extends control $this->view->apiList = $apiList; } - $this->setMenu($libID, $moduleID); + $this->setMenu($libID); $this->view->isRelease = $release > 0; $this->view->release = $release; @@ -435,21 +435,17 @@ class api extends control $this->setMenu($api->lib); - $example = array('example' => 'type,description'); - $example = json_encode($example, JSON_PRETTY_PRINT); - $options = array(); foreach($this->lang->api->paramsTypeOptions as $key => $item) { $options[] = array('label' => $item, 'value' => $key); } - $this->view->typeOptions = $options; - $this->view->gobackLink = $this->createLink('api', 'index', "libID={$api->lib}&moduleID={$api->module}"); - $this->view->user = $this->app->user->account; - $this->view->allUsers = $this->loadModel('user')->getPairs('devfirst|noclosed');; + $this->view->typeOptions = $options; + $this->view->gobackLink = $this->createLink('api', 'index', "libID={$api->lib}&moduleID={$api->module}"); + $this->view->user = $this->app->user->account; + $this->view->allUsers = $this->loadModel('user')->getPairs('devfirst|noclosed');; $this->view->moduleOptionMenu = $this->loadModel('tree')->getOptionMenu($api->lib, 'api', $startModuleID = 0); $this->view->moduleID = $api->module ? (int)$api->module : (int)$this->cookie->lastDocModule; - $this->view->example = $example; $this->view->title = $api->title . $this->lang->api->edit; $this->display(); @@ -469,7 +465,6 @@ class api extends control { $now = helper::now(); $params = fixer::input('post') - ->trim('title,path') ->remove('type') ->skipSpecial('params,response') ->add('addedBy', $this->app->user->account) @@ -495,9 +490,6 @@ class api extends control $lib = $this->doc->getLibByID($libID); $libName = isset($lib->name) ? $lib->name . $this->lang->colon : ''; - $example = array('example' => 'type,description'); - $example = json_encode($example, JSON_PRETTY_PRINT); - $this->getTypeOptions($libID); $this->view->gobackLink = $this->createLink('api', 'index', "libID=$libID&moduleID=$moduleID"); $this->view->user = $this->app->user->account; @@ -507,7 +499,6 @@ class api extends control $this->view->moduleOptionMenu = $this->loadModel('tree')->getOptionMenu($libID, 'api', $startModuleID = 0); $this->view->moduleID = $moduleID ? (int)$moduleID : (int)$this->cookie->lastDocModule; $this->view->libs = $libs; - $this->view->example = $example; $this->view->title = $libName . $this->lang->api->create; $this->view->users = $this->user->getPairs('nocode'); @@ -613,11 +604,10 @@ class api extends control * Set doc menu by method name. * * @param int $libID - * @param int $moduleID * @access public * @return void */ - private function setMenu($libID = 0, $moduleID = 0) + private function setMenu($libID = 0) { common::setMenuVars('doc', $libID); @@ -642,7 +632,7 @@ class api extends control if(common::hasPriv('api', 'create')) { $menu .= "
  • "; - $menu .= html::a(helper::createLink('api', 'create', "libID=$libID&moduleID=$moduleID"), " " . $this->lang->api->apiDoc, '', "data-app='{$this->app->tab}'"); + $menu .= html::a(helper::createLink('api', 'create', "libID=$libID"), " " . $this->lang->api->apiDoc, '', "data-app='{$this->app->tab}'"); $menu .= "
  • "; } diff --git a/module/api/lang/en.php b/module/api/lang/en.php index 7209faee4a..80cac3e0ff 100644 --- a/module/api/lang/en.php +++ b/module/api/lang/en.php @@ -14,53 +14,53 @@ $lang->api->common = 'API'; $lang->api->getModel = 'Super Model API'; $lang->api->sql = 'SQL Query API'; -$lang->api->index = 'Api Doc Home'; -$lang->api->editLib = 'Edit Api Doc'; -$lang->api->releases = 'Releases'; +$lang->api->index = 'Home'; +$lang->api->editLib = 'Edit'; +$lang->api->releases = 'Release'; $lang->api->deleteRelease = 'Delete Release'; -$lang->api->deleteLib = 'Delete Api Doc'; +$lang->api->deleteLib = 'Delete API Doc'; $lang->api->createRelease = 'Publish'; -$lang->api->createLib = 'Create Api Library'; -$lang->api->createApi = 'Create Api'; +$lang->api->createLib = 'Create API Library'; +$lang->api->createApi = 'Create API Document'; $lang->api->createAB = 'Create'; $lang->api->edit = 'Edit'; $lang->api->delete = 'Delete'; -$lang->api->position = 'Positions'; +$lang->api->position = 'Position'; $lang->api->startLine = "%s,%s"; $lang->api->desc = 'Description'; $lang->api->debug = 'Debug'; $lang->api->submit = 'Submit'; -$lang->api->url = 'Url'; +$lang->api->url = 'URL'; $lang->api->result = 'Result'; $lang->api->status = 'Status'; $lang->api->data = 'Content'; $lang->api->noParam = 'Get debugging does not require input parameters,'; -$lang->api->noModule = 'There is no directory under the interface library. Please maintain the directory first'; +$lang->api->noModule = 'No directory in the API library. Please add the directory first'; $lang->api->post = 'Please refer to the page form for post debugging'; -$lang->api->noUniqueName = 'Api library name already exists。'; -$lang->api->noUniqueVersion = 'Version already exists。'; +$lang->api->noUniqueName = 'The API library name exists.'; +$lang->api->noUniqueVersion = 'The version exists.'; $lang->api->version = 'Version'; $lang->api->createStruct = 'Create Data Structure'; $lang->api->editStruct = 'Edit Data Structure'; $lang->api->deleteStruct = 'Delete Data Structure'; -$lang->api->create = 'Ceate Doc'; -$lang->api->title = 'Interface Name'; -$lang->api->pageTitle = 'Api Library'; +$lang->api->create = 'Create API'; +$lang->api->title = 'Name'; +$lang->api->pageTitle = 'API Library'; $lang->api->module = 'Directory'; -$lang->api->apiDoc = 'Interface'; +$lang->api->apiDoc = 'API'; $lang->api->manageType = 'Manage Directory'; $lang->api->managePublish = 'Manage Version'; $lang->api->doing = 'Doing'; $lang->api->done = 'Done'; -$lang->api->basicInfo = 'Essential Information'; -$lang->api->apiDesc = 'Interface Description'; -$lang->api->confirmDelete = "Are you sure to delete this interface?"; -$lang->api->confirmDeleteLib = "Are you sure to delete this interface library?"; +$lang->api->basicInfo = 'Basic Information'; +$lang->api->apiDesc = 'Description'; +$lang->api->confirmDelete = "Do you want to delete this API?"; +$lang->api->confirmDeleteLib = "Do you want to delete this interface library?"; $lang->api->filterStruct = "use struct"; $lang->api->defaultVersion = "Current Version"; /* Common access control lang. */ -$lang->api->whiteList = 'White list'; +$lang->api->whiteList = 'Whitelist'; $lang->api->aclList['open'] = 'Open'; $lang->api->aclList['private'] = 'Private'; $lang->api->aclList['custom'] = 'Custom'; @@ -68,9 +68,9 @@ $lang->api->group = 'Group'; $lang->api->user = 'User'; $lang->api->noticeAcl = array( - 'open' => 'Users who can access the api library which the library belongs can access it.', - 'custom' => 'Users in the whiltelist can access it.', - 'private' => 'Only the one who created it can access it.', + 'open' => 'Users who can access the API library can access it.', + 'custom' => 'Users on the whiltelist can access it.', + 'private' => 'Only the one who creates it can access it.', ); /* fields of struct */ @@ -81,7 +81,7 @@ $lang->struct->field = 'Field'; $lang->struct->paramsType = 'Type'; $lang->struct->required = 'Require'; $lang->struct->desc = 'Description'; -$lang->struct->descPlaceholder = 'Parameter description'; +$lang->struct->descPlaceholder = 'Parameter Description'; $lang->struct->action = 'Action'; $lang->struct->addSubField = 'Add Subfield'; @@ -94,36 +94,36 @@ $lang->struct->typeOptions = array( /* fields of form */ $lang->api->struct = 'Data Structure'; -$lang->api->structName = 'Structure Name'; +$lang->api->structName = 'Name'; $lang->api->structType = 'Type'; $lang->api->structAttr = 'Attribute'; -$lang->api->structAddedBy = 'Creator'; -$lang->api->structAddedDate = 'Created Time'; -$lang->api->name = 'Interface Library Name'; -$lang->api->baseUrl = 'Base Url'; -$lang->api->baseUrlDesc = 'Site or path. for example, api.zentao.com or /v1'; +$lang->api->structAddedBy = 'CreatedBy'; +$lang->api->structAddedDate = 'Created'; +$lang->api->name = 'API Library Name'; +$lang->api->baseUrl = 'Base URL'; +$lang->api->baseUrlDesc = 'Site or path, e.g., api.zentao.com or /v1.'; $lang->api->desc = 'Description'; $lang->api->control = 'Access Control'; -$lang->api->noLib = 'There is no interface library at present。'; -$lang->api->noApi = 'There is no interface for the time being。'; -$lang->api->noStruct = 'There is no structure for the time being。'; -$lang->api->lib = 'Interface Library'; -$lang->api->apiList = 'Interface List'; -$lang->api->formTitle = 'Interface Name'; +$lang->api->noLib = 'No API library yet.'; +$lang->api->noApi = 'No API yet.'; +$lang->api->noStruct = 'No API yet.'; +$lang->api->lib = 'API Library'; +$lang->api->apiList = 'API List'; +$lang->api->formTitle = 'API Name'; $lang->api->path = 'Request Path'; -$lang->api->protocol = 'Request Protocol'; -$lang->api->method = 'Request Method'; -$lang->api->requestType = 'Request Type'; -$lang->api->status = 'Development Status'; -$lang->api->owner = 'Person In Charge'; -$lang->api->paramsExample = 'Response Example'; +$lang->api->protocol = 'Protocol'; +$lang->api->method = 'Method'; +$lang->api->requestType = 'Type'; +$lang->api->status = 'Status'; +$lang->api->owner = 'Owner'; +$lang->api->paramsExample = 'Request Example'; $lang->api->header = 'Request Header'; -$lang->api->query = 'Request Parameters'; +$lang->api->query = 'Parameter'; $lang->api->params = 'Request Body'; $lang->api->response = 'Response'; $lang->api->responseExample = 'Response Example'; $lang->api->res = new stdClass(); -$lang->api->res->name = '名称'; +$lang->api->res->name = 'Name'; $lang->api->res->desc = 'Description'; $lang->api->res->type = 'Type'; $lang->api->req = new stdClass(); @@ -198,14 +198,14 @@ $lang->api->allParamsTypeOptions = array_merge($lang->api->paramsTypeOptions, $l $lang->api->requiredOptions = array(0 => 'No', 1 => 'Yes'); $lang->doclib = new stdclass(); -$lang->doclib->name = 'Interface Library Name'; +$lang->doclib->name = 'API Library Name'; $lang->apistruct = new stdClass(); -$lang->apistruct->name = 'Structure Name'; +$lang->apistruct->name = 'Name'; $lang->api_lib_release = new stdClass(); $lang->api_lib_release->version = 'Version'; $lang->api->error = new stdclass(); -$lang->api->error->onlySelect = 'SQL interface only allow SELECT query.'; -$lang->api->error->disabled = 'For security reasons, this feature is disabled. You can go to the config directory and modify the configuration item %s to open this function.'; +$lang->api->error->onlySelect = 'SQL API only allows SELECT query.'; +$lang->api->error->disabled = 'For security reasons, this feature is disabled. Go to the config directory and modify the configuration item %s to enable it.'; diff --git a/module/api/lang/zh-cn.php b/module/api/lang/zh-cn.php index d8946ad167..b5506e0540 100755 --- a/module/api/lang/zh-cn.php +++ b/module/api/lang/zh-cn.php @@ -41,7 +41,7 @@ $lang->api->noUniqueName = '接口库名已存在。'; $lang->api->noUniqueVersion = '版本已存在。'; $lang->api->version = '版本'; $lang->api->createStruct = '创建数据结构'; -$lang->api->editStruct = '编辑数据结构'; +$lang->api->editStruct = '修改数据结构'; $lang->api->deleteStruct = '删除数据结构'; $lang->api->create = '创建接口'; $lang->api->title = '接口名称'; diff --git a/module/api/model.php b/module/api/model.php index 6ba82950aa..47c37c18ae 100644 --- a/module/api/model.php +++ b/module/api/model.php @@ -209,29 +209,28 @@ class apiModel extends model $now = helper::now(); $account = $this->app->user->account; $data = fixer::input('post') + ->remove('type') ->skipSpecial('params,response') ->add('editedBy', $account) ->add('editedDate', $now) - ->add('version', $oldApi->version) ->setDefault('product,module', 0) - ->remove('type') ->get(); - - $changes = common::createChanges($oldApi, $data); - if(!empty($changes)) $data->version = $oldApi->version + 1; + $data->id = $oldApi->id; + $data->version = $oldApi->version + 1; + $apiSpec = $this->getApiSpecByData($data); + + $this->dao->replace(TABLE_API_SPEC)->data($apiSpec)->exec(); + + unset($data->id); $this->dao->update(TABLE_API) ->data($data) ->autoCheck() ->batchCheck($this->config->api->edit->requiredFields, 'notempty') ->where('id')->eq($apiID) ->exec(); - - $data->id = $apiID; - $apiSpec = $this->getApiSpecByData($data); - $this->dao->replace(TABLE_API_SPEC)->data($apiSpec)->exec(); - return $changes; + return common::createChanges($oldApi, $data); } /** diff --git a/module/api/view/content.html.php b/module/api/view/content.html.php index 977e82f581..8dea001a45 100644 --- a/module/api/view/content.html.php +++ b/module/api/view/content.html.php @@ -1,4 +1,4 @@ -
    +
    @@ -95,14 +95,7 @@ $field = ''; for($i = 0; $i < $level; $i++) { - if($i + 1 < $level) - { - $field .= '    '; - } - else - { - $field .= '  ∟  '; - } + $field .= '  ∟  '; } $field .= $data['field']; $str .= '' . $field . ''; diff --git a/module/api/view/create.html.php b/module/api/view/create.html.php index 43cd351874..0bd168a361 100644 --- a/module/api/view/create.html.php +++ b/module/api/view/create.html.php @@ -12,7 +12,6 @@ ?> - struct->paramsType); api->module;?> - + diff --git a/module/api/view/edit.html.php b/module/api/view/edit.html.php index b49d94fa49..6130a06918 100644 --- a/module/api/view/edit.html.php +++ b/module/api/view/edit.html.php @@ -13,7 +13,6 @@ - lib);?>
    -
    api->noModule;?>
    -
    @@ -73,7 +71,7 @@
    -
    +
      diff --git a/module/api/view/releases.html.php b/module/api/view/releases.html.php index 1c1e1a7d69..ca0afb44b6 100644 --- a/module/api/view/releases.html.php +++ b/module/api/view/releases.html.php @@ -41,7 +41,7 @@ addedBy, '');?> addedDate;?> - createLink('api', 'deleteRelease', "libID=$libID&id=$release->id"), '', 'hiddenwin', "title='{$lang->api->deleteRelease}' class='btn'");?> + createLink('api', 'deleteRelease', "libID=$libID&id=$release->id"), '', 'hiddenwin', "title='{$lang->api->delete}' class='btn'");?> diff --git a/module/api/view/struct.html.php b/module/api/view/struct.html.php index f1983a737b..d7f744ab1f 100644 --- a/module/api/view/struct.html.php +++ b/module/api/view/struct.html.php @@ -49,8 +49,8 @@ addedDate;?> createLink('api', 'editStruct', "libID=$libID&structID=$struct->id"), '', '', "title='{$lang->api->editStruct}' class='btn'"); - if(common::hasPriv('api', 'deleteStruct')) echo html::a($this->createLink('api', 'deleteStruct', "libID=$libID&structID=$struct->id"), '', 'hiddenwin', "title='{$lang->api->deleteStruct}' class='btn'"); + if(common::hasPriv('api', 'editStruct')) echo html::a($this->createLink('api', 'editStruct', "libID=$libID&structID=$struct->id"), '', '', "title='{$lang->api->edit}' class='btn'"); + if(common::hasPriv('api', 'deleteStruct')) echo html::a($this->createLink('api', 'deleteStruct', "libID=$libID&structID=$struct->id"), '', 'hiddenwin', "title='{$lang->api->delete}' class='btn'"); ?> diff --git a/module/backup/lang/en.php b/module/backup/lang/en.php index a0139051a0..2b5109c58b 100644 --- a/module/backup/lang/en.php +++ b/module/backup/lang/en.php @@ -8,14 +8,14 @@ $lang->backup->restore = 'Restore'; $lang->backup->change = 'Edit Expiration'; $lang->backup->changeAB = 'Edit'; $lang->backup->rmPHPHeader = 'Remove PHP header'; -$lang->backup->setting = 'Setting'; +$lang->backup->setting = 'Settings'; -$lang->backup->settingAction = 'Backup Setting'; +$lang->backup->settingAction = 'Backup Settings'; $lang->backup->time = 'Date'; $lang->backup->files = 'Files'; -$lang->backup->allCount = 'All Count'; -$lang->backup->count = 'Backup Count'; +$lang->backup->allCount = 'All Files'; +$lang->backup->count = 'Backup Files'; $lang->backup->size = 'Size'; $lang->backup->status = 'Status'; diff --git a/module/block/control.php b/module/block/control.php index bd9add77db..fa837a08ef 100644 --- a/module/block/control.php +++ b/module/block/control.php @@ -230,7 +230,7 @@ class block extends control $commonField = 'common'; if($module == 'project' and $projectID) { - $project = $this->loadModel('project')->getByID($this->session->project); + $project = $this->loadModel('project')->getByID($projectID); $commonField = $project->model . 'common'; } @@ -595,9 +595,15 @@ class block extends control $this->session->set('storyList', $uri, 'product'); $this->session->set('testtaskList', $uri, 'qa'); + $tasks = $this->loadModel('task')->getUserSuspendedTasks($this->app->user->account); foreach($todos as $key => $todo) { - if($todo->date == '2030-01-01') unset($todos[$key]); + if($todo->date == '2030-01-01') + { + unset($todos[$key]); + continue; + } + if($todo->type == 'task' and isset($tasks[$todo->idvalue])) unset($todos[$key]); } $this->view->todos = $todos; @@ -886,7 +892,7 @@ class block extends control } $today = helper::today(); - if(isset($this->config->maxVersion)) $monday = date('Ymd', strtotime($this->loadModel('weekly')->getThisMonday($today))); + $monday = date('Ymd', strtotime($this->loadModel('weekly')->getThisMonday($today))); $tasks = $this->dao->select("project, sum(consumed) as totalConsumed, sum(if(status != 'cancel' and status != 'closed', `left`, 0)) as totalLeft") @@ -906,7 +912,7 @@ class block extends control $project->progress = $project->allStories == 0 ? 0 : round($project->doneStories / $project->allStories, 3) * 100; $project->executions = $this->project->getStats($projectID, 'all', 0, 0, 30, 'id_desc', $pager); } - elseif($project->model == 'waterfall' and isset($this->config->maxVersion)) + elseif($project->model == 'waterfall') { $begin = $project->begin; $weeks = $this->weekly->getWeekPairs($begin); @@ -1703,6 +1709,7 @@ class block extends control $objectCountList += array('risk' => 'riskCount', 'issue' => 'issueCount'); } + $tasks = $this->loadModel('task')->getUserSuspendedTasks($this->app->user->account); foreach($objectCountList as $objectType => $objectCount) { if(!isset($hasViewPriv[$objectType])) continue; @@ -1731,6 +1738,11 @@ class block extends control unset($objects[$key]); continue; } + if($todo->type == 'task' and isset($tasks[$todo->idvalue])) + { + unset($objects[$key]); + continue; + } $todo->begin = date::formatTime($todo->begin); $todo->end = date::formatTime($todo->end); @@ -1741,6 +1753,8 @@ class block extends control { $this->app->loadLang('task'); $this->app->loadLang('execution'); + + $objects = $this->loadModel('task')->getUserTasks($this->app->user->account, 'assignedTo', $limitCount); } if($objectType == 'bug') $this->app->loadLang('bug'); diff --git a/module/block/lang/en.php b/module/block/lang/en.php index 44689051f1..a39d9615cd 100644 --- a/module/block/lang/en.php +++ b/module/block/lang/en.php @@ -105,48 +105,16 @@ $lang->block->spent = 'Has Been Spent'; $lang->block->budget = 'Budget'; $lang->block->left = 'Residuals'; -$lang->block->default['waterfall']['project']['1']['title'] = 'Project Weekly'; -$lang->block->default['waterfall']['project']['1']['block'] = 'waterfallreport'; -$lang->block->default['waterfall']['project']['1']['source'] = 'project'; -$lang->block->default['waterfall']['project']['1']['grid'] = 8; - -$lang->block->default['waterfall']['project']['2']['title'] = 'Estimate'; -$lang->block->default['waterfall']['project']['2']['block'] = 'waterfallestimate'; -$lang->block->default['waterfall']['project']['2']['source'] = 'project'; -$lang->block->default['waterfall']['project']['2']['grid'] = 4; - $lang->block->default['waterfall']['project']['3']['title'] = 'Plan Gantt Chart'; $lang->block->default['waterfall']['project']['3']['block'] = 'waterfallgantt'; $lang->block->default['waterfall']['project']['3']['source'] = 'project'; $lang->block->default['waterfall']['project']['3']['grid'] = 8; -$lang->block->default['waterfall']['project']['4']['title'] = 'Progress Chart'; -$lang->block->default['waterfall']['project']['4']['block'] = 'waterfallprogress'; -$lang->block->default['waterfall']['project']['4']['grid'] = 4; - -$lang->block->default['waterfall']['project']['5']['title'] = 'Project Issue'; -$lang->block->default['waterfall']['project']['5']['block'] = 'waterfallissue'; -$lang->block->default['waterfall']['project']['5']['source'] = 'project'; -$lang->block->default['waterfall']['project']['5']['grid'] = 8; - -$lang->block->default['waterfall']['project']['5']['params']['type'] = 'all'; -$lang->block->default['waterfall']['project']['5']['params']['count'] = '15'; -$lang->block->default['waterfall']['project']['5']['params']['orderBy'] = 'id_desc'; - $lang->block->default['waterfall']['project']['6']['title'] = 'Dynamic'; $lang->block->default['waterfall']['project']['6']['block'] = 'projectdynamic'; $lang->block->default['waterfall']['project']['6']['grid'] = 4; $lang->block->default['waterfall']['project']['6']['source'] = 'project'; -$lang->block->default['waterfall']['project']['7']['title'] = 'Project Risk'; -$lang->block->default['waterfall']['project']['7']['block'] = 'waterfallrisk'; -$lang->block->default['waterfall']['project']['7']['source'] = 'project'; -$lang->block->default['waterfall']['project']['7']['grid'] = 8; - -$lang->block->default['waterfall']['project']['7']['params']['type'] = 'all'; -$lang->block->default['waterfall']['project']['7']['params']['count'] = '15'; -$lang->block->default['waterfall']['project']['7']['params']['orderBy'] = 'id_desc'; - $lang->block->default['scrum']['project']['1']['title'] = 'Project Overview'; $lang->block->default['scrum']['project']['1']['block'] = 'scrumoverview'; $lang->block->default['scrum']['project']['1']['grid'] = 8; diff --git a/module/block/lang/zh-cn.php b/module/block/lang/zh-cn.php index 7ce129a602..0d5f9392cb 100644 --- a/module/block/lang/zh-cn.php +++ b/module/block/lang/zh-cn.php @@ -105,48 +105,16 @@ $lang->block->spent = '已花费'; $lang->block->budget = '预算'; $lang->block->left = '剩余'; -$lang->block->default['waterfall']['project']['1']['title'] = '项目周报'; -$lang->block->default['waterfall']['project']['1']['block'] = 'waterfallreport'; -$lang->block->default['waterfall']['project']['1']['source'] = 'project'; -$lang->block->default['waterfall']['project']['1']['grid'] = 8; - -$lang->block->default['waterfall']['project']['2']['title'] = '估算'; -$lang->block->default['waterfall']['project']['2']['block'] = 'waterfallestimate'; -$lang->block->default['waterfall']['project']['2']['source'] = 'project'; -$lang->block->default['waterfall']['project']['2']['grid'] = 4; - $lang->block->default['waterfall']['project']['3']['title'] = "项目计划"; $lang->block->default['waterfall']['project']['3']['block'] = 'waterfallgantt'; $lang->block->default['waterfall']['project']['3']['source'] = 'project'; $lang->block->default['waterfall']['project']['3']['grid'] = 8; -$lang->block->default['waterfall']['project']['4']['title'] = '到目前为止项目进展趋势图'; -$lang->block->default['waterfall']['project']['4']['block'] = 'waterfallprogress'; -$lang->block->default['waterfall']['project']['4']['grid'] = 4; - -$lang->block->default['waterfall']['project']['5']['title'] = '项目问题'; -$lang->block->default['waterfall']['project']['5']['block'] = 'waterfallissue'; -$lang->block->default['waterfall']['project']['5']['source'] = 'project'; -$lang->block->default['waterfall']['project']['5']['grid'] = 8; - -$lang->block->default['waterfall']['project']['5']['params']['type'] = 'all'; -$lang->block->default['waterfall']['project']['5']['params']['count'] = '15'; -$lang->block->default['waterfall']['project']['5']['params']['orderBy'] = 'id_desc'; - $lang->block->default['waterfall']['project']['6']['title'] = '最新动态'; $lang->block->default['waterfall']['project']['6']['block'] = 'projectdynamic'; $lang->block->default['waterfall']['project']['6']['grid'] = 4; $lang->block->default['waterfall']['project']['6']['source'] = 'project'; -$lang->block->default['waterfall']['project']['7']['title'] = '项目风险'; -$lang->block->default['waterfall']['project']['7']['block'] = 'waterfallrisk'; -$lang->block->default['waterfall']['project']['7']['source'] = 'project'; -$lang->block->default['waterfall']['project']['7']['grid'] = 8; - -$lang->block->default['waterfall']['project']['7']['params']['type'] = 'all'; -$lang->block->default['waterfall']['project']['7']['params']['count'] = '15'; -$lang->block->default['waterfall']['project']['7']['params']['orderBy'] = 'id_desc'; - $lang->block->default['scrum']['project']['1']['title'] = '项目概况'; $lang->block->default['scrum']['project']['1']['block'] = 'scrumoverview'; $lang->block->default['scrum']['project']['1']['grid'] = 8; diff --git a/module/block/model.php b/module/block/model.php index 78066e4746..6c2acd1c37 100644 --- a/module/block/model.php +++ b/module/block/model.php @@ -166,7 +166,19 @@ class blockModel extends model { $data = array(); - $data['tasks'] = (int)$this->dao->select('count(*) AS count')->from(TABLE_TASK)->where('assignedTo')->eq($this->app->user->account)->andWhere('deleted')->eq(0)->fetch('count'); + $tasks = $this->dao->select('t1.id')->from(TABLE_TASK)->alias('t1') + ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id') + ->leftJoin(TABLE_PROJECT)->alias('t3')->on('t1.execution=t3.id') + ->where('t1.assignedTo')->eq($this->app->user->account) + ->andWhere('t2.status')->ne('suspended') + ->andWhere('t3.status')->ne('suspended') + ->andWhere('t2.type')->eq('project') + ->andWhere('t3.type')->in('sprint,stage') + ->andWhere('t1.deleted')->eq(0) + ->andWhere('t2.deleted')->eq(0) + ->andWhere('t3.deleted')->eq(0) + ->fetchAll('id'); + $data['tasks'] = isset($tasks) ? count($tasks) : 0; $data['doneTasks'] = (int)$this->dao->select('count(*) AS count')->from(TABLE_TASK)->where('assignedTo')->eq($this->app->user->account)->andWhere('deleted')->eq(0)->andWhere('status')->eq('done')->fetch('count'); $data['bugs'] = (int)$this->dao->select('count(*) AS count')->from(TABLE_BUG) ->where('assignedTo')->eq($this->app->user->account) diff --git a/module/branch/view/ajaxgetdropmenu.html.php b/module/branch/view/ajaxgetdropmenu.html.php index e31ed3807b..92114b05af 100644 --- a/module/branch/view/ajaxgetdropmenu.html.php +++ b/module/branch/view/ajaxgetdropmenu.html.php @@ -13,7 +13,7 @@ foreach($branches as $branchID => $branch) if($branchID == 'all' or empty($branchID) or $statusList[$branchID] == 'active') { - $activeBranchesHtml .= html::a($linkHtml, $branch, '', "class='$selected' data-key='{$branchesPinyin[$branch]}'"); + $activeBranchesHtml .= html::a($linkHtml, $branch, '', "class='$selected' data-key='{$branchesPinyin[$branch]}' data-app='{$this->app->tab}'"); } else { diff --git a/module/bug/config.php b/module/bug/config.php index 20c7c7f1b4..a597673e80 100644 --- a/module/bug/config.php +++ b/module/bug/config.php @@ -40,7 +40,7 @@ $config->bug->list->exportFields = 'id, product, branch, module, project, execut $config->bug->list->customCreateFields = 'execution,story,task,pri,severity,os,browser,deadline,mailto,keywords'; $config->bug->list->customBatchCreateFields = 'execution,steps,type,pri,deadline,severity,os,browser,keywords'; -$config->bug->list->customBatchEditFields = 'type,severity,pri,productplan,assignedTo,deadline,status,resolvedBy,resolution,os,browser,keywords'; +$config->bug->list->customBatchEditFields = 'type,severity,pri,productplan,assignedTo,deadline,resolvedBy,resolution,os,browser,keywords'; $config->bug->custom = new stdclass(); $config->bug->custom->createFields = $config->bug->list->customCreateFields; diff --git a/module/bug/control.php b/module/bug/control.php index c534447dca..d2ddbfb24d 100644 --- a/module/bug/control.php +++ b/module/bug/control.php @@ -408,15 +408,7 @@ class bug extends control if($this->app->tab == 'execution') { - if(!preg_match("/(m=|\/)execution(&f=|-)bug(&|-|\.)?/", $this->session->bugList)) - { - $location = $this->session->bugList; - } - else - { - $executionID = $this->post->execution ? $this->post->execution : $output['executionID']; - $location = $this->createLink('execution', 'bug', "executionID=$executionID"); - } + $location = $this->session->bugList ? $this->session->bugList : $this->createLink('execution', 'bug', "executionID={$output['executionID']}"); } elseif($this->app->tab == 'project') { @@ -678,6 +670,9 @@ class bug extends control } setcookie('bugModule', 0, 0, $this->config->webRoot, '', $this->config->cookieSecure, false); + + /* If link from no head then reload. */ + if(isonlybody()) die(js::reload('parent.parent')); die(js::locate($this->createLink('bug', 'browse', "productID={$productID}&branch=$branch&browseType=unclosed¶m=0&orderBy=id_desc"), 'parent')); } @@ -882,6 +877,7 @@ class bug extends control } } } + if(isonlybody()) die(js::reload('parent.parent')); die(js::locate($this->createLink('bug', 'view', "bugID=$bugID"), 'parent')); } @@ -958,7 +954,7 @@ class bug extends control { $objectID = $this->app->tab == 'project' ? $bug->project : $bug->execution; $productBranches = (isset($product->type) and $product->type != 'normal') ? $this->loadModel('execution')->getBranchByProduct($productID, $objectID) : array(); - $branches = isset($productBranches[$productID]) ? $productBranches[$productID] : array(); + $branches = isset($productBranches[$productID]) ? array(BRANCH_MAIN => $this->lang->branch->main) + $productBranches[$productID] : array(); } else { @@ -969,7 +965,7 @@ class bug extends control $this->view->productID = $productID; $this->view->product = $product; $this->view->productName = $this->products[$productID]; - $this->view->plans = $this->loadModel('productplan')->getPairs($productID, $bug->branch); + $this->view->plans = $this->loadModel('productplan')->getPairs($productID, $bug->branch, '', true); $this->view->projects = array(0 => '') + $this->product->getProjectPairsByProduct($productID, $bug->branch, $bug->project); $this->view->moduleOptionMenu = $this->tree->getOptionMenu($productID, $viewType = 'bug', $startModuleID = 0, $bug->branch); $this->view->currentModuleID = $currentModuleID; @@ -995,6 +991,11 @@ class bug extends control */ public function batchEdit($productID = 0, $branch = 0) { + if($this->app->tab == 'product') + { + $this->product->setMenu($productID); + } + if($this->post->titles) { $allChanges = $this->bug->batchUpdate(); @@ -1035,7 +1036,7 @@ class bug extends control $branchProduct = $product->type == 'normal' ? false : true; /* Set plans. */ - $plans = $this->loadModel('productplan')->getPairs($productID, $branch); + $plans = $this->loadModel('productplan')->getPairs($productID, $branch, '', true); $plans = array('' => '', 'ditto' => $this->lang->bug->ditto) + $plans; /* Set branches and modules. */ @@ -1257,6 +1258,33 @@ class bug extends control die(js::locate($this->session->bugList, 'parent')); } + /** + * Batch change the plan of bug. + * + * @param int $planID + * @access public + * @return void + */ + public function batchChangePlan($planID) + { + if($this->post->bugIDList) + { + $bugIDList = $this->post->bugIDList; + $bugIDList = array_unique($bugIDList); + unset($_POST['bugIDList']); + $allChanges = $this->bug->batchChangePlan($bugIDList, $planID); + if(dao::isError()) die(js::error(dao::getError())); + foreach($allChanges as $bugID => $changes) + { + $this->loadModel('action'); + $actionID = $this->action->create('bug', $bugID, 'Edited'); + $this->action->logHistory($actionID, $changes); + } + } + $this->loadModel('score')->create('ajax', 'batchOther'); + die(js::locate($this->session->bugList, 'parent')); + } + /** * Batch update assign of bug. * diff --git a/module/bug/css/batchcreate.css b/module/bug/css/batchcreate.css index 9cd6305f9e..dcfd01218a 100644 --- a/module/bug/css/batchcreate.css +++ b/module/bug/css/batchcreate.css @@ -1,3 +1,5 @@ +#importLinesModal .modal-dialog {width: 80%;} + .c-id {width: 50px !important;} .c-branch, .c-module, .c-os {width: 120px;} .c-execution {width: 130px;} diff --git a/module/bug/css/create.css b/module/bug/css/create.css index de274bd781..3f076d247e 100644 --- a/module/bug/css/create.css +++ b/module/bug/css/create.css @@ -46,3 +46,4 @@ html[lang='en'] #deadlineTd .input-group-addon {padding: 5px 18px;} #osBox {width: 190px;} #projectBox .required:after {right: 1px;} #branch_chosen {min-width: 90px;} +#notifyEmailTd .input-group-addon, #deadlineTd .input-group-addon {border-radius: 2px 0px 2px 0px !important;} diff --git a/module/bug/css/x.view.css b/module/bug/css/x.view.css index f60657557c..11e7188688 100644 --- a/module/bug/css/x.view.css +++ b/module/bug/css/x.view.css @@ -1,7 +1,16 @@ .main-actions {display: none;} -#main {margin-bottom: 40px;} +#main {margin-bottom: 40px; min-width: unset;} #mainMenu .pull-left>a {display: none;} #mainMenu .pull-left .divider {display: none;} #mainMenu .pull-right {display: none;} #mainContent .col-4 {display: none;} .modal-dialog {width: 90%;} +#mainMenu {position: fixed; width: 100%; z-index: 999;} +#scrollContent {margin-top: 35px; height: calc(100% - 70px); display: block; overflow: hidden;} +#scrollContent:hover{overflow: overlay;} +html, body, #main, .container { height: 100%;} +body * {font-size: 13px !important; line-height: 1.42857143; color: rgb(51, 51, 51);} +.page-title > .label-id {min-width: 25px; line-height: 13px;} +::-webkit-scrollbar {width: 10px; height: 10px;} +::-webkit-scrollbar-thumb:vertical {box-shadow: inset 1px 1px 0 rgb(0 0 0 / 10%), inset 0 -1px 0 rgb(0 0 0 / 7%); background-color: rgba(0, 0, 0, 0.2); border-radius: 10px; opacity: 0; transition: opacity 0.1s;} +.btn span{line-height: 16px; vertical-align: middle;} diff --git a/module/bug/js/common.js b/module/bug/js/common.js index c6cfc14dc0..f0c1927b20 100644 --- a/module/bug/js/common.js +++ b/module/bug/js/common.js @@ -55,12 +55,6 @@ function loadAll(productID) $('#taskIdBox').innerHTML = ''; // Reset the task. $('#task').chosen(); loadProductBranches(productID) - loadProductModules(productID); - loadProductProjects(productID); - loadProductBuilds(productID); - loadProductplans(productID); - loadProductStories(productID); - //loadTestTasks(productID); } } @@ -502,6 +496,12 @@ function loadProductBranches(productID) $('#branch').css('width', page == 'create' ? '120px' : '65px'); $('#branch').chosen(); } + + loadProductModules(productID); + loadProductProjects(productID); + loadProductBuilds(productID); + loadProductplans(productID); + loadProductStories(productID); }) } @@ -662,4 +662,14 @@ function setBranchRelated(branchID, productID, num) } setOpenedBuilds(buildLink, num); } + + if(config.currentMethod == 'batchedit') + { + planID = $('#plans' + num).val(); + planLink = createLink('product', 'ajaxGetPlans', 'productID=' + productID + '&branch=' + branchID + '&planID=' + planID + '&fieldID=' + num + '&needCreate=false&expired=¶m=skipParent'); + $('#plans' + num).parent('td').load(planLink, function() + { + $('#plans' + num).chosen(); + }); + } } diff --git a/module/bug/js/x.view.js b/module/bug/js/x.view.js index 9c48ea0651..7d372d2ede 100644 --- a/module/bug/js/x.view.js +++ b/module/bug/js/x.view.js @@ -28,8 +28,14 @@ $(function() xuanAction += "' + action + ""; }); - xuanAction += '
    '; - - $('body').append(xuanAction); + if(xuanAction != "
    ") + { + xuanAction += '
    '; + $('body').append(xuanAction); + } + else + { + $('#scrollContent').css('height', 'calc(100% - 36px)'); + } $('.xuancard-actions a.iframe').modalTrigger(); }) diff --git a/module/bug/lang/en.php b/module/bug/lang/en.php index 0155f909ba..902709a2f4 100644 --- a/module/bug/lang/en.php +++ b/module/bug/lang/en.php @@ -41,13 +41,15 @@ $lang->bug->steps = 'Repro Steps'; $lang->bug->status = 'Status'; $lang->bug->statusAB = 'Status'; $lang->bug->subStatus = 'Sub Status'; -$lang->bug->activatedCount = 'Activated Times'; +$lang->bug->activatedCount = 'Activation'; $lang->bug->activatedCountAB = 'Active'; $lang->bug->activatedDate = 'ActivatedDate'; $lang->bug->confirmed = 'Confirmed'; $lang->bug->confirmedAB = 'C'; $lang->bug->toTask = 'Convert to Task'; $lang->bug->toStory = 'Convert to Story'; +$lang->bug->feedbackBy = 'From Name'; +$lang->bug->notifyEmail = 'From Email'; $lang->bug->mailto = 'Mailto'; $lang->bug->openedBy = 'ReportedBy'; $lang->bug->openedByAB = 'Reporter'; @@ -63,8 +65,8 @@ $lang->bug->resolvedByAB = 'ResolvedBy'; $lang->bug->resolution = 'Resolution'; $lang->bug->resolutionAB = 'Resolution'; $lang->bug->resolvedBuild = 'Build'; -$lang->bug->resolvedDate = 'Resolved Date'; -$lang->bug->resolvedDateAB = 'ResolvedDate'; +$lang->bug->resolvedDate = 'Resolved'; +$lang->bug->resolvedDateAB = 'Resolved'; $lang->bug->deadline = 'Deadline'; $lang->bug->deadlineAB = 'Deadline'; $lang->bug->plan = 'Plan'; @@ -99,6 +101,7 @@ $lang->bug->edit = 'Edit Bug'; $lang->bug->batchEdit = 'Batch Edit'; $lang->bug->batchChangeModule = 'Batch Edit Modules'; $lang->bug->batchChangeBranch = 'Batch Edit Branches'; +$lang->bug->batchChangePlan = 'Batch Edit Plans'; $lang->bug->batchClose = 'Batch Close'; $lang->bug->assignTo = 'Assign'; $lang->bug->assignAction = 'Assign Bug'; diff --git a/module/bug/lang/zh-cn.php b/module/bug/lang/zh-cn.php index fbcbf7aa1a..aadb1f1fc8 100644 --- a/module/bug/lang/zh-cn.php +++ b/module/bug/lang/zh-cn.php @@ -48,6 +48,8 @@ $lang->bug->confirmed = '是否确认'; $lang->bug->confirmedAB = '确认'; $lang->bug->toTask = '转任务'; $lang->bug->toStory = "转{$lang->SRCommon}"; +$lang->bug->feedbackBy = '反馈者'; +$lang->bug->notifyEmail = '通知邮箱'; $lang->bug->mailto = '抄送给'; $lang->bug->openedBy = '由谁创建'; $lang->bug->openedByAB = '创建者'; @@ -99,6 +101,7 @@ $lang->bug->edit = '编辑Bug'; $lang->bug->batchEdit = '批量编辑'; $lang->bug->batchChangeModule = '批量修改模块'; $lang->bug->batchChangeBranch = '批量修改分支'; +$lang->bug->batchChangePlan = '批量修改计划'; $lang->bug->batchClose = '批量关闭'; $lang->bug->assignTo = '指派'; $lang->bug->assignAction = '指派Bug'; @@ -278,8 +281,9 @@ $lang->bug->statusList['active'] = '激活'; $lang->bug->statusList['resolved'] = '已解决'; $lang->bug->statusList['closed'] = '已关闭'; -$lang->bug->confirmedList[1] = '是'; -$lang->bug->confirmedList[0] = '否'; +$lang->bug->confirmedList[''] = ''; +$lang->bug->confirmedList[1] = '是'; +$lang->bug->confirmedList[0] = '否'; $lang->bug->resolutionList[''] = ''; $lang->bug->resolutionList['bydesign'] = '设计如此'; diff --git a/module/bug/model.php b/module/bug/model.php index 25e9413ddf..1e6d6c2c1d 100644 --- a/module/bug/model.php +++ b/module/bug/model.php @@ -85,7 +85,12 @@ class bugModel extends model /* Use classic mode to replace required project. */ if($this->config->systemMode == 'classic' and strpos($this->config->bug->create->requiredFields, 'project') !== false) $this->config->bug->create->requiredFields = str_replace('project', 'execution', $this->config->bug->create->requiredFields); - $this->dao->insert(TABLE_BUG)->data($bug)->autoCheck()->batchCheck($this->config->bug->create->requiredFields, 'notempty')->exec(); + $this->dao->insert(TABLE_BUG)->data($bug) + ->autoCheck() + ->checkIF($bug->notifyEmail, 'notifyEmail', 'email') + ->batchCheck($this->config->bug->create->requiredFields, 'notempty') + ->exec(); + if(!dao::isError()) { $bugID = $this->dao->lastInsertID(); @@ -94,6 +99,8 @@ class bugModel extends model $this->file->saveUpload('bug', $bugID); empty($bug->case) ? $this->loadModel('score')->create('bug', 'create', $bugID) : $this->loadModel('score')->create('bug', 'createFormCase', $bug->case); + if($bug->execution) $this->loadModel('kanban')->updateLane($bug->execution, 'bug'); + /* Callback the callable method to process the related data for object that is transfered to bug. */ if($from && is_callable(array($this, $this->config->bug->fromObjects[$from]['callback']))) call_user_func(array($this, $this->config->bug->fromObjects[$from]['callback']), $bugID); @@ -249,6 +256,7 @@ class bugModel extends model $bugID = $this->dao->lastInsertID(); $this->executeHooks($bugID); + if($bug->execution) $this->loadModel('kanban')->updateLane($bug->execution, 'bug'); /* When the bug is created by uploading the image, add the image to the file of the bug. */ $this->loadModel('score')->create('bug', 'create', $bugID); @@ -530,7 +538,7 @@ class bugModel extends model ->andWhere('tostory')->eq(0) ->andWhere('toTask')->eq(0) ->beginIF(!empty($products))->andWhere('product')->in($products)->fi() - ->beginIF($branch)->andWhere('branch')->in("0,$branch")->fi() + ->beginIF($branch !== '' and $branch !== 'all')->andWhere('branch')->in("0,$branch")->fi() ->beginIF(!empty($executions))->andWhere('execution')->in($executions)->fi() ->beginIF($excludeBugs)->andWhere('id')->notIN($excludeBugs)->fi() ->andWhere('deleted')->eq(0) @@ -661,6 +669,7 @@ class bugModel extends model ->batchCheck($this->config->bug->edit->requiredFields, 'notempty') ->checkIF($bug->resolvedBy, 'resolution', 'notempty') ->checkIF($bug->closedBy, 'resolution', 'notempty') + ->checkIF($bug->notifyEmail, 'notifyEmail', 'email') ->checkIF($bug->resolution == 'duplicate', 'duplicateBug', 'notempty') ->checkIF($bug->resolution == 'fixed', 'resolvedBuild','notempty') ->where('id')->eq((int)$bugID) @@ -678,6 +687,8 @@ class bugModel extends model if(!empty($bug->resolvedBy)) $this->loadModel('score')->create('bug', 'resolve', $bugID); $this->file->updateObjectID($this->post->uid, $bugID, 'bug'); + if($bug->execution and $bug->status != $oldBug->status) $this->loadModel('kanban')->updateLane($bug->execution, 'bug'); + return common::createChanges($oldBug, $bug); } } @@ -917,6 +928,7 @@ class bugModel extends model if(!dao::isError()) { $this->loadModel('score')->create('bug', 'confirmBug', $oldBug); + if($oldBug->execution) $this->loadModel('kanban')->updateLane($oldBug->execution, 'bug'); return common::createChanges($oldBug, $bug); } } @@ -1029,6 +1041,7 @@ class bugModel extends model if(!dao::isError()) { $this->loadModel('score')->create('bug', 'resolve', $oldBug); + if($oldBug->execution) $this->loadModel('kanban')->updateLane($oldBug->execution, 'bug'); /* Link bug to build and release. */ $this->linkBugToBuild($bugID, $bug->resolvedBuild); @@ -1096,6 +1109,35 @@ class bugModel extends model return $allChanges; } + /** + * Batch change the plan of bug. + * + * @param array $bugIDList + * @param int $planID + * @access public + * @return array + */ + public function batchChangePlan($bugIDList, $planID) + { + $now = helper::now(); + $allChanges = array(); + $oldBugs = $this->getByList($bugIDList); + foreach($bugIDList as $bugID) + { + $oldBug = $oldBugs[$bugID]; + if($planID == $oldBug->plan) continue; + + $bug = new stdclass(); + $bug->lastEditedBy = $this->app->user->account; + $bug->lastEditedDate = $now; + $bug->plan = $planID; + + $this->dao->update(TABLE_BUG)->data($bug)->autoCheck()->where('id')->eq((int)$bugID)->exec(); + if(!dao::isError()) $allChanges[$bugID] = common::createChanges($oldBug, $bug); + } + return $allChanges; + } + /** * Batch resolve bugs. * @@ -1161,6 +1203,7 @@ class bugModel extends model $this->dao->update(TABLE_BUG)->data($bug)->where('id')->eq($bugID)->exec(); $this->executeHooks($bugID); + if($oldBug->execution) $this->loadModel('kanban')->updateLane($oldBug->execution, 'bug'); $changes[$bugID] = common::createChanges($oldBug, $bug); } @@ -1179,7 +1222,12 @@ class bugModel extends model */ public function activate($bugID) { - $oldBug = $this->getById($bugID); + $bugID = (int)$bugID; + $oldBug = $this->getById($bugID); + $solveBuild = $this->dao->select('id') + ->from(TABLE_BUILD) + ->where("CONCAT(',', bugs, ',')")->like("%,{$bugID},%") + ->fetch('id'); $now = helper::now(); $bug = fixer::input('post') ->setDefault('assignedTo', $oldBug->resolvedBy) @@ -1205,19 +1253,15 @@ class bugModel extends model $this->dao->update(TABLE_BUG)->data($bug)->autoCheck()->where('id')->eq((int)$bugID)->exec(); $this->dao->update(TABLE_BUG)->set('activatedCount = activatedCount + 1')->where('id')->eq((int)$bugID)->exec(); - $openedBuilds = explode(',', $oldBug->openedBuild); - if($openedBuilds) + if($solveBuild) { $this->loadModel('build'); - foreach($openedBuilds as $openedBuild) - { - $build = $this->build->getByID($openedBuild); - if(empty($build)) continue; - $build->bugs = trim(str_replace(",$bugID,", ',', ",$build->bugs,"), ','); - $this->dao->update(TABLE_BUILD)->set('bugs')->eq($build->bugs)->where('id')->eq((int)$openedBuild)->exec(); - } + $build = $this->build->getByID($solveBuild); + $build->bugs = trim(str_replace(",$bugID,", ',', ",$build->bugs,"), ','); + $this->dao->update(TABLE_BUILD)->set('bugs')->eq($build->bugs)->where('id')->eq((int)$solveBuild)->exec(); } + if($oldBug->execution) $this->loadModel('kanban')->updateLane($oldBug->execution, 'bug'); $bug->activatedCount += 1; return common::createChanges($oldBug, $bug); } @@ -1246,6 +1290,7 @@ class bugModel extends model ->get(); $this->dao->update(TABLE_BUG)->data($bug)->autoCheck()->where('id')->eq((int)$bugID)->exec(); + if($oldBug->execution) $this->loadModel('kanban')->updateLane($oldBug->execution, 'bug'); return common::createChanges($oldBug, $bug); } @@ -1583,7 +1628,9 @@ class bugModel extends model ->beginIF($type == 'noclosed')->andWhere('status')->ne('closed')->fi() ->beginIF($build)->andWhere("CONCAT(',', openedBuild, ',') like '%,$build,%'")->fi() ->beginIF($excludeBugs)->andWhere('id')->notIN($excludeBugs)->fi() - ->orderBy($orderBy)->page($pager)->fetchAll(); + ->orderBy($orderBy) + ->page($pager) + ->fetchAll('id'); } $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'bug'); @@ -1602,7 +1649,7 @@ class bugModel extends model * @access public * @return array */ - public function getProductLeftBugs($build, $productID, $branch = 0, $linkedBugs = '', $pager = null) + public function getProductLeftBugs($build, $productID, $branch = '', $linkedBugs = '', $pager = null) { $build = $this->dao->select('*')->from(TABLE_BUILD)->where('id')->eq($build)->fetch(); if(empty($build->execution)) return array(); @@ -1625,7 +1672,7 @@ class bugModel extends model ->andWhere("(status = 'active' OR resolvedDate > '{$execution->end}')") ->andWhere('openedBuild')->notin($beforeBuilds) ->beginIF($linkedBugs)->andWhere('id')->notIN($linkedBugs)->fi() - ->beginIF($branch)->andWhere('branch')->in("0,$branch")->fi() + ->beginIF($branch !== '')->andWhere('branch')->in("0,$branch")->fi() ->page($pager) ->fetchAll(); @@ -2499,7 +2546,7 @@ class bugModel extends model } $allBranch = "`branch` = 'all'"; - if($branch !== 'all' and strpos($bugQuery, '`branch` =') === false) $bugQuery .= " AND `branch` in('$branch')"; + 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'"; diff --git a/module/bug/view/batchedit.html.php b/module/bug/view/batchedit.html.php index c75c2db803..caad3f41c6 100644 --- a/module/bug/view/batchedit.html.php +++ b/module/bug/view/batchedit.html.php @@ -59,7 +59,6 @@ '>bug->productplan;?> '>bug->assignedTo;?> '>bug->deadline;?> - '>bug->status;?> '>bug->os;?> '>bug->browser;?> '>bug->keywords;?> @@ -119,7 +118,6 @@ ' style='overflow:visible'>plan, "class='form-control chosen'");?> ' style='overflow:visible'>assignedTo, "class='form-control chosen'");?> ' style='overflow:visible'>deadline, "class='form-control form-date'");?> - >status, 'class=form-control');?> >os, 'class=form-control');?> >browser, 'class=form-control');?> >keywords, 'class=form-control');?> @@ -143,7 +141,7 @@ - + app->tab == 'product' ? html::a($this->session->bugList, $lang->goback, '', "class='btn btn-back btn-wide'") : html::backButton();?> diff --git a/module/bug/view/create.html.php b/module/bug/view/create.html.php index 7e7b8b7b01..337d100e3e 100644 --- a/module/bug/view/create.html.php +++ b/module/bug/view/create.html.php @@ -146,6 +146,26 @@ if($this->app->tab == 'project') js::set('objectID', $projectID);
    + + bug->feedbackBy;?> + + +
    + bug->notifyEmail?> + +
    + + + + +
    + bug->feedbackBy?> + + bug->notifyEmail?> + +
    + + diff --git a/module/bug/view/edit.html.php b/module/bug/view/edit.html.php index 30cfc7cd40..1c1d21ae3e 100644 --- a/module/bug/view/edit.html.php +++ b/module/bug/view/edit.html.php @@ -137,7 +137,7 @@ if($this->app->tab == 'project') js::set('objectID', $bug->project); bug->status;?> - bug->statusList, $bug->status, "class='form-control chosen'");?> + bug->statusList, $bug->status);?> bug->confirmed;?> @@ -151,6 +151,14 @@ if($this->app->tab == 'project') js::set('objectID', $bug->project); bug->deadline;?> deadline, "class='form-control form-date'");?> + + bug->feedbackBy;?> + feedbackBy, "class='form-control'");?> + + + bug->notifyEmail;?> + notifyEmail, "class='form-control'");?> + bug->os;?> bug->osList, $bug->os, "class='form-control chosen'");?> diff --git a/module/bug/view/view.html.php b/module/bug/view/view.html.php index 0ec59c25e7..bb5d11e559 100644 --- a/module/bug/view/view.html.php +++ b/module/bug/view/view.html.php @@ -37,6 +37,9 @@
    +app->getViewType() == 'xhtml'):?> +
    +
    @@ -65,7 +68,9 @@ ?>
    printExtendFields($bug, 'div', "position=left&inForm=0&inCell=1");?> -
    + app->getViewType() != 'xhtml'):?> +
    + id"; $extraParams = "extras=bugID=$bug->id"; @@ -88,7 +93,7 @@ if($this->app->tab != 'product') { - common::printIcon('bug', 'toStory', "product=$bug->product&branch=$bug->branch&module=0&story=0&execution=0&bugID=$bug->id", $bug, 'button', $lang->icons['story'], '', '', '', "data-app='product'", $lang->bug->toStory); + common::printIcon('bug', 'toStory', "product=$bug->product&branch=$bug->branch&module=0&story=0&execution=0&bugID=$bug->id", $bug, 'button', $lang->icons['story'], '', '', '', "data-app='" . $this->app->tab . "'", $lang->bug->toStory); common::printIcon('bug', 'createCase', $convertParams, $bug, 'button', 'sitemap'); } @@ -223,6 +228,14 @@ ?> + + bug->feedbackBy;?> + feedbackBy;?> + + + bug->notifyEmail;?> + notifyEmail;?> + bug->os;?> bug->osList[$bug->os];?> @@ -300,7 +313,11 @@ if($bug->openedBuild) { $openedBuilds = explode(',', $bug->openedBuild); - foreach($openedBuilds as $openedBuild) isset($builds[$openedBuild]) ? print($builds[$openedBuild] . '
    ') : print($openedBuild . '
    '); + foreach($openedBuilds as $openedBuild) + { + if(!$openedBuild) continue; + isset($builds[$openedBuild]) ? print($builds[$openedBuild] . '
    ') : print($openedBuild . '
    '); + } } else { @@ -394,6 +411,9 @@ printExtendFields($bug, 'div', "position=right&inForm=0&inCell=1");?>
    +app->getViewType() == 'xhtml'):?> +
    +
    diff --git a/module/build/control.php b/module/build/control.php index 77fa967f8f..b21ca4419d 100644 --- a/module/build/control.php +++ b/module/build/control.php @@ -492,8 +492,8 @@ class build extends control $this->config->product->search['actionURL'] = $this->createLink('build', 'view', "buildID=$buildID&type=story&link=true¶m=" . helper::safe64Encode("&browseType=bySearch&queryID=myQueryID")); $this->config->product->search['queryID'] = $queryID; $this->config->product->search['style'] = 'simple'; - $this->config->product->search['params']['plan']['values'] = $this->loadModel('productplan')->getForProducts(array($build->product => $build->product)); - $this->config->product->search['params']['module']['values'] = $this->tree->getOptionMenu($build->product, 'story', 0); + $this->config->product->search['params']['plan']['values'] = $this->loadModel('productplan')->getPairsForStory($build->product, $build->branch, 'skipParent'); + $this->config->product->search['params']['module']['values'] = $this->tree->getOptionMenu($build->product, 'story', 0, $build->branch); $this->config->product->search['params']['status'] = array('operator' => '=', 'control' => 'select', 'values' => $this->lang->story->statusList); if($product->type == 'normal') @@ -503,9 +503,11 @@ class build extends control } else { - $this->config->product->search['fields']['branch'] = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]); - $branches = array('' => '') + $this->loadModel('branch')->getPairs($build->product, 'noempty'); - if($build->branch) $branches = array('' => '', $build->branch => $branches[$build->branch]); + $branchPairs = $this->loadModel('branch')->getPairs($build->product, 'noempty'); + $branches = array('' => '') + array(BRANCH_MAIN => $this->lang->branch->main); + if($build->branch) $branches += array($build->branch => $branchPairs[$build->branch]); + + $this->config->product->search['fields']['branch'] = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]); $this->config->product->search['params']['branch']['values'] = $branches; } $this->loadModel('search')->setSearchParams($this->config->product->search); @@ -516,7 +518,7 @@ class build extends control } else { - $allStories = $this->story->getExecutionStories($build->execution, 0, 0, 't1.`order`_desc', 'byProduct', $build->product, 'story', $build->stories, $pager); + $allStories = $this->story->getExecutionStories($build->execution, $build->product, 0, 't1.`order`_desc', 'byBranch', $build->branch, 'story', $build->stories, $pager); } $this->view->allStories = $allStories; @@ -609,8 +611,8 @@ class build extends control $this->config->bug->search['actionURL'] = $this->createLink('build', 'view', "buildID=$buildID&type=bug&link=true¶m=" . helper::safe64Encode("&browseType=bySearch&queryID=myQueryID")); $this->config->bug->search['queryID'] = $queryID; $this->config->bug->search['style'] = 'simple'; - $this->config->bug->search['params']['plan']['values'] = $this->loadModel('productplan')->getForProducts(array($build->product => $build->product)); - $this->config->bug->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($build->product, $viewType = 'bug', $startModuleID = 0); + $this->config->bug->search['params']['plan']['values'] = $this->loadModel('productplan')->getPairsForStory($build->product, $build->branch, 'skipParent'); + $this->config->bug->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($build->product, 'bug', 0, $build->branch); $this->config->bug->search['params']['execution']['values'] = $this->loadModel('product')->getExecutionPairsByProduct($build->product, 0, 'id_desc', $this->session->project); $this->config->bug->search['params']['openedBuild']['values'] = $this->build->getProductBuildPairs($build->product, $branch = 0, $params = ''); $this->config->bug->search['params']['resolvedBuild']['values'] = $this->config->bug->search['params']['openedBuild']['values']; @@ -624,9 +626,11 @@ class build extends control } else { - $this->config->bug->search['fields']['branch'] = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]); - $branches = array('' => '') + $this->loadModel('branch')->getPairs($build->product, 'noempty'); - if($build->branch) $branches = array('' => '', $build->branch => $branches[$build->branch]); + $branchPairs = $this->loadModel('branch')->getPairs($build->product, 'noempty'); + $branches = array('' => '') + array(BRANCH_MAIN => $this->lang->branch->main); + if($build->branch) $branches += array($build->branch => $branchPairs[$build->branch]); + + $this->config->bug->search['fields']['branch'] = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]); $this->config->bug->search['params']['branch']['values'] = $branches; } $this->loadModel('search')->setSearchParams($this->config->bug->search); diff --git a/module/build/js/common.js b/module/build/js/common.js index e92d9470ad..599c9cba24 100644 --- a/module/build/js/common.js +++ b/module/build/js/common.js @@ -15,7 +15,7 @@ function loadBranches(productID) oldBranch = productGroups[productID]['branches']; } - executionID = $('#execution').val(); + executionID = currentTab == 'execution' ? executionID : $('#execution').val(); $.get(createLink('branch', 'ajaxGetBranches', 'productID=' + productID + '&oldBranch=0¶m=active&projectID=' + executionID), function(data) { if(data) diff --git a/module/build/view/create.html.php b/module/build/view/create.html.php index 1ede2bdad7..996195e756 100644 --- a/module/build/view/create.html.php +++ b/module/build/view/create.html.php @@ -92,5 +92,7 @@
    - + + +app->tab);?> diff --git a/module/ci/model.php b/module/ci/model.php index d37d81d82e..9943a6fcaa 100644 --- a/module/ci/model.php +++ b/module/ci/model.php @@ -54,7 +54,6 @@ class ciModel extends model */ public function syncCompileStatus($compile) { - /* Max retry times is: 3. */ if($compile->times >= 3) { $this->dao->update(TABLE_COMPILE)->set('status')->eq('failure')->where('id')->eq($compile->id)->exec(); diff --git a/module/common/lang/common.php b/module/common/lang/common.php index 57a3c16b1c..a633d01ce2 100644 --- a/module/common/lang/common.php +++ b/module/common/lang/common.php @@ -4,11 +4,15 @@ $lang->index = new stdclass(); $lang->my = new stdclass(); $lang->todo = new stdclass(); $lang->program = new stdclass(); +$lang->programplan = new stdclass(); $lang->product = new stdclass(); $lang->project = new stdclass(); +$lang->design = new stdclass(); +$lang->stage = new stdclass(); $lang->scrum = new stdclass(); $lang->waterfall = new stdclass(); $lang->execution = new stdclass(); +$lang->kanban = new stdclass(); $lang->story = new stdclass(); $lang->release = new stdclass(); $lang->branch = new stdclass(); diff --git a/module/common/lang/en.php b/module/common/lang/en.php index adc6163ad9..e2e937bfb1 100644 --- a/module/common/lang/en.php +++ b/module/common/lang/en.php @@ -45,34 +45,38 @@ $lang->runInfo = "
    Z PUBLIC LICENSE 1.2 . Without authorization, I should not remove, hide or cover any logos/links of ZenTao."; $lang->designedByAIUX = " AIUX"; -$lang->reset = 'Reset'; -$lang->cancel = 'Cancel'; -$lang->refresh = 'Refresh'; -$lang->edit = 'Edit'; -$lang->delete = 'Delete'; -$lang->close = 'Close'; -$lang->unlink = 'Unlink'; -$lang->import = 'Import'; -$lang->export = 'Export'; -$lang->setFileName = 'File Name'; -$lang->submitting = 'Saving...'; -$lang->save = 'Save'; -$lang->confirm = 'Confirm'; -$lang->preview = 'View'; -$lang->goback = 'Back'; -$lang->goPC = 'PC'; -$lang->more = 'More'; -$lang->moreLink = 'MORE'; -$lang->day = ' Day'; -$lang->customConfig = 'Custom Config'; -$lang->public = 'Public'; -$lang->trunk = 'Trunk'; -$lang->sort = 'Order'; -$lang->required = 'Required'; -$lang->noData = 'No data.'; -$lang->fullscreen = 'Fullscreen'; -$lang->retrack = 'Retrack'; -$lang->whitelist = 'Access whitelist'; +$lang->reset = 'Reset'; +$lang->cancel = 'Cancel'; +$lang->refresh = 'Refresh'; +$lang->create = 'Create'; +$lang->edit = 'Edit'; +$lang->delete = 'Delete'; +$lang->close = 'Close'; +$lang->unlink = 'Unlink'; +$lang->import = 'Import'; +$lang->export = 'Export'; +$lang->setFileName = 'File Name'; +$lang->submitting = 'Saving...'; +$lang->save = 'Save'; +$lang->confirm = 'Confirm'; +$lang->preview = 'View'; +$lang->goback = 'Back'; +$lang->goPC = 'PC'; +$lang->more = 'More'; +$lang->moreLink = 'MORE'; +$lang->day = ' Day'; +$lang->customConfig = 'Custom Config'; +$lang->public = 'Public'; +$lang->trunk = 'Trunk'; +$lang->sort = 'Order'; +$lang->required = 'Required'; +$lang->noData = 'No data.'; +$lang->fullscreen = 'Fullscreen'; +$lang->retrack = 'Retrack'; +$lang->whitelist = 'Access whitelist'; +$lang->globalSetting = 'Global Setting'; +$lang->waterfallModel = 'Waterfall'; +$lang->all = 'All'; $lang->actions = 'Action'; $lang->restore = 'Reset'; @@ -137,6 +141,7 @@ $lang->program->common = 'Program'; $lang->product->common = 'Product'; $lang->project->common = 'Project'; $lang->execution->common = $config->systemMode == 'new' ? 'Execution' : $lang->executionCommon; +$lang->kanban->common = 'Kanban'; $lang->qa->common = 'QA'; $lang->devops->common = 'DevOps'; $lang->doc->common = 'Doc'; @@ -161,6 +166,13 @@ $lang->dept->common = 'Dept'; $lang->upgrade->common = 'Update'; $lang->program->list = 'Program List'; $lang->program->kanban = 'Program Kanban'; +$lang->design->common = 'Design'; +$lang->design->HLDS = 'HLDS'; +$lang->design->DDS = 'DDS'; +$lang->design->DBDS = 'DBDS'; +$lang->design->ADS = 'ADS'; +$lang->stage->common = 'Stage'; +$lang->stage->list = 'Stage List'; $lang->execution->list = "{$lang->executionCommon} List"; $lang->personnel->common = 'Member'; @@ -179,38 +191,37 @@ $lang->score->shortCommon = 'Score'; $lang->testreport->shortCommon = 'Report'; $lang->qa->shortCommon = 'QA'; -$lang->dashboard = 'Dashboard'; -$lang->contribute = 'Contribute'; -$lang->dynamic = 'Dynamic'; -$lang->contact = 'Contacts'; -$lang->whitelist = 'Whitelist'; -$lang->roadmap = 'Roadmap'; -$lang->track = 'Track'; -$lang->settings = 'Settings'; -$lang->overview = 'Overview'; -$lang->module = 'Module'; -$lang->priv = 'Privilege'; -$lang->design = 'Design'; -$lang->other = 'Other'; -$lang->estimation = 'Estimation'; -$lang->issue = 'Issue'; -$lang->risk = 'Risk'; -$lang->measure = 'Report'; -$lang->treeView = 'Tree View'; -$lang->groupView = 'Group View'; -$lang->kanban = 'Kanban'; -$lang->burn = 'Burndown'; -$lang->view = 'View'; -$lang->intro = 'Introduction'; -$lang->indexPage = 'Index'; -$lang->model = 'Model'; -$lang->redev = 'Develop'; -$lang->browser = 'Browser'; -$lang->db = 'Database'; -$lang->editor = 'Editor'; -$lang->timezone = 'Timezone'; -$lang->security = 'Security'; -$lang->calendar = 'Calendar'; +$lang->dashboard = 'Dashboard'; +$lang->contribute = 'Contribute'; +$lang->dynamic = 'Dynamic'; +$lang->contact = 'Contacts'; +$lang->whitelist = 'Whitelist'; +$lang->roadmap = 'Roadmap'; +$lang->track = 'Track'; +$lang->settings = 'Settings'; +$lang->overview = 'Overview'; +$lang->module = 'Module'; +$lang->priv = 'Privilege'; +$lang->other = 'Other'; +$lang->estimation = 'Estimation'; +$lang->issue = 'Issue'; +$lang->risk = 'Risk'; +$lang->measure = 'Report'; +$lang->treeView = 'Tree View'; +$lang->groupView = 'Group View'; +$lang->executionKanban = 'Kanban'; +$lang->burn = 'Burndown'; +$lang->view = 'View'; +$lang->intro = 'Introduction'; +$lang->indexPage = 'Index'; +$lang->model = 'Model'; +$lang->redev = 'Develop'; +$lang->browser = 'Browser'; +$lang->db = 'Database'; +$lang->editor = 'Editor'; +$lang->timezone = 'Timezone'; +$lang->security = 'Security'; +$lang->calendar = 'Calendar'; $lang->my->work = 'Work'; @@ -229,8 +240,8 @@ $lang->doc->api = 'API'; $lang->doc->execution = $lang->execution->common; $lang->doc->custom = 'Custom'; $lang->doc->wiki = 'WIKI'; -$lang->doc->apiDoc = 'Doc'; -$lang->doc->apiStruct = 'Struct'; +$lang->doc->apiDoc = 'API Docuemnt'; +$lang->doc->apiStruct = 'Data Structure'; $lang->product->list = $lang->productCommon . ' List'; $lang->product->kanban = $lang->productCommon . ' Kanban'; diff --git a/module/common/lang/menu.php b/module/common/lang/menu.php index edf51e6c38..0ad42439f1 100644 --- a/module/common/lang/menu.php +++ b/module/common/lang/menu.php @@ -155,7 +155,7 @@ $lang->product->menu = new stdclass(); $lang->product->menu->dashboard = array('link' => "{$lang->dashboard}|product|dashboard|productID=%s"); if($config->URAndSR) $lang->product->menu->requirement = array('link' => "$lang->URCommon|product|browse|productID=%s&branch=&browseType=unclosed¶m=0&storyType=requirement", 'alias' => 'batchedit', 'subModule' => 'story'); $lang->product->menu->story = array('link' => "$lang->SRCommon|product|browse|productID=%s", 'alias' => 'batchedit', 'subModule' => 'story'); -$lang->product->menu->plan = array('link' => "{$lang->productplan->shortCommon}|productplan|browse|productID=%s", 'subModule' => 'productplan'); +$lang->product->menu->plan = array('link' => "{$lang->productplan->shortCommon}|productplan|browse|productID=%s", 'subModule' => 'productplan,bug'); $lang->product->menu->release = array('link' => "{$lang->release->common}|release|browse|productID=%s", 'subModule' => 'release'); $lang->product->menu->roadmap = array('link' => "{$lang->roadmap}|product|roadmap|productID=%s"); if($config->systemMode == 'new') $lang->product->menu->project = array('link' => "{$lang->project->common}|product|project|status=all&productID=%s"); @@ -239,6 +239,52 @@ $lang->scrum->menu->settings['subMenu']->whitelist = array('link' => "{$lang-> $lang->scrum->menu->settings['subMenu']->stakeholder = array('link' => "{$lang->stakeholder->common}|stakeholder|browse|project=%s", 'subModule' => 'stakeholder'); $lang->scrum->menu->settings['subMenu']->group = array('link' => "{$lang->priv}|project|group|project=%s", 'alias' => 'group,manageview,managepriv'); +/* Waterfall menu. */ +$lang->waterfall->menu = new stdclass(); +$lang->waterfall->menu->index = array('link' => "$lang->dashboard|project|index|project=%s"); +$lang->waterfall->menu->programplan = array('link' => "{$lang->productplan->shortCommon}|programplan|browse|project=%s&productID=0&type=lists", 'subModule' => 'programplan'); +$lang->waterfall->menu->execution = array('link' => "{$lang->stage->common}|project|execution|status=all&projectID=%s"); +$lang->waterfall->menu->story = array('link' => "$lang->SRCommon|projectstory|story|project=%s", 'subModule' => 'projectstory,tree', 'exclude' => 'projectstory-track'); +$lang->waterfall->menu->design = array('link' => "{$lang->design->common}|design|browse|project=%s"); +$lang->waterfall->menu->qa = array('link' => "{$lang->qa->common}|project|bug|projectID=%s", 'subModule' => 'testcase,testtask,bug', 'alias' => 'bug,testtask,testcase'); +$lang->waterfall->menu->devops = array('link' => "{$lang->repo->common}|repo|browse|repoID=0&branchID=&objectID=%s", 'subModule' => 'repo'); +$lang->waterfall->menu->build = array('link' => "{$lang->build->common}|project|build|project=%s"); +$lang->waterfall->menu->release = array('link' => "{$lang->release->common}|projectrelease|browse|project=%s", 'subModule' => 'projectrelease'); +$lang->waterfall->menu->dynamic = array('link' => "$lang->dynamic|project|dynamic|project=%s"); + +$lang->waterfall->menu->settings = $lang->scrum->menu->settings; +$lang->waterfall->dividerMenu = ',programplan,build,dynamic,'; + +/* Waterfall menu order. */ +$lang->waterfall->menuOrder[5] = 'index'; +$lang->waterfall->menuOrder[15] = 'programplan'; +$lang->waterfall->menuOrder[20] = 'execution'; +$lang->waterfall->menuOrder[25] = 'story'; +$lang->waterfall->menuOrder[30] = 'design'; +$lang->waterfall->menuOrder[35] = 'devops'; +$lang->waterfall->menuOrder[55] = 'qa'; +$lang->waterfall->menuOrder[60] = 'doc'; +$lang->waterfall->menuOrder[65] = 'build'; +$lang->waterfall->menuOrder[70] = 'release'; +$lang->waterfall->menuOrder[80] = 'dynamic'; + +$lang->waterfall->menu->doc['subMenu'] = new stdclass(); + +$lang->waterfall->menu->programplan['subMenu'] = new stdclass(); +$lang->waterfall->menu->programplan['subMenu']->lists = array('link' => "{$lang->stage->list}|programplan|browse|projectID=%s&productID=0&type=lists", 'alias' => 'create'); + +$lang->waterfall->menu->qa['subMenu'] = new stdclass(); +$lang->waterfall->menu->qa['subMenu']->bug = array('link' => "{$lang->bug->common}|project|bug|projectID=%s", 'subModule' => 'bug'); +$lang->waterfall->menu->qa['subMenu']->testcase = array('link' => "{$lang->testcase->shortCommon}|project|testcase|projectID=%s", 'subModule' => 'testsuite,testcase,caselib,tree'); +$lang->waterfall->menu->qa['subMenu']->testtask = array('link' => "{$lang->testtask->common}|project|testtask|projectID=%s", 'subModule' => 'testtask', 'class' => 'dropdown dropdown-hover'); + +$lang->waterfall->menu->design['subMenu'] = new stdclass(); +$lang->waterfall->menu->design['subMenu']->all = array('link' => "$lang->all|design|browse|projectID=%s&productID=0&browseType=all"); +$lang->waterfall->menu->design['subMenu']->hlds = array('link' => "{$lang->design->HLDS}|design|browse|projectID=%s&productID=0&browseType=HLDS"); +$lang->waterfall->menu->design['subMenu']->dds = array('link' => "{$lang->design->DDS}|design|browse|projectID=%s&productID=0&browseType=DDS"); +$lang->waterfall->menu->design['subMenu']->dbds = array('link' => "{$lang->design->DBDS}|design|browse|projectID=%s&productID=0&browseType=DBDS"); +$lang->waterfall->menu->design['subMenu']->ads = array('link' => "{$lang->design->ADS}|design|browse|projectID=%s&productID=0&browseType=ADS"); +$lang->waterfall->menu->design['subMenu']->bysearch = array('link' => ' ' . $lang->searchAB . ''); /* Execution menu. */ $lang->execution->homeMenu = new stdclass(); @@ -247,7 +293,7 @@ if($config->systemMode == 'new') $lang->execution->homeMenu->executionkanban = a $lang->execution->menu = new stdclass(); $lang->execution->menu->task = array('link' => "{$lang->task->common}|execution|task|executionID=%s", 'subModule' => 'task,tree', 'alias' => 'importtask,importbug'); -$lang->execution->menu->kanban = array('link' => "$lang->kanban|execution|kanban|executionID=%s"); +$lang->execution->menu->kanban = array('link' => "$lang->executionKanban|execution|kanban|executionID=%s"); $lang->execution->menu->burn = array('link' => "$lang->burn|execution|burn|executionID=%s"); $lang->execution->menu->view = array('link' => "$lang->view|execution|grouptask|executionID=%s", 'alias' => 'grouptask,tree,taskeffort,gantt,calendar,relation,maintainrelation'); $lang->execution->menu->story = array('link' => "$lang->SRCommon|execution|story|executionID=%s", 'subModule' => 'story', 'alias' => 'batchcreate,linkstory,storykanban'); @@ -415,13 +461,24 @@ $lang->company->menuOrder[30] = 'addUser'; $lang->admin->menu = new stdclass(); $lang->admin->menu->index = array('link' => "$lang->indexPage|admin|index", 'alias' => 'register,certifytemail,certifyztmobile,ztcompany'); $lang->admin->menu->company = array('link' => "{$lang->personnel->common}|company|browse|", 'subModule' => ',user,dept,group,'); -$lang->admin->menu->model = array('link' => "$lang->model|custom|browsestoryconcept|", 'subModule' => 'holiday'); +$lang->admin->menu->model = array('link' => "$lang->model|custom|browsestoryconcept|", 'class' => 'dropdown dropdown-hover', 'exclude' => 'custom-index,custom-set,custom-product,custom-execution,custom-required,custom-flow,custom-score,custom-feedback,custom-timezone,custom-mode'); $lang->admin->menu->custom = array('link' => "{$lang->custom->common}|custom|index", 'exclude' => 'custom-browsestoryconcept,custom-timezone,custom-estimate'); $lang->admin->menu->extension = array('link' => "{$lang->extension->common}|extension|browse", 'subModule' => 'extension'); $lang->admin->menu->dev = array('link' => "$lang->redev|dev|api", 'alias' => 'db', 'subModule' => 'dev,editor,entry'); $lang->admin->menu->message = array('link' => "{$lang->message->common}|message|index", 'subModule' => 'message,mail,webhook'); $lang->admin->menu->system = array('link' => "{$lang->admin->system}|backup|index", 'subModule' => 'cron,backup,action,admin,search', 'exclude' => 'admin-index,admin-xuanxuan,admin-register,admin-ztcompany'); +$lang->admin->menu->model['dropMenu'] = new stdclass(); +$lang->admin->menu->model['dropMenu']->allModel = array('link' => "{$lang->globalSetting}|custom|browsestoryconcept|", 'subModule' => 'measurement,report,sqlbuilder,subject,custom,meetingroom,baseline'); +$lang->admin->menu->model['dropMenu']->waterfall = array('link' => "{$lang->waterfallModel}|stage|setType|", 'subModule' => 'stage,auditcl,cmcl,process,activity,zoutput,classify,reviewcl,reviewsetting'); + +$lang->admin->menu->allModel['subMenu'] = new stdclass(); +$lang->admin->menu->allModel['subMenu']->storyConcept = array('link' => "{$lang->storyConcept}|custom|browsestoryconcept|"); +$lang->admin->menu->allModel['menuOrder'][5] = 'storyConcept'; + +$lang->admin->menu->waterfall['subMenu'] = new stdclass(); +$lang->admin->menu->waterfall['subMenu']->stage = array('link' => '阶段|stage|setType|', 'subModule' => 'stage'); + /* Admin menu order. */ $lang->admin->menuOrder[5] = 'index'; $lang->admin->menuOrder[10] = 'company'; @@ -432,11 +489,6 @@ $lang->admin->menuOrder[30] = 'extension'; $lang->admin->menuOrder[35] = 'dev'; $lang->admin->menuOrder[40] = 'system'; -$lang->admin->menu->model['subMenu'] = new stdclass(); -$lang->admin->menu->model['subMenu']->storyConcept = array('link' => "{$lang->storyConcept}|custom|browsestoryconcept|"); - -$lang->admin->menu->model['menuOrder'][5] = 'storyConcept'; - $lang->admin->menu->message['subMenu'] = new stdclass(); $lang->admin->menu->message['subMenu']->message = new stdclass(); $lang->admin->menu->message['subMenu']->mail = array('link' => "{$lang->mail->common}|mail|index", 'subModule' => 'mail'); @@ -507,6 +559,8 @@ $lang->navGroup->story = 'product'; $lang->navGroup->project = 'project'; $lang->navGroup->deploy = 'project'; +$lang->navGroup->programplan = 'project'; +$lang->navGroup->design = 'project'; $lang->navGroup->stakeholder = 'project'; $lang->navGroup->projectbuild = 'project'; @@ -522,10 +576,13 @@ $lang->navGroup->build = 'project'; $lang->navGroup->measrecord = 'project'; $lang->navGroup->milestone = 'project'; -$lang->navGroup->execution = 'execution'; -$lang->navGroup->task = 'execution'; -$lang->navGroup->build = 'execution'; -$lang->navGroup->team = 'execution'; +$lang->navGroup->kanban = 'execution'; +$lang->navGroup->execution = 'execution'; +$lang->navGroup->task = 'execution'; +$lang->navGroup->build = 'execution'; +$lang->navGroup->team = 'execution'; +$lang->navGroup->kanbancolumn = 'execution'; +$lang->navGroup->kanbanlane = 'execution'; $lang->navGroup->doc = 'doc'; $lang->navGroup->doclib = 'doc'; diff --git a/module/common/lang/zh-cn.php b/module/common/lang/zh-cn.php index 1584f48ac4..8a25586e23 100644 --- a/module/common/lang/zh-cn.php +++ b/module/common/lang/zh-cn.php @@ -45,34 +45,38 @@ $lang->runInfo = "
    《Z PUBLIC LICENSE授权协议1.2》。未经许可,不得去除、隐藏或遮掩禅道软件的任何标志及链接。"; $lang->designedByAIUX = " 艾体验设计"; -$lang->reset = '重填'; -$lang->cancel = '取消'; -$lang->refresh = '刷新'; -$lang->edit = '编辑'; -$lang->delete = '删除'; -$lang->close = '关闭'; -$lang->unlink = '移除'; -$lang->import = '导入'; -$lang->export = '导出'; -$lang->setFileName = '文件名:'; -$lang->submitting = '稍候...'; -$lang->save = '保存'; -$lang->confirm = '确认'; -$lang->preview = '查看'; -$lang->goback = '返回'; -$lang->goPC = 'PC版'; -$lang->more = '更多'; -$lang->moreLink = 'More'; -$lang->day = '天'; -$lang->customConfig = '自定义'; -$lang->public = '公共'; -$lang->trunk = '主干'; -$lang->sort = '排序'; -$lang->required = '必填'; -$lang->noData = '暂无'; -$lang->fullscreen = '全屏'; -$lang->retrack = '收起'; -$lang->whitelist = '访问白名单'; +$lang->reset = '重填'; +$lang->cancel = '取消'; +$lang->refresh = '刷新'; +$lang->create = '新建'; +$lang->edit = '编辑'; +$lang->delete = '删除'; +$lang->close = '关闭'; +$lang->unlink = '移除'; +$lang->import = '导入'; +$lang->export = '导出'; +$lang->setFileName = '文件名:'; +$lang->submitting = '稍候...'; +$lang->save = '保存'; +$lang->confirm = '确认'; +$lang->preview = '查看'; +$lang->goback = '返回'; +$lang->goPC = 'PC版'; +$lang->more = '更多'; +$lang->moreLink = 'More'; +$lang->day = '天'; +$lang->customConfig = '自定义'; +$lang->public = '公共'; +$lang->trunk = '主干'; +$lang->sort = '排序'; +$lang->required = '必填'; +$lang->noData = '暂无'; +$lang->fullscreen = '全屏'; +$lang->retrack = '收起'; +$lang->whitelist = '访问白名单'; +$lang->globalSetting = '全局设置'; +$lang->waterfallModel = '瀑布模型'; +$lang->all = '所有'; $lang->actions = '操作'; $lang->restore = '恢复默认'; @@ -106,6 +110,9 @@ $lang->lineNumber = '行号'; $lang->tutorialConfirm = '检测到你尚未退出新手教程模式,是否现在退出?'; $lang->levelExceeded = '层级已超过显示范围,更多信息请前往网页端查看或者是通过搜索方式查看。'; +$lang->serviceAgreement = "服务协议"; +$lang->privacyPolicy = "隐私政策"; + $lang->preShortcutKey = '[快捷键:←]'; $lang->nextShortcutKey = '[快捷键:→]'; $lang->backShortcutKey = '[快捷键:Alt+↑]'; @@ -138,6 +145,7 @@ $lang->program->common = '项目集'; $lang->product->common = '产品'; $lang->project->common = '项目'; $lang->execution->common = $config->systemMode == 'new' ? '执行' : $lang->executionCommon; +$lang->kanban->common = '看板'; $lang->qa->common = '测试'; $lang->devops->common = 'DevOps'; $lang->doc->common = '文档'; @@ -162,6 +170,13 @@ $lang->dept->common = '部门'; $lang->upgrade->common = '升级'; $lang->program->list = '项目集列表'; $lang->program->kanban = '项目集看板'; +$lang->design->common = '设计'; +$lang->design->HLDS = '概要设计'; +$lang->design->DDS = '详细设计'; +$lang->design->DBDS = '数据库设计'; +$lang->design->ADS = '接口设计'; +$lang->stage->common = '阶段'; +$lang->stage->list = '阶段列表'; $lang->execution->list = "{$lang->executionCommon}列表"; $lang->personnel->common = '人员'; @@ -180,38 +195,37 @@ $lang->score->shortCommon = '积分'; $lang->testreport->shortCommon = '报告'; $lang->qa->shortCommon = 'QA'; -$lang->dashboard = '仪表盘'; -$lang->contribute = '贡献'; -$lang->dynamic = '动态'; -$lang->contact = '联系人'; -$lang->whitelist = '白名单'; -$lang->roadmap = '路线图'; -$lang->track = '矩阵'; -$lang->settings = '设置'; -$lang->overview = '概况'; -$lang->module = '模块'; -$lang->priv = '权限'; -$lang->design = '设计'; -$lang->other = '其他'; -$lang->estimation = '估算'; -$lang->issue = '问题'; -$lang->risk = '风险'; -$lang->measure = '度量'; -$lang->treeView = '树状图'; -$lang->groupView = '分组视图'; -$lang->kanban = '看板'; -$lang->burn = '燃尽图'; -$lang->view = '视图'; -$lang->intro = '介绍'; -$lang->indexPage = '首页'; -$lang->model = '模型'; -$lang->redev = '二次开发'; -$lang->browser = '浏览器'; -$lang->db = '数据库'; -$lang->editor = '编辑器'; -$lang->timezone = '时区'; -$lang->security = '安全'; -$lang->calendar = '日程'; +$lang->dashboard = '仪表盘'; +$lang->contribute = '贡献'; +$lang->dynamic = '动态'; +$lang->contact = '联系人'; +$lang->whitelist = '白名单'; +$lang->roadmap = '路线图'; +$lang->track = '矩阵'; +$lang->settings = '设置'; +$lang->overview = '概况'; +$lang->module = '模块'; +$lang->priv = '权限'; +$lang->other = '其他'; +$lang->estimation = '估算'; +$lang->issue = '问题'; +$lang->risk = '风险'; +$lang->measure = '度量'; +$lang->treeView = '树状图'; +$lang->groupView = '分组视图'; +$lang->executionKanban = '看板'; +$lang->burn = '燃尽图'; +$lang->view = '视图'; +$lang->intro = '介绍'; +$lang->indexPage = '首页'; +$lang->model = '模型'; +$lang->redev = '二次开发'; +$lang->browser = '浏览器'; +$lang->db = '数据库'; +$lang->editor = '编辑器'; +$lang->timezone = '时区'; +$lang->security = '安全'; +$lang->calendar = '日程'; $lang->my->work = '待处理'; diff --git a/module/common/lang/zh-tw.php b/module/common/lang/zh-tw.php index bde2294f99..7234171ad3 100644 --- a/module/common/lang/zh-tw.php +++ b/module/common/lang/zh-tw.php @@ -179,37 +179,37 @@ $lang->score->shortCommon = '積分'; $lang->testreport->shortCommon = '報告'; $lang->qa->shortCommon = 'QA'; -$lang->dashboard = '儀表盤'; -$lang->contribute = '貢獻'; -$lang->dynamic = '動態'; -$lang->contact = '聯繫人'; -$lang->whitelist = '白名單'; -$lang->roadmap = '路線圖'; -$lang->track = '矩陣'; -$lang->settings = '設置'; -$lang->overview = '概況'; -$lang->module = '模組'; -$lang->priv = '權限'; -$lang->design = '設計'; -$lang->other = '其他'; -$lang->estimation = '估算'; -$lang->issue = '問題'; -$lang->risk = '風險'; -$lang->measure = '度量'; -$lang->treeView = '樹狀圖'; -$lang->groupView = '分組視圖'; -$lang->kanban = '看板'; -$lang->burn = '燃盡圖'; -$lang->view = '視圖'; -$lang->intro = '介紹'; -$lang->indexPage = '首頁'; -$lang->model = '模型'; -$lang->redev = '二次開發'; -$lang->browser = '瀏覽器'; -$lang->db = '資料庫'; -$lang->editor = '編輯器'; -$lang->timezone = '時區'; -$lang->security = '安全'; +$lang->dashboard = '儀表盤'; +$lang->contribute = '貢獻'; +$lang->dynamic = '動態'; +$lang->contact = '聯繫人'; +$lang->whitelist = '白名單'; +$lang->roadmap = '路線圖'; +$lang->track = '矩陣'; +$lang->settings = '設置'; +$lang->overview = '概況'; +$lang->module = '模組'; +$lang->priv = '權限'; +$lang->design = '設計'; +$lang->other = '其他'; +$lang->estimation = '估算'; +$lang->issue = '問題'; +$lang->risk = '風險'; +$lang->measure = '度量'; +$lang->treeView = '樹狀圖'; +$lang->groupView = '分組視圖'; +$lang->executionKanban = '看板'; +$lang->burn = '燃盡圖'; +$lang->view = '視圖'; +$lang->intro = '介紹'; +$lang->indexPage = '首頁'; +$lang->model = '模型'; +$lang->redev = '二次開發'; +$lang->browser = '瀏覽器'; +$lang->db = '資料庫'; +$lang->editor = '編輯器'; +$lang->timezone = '時區'; +$lang->security = '安全'; $lang->calendar = '日程'; $lang->my->work = '待處理'; diff --git a/module/common/model.php b/module/common/model.php index a752ccf8c8..ab3e1d7e8f 100644 --- a/module/common/model.php +++ b/module/common/model.php @@ -320,7 +320,6 @@ class commonModel extends model if($module == 'block' and $method == 'main') return true; if($module == 'block' and $method == 'delete') return true; if($module == 'product' and $method == 'showerrornone') return true; - if($module == 'report' and $method == 'annualdata') return true; } return false; } @@ -848,6 +847,9 @@ class commonModel extends model $linkPart = explode('|', $menu['link']); if(!isset($linkPart[2])) continue; $method = $linkPart[2]; + + if($currentModule == 'report' and $method == 'annualData') continue; // Skip some pages that do not require permissions. + if(common::hasPriv($currentModule, $method)) { $display = true; @@ -1843,13 +1845,18 @@ EOD; $typeOnlyCondition = $type . 'OnlyCondition'; $queryCondition = $this->session->$queryCondition; + $preAndNextObject = new stdClass(); + $preAndNextObject->pre = ''; + $preAndNextObject->next = ''; + if(empty($queryCondition)) return $preAndNextObject; + $table = $this->config->objectTables[$type]; $orderBy = $type . 'OrderBy'; $orderBy = $this->session->$orderBy; - if(empty($queryCondition) or $this->session->$typeOnlyCondition) + if($this->session->$typeOnlyCondition) { $sql = $this->dao->select('*')->from($table) - ->beginIF($queryCondition != false)->where($queryCondition)->fi() + ->where($queryCondition) ->beginIF($orderBy != false)->orderBy($orderBy)->fi() ->get(); } @@ -1877,10 +1884,6 @@ EOD; $existsObjectList = $this->session->$objectIdListKey; } - $preAndNextObject = new stdClass(); - $preAndNextObject->pre = ''; - $preAndNextObject->next = ''; - $preObj = false; if(isset($existsObjectList['objectList'])) { @@ -2150,8 +2153,6 @@ EOD; $rights = $app->user->rights['rights']; $acls = $app->user->rights['acls']; - if((($app->user->account != 'guest') or ($app->company->guest and $app->user->account == 'guest')) and $module == 'report' and $method == 'annualdata') return true; - if(isset($rights[$module][$method])) { if(!commonModel::hasDBPriv($object, $module, $method)) return false; @@ -2854,15 +2855,13 @@ EOD; { if(empty($markdown)) return false; - $markdown = str_replace('&', '&', $markdown); - global $app; - $hyperdown = $app->loadClass('hyperdown'); - $content = $hyperdown->makeHtml($markdown); - - $content = htmlspecialchars_decode($content); - $content = fixer::stripDataTags($content); - return $content; + $app->loadClass('parsedown', true); + return parsedown::instance() + ->setSafeMode(true) + ->setBreaksEnabled(true) + ->setMarkupEscaped(true) + ->text($markdown); } } diff --git a/module/common/view/datatable.fix.html.php b/module/common/view/datatable.fix.html.php index cf7801bc0b..33a4c28c46 100644 --- a/module/common/view/datatable.fix.html.php +++ b/module/common/view/datatable.fix.html.php @@ -1,5 +1,5 @@ -moduleName;?> -methodName;?> +app->rawModule;?> +app->rawMethod;?> moduleName . ucfirst($this->methodName);?> +
    +
    + +
    +

    design->noDesign;?>

    +
    + +
    + + recTotal}&recPerPage={$pager->recPerPage}";?> + + + + + + + + + + + + + + + + + + + + + + + + +
    design->id);?> design->type);?> design->name);?> design->createdBy);?> design->createdDate);?>design->assignedTo);?> design->actions;?>
    id);?> design->typeList, $design->type);?>createLink('design', 'view', "id={$design->id}"), $design->name);?> createdBy);?>createdDate, 0, 11);?> design->printAssignedHtml($design, $users);?> + id}"; + common::printIcon('design', 'edit', $vars, $design, 'list', 'alter', '', '', '', '', '', $design->project); + common::printIcon('design', 'viewCommit', $vars, $design, 'list', 'list-alt', '', 'iframe showinonlybody', true); + common::printIcon('design', 'delete', $vars, $design, 'list', 'trash', 'hiddenwin', '', '', '', '', $design->project); + ?> +
    + +
    + +
    + diff --git a/module/design/view/create.html.php b/module/design/view/create.html.php new file mode 100644 index 0000000000..4a0be47622 --- /dev/null +++ b/module/design/view/create.html.php @@ -0,0 +1,60 @@ + + * @package design + * @version $Id: create.html.php 4903 2020-09-02 09:32:59Z tianshujie@easycorp.ltd $ + * @link http://www.zentao.net + */ +?> + + + +
    +
    +
    +

    design->create;?>

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    design->product;?>
    design->story;?>
    design->type;?>design->typeList, '', "class='form-control chosen'");?>
    design->name;?>
    design->desc;?>
    design->file;?>fetch('file', 'buildform', 'fileCount=1&percent=0.85');?>
    +
    +
    +
    + diff --git a/module/design/view/edit.html.php b/module/design/view/edit.html.php new file mode 100644 index 0000000000..06afd43ef0 --- /dev/null +++ b/module/design/view/edit.html.php @@ -0,0 +1,98 @@ + + * @package design + * @version $Id: edit.html.php 4903 2020-09-02 09:32:59Z tianshujie@easycorp.ltd $ + * @link http://www.zentao.net + */ +?> + + +type);?> +
    +
    +
    +

    design->edit;?>

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    design->product;?>product, "class='form-control chosen'");?>
    design->story;?>story, "class='form-control chosen'");?>
    design->type;?>design->typeList, $design->type, "class='form-control chosen'");?>
    design->name;?>name, "class='form-control'");?>
    design->desc;?>desc, 'class="form-control"');?>
    design->file;?>fetch('file', 'buildform', 'fileCount=1&percent=0.85');?>
    story->checkAffection;?> +
    + +
    +
    + + + + + + + + + + + + + tasks as $task):?> + + + ; + + + + + + + +
    task->id;?> task->name;?> task->assignedTo;?>task->status;?> task->consumed;?> task->left;?>
    id?>createLink('task', 'view', "taskID=$task->id"), $task->name, '_blank');?>assignedTo);?>processStatus('task', $task);?>consumed;?>left;?>
    +
    +
    +
    +
    +
    +
    +
    + diff --git a/module/design/view/linkcommit.html.php b/module/design/view/linkcommit.html.php new file mode 100644 index 0000000000..540e59b1f8 --- /dev/null +++ b/module/design/view/linkcommit.html.php @@ -0,0 +1,88 @@ + + * @package design + * @version $Id: linkcommit.html.php 4903 2020-09-02 09:32:59Z tianshujie@easycorp.ltd $ + * @link http://www.zentao.net + */ +?> + +
    +
    +

    + id;?> + name?> + arrow . $lang->design->linkCommit;?> +

    +
    + +
    +

    design->noCommit;?>

    +
    + + +
    method='post'> + + + + + + SCM) and $repo->SCM == 'Git'):?> + + + + + + + + + + + + + SCM == 'Git'):?> + + + + + comment, ENT_QUOTES);?> + + + + +
    repo->revisionA?>repo->commit?>repo->time?>repo->committer?>repo->comment?>
    +
    + + +
    +
    repo->createLink('revision', "repoID=$repoID&revision={$log->revision}"), $repo->SCM == 'Git' ? substr($log->revision, 0, 10) : $log->revision);?>commit?>time, 0, 10);?>committer, $log->committer);?>comment?>
    + +
    + +
    + +design->errorDate);?> + diff --git a/module/design/view/view.html.php b/module/design/view/view.html.php new file mode 100644 index 0000000000..e7b71d3a08 --- /dev/null +++ b/module/design/view/view.html.php @@ -0,0 +1,92 @@ + + * @package design + * @version $Id: view.html.php 4903 2020-09-02 09:32:59Z tianshujie@easycorp.ltd $ + * @link http://www.zentao.net + */ +?> + +type);?> + +
    +
    +
    +
    +
    design->desc;?>
    +
    + desc;?> +
    +
    + fetch('file', 'printFiles', array('files' => $design->files, 'fieldset' => 'true'));?> +
    +
    +
    +
    + session->designList);?> +
    ";?> + deleted):?> + id", $design, 'button', '', '', 'iframe showinonlybody', true); + common::printIcon('design', 'linkCommit', "designID=$design->id", $design, 'button', 'link', '', 'iframe showinonlybody', true); + common::printIcon('design', 'edit', "designID=$design->id", $design, 'button', 'alter'); + common::printIcon('design', 'delete', "designID=$design->id", $design, 'button', 'trash', 'hiddenwin'); + ?> + +
    +
    +
    +
    +
    +
    +
    design->basicInfo;?>
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    design->type;?>design->typeList, $design->type);?>
    design->product;?>productName;?>
    design->story;?>story ? html::a($this->createLink('story', 'view', "id=$design->story"), zget($stories, $design->story)) : '';?>
    design->submission;?>commit;?>
    design->createdBy;?>createdBy);?>
    design->createdDate;?>createdDate, 0, 11);?>
    +
    +
    +
    +
    +
    + diff --git a/module/design/view/viewcommit.html.php b/module/design/view/viewcommit.html.php new file mode 100644 index 0000000000..883dcd88b5 --- /dev/null +++ b/module/design/view/viewcommit.html.php @@ -0,0 +1,63 @@ + + * @package design + * @version $Id: viewcommit.html.php 4903 2020-09-02 09:32:59Z tianshujie@easycorp.ltd $ + * @link http://www.zentao.net + */ +?> + + +
    +
    +

    + id;?> + name?> + arrow . $lang->design->submission;?> +

    +
    +
    + id", "" . $lang->design->linkCommit, '_blank', "class='btn btn-primary'");?> +
    + commit)):?> +
    +

    design->noCommit;?>

    +
    + + + + + + + + + + + + + commit as $commit):?> + + + + + + + + + +
    design->submission;?>design->commitBy;?>design->commitDate;?>design->comment;?> design->actions;?>
    id"), "#$commit->id", '_blank');?>committer, $commit->committer);?>time, 0, 11);?>comment;?> + id&commitID=$commit->id", $design, 'list', 'unlink', 'hiddenwin', 'iframe showinonlybody', true);?> +
    + + +
    + diff --git a/module/doc/css/common.css b/module/doc/css/common.css index 6e3ba241d8..abf19b59c7 100644 --- a/module/doc/css/common.css +++ b/module/doc/css/common.css @@ -96,3 +96,6 @@ ol, ul {margin-bottom: 0} .c-product, .c-execution, .c-lib {width: 80px !important;} .header-btn .btn > .text {text-overflow: unset !important;} + +.tree li.active > .tree-group a {font-weight: 700; color: #0c64eb;} +.tree li>.list-toggle {left: -4px; top: 0;} diff --git a/module/doc/css/objectlibs.css b/module/doc/css/objectlibs.css index e78f8ac770..8ced76e32c 100644 --- a/module/doc/css/objectlibs.css +++ b/module/doc/css/objectlibs.css @@ -2,7 +2,7 @@ .addbtn {padding-top: 22px; height: 63px; border: 1px dashed #ddd; width: 60px;} .addbtn .icon-plus {font-size: 18px; display: block; opacity: 0.5; transition: opacity .2s; text-shadow: 1px 1px 3px rgba(0,0,0,.2);} .addbtn:hover .icon-plus {opacity: .9; animation: flash-icon 1s linear alternate infinite;} -#subHeader #dropMenu {min-width: 250px; box-sizing: inhert; max-height: inherit;} +#subHeader #dropMenu {min-width: 250px; box-sizing: inherit; max-height: inherit;} #subHeader #dropMenu .table-col .list-group {padding-top: 10px;} .main-col .block-files .panel-heading {padding-right: 20px;} .main-col .block-files .panel-heading .panel-title {height: 35px; line-height: 30px;} @@ -41,3 +41,9 @@ .title {font-size: 20px !important;} .article-content.comment {width: 100% !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} diff --git a/module/doc/js/objectlibs.js b/module/doc/js/objectlibs.js index 8830c91235..992cb3cae6 100644 --- a/module/doc/js/objectlibs.js +++ b/module/doc/js/objectlibs.js @@ -133,7 +133,7 @@ $(function() $('#content').on('click', '.outline .outline-toggle i.icon-angle-right', function() { - $('.article-content').css('width', '85%'); + $('.article-content').width('85%'); $('.outline').css({'min-width' : '180px', 'border-left' : '2px solid #efefef'}); $(this).removeClass('icon-angle-right').addClass('icon-angle-left').css('left', '-9px'); $('.outline-content').show(); diff --git a/module/doc/lang/en.php b/module/doc/lang/en.php index b282541b4b..f57d7a6036 100644 --- a/module/doc/lang/en.php +++ b/module/doc/lang/en.php @@ -29,12 +29,12 @@ $lang->doc->files = 'Files'; $lang->doc->addedBy = 'Author'; $lang->doc->addedDate = 'Added'; $lang->doc->editedBy = 'UpdatedBy'; -$lang->doc->editedDate = 'UpdatedDate'; +$lang->doc->editedDate = 'Updated'; $lang->doc->version = 'Version'; -$lang->doc->basicInfo = 'Basic Info'; +$lang->doc->basicInfo = 'Basic Information'; $lang->doc->deleted = 'Deleted'; $lang->doc->fileObject = 'Dependent Item'; -$lang->doc->whiteList = 'White List'; +$lang->doc->whiteList = 'Whitelist'; $lang->doc->contentType = 'Format'; $lang->doc->separator = ""; $lang->doc->fileTitle = 'File Name'; @@ -51,7 +51,7 @@ $lang->doc->item = ' Items'; $lang->doc->num = 'Documents'; $lang->doc->searchResult = 'Search Result'; $lang->doc->mailto = 'Mailto'; -$lang->doc->noModule = 'No document in this lib, please create it'; +$lang->doc->noModule = 'No document in this library. Create one.'; $lang->doc->noChapter = 'No chapters or articles in this book. Please add chapters and articles.'; $lang->doc->views = 'Views'; $lang->doc->draft = 'Draft'; @@ -73,7 +73,7 @@ $lang->doc->todayEdited = 'Updated Today'; $lang->doc->pastEdited = 'Total Updated'; $lang->doc->myDoc = 'My Documents'; $lang->doc->myCollection = 'My Favorites'; -$lang->doc->tableContents = 'Catalog'; +$lang->doc->tableContents = 'Directory'; /* Methods list */ $lang->doc->index = 'Document Home'; @@ -91,12 +91,12 @@ $lang->doc->manageType = 'Manage Category'; $lang->doc->editType = 'Edit'; $lang->doc->deleteType = 'Delete'; $lang->doc->addType = 'Add'; -$lang->doc->childType = 'Catalog'; -$lang->doc->catalogName = 'Catalog Name'; +$lang->doc->childType = 'Directory'; +$lang->doc->catalogName = 'Name'; $lang->doc->collect = 'Add Favorite'; $lang->doc->cancelCollection = 'Remove Favorite'; $lang->doc->deleteFile = 'Delete File'; -$lang->doc->menuTitle = 'Menu'; +$lang->doc->menuTitle = 'Direcotory'; $lang->doc->collectAction = 'Add Favorite'; diff --git a/module/doc/model.php b/module/doc/model.php index ef211b0820..bcf3982815 100644 --- a/module/doc/model.php +++ b/module/doc/model.php @@ -737,7 +737,7 @@ class docModel extends model } unset($doc->contentType); - $doc->draft = $doc->content; + $doc->draft = isset($doc->content) ? $doc->content : ''; $this->dao->update(TABLE_DOC)->data($doc, 'content') ->autoCheck() ->batchCheck($requiredFields, 'notempty') @@ -2274,7 +2274,7 @@ EOT; if($startModule) $startModulePath = $startModule->path . '%'; } - $docs = $this->dao->select('*')->from(TABLE_DOC) + $docs = $this->dao->select('*')->from(TABLE_DOC) ->where('lib')->eq($rootID) ->andWhere('deleted')->eq(0) ->fetchAll(); @@ -2308,14 +2308,26 @@ EOT; { if(!$docID and $currentMethod != 'tablecontents') $docID = $doc->id; - $treeMenu[0] .= 'id == $docID ? ' class="active"' : ' class="independent"') . '>'; + $treeMenu[0] .= 'id == $docID ? ' class="active"' : ' class="independent"') . " data-id=$doc->id>"; if($currentMethod == 'tablecontents') { $treeMenu[0] .= '' . zget($users, $doc->editedBy) . '  ' . $doc->editedDate . ''; } - - $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($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')) + { + $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}"); + $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}'"); + } $treeMenu[0] .= ''; } @@ -2362,13 +2374,28 @@ EOT; else { if(!$docID and $currentMethod != 'tablecontents') $docID = $doc->id; - $treeMenu[$module->id] .= 'id == $docID ? ' class="active"' : ' class="doc"') . '>'; + $treeMenu[$module->id] .= 'id == $docID ? ' class="active"' : ' class="doc"') . " data-id=$doc->id>"; if($currentMethod == 'tablecontents') { $treeMenu[$module->id] .= '' . zget($users, $doc->editedBy) . '  ' . $doc->editedDate . ''; } - $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($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')) + { + $treeMenu[$module->id] .= "
    "; + $treeMenu[$module->id] .= html::a(helper::createLink('doc', 'edit', "docID=$docID&comment=false&objectType=$type&objectID=$objectID&libID=$libID"), "", '', "title={$this->lang->doc->edit}"); + $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}'"); + } $treeMenu[$module->id] .= ''; } @@ -2381,7 +2408,22 @@ EOT; } else { - $li = "" . $module->name . ''; + if($currentMethod == 'tablecontents') + { + $li = "" . $module->name . ''; + } + else + { + $li = "
    " . $module->name . ''; + if(common::hasPriv('tree', 'edit') or common::hasPriv('tree', 'browse')) + { + $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->editType}"); + $li .= '
    '; + } + $li .= '
    '; + } } if($treeMenu[$module->id]) { @@ -2410,7 +2452,7 @@ EOT; } } - $treeMenu[$module->parent] .= '
  • ' . $li . '
  • '; + $treeMenu[$module->parent] .= '
  • ' . $li . '
  • '; } /** diff --git a/module/doc/view/side.html.php b/module/doc/view/side.html.php index 837b2e8027..98fda2cc23 100644 --- a/module/doc/view/side.html.php +++ b/module/doc/view/side.html.php @@ -15,149 +15,109 @@ if(empty($type)) $type = 'product'; $sideWidth = common::checkNotCN() ? '270' : '238'; ?>
    -
    -
    - - "; - echo html::a('javascript:;', "", '', "data-toggle='dropdown' class='btn btn-link'"); - echo "
    '; - } +
    +
    + + "; + echo html::a('javascript:;', "", '', "data-toggle='dropdown' class='btn btn-link'"); + echo "
    '; + } - if($type == 'book' and ($canEditLib or $canManageBook)) - { - echo "'; - } - ?> -
    - -
    -
    - doc->noChapter : $lang->doc->noModule;?> -
    -
    - - - - - - + if($type == 'book' and ($canEditLib or $canManageBook)) + { + echo "'; + } + ?>
    - + 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 ad7086de69..fbd5b4af95 100644 --- a/module/execution/control.php +++ b/module/execution/control.php @@ -40,6 +40,7 @@ class execution extends control $this->loadModel('project'); + if(defined('IN_UPGRADE') and IN_UPGRADE) return false; $this->executions = $this->execution->getPairs(0, 'all', 'nocode'); $skipCreateStep = array('computeburn', 'ajaxgetdropmenu', 'executionkanban', 'ajaxgetteammembers'); if(!in_array($this->methodName, $skipCreateStep) and $this->app->tab == 'execution') @@ -137,6 +138,7 @@ class execution extends control $this->loadModel('task'); $this->loadModel('datatable'); $this->loadModel('setting'); + $this->loadModel('product'); if(common::hasPriv('execution', 'create')) $this->lang->TRActions = html::a($this->createLink('execution', 'create'), " " . $this->lang->execution->create, '', "class='btn btn-primary'"); @@ -148,7 +150,7 @@ class execution extends control /* Get products by execution. */ $execution = $this->commonAction($executionID, $status); $executionID = $execution->id; - $products = $this->loadModel('product')->getProductPairsByProject($executionID); + $products = $this->product->getProductPairsByProject($executionID); setcookie('preExecutionID', $executionID, $this->config->cookieLife, $this->config->webRoot, '', false, true); /* Save the recently five executions visited in the cookie. */ @@ -216,6 +218,19 @@ class execution extends control $tasks = $this->execution->getTasks($productID, $executionID, $this->executions, $browseType, $queryID, $moduleID, $sort, $pager); } + /* Get product. */ + if(empty($productID)) + { + $productModule = $this->tree->getById($moduleID); + if(!empty($productModule) and $productModule->type != 'task') $product = $this->product->getById($productModule->root); + } + if(empty($product)) $product = $this->product->getById($productID); + + if(!empty($product) and $product->type != 'normal') + { + $this->lang->datatable->showBranch = sprintf($this->lang->datatable->showBranch, $this->lang->product->branchName[$product->type]); + } + /* Build the search form. */ $actionURL = $this->createLink('execution', 'task', "executionID=$executionID&status=bySearch¶m=myQueryID"); $this->config->execution->search['onMenuBar'] = 'yes'; @@ -245,6 +260,7 @@ class execution extends control $this->view->executionID = $executionID; $this->view->execution = $execution; $this->view->productID = $productID; + $this->view->product = $product; $this->view->modules = $this->tree->getTaskOptionMenu($executionID, 0, 0, $showAllModule ? 'allModule' : ''); $this->view->moduleID = $moduleID; $this->view->moduleTree = $this->tree->getTaskTreeMenu($executionID, $productID, $startModuleID = 0, array('treeModel', 'createTaskLink'), $extra); @@ -671,6 +687,7 @@ class execution extends control $this->loadModel('story'); $this->loadModel('user'); $this->loadModel('datatable'); + $this->app->loadLang('datatable'); $this->app->loadLang('testcase'); $type = strtolower($type); @@ -771,15 +788,29 @@ class execution extends control $storyBugs = $this->loadModel('bug')->getStoryBugCounts($storyIdList, $executionID); $storyCases = $this->loadModel('testcase')->getStoryCaseCounts($storyIdList); - $plans = $this->execution->getPlans($products); + $plans = $this->execution->getPlans($products, 'skipParent|withMainPlan'); $allPlans = array('' => ''); if(!empty($plans)) { foreach($plans as $plan) $allPlans += $plan; } - if($this->cookie->storyModuleParam) $this->view->module = $this->loadModel('tree')->getById($this->cookie->storyModuleParam); - if($this->cookie->storyProductParam) $this->view->product = $this->loadModel('product')->getById($this->cookie->storyProductParam); + if($this->cookie->storyModuleParam) + { + $module = $this->loadModel('tree')->getById($this->cookie->storyModuleParam); + $this->view->module = $module; + + $product = $this->product->getById($module->root); + $this->lang->datatable->showBranch = sprintf($this->lang->datatable->showBranch, $this->lang->product->branchName[$product->type]); + } + + if($this->cookie->storyProductParam) + { + $product = $this->loadModel('product')->getById($this->cookie->storyProductParam); + $this->view->product = $product; + $this->lang->datatable->showBranch = sprintf($this->lang->datatable->showBranch, $this->lang->product->branchName[$product->type]); + } + if($this->cookie->storyBranchParam) { $branchID = $this->cookie->storyBranchParam; @@ -806,7 +837,7 @@ class execution extends control $this->view->orderBy = $orderBy; $this->view->type = $this->session->executionStoryBrowseType; $this->view->param = $param; - $this->view->moduleTree = $this->loadModel('tree')->getProjectStoryTreeMenu($executionID, $startModuleID = 0, array('treeModel', 'createStoryLink')); + $this->view->moduleTree = $this->loadModel('tree')->getProjectStoryTreeMenu($executionID, 0, array('treeModel', 'createStoryLink')); $this->view->modulePairs = $modulePairs; $this->view->tabID = 'story'; $this->view->storyTasks = $storyTasks; @@ -1236,6 +1267,8 @@ class execution extends control unset($this->lang->doc->menu->execution['subMenu']); } + $project = $this->project->getByID($projectID); + $extra = str_replace(array(',', ' '), array('&', ''), $extra); parse_str($extra, $output); @@ -1285,7 +1318,7 @@ class execution extends control $projectID = $copyExecution->project; $products = $this->loadModel('product')->getProducts($copyExecutionID); $branches = $this->project->getBranchesByProject($copyExecutionID); - $plans = $this->loadModel('productplan')->getGroupByProduct(array_keys($products)); + $plans = $this->loadModel('productplan')->getGroupByProduct(array_keys($products), 'skipParent|unexpired'); $branchGroups = $this->execution->getBranchByProduct(array_keys($products), $projectID); $linkedBranches = array(); @@ -1310,7 +1343,11 @@ class execution extends control ->where('t1.id')->eq($plan->product) ->fetchAll('id'); - $productPlan = $this->loadModel('productplan')->getPairs($plan->product, $plan->branch, 'unexpired'); + $productPlan = $this->loadModel('productplan')->getPairsForStory($plan->product, $plan->branch, 'skipParent|unexpired|withMainPlan'); + $linkedBranches = array(); + $linkedBranches[$plan->product][$plan->branch] = $plan->branch; + + $this->view->linkedBranches = $linkedBranches; } if(!empty($_POST)) @@ -1482,7 +1519,8 @@ class execution extends control $executions = array('' => '') + $this->executions; $execution = $this->execution->getById($executionID); - $managers = $this->execution->getDefaultManagers($executionID); + $managers = $this->execution->getDefaultManagers($executionID); + /* Remove current execution from the executions. */ unset($executions[$executionID]); @@ -1498,7 +1536,7 @@ class execution extends control $linkedBranches = array(); $linkedProducts = $this->product->getProducts($executionID); $branches = $this->project->getBranchesByProject($executionID); - $plans = $this->productplan->getGroupByProduct(array_keys($linkedProducts)); + $plans = $this->productplan->getGroupByProduct(array_keys($linkedProducts), 'skipParent'); $executionStories = $this->project->getStoriesByProject($executionID); /* If the story of the product which linked the execution, you don't allow to remove the product. */ @@ -1511,6 +1549,7 @@ class execution extends control { $linkedBranches[$productID][$branchID] = $branchID; $productPlans[$productID][$branchID] = isset($plans[$productID][$branchID]) ? $plans[$productID][$branchID] : array(); + if($branchID != BRANCH_MAIN and isset($plans[$productID][BRANCH_MAIN])) $productPlans[$productID][$branchID] += $plans[$productID][BRANCH_MAIN]; if(!empty($executionStories[$productID][$branchID])) { array_push($unmodifiableProducts, $productID); @@ -1891,50 +1930,81 @@ class execution extends control * Kanban. * * @param int $executionID - * @param string $type + * @param string $browseType story|bug|task|all * @param string $orderBy + * @param string $groupBy * @access public * @return void */ - public function kanban($executionID, $type = 'story', $orderBy = 'order_asc') + public function kanban($executionID, $browseType = '', $orderBy = 'order_asc', $groupBy = '') { + if(empty($browseType)) $browseType = $this->session->kanbanType ? $this->session->kanbanType : 'all'; + if(empty($groupBy) and $browseType != 'all') $groupBy = $this->session->{'kanbanGroupBy' . $browseType} ? $this->session->{'kanbanGroupBy' . $browseType} : 'default'; + if(empty($groupBy) and $browseType == 'all') $groupBy = 'default'; + /* Save to session. */ $uri = $this->app->getURI(true); - $this->app->session->set('taskList', $uri, 'execution'); - $this->app->session->set('bugList', $uri, 'qa'); + $this->app->session->set('taskList', $uri, 'execution'); + $this->app->session->set('bugList', $uri, 'qa'); + $this->app->session->set('kanbanType', $browseType, 'execution'); + $this->app->session->set('kanbanGroupBy' . $browseType, $groupBy, 'execution'); - /* Compatibility IE8*/ + /* Load language. */ + $this->app->loadLang('story'); + $this->app->loadLang('task'); + $this->app->loadLang('bug'); + + /* Compatibility IE8. */ if(strpos($this->server->http_user_agent, 'MSIE 8.0') !== false) header("X-UA-Compatible: IE=EmulateIE7"); + $kanbanGroup = $this->loadModel('kanban')->getExecutionKanban($executionID, $browseType, $groupBy); + if(empty($kanbanGroup)) + { + $this->kanban->createLanes($executionID, $browseType, $groupBy); + $kanbanGroup = $this->kanban->getExecutionKanban($executionID, $browseType, $groupBy); + } + $this->execution->setMenu($executionID); $execution = $this->loadModel('execution')->getById($executionID); - $tasks = $this->execution->getKanbanTasks($executionID, "id"); - $bugs = $this->loadModel('bug')->getExecutionBugs($executionID); - $stories = $this->loadModel('story')->getExecutionStories($executionID, 0, 0, $orderBy); /* Determines whether an object is editable. */ $canBeChanged = common::canModify('execution', $execution); - $kanbanGroup = $this->execution->getKanbanGroupData($stories, $tasks, $bugs, $type); - $kanbanSetting = $this->execution->getKanbanSetting(); + /* Get execution's product. */ + $productID = 0; + $products = $this->loadModel('product')->getProducts($executionID); + if($products) $productID = key($products); + + $plans = $this->execution->getPlans($products); + $allPlans = array('' => ''); + if(!empty($plans)) + { + foreach($plans as $plan) $allPlans += $plan; + } + + $userList = array(); + $avatarPairs = $this->dao->select('account, avatar')->from(TABLE_USER)->where('deleted')->eq(0)->fetchPairs(); + foreach($avatarPairs as $account => $avatar) + { + if(!$avatar) continue; + $userList[$account]['avatar'] = $avatar; + } $this->view->title = $this->lang->execution->kanban; $this->view->position[] = html::a($this->createLink('execution', 'browse', "executionID=$executionID"), $execution->name); $this->view->position[] = $this->lang->execution->kanban; - $this->view->stories = $stories; $this->view->realnames = $this->loadModel('user')->getPairs('noletter'); $this->view->storyOrder = $orderBy; $this->view->orderBy = 'id_asc'; $this->view->executionID = $executionID; - $this->view->browseType = ''; - $this->view->execution = $execution; - $this->view->type = $type; + $this->view->productID = $productID; + $this->view->allPlans = $allPlans; + $this->view->browseType = $browseType; $this->view->kanbanGroup = $kanbanGroup; - $this->view->kanbanColumns = $this->execution->getKanbanColumns($kanbanSetting); - $this->view->statusMap = $canBeChanged ? $this->execution->getKanbanStatusMap($kanbanSetting) : array(); - $this->view->statusList = $this->execution->getKanbanStatusList($kanbanSetting); - $this->view->colorList = $this->execution->getKanbanColorList($kanbanSetting); + $this->view->execution = $execution; + $this->view->groupBy = $groupBy; $this->view->canBeChanged = $canBeChanged; + $this->view->userList = $userList; $this->display(); } @@ -1949,7 +2019,7 @@ class execution extends control { $this->loadModel('project'); $projects = $this->project->getPairsByProgram(0, 'noclosed'); - $executions = $this->project->getStats(0, 'all'); + $executions = $this->project->getStats(0, 'all', 0, 0, 30, 'id_desc'); $teams = $this->dao->select('root,account')->from(TABLE_TEAM) ->where('root')->in($this->app->user->view->sprints) @@ -1977,18 +2047,38 @@ class execution extends control $statusCount[$status] += isset($kanbanGroup[$projectID][$status]) ? count($kanbanGroup[$projectID][$status]) : 0; - /* Max 5 closed executions. */ + /* Max 2 closed executions. */ if($status == 'closed') { - if(isset($myExecutions[$status]) and count($myExecutions[$status]) >= 5) $myExecutions[$status] = array_slice($myExecutions[$status], 0, 5, true); - if(isset($kanbanGroup[$projectID][$status]) and count($kanbanGroup[$projectID][$status]) >= 5) $kanbanGroup[$projectID][$status] = array_slice($kanbanGroup[$projectID][$status], 0, 5, true); + if(isset($myExecutions[$status]) and count($myExecutions[$status]) > 2) + { + foreach($myExecutions[$status] as $executionID => $execution) + { + unset($myExecutions[$status][$executionID]); + $myExecutions[$status][$execution->closedDate] = $execution; + } + + krsort($myExecutions[$status]); + $myExecutions[$status] = array_slice($myExecutions[$status], 0, 2, true); + } + + if(isset($kanbanGroup[$projectID][$status]) and count($kanbanGroup[$projectID][$status]) > 2) + { + foreach($kanbanGroup[$projectID][$status] as $executionID => $execution) + { + unset($kanbanGroup[$projectID][$status][$executionID]); + $kanbanGroup[$projectID][$status][$execution->closedDate] = $execution; + } + + krsort($kanbanGroup[$projectID][$status]); + $kanbanGroup[$projectID][$status] = array_slice($kanbanGroup[$projectID][$status], 0, 2); + } } } if(empty($kanbanGroup[$projectID])) continue; $projectCount++; } - krsort($kanbanGroup); $this->view->title = $this->lang->execution->executionKanban; $this->view->kanbanGroup = empty($myExecutions) ? $kanbanGroup : array($myExecutions) + $kanbanGroup; @@ -2114,6 +2204,14 @@ class execution extends control } } + /* Close the page when there is no data. */ + $hasData = false; + foreach($datas as $data) + { + if(!empty($data)) $hasData = true; + } + if(!$hasData) die(js::alert($this->lang->execution->noPrintData) . js::close()); + $this->execution->saveKanbanData($executionID, $originalDatas); $hasBurn = $this->post->content == 'all'; @@ -2139,8 +2237,7 @@ class execution extends control $this->execution->setMenu($executionID); $execution = $this->execution->getById($executionID); - $this->view->position[] = html::a($this->createLink('execution', 'browse', "executionID=$executionID"), $execution->name); - $this->view->position[] = $this->lang->execution->printKanban; + $this->view->executionID = $executionID; $this->display(); } @@ -2466,6 +2563,8 @@ class execution extends control { $this->execution->linkStory($objectID); if($object->type != 'project' and $object->project != 0) $this->execution->linkStory($object->project); + + if(isonlybody()) die(js::reload('parent')); die(js::locate($browseLink)); } @@ -2481,22 +2580,25 @@ class execution extends control $queryID = ($browseType == 'bySearch') ? (int)$param : 0; /* Set modules and branches. */ - $modules = array(); - $branchPairs = array(); - $branches = $this->project->getBranchesByProject($objectID); - $productType = 'normal'; + $modules = array(); + $branchIDList = array(BRANCH_MAIN); + $branches = $this->project->getBranchesByProject($objectID); + $productType = 'normal'; $this->loadModel('tree'); $this->loadModel('branch'); foreach($products as $product) { - $productModules = $this->tree->getOptionMenu($product->id); - foreach($productModules as $moduleID => $moduleName) $modules[$moduleID] = ((count($products) >= 2 and $moduleID != 0) ? $product->name : '') . $moduleName; + $productModules = $this->tree->getOptionMenu($product->id, 'story', 0, array_keys($branches[$product->id])); + foreach($productModules as $branch => $branchModules) + { + foreach($branchModules as $moduleID => $moduleName) $modules[$moduleID] = ((count($products) >= 2 and $moduleID != 0) ? $product->name : '') . $moduleName; + } if($product->type != 'normal') { $productType = $product->type; if(isset($branches[$product->id])) { - foreach($branches[$product->id] as $branchID => $branch) $branchPairs[$branchID] = $branchID; + foreach($branches[$product->id] as $branchID => $branch) $branchIDList[$branchID] = $branchID; } } } @@ -2508,11 +2610,11 @@ class execution extends control if($browseType == 'bySearch') { - $allStories = $this->story->getBySearch('', 0, $queryID, 'id', $objectID); + $allStories = $this->story->getBySearch('', '', $queryID, 'id', $objectID); } else { - $allStories = $this->story->getProductStories(array_keys($products), $branchPairs, $moduleID = '0', $status = 'active', 'story', 'id_desc', $hasParent = false, '', $pager = null); + $allStories = $this->story->getProductStories(array_keys($products), $branchIDList, $moduleID = '0', $status = 'active', 'story', 'id_desc', $hasParent = false, '', $pager = null); } $linkedStories = $this->story->getExecutionStoryPairs($objectID); @@ -2938,7 +3040,7 @@ class execution extends control * @access public * @return void */ - public function all($status = 'all', $projectID = 0, $orderBy = 'id_desc', $productID = 0, $recTotal = 0, $recPerPage = 10, $pageID = 1) + public function all($status = 'all', $projectID = 0, $orderBy = 'order_asc', $productID = 0, $recTotal = 0, $recPerPage = 10, $pageID = 1) { $this->app->loadLang('my'); $this->app->loadLang('product'); @@ -3182,13 +3284,14 @@ class execution extends control /** * Import stories by plan. * - * @param int $executionID - * @param int $planID - * @param int $productID + * @param int $executionID + * @param int $planID + * @param int $productID + * @param string $fromMethod * @access public * @return void */ - public function importPlanStories($executionID, $planID, $productID = 0) + public function importPlanStories($executionID, $planID, $productID = 0, $fromMethod = 'story') { $planStories = $planProducts = array(); $planStory = $this->loadModel('story')->getPlanStories($planID); @@ -3223,7 +3326,7 @@ class execution extends control $param = "projectID=$executionID"; } if($count != 0) echo js::alert(sprintf($this->lang->execution->haveDraft, $count)) . js::locate($this->createLink($moduleName, 'story', $param)); - die(js::locate(helper::createLink($moduleName, 'story', $param), 'parent')); + die(js::locate(helper::createLink($moduleName, $fromMethod, $param))); } /** @@ -3309,4 +3412,62 @@ class execution extends control $this->view->users = $this->loadModel('user')->getPairs('noletter'); $this->display(); } + + /** + *Ajax get group menu of lanes. + * + * @param string $type all|syory|task|bug + * @param string $group + * @access public + * @return void + */ + public function ajaxGetGroup($type, $group = 'default') + { + $this->app->loadLang('kanban'); + $groups = array(); + $groups = $this->lang->kanban->group->$type; + die(html::select("group", $groups, $group, 'class="form-control chosen" data-max_drop_width="215"')); + } + + /** + * Ajax update kanban. + * + * @param int $executionID + * @param string $enterTime + * @param string $browseType + * @param string $groupBy + * @access public + * @return void + */ + public function ajaxUpdateKanban($executionID = 0, $enterTime = '', $browseType = '', $groupBy = '') + { + $enterTime = date('Y-m-d H:i:s', $enterTime); + $lastEditedTime = $this->dao->select("max(lastEditedTime) as lastEditedTime")->from(TABLE_KANBANLANE)->where('execution')->eq($executionID)->fetch('lastEditedTime'); + + if($lastEditedTime > $enterTime) + { + $kanbanGroup = $this->loadModel('kanban')->getExecutionKanban($executionID, $browseType, $groupBy); + die(json_encode($kanbanGroup)); + } + else + { + die(''); + } + } + + /** + * AJAX: Update the execution name. + * + * @param int $executionID + * @param string $newExecutionName + * @access public + * @return bool + */ + public function ajaxUpdateExecutionName($executionID, $newExecutionName) + { + $this->dao->update(TABLE_EXECUTION)->set('name')->eq($newExecutionName)->where('id')->eq($executionID)->exec(); + if(dao::isError()) echo false; + + echo true; + } } diff --git a/module/execution/css/executionkanban.css b/module/execution/css/executionkanban.css index f941cab7f2..9fa5db2f43 100644 --- a/module/execution/css/executionkanban.css +++ b/module/execution/css/executionkanban.css @@ -1,23 +1,8 @@ -.main-table .table {cursor: default; border-collapse: unset;} - -.boards {display: table; table-layout: fixed; width: 100%; min-width: 140px;} -.board {display: table-cell; width: 25%; padding: 10px;} -.board > div {max-height: 230px; overflow: auto;} -.board:not(:last-child), .board-project {border-right: 2px solid #fff;} -.board-item {border: 1px solid #EBEBEB; padding: 5px 10px; cursor: default; border-radius: 2px; background-color: #fff;} -.board-item:hover {border-color: #ccc;} -.board-item + .board-item {margin-top: 10px;} - -#kanban {overflow-y: auto;} -#kanban thead > tr > th:not(:last-child) {border-right: 2px solid #efefef;} -#kanban tbody > tr > td {line-height: 24px;} -#kanban tbody > tr > td {background-color: #F5F5F5;} -#kanban tbody > tr:not(:last-child) > td {border-bottom: 5px solid #fff !important;} - -.s-doing .table-row .table-col:last-child, .c-progress {width: 30px; padding-left: 3px; padding-top: 1px;} -.table-row .table-col:first-child, .board-project {overflow: hidden; white-space: nowrap;} - -.table-grouped > tbody > tr, -.table-grouped > tbody > tr:hover {background: #fff !important;} - -.projectColor {width: 5px; padding: 0px !important; border-right: unset !important} +#kanbanList .panel-body {padding: 10px!important;} +#kanbanList .kanban-header {min-height: 32px!important;} +#kanbanList .kanban-header-col {height: 32px!important; padding: 0!important;} +#kanbanList .kanban-header-col > .title {padding: 0 5px!important;} +#kanbanList .kanban-col[data-type="project"] .kanban-lane-items {height: 100%; display: flex; flex-direction: column; justify-content: center;} +#kanbanList .kanban-lane-name {width: 5px; margin-right: 15px;} +#kanbanList .kanban-lane-name > .text {display: none;} +#kanbanList .kanban-item.link-block > a {cursor: move;} diff --git a/module/execution/css/kanban.css b/module/execution/css/kanban.css index a00e56c37d..daa4d7b0d7 100644 --- a/module/execution/css/kanban.css +++ b/module/execution/css/kanban.css @@ -1,65 +1,45 @@ -.boards {display: table; table-layout: fixed; width: auto; min-height: 50px; min-width: 140px;} -.boards .board:not(:last-child) {border-right: 1px solid #ddd;} -.board {display: table-cell; width: 16.666666667%; padding: 10px; border: 1px solid transparent; transition: background-color .2s, opacity .2s; vertical-align: top;} -.board-item {border: 1px solid #EBEBEB; padding: 5px 10px; cursor: move; border-radius: 2px; background-color: #fff; transition: border .2s, opacity .2s; overflow: hidden;} -.board-item.disabled {cursor: not-allowed;} -.board-item:hover {border-color: #ccc;} -.board-item > .title {line-height: 20px; max-height: 40px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; word-break: break-all;} -.board-item > .info {line-height: 22px; margin-top: 6px;} -.board-item + .board-item {margin-top: 10px;} -.board-item .task-left {float: right; font-size: 12px; color: #a6aab8;} -.boards-wrapper {overflow: auto;} -.board.can-drop-in {background-color: #fff !important; opacity: 1 !important;} -.c-board.dragging {color: #006af1;} -.board.drop-to {background-color: #fff0d5 !important;} -.board-drag-holder {background: #ccc; background: rgba(0, 0, 0, .1); border-radius: 2px; border: 1px solid rgba(0,0,0,.05); display: none;} -.board.drop-to .board-drag-holder {display: block;} -.board-item + .board-drag-holder {margin-top: 10px;} -.board-item.dragging {opacity: .35;} -.board-item .btn {padding: 2px 10px 2px 23px; font-size: 12px; position: relative; left: -2px; height: 24px;} -.board-item .btn-icon-left > .icon {width: 22px; height: 22px; line-height: 20px; font-size: 14px; opacity: 1; top: 1px;} -.board-item .btn-icon-left > span {max-width: 80px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; display: inline-block;} -.board-title {padding-right: 20px;} -.board-actions {position: absolute; right: 3px; top: 3px;} -.board-actions.nav > li > a {padding: 3px 3px;} +#main {padding-bottom: 10px;} +#main > .container {max-width: 1960px!important; padding: 0 10px} -#kanban {overflow-y: auto; min-height: 350px;} -#kanban > .table {min-width: 1100px; table-layout: auto;} -#kanban.dragging .boards.dragging .board {background: #eee; opacity: .35; border-color: #f1f1f1;} -#kanban thead > tr > th {padding-left: 0; padding-right: 0;} -#kanban thead .c-side {padding: 5px; max-width: 15%; min-width: 200px;} -#kanban .label-pri {width: 16px; height: 16px; line-height: 12px; min-width: 16px; font-size: 12px; padding: 0;} -#kanban tbody > tr > td {line-height: 24px;} -#kanban .c-board {border-bottom: 4px #EAF3FC solid; min-width: 140px;} -#kanban .c-board.s-wait {border-color: #7EC5FF;} -#kanban .c-board.s-doing {border-color: #0991FF;} -#kanban .c-board.s-pause {border-color: #fdc137;} -#kanban .c-board.s-done {border-color: #0BD986;} -#kanban .c-board.s-cancel {border-color: #CBD0DB;} -#kanban .c-board.s-closed {border-color: #838A9D;} -#kanban .table-grouped tbody > tr:hover {background: transparent;} -#kanban .c-boards, #kanban td.c-side {border-bottom: 1px solid #eee;} -#kanban .group-info > span + span {display: inline-block; margin-left: 8px;} -#kanban th.c-board {color: #3c4353; font-weight: bold;} -#kanban .fix-table-copy-wrapper {overflow: visible !important;} -#kanban .fix-table-copy-wrapper th.c-board {color: #eee;} -#kanban .fix-table-copy-wrapper th.c-board .btn-link {color: #fff;} -#kanban .group-title {word-break: break-all; line-height: 16px; padding: 3px 0; display: block;} +.kanban + .kanban {margin-top: 15px} +.kanban-item {position: relative;} +.kanban-item > .title { display: block;white-space: nowrap; overflow: hidden; text-overflow: ellipsis} +.kanban-item > .infos {position: relative; margin-top: 5px} +.kanban-item > .infos > .info + .info {margin-left: 8px} +.kanban-item > .infos > .info-id, +.kanban-item > .infos > .info-deadline, +.kanban-item > .infos > .info-estimate {font-size: 12px; position: relative; top: 2px} +.kanban-item > .infos > .label-pri {min-width: 16px; line-height: 14px; height: 16px; padding: 0} +.kanban-item > .infos > .label-severity {transform: scale(.75)} +.kanban-item > .infos > .avatar {position: absolute; right: 0; top: 0} +.kanban-item > .actions {position: absolute; top: 4px; right: 4px; opacity: 0;} +.kanban-item:hover > .actions {opacity: 1;} +.kanban-item > .actions > a {display: block; float: left; width: 20px; height: 20px; line-height: 20px; text-align: center; border-radius: 4px; opacity: .7;} +.kanban-item > .actions > a:hover {background-color: rgba(0,0,0,.075); opacity: 1;} -.table-grouped > tbody > tr, -.table-grouped > tbody > tr:hover {background: #fff !important;} +.kanban-header-col > .actions {padding-top: 22px} +.kanban-header-parent-col .kanban-header-col > .actions {padding-top: 6px; padding-right: 2px;} +.kanban-header-col > .actions > a {display: block; float: left; width: 20px; height: 20px; line-height: 20px; text-align: center; border-radius: 4px; opacity: .7;} +.kanban-header-col > .actions > a:hover {background-color: rgba(0,0,0,.075); opacity: 1;} -.fixedSide {position: fixed; background-color: rgb(75, 75, 75, .85); color: #eee; z-index: 999;} -.fixedSide .dropdown-menu {background-color: rgb(75, 75, 75, .85);} -.fixedSide .dropdown-menu > li > a {color: #eee;} -.fixedSide .btn-link, .fixedSide a , .fixedSide .text-muted {color: #eee;} -.fixedSide .c-board {border-bottom: 4px #EAF3FC solid; min-width: 140px;} -.fixedSide th.c-board {color: #3c4353; font-weight: bold;} -.fixedSide td.c-side {border-bottom: 1px solid #eee;} -.fixedSide thead .c-side {padding: 5px !important; max-width: 15%; min-width: 200px;} -.fixedSide thead > tr > th {padding-left: 0; padding-right: 0;} -.fixedSide tbody > tr > td {line-height: 24px;} -.fixedSide thead > tr > th, .fixedSide tbody > tr > td {min-height: 36px; padding: 2px 8px !important;} -.fixedSide .group-title {word-break: break-all; line-height: 16px; padding: 3px 0; display: block;} -.fixedSide .group-info > span + span {display: inline-block; margin-left: 8px;} -.fixedSide .label-pri {width: 16px; height: 16px; line-height: 12px; min-width: 16px; font-size: 12px; padding: 0;} +.kanban-lane-name > .actions {position: absolute; top: 0; left: 0; right: 0; opacity: 0;} +.kanban-lane-name:hover > .actions {opacity: 1;} +.kanban-lane-name > .actions > a {display: block; width: 20px; height: 20px; line-height: 20px; text-align: center; opacity: .7; color: #fff;} +.kanban-lane-name > .actions > a:hover {background-color: rgba(0,0,0,.2); opacity: 1;} + +.kanban-affixed .kanban-header-col > .title > .text, +.kanban-affixed .kanban-header-col > .title > .count {color: #fff!important;} + +#kanbanContainer {margin: 0;} +#kanbanContainer > .panel-body {overflow: auto;} +#kanbans .kanban {min-height: initial;} +#kanbans .kanban-lane {min-height: 150px;} +#kanbans .kanban-affixed > .kanban-header {top: 100px;} + +#type_chosen .icon-kanban {padding-right: 10px; font-size: 15px;} + +.dropdown-menu a {text-align: left;} +.scrollbar-hover {max-height: 2000px; overflow: scroll !important;} +.c-type {width: 150px !important; overflow: unset;} +.c-group {width: 190px !important; overflow: unset;} +.c-type {margin-left: 0px !important;} diff --git a/module/execution/css/linkstory.css b/module/execution/css/linkstory.css index cb2a740d7a..bbabdce486 100644 --- a/module/execution/css/linkstory.css +++ b/module/execution/css/linkstory.css @@ -1 +1,2 @@ +.c-object {width: 160px;} .c-branch, .c-estimate {width: 80px;} diff --git a/module/execution/js/all.js b/module/execution/js/all.js index 057d4360c4..be30acdd24 100644 --- a/module/execution/js/all.js +++ b/module/execution/js/all.js @@ -1,10 +1,10 @@ $(function() { - $('#projectTableList').on('sort.sortable', function(e, data) + $('#executionTableList').on('sort.sortable', function(e, data) { var list = ''; for(i = 0; i < data.list.length; i++) list += $(data.list[i].item).attr('data-id') + ','; - $.post(createLink('project', 'updateOrder'), {'projects' : list, 'orderBy' : orderBy}); + $.post(createLink('execution', 'updateOrder'), {'executions' : list, 'orderBy' : orderBy}); }); }); diff --git a/module/execution/js/common.js b/module/execution/js/common.js index a5c97acf63..66f811f18b 100644 --- a/module/execution/js/common.js +++ b/module/execution/js/common.js @@ -162,11 +162,20 @@ function loadBranches(product) $inputgroup.addClass('has-branch').append(data); $inputgroup.find('select:last').attr('name', 'branch[' + index + ']').attr('id', 'branch' + index).attr('onchange', "loadPlans('#products" + index + "', this.value)").chosen(); } - }); - loadPlans(product); + var branchID = $('#branch' + index).val(); + loadPlans(product, branchID); + }); } +/** + * Load plans by product id. + * + * @param int $product + * @param int $branchID + * @access public + * @return void + */ function loadPlans(product, branchID) { if($('#plansBox').size() == 0) return false; @@ -177,7 +186,7 @@ function loadPlans(product, branchID) if(typeof(planID) == 'undefined') planID = 0; planID = $("select#plans" + productID).val() != '' ? $("select#plans" + productID).val() : planID; - $.get(createLink('product', 'ajaxGetPlans', "productID=" + productID + '&branch=' + branchID + '&planID=' + planID + '&fieldID&needCreate=&expired=' + ((config.currentMethod == 'create' || config.currentMethod == 'edit') ? 'unexpired' : '')), function(data) + $.get(createLink('product', 'ajaxGetPlans', "productID=" + productID + '&branch=0,' + branchID + '&planID=' + planID + '&fieldID&needCreate=&expired=' + (config.currentMethod == 'create' ? 'unexpired' : '') + '¶m=skipParent'), function(data) { if(data) { @@ -189,7 +198,12 @@ function loadPlans(product, branchID) }); } - +/** + * Adjust product box margin. + * + * @access public + * @return void + */ function adjustProductBoxMargin() { var productRows = Math.ceil($('#productsBox > .row > .col-sm-4').length / 3); @@ -202,6 +216,12 @@ function adjustProductBoxMargin() } } +/** + * Adjust plan box margin. + * + * @access public + * @return void + */ function adjustPlanBoxMargin() { var planRows = Math.ceil($('#plansBox > .row > .col-sm-4').length / 3); diff --git a/module/execution/js/executionkanban.js b/module/execution/js/executionkanban.js new file mode 100644 index 0000000000..373b92e045 --- /dev/null +++ b/module/execution/js/executionkanban.js @@ -0,0 +1,170 @@ +/** + * Process kanban data + * @returns {Object} kanban data + */ +function processKanbanData() +{ + /* Generate columns */ + var columns = [{id: 'project', type: 'project', name: window.langDoingProject, cardType: 'span'}]; + $.each(kanbanColumns, function(type, name) + { + columns.push({id: type, type: type, name: name, cardType: 'execution'}); + }); + + /* Format lanes data */ + var lanes = []; + $.each(kanbanGroup, function(projectID, statusMap) + { + var projectName = +projectID ? projectNames[projectID] : langMyExecutions; + var cards = {project: [{id: projectID, name: projectName}]}; + + $.each(kanbanColumns, function(type) + { + var cardList = []; + var executions = statusMap[type]; + + if(!executions) return; + + $.each(executions, function(index, execution) + { + var executionCard = $.extend({}, execution, {id: projectID + '-' + execution.id, _id: execution.id}); + cardList.push(executionCard); + }); + cards[type] = cardList; + }); + + lanes.push({id: projectID, name: projectName, cards: cards}); + }); + + return {id: 'executions', columns: columns, lanes: lanes}; +} + +/* +* Find drop columns +* @param {JQuery} $element Drag element +* @param {JQuery} $root Dnd root element +*/ +function findDropColumns($element, $root) +{ + var $col = $element.closest('.kanban-col'); + var col = $col.data(); + var lane = $col.closest('.kanban-lane').data(); + var kanbanID = $root.data('id'); + var kanbanRules = window.kanbanDropRules ? window.kanbanDropRules[kanbanID] : null; + + if(!kanbanRules) return $root.find('.kanban-lane[data-id="' + lane.id + '"] .kanban-lane-col:not([data-type="project"],[data-type="' + col.type + '"])'); + + var colRules = kanbanRules[col.type]; + var lane = $col.closest('.kanban-lane').data('lane'); + return $root.find('.kanban-lane-col').filter(function() + { + if(!colRules) return false; + if(colRules === true) return true; + + var $newCol = $(this); + var newCol = $newCol.data(); + if(newCol.id === col.id) return false; + + var $newLane = $newCol.closest('.kanban-lane'); + var newLane = $newLane.data('lane'); + return colRules.indexOf(newCol.type) > -1 && newLane.id === lane.id; + }); +} + +/** + * Change column type for a card + + * @param {Object} card Card object + * @param {String} fromColType The column type before change + * @param {String} toColType The column type after change + * @param {String} kanbanID Kanban ID + */ +function changeCardColType(card, fromColType, toColType, kanbanID) +{ + if(typeof card == 'undefined') return false; + var cardID = card.id; + var executionID = cardID.substr(cardID.indexOf("-") + 1);; + var showIframe = false; + + if(toColType == 'doing') + { + if(fromColType == 'wait' && priv.canStart) + { + var link = createLink('execution', 'start', 'executionID=' + executionID, '', true); + showIframe = true; + } + if((fromColType == 'suspended' || fromColType == 'closed') && priv.canActivate) + { + var link = createLink('execution', 'activate', 'executionID=' + executionID, '', true); + showIframe = true; + } + } + else if(toColType == 'suspended') + { + if((fromColType == 'wait' || fromColType == 'doing') && priv.canSuspend) + { + var link = createLink('execution', 'suspend', 'executionID=' + executionID, '', true); + showIframe = true; + } + } + else if(toColType == 'closed') + { + if(priv.canClose) + { + var link = createLink('execution', 'close', 'executionID=' + executionID, '', true); + showIframe = true; + } + } + + if(showIframe) + { + var modalTrigger = new $.zui.ModalTrigger({type: 'iframe', width: '80%', url: link}); + modalTrigger.show(); + } + + /* + // TODO: The server must return a updated kanban data 服务器返回更新后的看板数据 + + // 调用 updateKanban 更新看板数据 + updateKanban(kanbanID, newKanbanData); + */ +} + +/** + * Handle finish drop task + * @param {Object} event Event object + * @returns {void} + */ +function handleFinishDrop(event) +{ + var $card = $(event.element); // The drag card + var $dragCol = $card.closest('.kanban-lane-col'); + var $dropCol = $(event.target); + + /* Get d-n-d(drag and drop) infos 获取拖放操作相关信息 */ + var card = $card.data('item'); + var fromColType = $dragCol.data('type'); + var toColType = $dropCol.data('type'); + var kanbanID = $card.closest('.kanban').data('id'); + + changeCardColType(card, fromColType, toColType, kanbanID); +} + +$(function() +{ + var kanbanGroup = window.kanbanGroup; + if(!kanbanGroup) return; + + $('#kanban').kanban( + { + data: processKanbanData(), + // noLaneName: true, + droppable: + { + selector: '.kanban-item:not(.kanban-item-span)', + target: findDropColumns, + finish: handleFinishDrop, + mouseButton: 'left' + }, + }); +}); diff --git a/module/execution/js/kanban.js b/module/execution/js/kanban.js index 001ec4c5df..8ce3c9e1f3 100644 --- a/module/execution/js/kanban.js +++ b/module/execution/js/kanban.js @@ -1,253 +1,886 @@ -$(function() +function changeView(view) { - var isFirefox = $.zui.browser.firefox; - var adjustBoardsHeight = function() + var link = createLink('execution', 'kanban', "executionID=" + executionID + '&type=' + view); + location.href = link; +} + +/** + * Render user avatar + * @param {String|{account: string, avatar: string}} user User account or user object + * @returns {string} + */ +function renderUserAvatar(user, objectType, objectID) +{ + var $noPrivAndNoAssigned = $('
    '); + if(objectType == 'task') { - var $cBoards = $('.c-boards'); - var viewHeight = $(window).height() - $('#header').height() - $('#footer').height() - 111; - if ($cBoards.length === 1) + if(!priv.canAssignTask && !user) return $noPrivAndNoAssigned; + var link = createLink('task', 'assignto', 'executionID=' + executionID + '&id=' + objectID, '', true); + } + if(objectType == 'story') + { + if(!priv.canAssignStory && !user) return $noPrivAndNoAssigned; + var link = createLink('story', 'assignto', 'id=' + objectID, '', true); + } + if(objectType == 'bug') + { + if(!priv.canAssignBug && !user) return $noPrivAndNoAssigned; + var link = createLink('bug', 'assignto', 'id=' + objectID, '', true); + } + + if(!user) return $(''); + + if(typeof user === 'string') user = {account: user}; + if(!user.avatar && window.userList && window.userList[user.account]) user = window.userList[user.account]; + + var $noPrivAvatar = $('
    ').avatar({user: user}); + if(objectType == 'task' && !priv.canAssignTask) return $noPrivAvatar; + if(objectType == 'story' && !priv.canAssignStory) return $noPrivAvatar; + if(objectType == 'bug' && !priv.canAssignBug) return $noPrivAvatar; + + return $('').avatar({user: user}); +} + +/** + * Render deadline + * @param {String|Date} deadline Deadline + * @returns {JQuery} + */ +function renderDeadline(deadline) +{ + if(deadline == '0000-00-00') return; + + var date = $.zui.createDate(deadline); + var now = new Date(); + now.setHours(0); + now.setMinutes(0); + now.setSeconds(0); + now.setMilliseconds(0); + var isEarlyThanToday = date.getTime() < now.getTime(); + var deadlineDate = $.zui.formatDate(date, 'MM-dd'); + + return $('').text(deadlineLang + ' ' + deadlineDate).addClass(isEarlyThanToday ? 'text-red' : 'text-muted'); +} + +/** + * Render story item 提供方法渲染看板中的需求卡片 + * @param {Object} item Story item object + * @param {JQuery} $item Kanban item element + * @param {Object} col Column object + * @returns {JQuery} $item Kanban item element + */ +function renderStoryItem(item, $item, col) +{ + var $title = $item.find('.title'); + if(!$title.length) + { + $title = $(' ') + .attr('href', $.createLink('story', 'view', 'storyID=' + item.id, '', true)); + $title.appendTo($item); + } + $title.attr('title', item.title).find('.text').text(item.title); + + var $infos = $item.find('.infos'); + if(!$infos.length) + { + $infos = $('
    ').appendTo($item); + } + $infos.html( + [ + '#' + item.id + '', + '' + item.pri + '', + item.estimate ? '' + item.estimate + 'h' : '', + ].join('')); + $infos.append(renderUserAvatar(item.assignedTo, 'story', item.id)); + + var $actions = $item.find('.actions'); + if(!$actions.length && item.menus.length) + { + $actions = $([ + '
    ', + '', + '', + '', + '
    ' + ].join('')).appendTo($item); + } + + $item.attr('data-type', 'story').addClass('kanban-item-story'); + + return $item; +} + +/** + * Render bug item 提供方法渲染看板中的 Bug 卡片 + * @param {Object} item Bug item object + * @param {JQuery} $item Kanban item element + * @param {Object} col Column object + * @returns {JQuery} $item Kanban item element + */ +function renderBugItem(item, $item, col) +{ + var $title = $item.find('.title'); + if(!$title.length) + { + $title = $(' ') + .attr('href', $.createLink('bug', 'view', 'bugID=' + item.id, '', true)); + $title.appendTo($item); + } + $title.attr('title', item.title).find('.text').text(item.title); + + var $infos = $item.find('.infos'); + if(!$infos.length) + { + $infos = $('
    ').appendTo($item); + } + $infos.html( + [ + '#' + item.id + '', + '', + '' + item.pri + '', + ].join('')); + if(item.deadline) $infos.append(renderDeadline(item.deadline)); + $infos.append(renderUserAvatar(item.assignedTo, 'bug', item.id)); + + var $actions = $item.find('.actions'); + if(!$actions.length && item.menus.length) + { + $actions = $([ + '
    ', + '', + '', + '', + '
    ' + ].join('')).appendTo($item); + } + + $item.attr('data-type', 'bug').addClass('kanban-item-bug'); + + return $item; +} + +/** + * Render task item 提供方法渲染看板中的任务卡片 + * @param {Object} item Task item object + * @param {JQuery} $item Kanban item element + * @param {Object} col Column object + * @returns {JQuery} $item Kanban item element + */ +function renderTaskItem(item, $item, col) +{ + var $title = $item.find('.title'); + if(!$title.length) + { + $title = $(' ') + .attr('href', $.createLink('task', 'view', 'taskID=' + item.id, '', true)); + $title.appendTo($item); + } + $title.attr('title', item.name).find('.text').text(item.name); + + var $infos = $item.find('.infos'); + if(!$infos.length) + { + $infos = $('
    ').appendTo($item); + } + $infos.html( + [ + '#' + item.id + '', + '' + item.pri + '', + item.estimate ? '' + item.estimate + 'h' : '', + ].join('')); + if(item.deadline) $infos.append(renderDeadline(item.deadline)); + $infos.append(renderUserAvatar(item.assignedTo, 'task', item.id)); + + var $actions = $item.find('.actions'); + if(!$actions.length && item.menus.length) + { + $actions = $([ + '
    ', + '', + '', + '', + '
    ' + ].join('')).appendTo($item); + } + + $item.attr('data-type', 'task').addClass('kanban-item-task'); + + return $item; +} + +/* Add column renderer/ 添加特定列类型或列条目类型渲染方法 */ +addColumnRenderer('story', renderStoryItem); +addColumnRenderer('bug', renderBugItem); +addColumnRenderer('task', renderTaskItem); + +/** + * Render column count 渲染看板列头上的卡片数目 + * @param {JQuery} $count Kanban count element + * @param {number} count Column cards count + * @param {number} col Column object + * @param {Object} kanban Kanban intance + */ +function renderColumnCount($count, count, col) +{ + var text = count + '/' + (col.limit < 0 ? '' : col.limit); + $count.html(text + ''); +} + +/** + * Render header column 渲染看板列头部 + * @param {JQuery} $col Header column element + * @param {Object} col Header column object + * @param {JQuery} $header Header element + * @param {Object} kanban Kanban object + */ +function renderHeaderCol($col, col, $header, kanban) +{ + if(col.asParent) $col = $col.children('.kanban-header-col'); + if(!$col.children('.actions').length) + { + var $actions = $('
    '); + var printStoryButton = printTaskButton = printBugButton = false; + if(priv.canCreateStory || priv.canBatchCreateStory || priv.canLinkStory || priv.canLinkStoryByPlane) printStoryButton = true; + if(priv.canCreateTask || priv.canBatchCreateTask) printTaskButton = true; + if(priv.canCreateBug || priv.canBatchCreateBug) printBugButton = true; + + if((col.type === 'backlog' && printStoryButton) || (col.type === 'wait' && printTaskButton) || (col.type == 'unconfirmed' && printBugButton)) { - var $boardsWrapper = $cBoards.find('.boards-wrapper'); - $boardsWrapper.css('min-height', viewHeight); - if($boardsWrapper.height() > $boardsWrapper.find('.boards').height()) - { - $boardsWrapper.find('.boards').css(isFirefox ? 'height' : 'min-height', $boardsWrapper.height() - 1); - } - return + $actions.append([ + '', + '', + '' + ].join('')); } - $cBoards.each(function() - { - var $theBoards = $(this); + $actions.append([ + '', + '', + '' + ].join('')); + $actions.appendTo($col); + } +} - var $boardsWrapper = $theBoards.find('.boards-wrapper'); - var minHeight = Math.min($theBoards.prev().find('.board-story').outerHeight() + 4, viewHeight); - $boardsWrapper.css({maxHeight: viewHeight, minHeight: minHeight}); - if($boardsWrapper.height() > $boardsWrapper.find('.boards').height()) +/** + * Render lane name 渲染看板泳道名称 + * @param {JQuery} $name Name element + * @param {Object} lane Lane object + * @param {JQuery} $kanban $kanban element + * @param {Object} columns Kanban columns + * @param {Object} kanban Kanban object + */ +function renderLaneName($name, lane, $kanban, columns, kanban) +{ + if(lane.id != 'story' && lane.id != 'task' && lane.id != 'bug') return false; + if(!$name.children('.actions').length && (priv.canSetLane || priv.canMoveLane)) + { + $([ + '
    ', + '', + '', + '', + '
    ' + ].join('')).appendTo($name); + } +} + +/** + * Updata kanban data + * 更新看板上的数据 + * @param {string} kanbanID Kanban id 看板 ID + * @param {Object} data Kanban data 看板数据 + */ +function updateKanban(kanbanID, data) +{ + var $kanban = $('#kanban-' + kanbanID); + if(!$kanban.length) return; + + $kanban.data('zui.kanban').render(data); +} + +/** + * Create kanban in page + * 在界面上创建一个看板界面 + * @param {string} kanbanID Kanban id 看板 ID + * @param {Object} data Kanban data 看板数据 + * @param {Object} options Kanban options 组件初始化数据 看板名称 + */ +function createKanban(kanbanID, data, options) +{ + var $kanban = $('#kanban-' + kanbanID); + if($kanban.length) return updateKanban(kanbanID, data); + + $kanban = $('
    ').appendTo('#kanbans'); + $kanban.kanban($.extend({data: data}, options)); +} + +function fullScreen() +{ + var element = document.getElementById('kanbanContainer'); + var requestMethod = element.requestFullScreen || element.webkitRequestFullScreen || element.mozRequestFullScreen || element.msRequestFullScreen; + if(requestMethod) + { + var afterEnterFullscreen = function() + { + $('#kanbanContainer').addClass('scrollbar-hover'); + $('.actions').hide(); + $('#kanbanContainer a.iframe').each(function() { - var $boards = $boardsWrapper.find('.boards'); - $boards.css({maxHeight: $theBoards.height(), minHeight: minHeight}); - if ($boards.outerHeight() < minHeight) $boards.css('height', minHeight); - } - }); - }; - adjustBoardsHeight(); - - var boardID = ''; - var onlybody = config.requestType == 'GET' ? "&onlybody=yes" : "?onlybody=yes"; - $.cookie('selfClose', 0, {expires:config.cookieLife, path:config.webRoot}); - var $kanban = $('#kanban'); - - // Get scrollbar width - var getScrollbarWidth = function () - { - var outer = document.createElement("div"); - outer.style.visibility = "hidden"; - outer.style.width = "100px"; - outer.style.msOverflowStyle = "scrollbar"; // needed for WinJS apps - - document.body.appendChild(outer); - - var widthNoScroll = outer.offsetWidth; - // force scrollbars - outer.style.overflow = "scroll"; - - // add innerdiv - var inner = document.createElement("div"); - inner.style.width = "100%"; - outer.appendChild(inner); - - var widthWithScroll = inner.offsetWidth; - - // remove divs - outer.parentNode.removeChild(outer); - - return widthNoScroll - widthWithScroll; - }; - - var scrollbarWidth = getScrollbarWidth(); - var fixBoardWidth = function() - { - var $table = $kanban.children('.table:first'); - var kanbanWidth = $table.width(); - var $cBoards = $table.find('thead>tr>th.c-board:not(.c-side)'); - var boardCount = $cBoards.length; - var $cSide = $table.find('thead>tr>th.c-board.c-side'); - var totalWidth = kanbanWidth - scrollbarWidth - 1; - if ($cSide.length) totalWidth = totalWidth - ($cSide.outerWidth() + 5); - var cBoardWidth = Math.floor(totalWidth/boardCount); - $cBoards.not(':last').width(cBoardWidth); - if ($cSide.length) $cBoards.first().width(cBoardWidth + (isFirefox ? 0 : 5)); - $kanban.find('.boards > .board').width(cBoardWidth - (isFirefox ? 21 : 22)); - }; - fixBoardWidth(); - - var updateUI = function() - { - fixBoardWidth(); - adjustBoardsHeight(); - $kanban.data('zui.table').updateFixUI(); - }; - - $(window).on('resize', updateUI); - - var refresh = function(force) - { - var selfClose = $.cookie('selfClose'); - $.cookie('selfClose', 0, {expires:config.cookieLife, path:config.webRoot}); - if(selfClose == 1 || force) - { - $kanban.load(location.href + ' #kanban>*', updateUI); - } - }; - window.refreshKanban = refresh; - - var kanbanModalTrigger = new $.zui.ModalTrigger({type: 'iframe', width: 800}); - var dropTo = function(id, from, to, type) - { - if(statusMap[type][from] && statusMap[type][from][to]) - { - var method = statusMap[type][from][to]; - var link = $.createLink(type, method, 'id=' + id + '&subStatus=' + to); - if(method == 'ajaxChangeSubStatus') - { - $.getJSON(link, function(response) + if($(this).hasClass('iframe')) { - if(response.result == 'fail' && response.message) - { - bootAlert(response.message); - setTimeout(function(){location.reload();}, 1000); - } - }); + var href = $(this).attr('href'); + $(this).removeClass('iframe'); + $(this).attr('href', 'javascript:void(0)'); + $(this).attr('href-bak', href); + } + }) + $.cookie('isFullScreen', 1); + } + + var whenFailEnterFullscreen = function() + { + exitFullScreen(); + } + + try + { + var result = requestMethod.call(element); + if(result && (typeof result.then === 'function' || result instanceof window.Promise)) + { + result.then(afterEnterFullscreen).catch(whenFailEnterFullscreen); } else { - kanbanModalTrigger.show( - { - url: link + onlybody, - shown: function(){$('.modal-iframe').addClass('with-titlebar').data('cancel-reload', true)}, - width: 900, - hidden: refresh - }); + afterEnterFullscreen(); } } - - /* Keep the draged element stay in the new place. */ - return true; - }; - - $kanban.droppable( - { - selector: '.board-item:not(.disabled)', - target: function($ele) + catch (error) { - var itemType = $ele.data('type'); - var $board = $ele.closest('.board'); - var type = $board.data('type'); - return $board.siblings('.board').filter(function() - { - var typeMap = statusMap[itemType]; - var actionMap = typeMap && typeMap[type]; - return !!actionMap && actionMap[$(this).data('type')]; - }); - }, - start: function(e) - { - $kanban.addClass('dragging'); - e.targets.addClass('can-drop-in'); - var $item = $(e.element).addClass('dragging'); - $item.closest('.boards').addClass('dragging'); - }, - drag: function(e) - { - var $item = $(e.element); - var $target = $(e.target); - var $holder = $target.find('.board-drag-holder'); - if (!$holder.length) $holder = $('
    ').appendTo($target); - $kanban.find('.c-board.dragging').removeClass('dragging'); - $kanban.find('.c-board.s-' + $target.data('type')).addClass('dragging'); - $holder.height($item.outerHeight()); - }, - drop: function(e) - { - var result = dropTo(e.element.data('id'), e.element.closest('.board').data('type'), e.target.data('type'), e.element.data('type')); - if(result !== false) - { - e.element.insertBefore(e.target.find('.board-drag-holder')); - } - }, - finish: function() - { - $kanban.removeClass('dragging').find('.can-drop-in').removeClass('can-drop-in'); - $kanban.find('.dragging').removeClass('dragging'); + whenFailEnterFullscreen(error); } - }); - - $kanban.on('click', '.kanbaniframe', function(e) - { - var $link = $(this); - kanbanModalTrigger.show( - { - url: $link.attr('href'), - shown: function(){$('.modal-iframe').addClass('with-titlebar').data('cancel-reload', true)}, - hidden: refresh, - width: $(this).is('.task-assignedTo,.bug-assignedTo') ? 800 : 1100 - }); - return false; - }); - - fixKanbanSide($kanban); -}); - -function fixKanbanSide($kanban) -{ - if($kanban.length == 0) return false; - - fixSideInit(); - $kanban.scroll(fixSide);//Fix kanban side when scrolling. - - var tableWidth, kanbanOffset, fixedSide, $fixedSide; - function fixSide() - { - kanbanOffset = $kanban.offset().left; - $fixedSide = $kanban.parent().find('.fixedSide'); - if($fixedSide.length <= 0 && kanbanOffset < $kanban.scrollLeft()) - { - var $th = $kanban.find('table thead tr th:first'); - - tableWidth = $th.width(); - - fixedSide = "'; - $kanban.find('table tbody tr').each(function() - { - var $td = $(this).find('td:first'); - fixedSide = fixedSide + "'; - }); - fixedSide = fixedSide + '
    " + $th.html()+ '
    " + $td.html() + '
    '; - - $kanban.before(fixedSide); - - $('.fixedSide').width(tableWidth); - $('.fixedSide').css('top', $kanban.offset()); - - /* Reset height. */ - var index = 1; - $('.fixedSide tbody tr').each(function() - { - var $td = $kanban.find('table tbody tr:nth-child(' + index + ') td:first'); - - if($(this).find('td:first div:first').length == 0) $(this).find('td:first').html('
    '); - $(this).find('td:first div:first').height($td.height()); - - index++; - }) - } - if($fixedSide.length > 0 && kanbanOffset >= $kanban.scrollLeft()) $fixedSide.remove(); - } - function fixSideInit() - { - $fixedSide = $kanban.parent().find('.fixedSide'); - if($fixedSide.length > 0) $fixedSide.remove(); - fixSide(); } } + +/** + * Exit full screen. + * + * @access public + * @return void + */ +function exitFullScreen() +{ + $('#kanbanContainer').removeClass('scrollbar-hover'); + $('.actions').show(); + $('#kanbanContainer a').each(function() + { + var hrefBak = $(this).attr('href-bak'); + if(hrefBak) + { + $(this).addClass('iframe'); + $(this).attr('href', hrefBak); + } + }) + $.cookie('isFullScreen', 0); +} + +document.addEventListener('fullscreenchange', function (e) +{ + if(!document.fullscreenElement) exitFullScreen(); +}); + +document.addEventListener('webkitfullscreenchange', function (e) +{ + if(!document.webkitFullscreenElement) exitFullScreen(); +}); + +document.addEventListener('mozfullscreenchange', function (e) +{ + if(!document.mozFullScreenElement) exitFullScreen(); +}); + +document.addEventListener('msfullscreenChange', function (e) +{ + if(!document.msfullscreenElement) exitFullScreen(); +}); + +/* Define drag and drop rules */ +if(!window.kanbanDropRules) +{ + window.kanbanDropRules = + { + story: + { + blacklog: true, + ready: ['blacklog', 'dev-doing'], + 'dev-doing': ['dev-done'], + 'dev-done': ['test-doing'], + 'test-doing': ['test-done'], + 'test-done': ['accepted'], + 'accepted': ['published'], + 'published': false, + } + } +} + +/* + * Find drop columns + * @param {JQuery} $element Drag element + * @param {JQuery} $root Dnd root element + */ +function findDropColumns($element, $root) +{ + var $col = $element.closest('.kanban-col'); + var col = $col.data(); + var kanbanID = $root.data('id'); + var kanbanRules = window.kanbanDropRules ? window.kanbanDropRules[kanbanID] : null; + + if(!kanbanRules) return $root.find('.kanban-lane-col:not([data-type="' + col.type + '"])'); + + var colRules = kanbanRules[col.type]; + var lane = $col.closest('.kanban-lane').data('lane'); + return $root.find('.kanban-lane-col').filter(function() + { + if(!colRules) return false; + if(colRules === true) return true; + + var $newCol = $(this); + var newCol = $newCol.data(); + if(newCol.id === col.id) return false; + + var $newLane = $newCol.closest('.kanban-lane'); + var newLane = $newLane.data('lane'); + var canDropHere = colRules.indexOf(newCol.type) > -1 && newLane.id === lane.id; + if(canDropHere) $newCol.addClass('can-drop-here'); + return canDropHere; + }); +} + +/** + * Change column type for a card + * 变更卡片类型 + * @param {Object} card Card object + * @param {String} fromColType The column type before change + * @param {String} toColType The column type after change + * @param {String} kanbanID Kanban ID + */ +function changeCardColType(card, fromColType, toColType, kanbanID) +{ + if(typeof card == 'undefined') return false; + var objectID = card.id; + var showIframe = false; + + if(kanbanID == 'task') + { + if(toColType == 'developed') + { + if((fromColType == 'developing' || fromColType == 'wait') && priv.canFinishTask) + { + var link = createLink('task', 'finish', 'taskID=' + objectID, '', true); + showIframe = true; + } + } + else if(toColType == 'pause') + { + if(fromColType == 'developing' && priv.canPauseTask) + { + var link = createLink('task', 'pause', 'taskID=' + objectID, '', true); + showIframe = true; + } + } + else if(toColType == 'developing') + { + if((fromColType == 'pause' || fromColType == 'cancel' || fromColType == 'closed' || fromColType == 'developed') && priv.canActivateTask) + { + var link = createLink('task', 'activate', 'taskID=' + objectID, '', true); + showIframe = true; + } + if(fromColType == 'wait' && priv.canStartTask) + { + var link = createLink('task', 'start', 'taskID=' + objectID, '', true); + showIframe = true; + } + } + else if(toColType == 'canceled') + { + if((fromColType == 'developing' || fromColType == 'wait' || fromColType == 'pause') && priv.canCancelTask) + { + var link = createLink('task', 'cancel', 'taskID=' + objectID, '', true); + showIframe = true; + } + } + else if(toColType == 'closed') + { + if((fromColType == 'developed' || fromColType == 'canceled') && priv.canCloseTask) + { + var link = createLink('task', 'close', 'taskID=' + objectID, '', true); + showIframe = true; + } + } + } + + if(showIframe) + { + var modalTrigger = new $.zui.ModalTrigger({type: 'iframe', width: '80%', url: link}); + modalTrigger.show(); + } + + /* + // TODO: The server must return a updated kanban data 服务器返回更新后的看板数据 + + // 调用 updateKanban 更新看板数据 + updateKanban(kanbanID, newKanbanData); + */ +} + +/** + * Handle finish drop task + * @param {Object} event Event object + * @returns {void} + */ +function handleFinishDrop(event) +{ + var $card = $(event.element); // The drag card + var $dragCol = $card.closest('.kanban-lane-col'); + var $dropCol = $(event.target); + + /* Get d-n-d(drag and drop) infos 获取拖放操作相关信息 */ + var card = $card.data('item'); + var fromColType = $dragCol.data('type'); + var toColType = $dropCol.data('type'); + var kanbanID = $card.closest('.kanban').data('id'); + + changeCardColType(card, fromColType, toColType, kanbanID); + + $('#kanbans').find('.can-drop-here').removeClass('can-drop-here'); +} + +/** + * Handle sort cards in column 处理对列卡片进行排序 + */ +function handleSortColCards() +{ + /* TODO: handle sort cards from column contextmenu */ + return false; +} + +/** + * Create column menu 创建列操作菜单 + * @returns {Object[]} + */ +function createColumnMenu(options) +{ + var $col = options.$trigger.closest('.kanban-col'); + var col = $col.data('col'); + var kanbanID = options.kanban; + + var items = []; + if(priv.canEditName) items.push({label: executionLang.editName, url: $.createLink('kanban', 'setColumn', 'col=' + col.columnID + '&executionID=' + executionID), className: 'iframe', attrs: {'data-width': '500px'}}) + if(priv.canSetWIP) items.push({label: executionLang.setWIP, url: $.createLink('kanban', 'setWIP', 'col=' + col.columnID + '&executionID=' + executionID), className: 'iframe', attrs: {'data-width': '500px'}}) + if(priv.canSortCards) items.push({label: executionLang.sortColumn, items: ['按ID倒序', '按ID顺序'], className: 'iframe', onClick: handleSortColCards}) + return items; +} + +/** + * Create column create button menu 创建列添加按钮操作菜单 + * @returns {Object[]} + */ +function createColumnCreateMenu(options) +{ + var $col = options.$trigger.closest('.kanban-col'); + var col = $col.data('col'); + var items = []; + + if(col.laneType == 'story') + { + if(priv.canCreateStory) items.push({label: storyLang.create, url: $.createLink('story', 'create', 'productID=' + productID, '', true), className: 'iframe'}); + if(priv.canBatchCreateStory) items.push({label: executionLang.batchCreateStroy, url: $.createLink('story', 'batchcreate', 'productID=' + productID + '&branch=0&moduleID=0&storyID=0&executionID=' + executionID, '', true), className: 'iframe', attrs: {'data-width': '90%'}}); + if(priv.canLinkStory) items.push({label: executionLang.linkStory, url: $.createLink('execution', 'linkStory', 'executionID=' + executionID, '', true), className: 'iframe', attrs: {'data-width': '90%'}}); + if(priv.canLinkStoryByPlane) items.push({label: executionLang.linkStoryByPlan, url: '#linkStoryByPlan', 'attrs' : {'data-toggle': 'modal'}}); + } + else if(col.laneType == 'bug') + { + if(priv.canCreateBug) items.push({label: bugLang.create, url: $.createLink('bug', 'create', 'productID=0&moduleID=0&extra=executionID=' + executionID, '', true), className: 'iframe'}); + if(priv.canBatchCreateBug) items.push({label: bugLang.batchCreate, url: $.createLink('bug', 'batchcreate', 'productID=' + productID + '&moduleID=0&executionID=' + executionID, '', true), className: 'iframe'}); + } + else + { + if(priv.canCreateTask) items.push({label: taskLang.create, url: $.createLink('task', 'create', 'executionID=' + executionID, '', true), className: 'iframe'}); + if(priv.canBatchCreateTask) items.push({label: taskLang.batchCreate, url: $.createLink('task', 'batchcreate', 'executionID=' + executionID, '', true), className: 'iframe'}); + } + return items; +} + +/** + * Create lane menu 创建泳道操作菜单 + * @returns {Object[]} + */ +function createLaneMenu(options) +{ + var $lane = options.$trigger.closest('.kanban-lane'); + var $kanban = $lane.closest('.kanban'); + var lane = $lane.data('lane'); + var kanbanID = options.kanban; + var upTargetKanban = $kanban.prev('.kanban').length ? $kanban.prev('.kanban').data('id') : ''; + var downTargetKanban = $kanban.next('.kanban').length ? $kanban.next('.kanban').data('id') : ''; + + var items = []; + if(priv.canSetLane) items.push({label: kanbanLang.setLane, icon: 'edit', url: $.createLink('kanban', 'setLane', 'lane=' + lane.laneID + '&executionID=' + executionID), className: 'iframe'}); + if(priv.canMoveLane) items.push( + {label: kanbanLang.moveUp, icon: 'arrow-up', url: $.createLink('kanban', 'laneMove', 'executionID=' + executionID + '¤tLane=' + lane.id + '&targetLane=' + upTargetKanban), className: 'iframe', disabled: !$kanban.prev('.kanban').length}, + {label: kanbanLang.moveDown, icon: 'arrow-down', url: $.createLink('kanban', 'laneMove', 'executionID=' + executionID + '¤tLane=' + lane.id + '&targetLane=' + downTargetKanban), className: 'iframe', disabled: !$kanban.next('.kanban').length} + ); + + var bounds = options.$trigger[0].getBoundingClientRect(); + items.$options = {x: bounds.right, y: bounds.top}; + return items; +} + +/** + * Create story menu 创建需求卡片操作菜单 + * @returns {Object[]} + */ +function createStoryMenu(options) +{ + var $card = options.$trigger.closest('.kanban-item'); + var story = $card.data('item'); + + var items = []; + $.each(story.menus, function() + { + var item = {label: this.label, icon: this.icon, url: this.url, attrs: {'data-toggle': 'modal', 'data-type': 'iframe'}}; + if(this.size) item.attrs['data-width'] = this.size; + + if(this.icon == 'unlink') item = {label: this.label, icon: this.icon, url: this.url, attrs: {'target': 'hiddenwin'}}; + items.push(item); + }); + + return items; +} + +/** + * Create bug menu 创建 Bug 卡片操作菜单 + * @returns {Object[]} + */ +function createBugMenu(options) +{ + var $card = options.$trigger.closest('.kanban-item'); + var bug = $card.data('item'); + + var items = []; + $.each(bug.menus, function() + { + var item = {label: this.label, icon: this.icon, url: this.url, attrs: {'data-toggle': 'modal', 'data-type': 'iframe'}}; + if(this.size) item.attrs['data-width'] = this.size; + + items.push(item); + }); + + return items; +} + + /** + * Create task menu 创建任务卡片操作菜单 + * @returns {Object[]} + */ +function createTaskMenu(options) +{ + var $card = options.$trigger.closest('.kanban-item'); + var task = $card.data('item'); + + var items = []; + $.each(task.menus, function() + { + var item = {label: this.label, icon: this.icon, url: this.url, attrs: {'data-toggle': 'modal', 'data-type': 'iframe'}}; + if(this.size) item.attrs['data-width'] = this.size; + + items.push(item); + }); + + return items; +} + +/** Resize kanban container size */ +function resizeKanbanContainer() +{ + var $container = $('#kanbanContainer'); + var maxHeight = window.innerHeight - 98 - 15; + if($.cookie('isFullScreen') == 1) maxHeight = window.innerHeight - 15; + $container.children('.panel-body').css('max-height', maxHeight); +} + +/* Define menu creators */ +window.menuCreators = +{ + column: createColumnMenu, + columnCreate: createColumnCreateMenu, + lane: createLaneMenu, + story: createStoryMenu, + bug: createBugMenu, + task: createTaskMenu, +}; + +/* Set kanban affix container */ +window.kanbanAffixContainer = '#kanbanContainer>.panel-body'; + +/* Overload kanban default options */ +$.extend($.fn.kanban.Constructor.DEFAULTS, +{ + onRender: function() + { + var maxWidth = 0; + $('#kanbans .kanban-board').each(function() + { + maxWidth = Math.max(maxWidth, $(this).outerWidth()); + }); + $('#kanbans').css('min-width', maxWidth); + } +}); + +/* Example code: */ +$(function() +{ + $.cookie('isFullScreen', 0); + + /* Common options 用于初始化看板的通用选项 */  + var commonOptions = + { + maxColHeight: 'auto', + minColWidth: 240, + maxColWidth: 240, + showCount: true, + showZeroCount: true, + fluidBoardWidth: true, + droppable: + { + target: findDropColumns, + finish: handleFinishDrop, + mouseButton: 'left' + }, + onRenderHeaderCol: renderHeaderCol, + onRenderLaneName: renderLaneName, + onRenderCount: renderColumnCount + }; + + /* Create kanban 创建看板 */ + if(groupBy == 'default') + { + var kanbanLane = ''; + for(var i in kanbanList) + { + if(kanbanList[i] == 'story') kanbanLane = kanbanGroup.story; + if(kanbanList[i] == 'bug') kanbanLane = kanbanGroup.bug; + if(kanbanList[i] == 'task') kanbanLane = kanbanGroup.task; + + if(browseType == kanbanList[i] || browseType == 'all') createKanban(kanbanList[i], kanbanLane, commonOptions); + } + } + else + { + /* Create kanban by group. 分泳道创建看板. */ + createKanban(browseType, kanbanGroup[groupBy], commonOptions); + } + + /* Init iframe modals */ + $(document).on('click', '#kanbans .iframe,.contextmenu-menu .iframe', function(event) + { + var $link = $(this); + if($link.data('zui.modaltrigger')) return; + $link.modalTrigger({show: true}); + event.preventDefault(); + }); + + /* Init contextmenu */ + $('#kanbans').on('click', '[data-contextmenu]', function(event) + { + var $trigger = $(this); + var menuType = $trigger.data('contextmenu'); + + var menuCreator = window.menuCreators[menuType]; + if(!menuCreator) return; + + var options = $.extend({event, $trigger: $trigger}, $trigger.data()); + var items = menuCreator(options); + if(!items || !items.length) return; + + $.zui.ContextMenu.show(items, items.$options || {event: event}); + }); + + /* Resize kanban container on window resize */ + resizeKanbanContainer(); + $(window).on('resize', resizeKanbanContainer); + + /* Hide contextmenu when page scroll */ + $(window).on('scroll', function() + { + $.zui.ContextMenu.hide(); + }); + + $('#toStoryButton').on('click', function() + { + var planID = $('#plan').val(); + if(planID) + { + location.href = createLink('execution', 'importPlanStories', 'executionID=' + executionID + '&planID=' + planID + '&productID=0&fromMethod=kanban'); + } + }) + + $('#type_chosen .chosen-single span').prepend(''); + $('#group_chosen .chosen-single span').prepend(kanbanLang.laneGroup + ': '); + + /* Ajax update kanban. */ + setInterval(function() + { + $.get(createLink('execution', 'ajaxUpdateKanban', "executionID=" + executionID + "&entertime=" + entertime + "&browseType=" + browseType + "&groupBy=" + groupBy), function(data) + { + if(data) + { + kanbanGroup = $.parseJSON(data); + if(groupBy == 'default') + { + var kanbanLane = ''; + for(var i in kanbanList) + { + if(kanbanList[i] == 'story') kanbanLane = kanbanGroup.story; + if(kanbanList[i] == 'bug') kanbanLane = kanbanGroup.bug; + if(kanbanList[i] == 'task') kanbanLane = kanbanGroup.task; + + if(browseType == kanbanList[i] || browseType == 'all') updateKanban(kanbanList[i], kanbanLane); + } + } + else + { + updateKanban(browseType, kanbanGroup[groupBy]); + } + } + }); + }, 10000); +}); + +$('#type').change(function() +{ + var type = $('#type').val(); + if(type != 'all') + { + $('.c-group').show(); + $.get(createLink('execution', 'ajaxGetGroup', 'type=' + type), function(data) + { + $('#group_chosen').remove(); + $('#group').replaceWith(data); + $('#group').chosen(); + }) + } + + var link = createLink('execution', 'kanban', "executionID=" + executionID + '&type=' + type); + location.href = link; +}); + +$('.c-group').change(function() +{ + $('.c-group').show(); + + var type = $('#type').val(); + var group = $('#group').val(); + var link = createLink('execution', 'kanban', 'executionID=' + executionID + '&type=' + type + '&orderBy=order_asc' + '&groupBy=' + group); + location.href = link; +}); diff --git a/module/execution/lang/de.php b/module/execution/lang/de.php index 8e7837fbd6..4d0a8a8d43 100644 --- a/module/execution/lang/de.php +++ b/module/execution/lang/de.php @@ -376,6 +376,11 @@ $lang->execution->kanbanHideCols = 'Geschlossene und abgebrochene Spalten in K $lang->execution->kanbanShowOption = 'Aufklappen'; $lang->execution->kanbanColsColor = 'Spaltenfarben'; +$lang->execution->kanbanViewList['all'] = 'All'; +$lang->execution->kanbanViewList['story'] = "{$lang->SRCommon}"; +$lang->execution->kanbanViewList['bug'] = 'Bug'; +$lang->execution->kanbanViewList['task'] = 'Task'; + $lang->kanbanSetting = new stdclass(); $lang->kanbanSetting->noticeReset = 'Möchten Sie die Einstellungen des Kanbans zurücksetzen?'; $lang->kanbanSetting->optionList['0'] = 'Verstecken'; diff --git a/module/execution/lang/en.php b/module/execution/lang/en.php index cbfca1a77d..e0ee3196dd 100644 --- a/module/execution/lang/en.php +++ b/module/execution/lang/en.php @@ -10,97 +10,104 @@ * @link http://www.zentao.net */ /* Fields. */ -$lang->execution->allExecutions = 'All ' . $lang->execution->common . 's'; -$lang->execution->allExecutionAB = 'Execution List'; -$lang->execution->id = $lang->executionCommon . ' ID'; -$lang->execution->type = $lang->executionCommon . 'Type'; -$lang->execution->name = $lang->executionCommon . 'Name'; -$lang->execution->code = $lang->executionCommon . 'Code'; -$lang->execution->project = 'Project'; -$lang->execution->execName = 'Execution Name'; -$lang->execution->execCode = 'Execution Code'; -$lang->execution->execType = 'Execution Type'; -$lang->execution->stage = 'Stage'; -$lang->execution->pri = 'Priority'; -$lang->execution->openedBy = 'OpenedBy'; -$lang->execution->openedDate = 'OpenedDate'; -$lang->execution->closedBy = 'ClosedBy'; -$lang->execution->closedDate = 'ClosedDate'; -$lang->execution->canceledBy = 'CanceledBy'; -$lang->execution->canceledDate = 'CanceledDate'; -$lang->execution->begin = 'Begin'; -$lang->execution->end = 'End'; -$lang->execution->dateRange = 'Duration'; -$lang->execution->realBegan = 'Actual start'; -$lang->execution->realEnd = 'Actual end'; -$lang->execution->to = 'To'; -$lang->execution->days = 'Available Days'; -$lang->execution->day = ' Days'; -$lang->execution->workHour = ' Hours'; -$lang->execution->workHourUnit = 'H'; -$lang->execution->totalHours = 'Available Hours'; -$lang->execution->totalDays = 'Available Days'; -$lang->execution->status = $lang->executionCommon . 'Status'; -$lang->execution->execStatus = 'Status'; -$lang->execution->subStatus = 'Sub Status'; -$lang->execution->desc = $lang->executionCommon . 'Description'; -$lang->execution->execDesc = 'Description'; -$lang->execution->owner = 'Owner'; -$lang->execution->PO = "{$lang->executionCommon} Owner"; -$lang->execution->PM = "{$lang->executionCommon} Manager"; -$lang->execution->execPM = "Execution Manager"; -$lang->execution->QD = 'Test Manager'; -$lang->execution->RD = 'Release Manager'; -$lang->execution->release = 'Release'; -$lang->execution->acl = 'Access Control'; -$lang->execution->teamname = 'Team Name'; -$lang->execution->updateOrder = 'Rank'; -$lang->execution->order = "Rank {$lang->executionCommon}"; -$lang->execution->orderAB = "Rank"; -$lang->execution->products = "Link {$lang->productCommon}"; -$lang->execution->whitelist = 'Whitelist'; -$lang->execution->addWhitelist = 'Add Whitelist'; -$lang->execution->unbindWhitelist = 'Remove Whitelist'; -$lang->execution->totalEstimate = 'Estimates'; -$lang->execution->totalConsumed = 'Cost'; -$lang->execution->totalLeft = 'Left'; -$lang->execution->progress = ' Progress'; -$lang->execution->hours = 'Estimates: %s, Cost: %s, Left: %s.'; -$lang->execution->viewBug = 'Bugs'; -$lang->execution->noProduct = "No {$lang->productCommon} yet."; -$lang->execution->createStory = "Create Story"; -$lang->execution->storyTitle = "Story Name"; -$lang->execution->all = "All {$lang->executionCommon}s"; -$lang->execution->undone = 'Unfinished '; -$lang->execution->unclosed = 'Unclosed'; -$lang->execution->typeDesc = "OPS {$lang->executionCommon} has no {$lang->SRCommon}, Bug, Build, or Test features."; -$lang->execution->mine = 'Mine: '; -$lang->execution->involved = 'Mine'; -$lang->execution->other = 'Others'; -$lang->execution->deleted = 'Deleted'; -$lang->execution->delayed = 'Delayed'; -$lang->execution->product = $lang->execution->products; -$lang->execution->readjustTime = "Adjust {$lang->executionCommon} Begin and End"; -$lang->execution->readjustTask = 'Adjust Task Begin and End'; -$lang->execution->effort = 'Effort'; -$lang->execution->storyEstimate = 'Story Estimate'; -$lang->execution->newEstimate = 'New Estimate'; -$lang->execution->reestimate = 'Reestimate'; -$lang->execution->selectRound = 'Select Round'; -$lang->execution->average = 'Average'; -$lang->execution->relatedMember = 'Team'; -$lang->execution->watermark = 'Exported by ZenTao'; -$lang->execution->burnXUnit = '(Date)'; -$lang->execution->burnYUnit = '(Hours)'; -$lang->execution->waitTasks = 'Waiting Tasks'; -$lang->execution->viewByUser = 'By User'; -$lang->execution->oneProduct = "Only one stage can be linked {$lang->productCommon}"; -$lang->execution->noLinkProduct = "Stage not linked {$lang->productCommon}"; -$lang->execution->recent = 'Recent visits: '; -$lang->execution->copyNoExecution = 'There are no ' . $lang->executionCommon . 'available to copy.'; -$lang->execution->noTeam = 'No team members at the moment'; -$lang->execution->or = ' or '; -$lang->execution->selectProject = 'Please select project'; +$lang->execution->allExecutions = 'All ' . $lang->execution->common . 's'; +$lang->execution->allExecutionAB = 'Execution List'; +$lang->execution->id = $lang->executionCommon . ' ID'; +$lang->execution->type = $lang->executionCommon . 'Type'; +$lang->execution->name = $lang->executionCommon . 'Name'; +$lang->execution->code = $lang->executionCommon . 'Code'; +$lang->execution->project = 'Project'; +$lang->execution->execName = 'Execution Name'; +$lang->execution->execCode = 'Execution Code'; +$lang->execution->execType = 'Execution Type'; +$lang->execution->stage = 'Stage'; +$lang->execution->pri = 'Priority'; +$lang->execution->openedBy = 'OpenedBy'; +$lang->execution->openedDate = 'OpenedDate'; +$lang->execution->closedBy = 'ClosedBy'; +$lang->execution->closedDate = 'ClosedDate'; +$lang->execution->canceledBy = 'CanceledBy'; +$lang->execution->canceledDate = 'CanceledDate'; +$lang->execution->begin = 'Begin'; +$lang->execution->end = 'End'; +$lang->execution->dateRange = 'Duration'; +$lang->execution->realBeganAB = 'Actual Begin'; +$lang->execution->realEndAB = 'Actual End'; +$lang->execution->realBegan = 'Actual Begin'; +$lang->execution->realEnd = 'Actual End'; +$lang->execution->to = 'To'; +$lang->execution->days = ' Days'; +$lang->execution->day = ' Days'; +$lang->execution->workHour = ' Hours'; +$lang->execution->workHourUnit = 'H'; +$lang->execution->totalHours = ' Hours'; +$lang->execution->totalDays = ' Days'; +$lang->execution->status = $lang->executionCommon . 'Status'; +$lang->execution->execStatus = 'Status'; +$lang->execution->subStatus = 'Sub Status'; +$lang->execution->desc = $lang->executionCommon . 'Description'; +$lang->execution->execDesc = 'Description'; +$lang->execution->owner = 'Owner'; +$lang->execution->PO = "{$lang->executionCommon} Owner"; +$lang->execution->PM = "{$lang->executionCommon} Manager"; +$lang->execution->execPM = "Execution Manager"; +$lang->execution->QD = 'Test Manager'; +$lang->execution->RD = 'Release Manager'; +$lang->execution->release = 'Release'; +$lang->execution->acl = 'Access Control'; +$lang->execution->teamname = 'Team Name'; +$lang->execution->updateOrder = 'Rank'; +$lang->execution->order = "Rank {$lang->executionCommon}"; +$lang->execution->orderAB = "Rank"; +$lang->execution->products = "Link {$lang->productCommon}"; +$lang->execution->whitelist = 'Whitelist'; +$lang->execution->addWhitelist = 'Add Whitelist'; +$lang->execution->unbindWhitelist = 'Remove Whitelist'; +$lang->execution->totalEstimate = 'Estimates'; +$lang->execution->totalConsumed = 'Cost'; +$lang->execution->totalLeft = 'Left'; +$lang->execution->progress = ' Progress'; +$lang->execution->hours = 'Estimates: %s, Cost: %s, Left: %s.'; +$lang->execution->viewBug = 'Bugs'; +$lang->execution->noProduct = "No {$lang->productCommon} yet."; +$lang->execution->createStory = "Create Story"; +$lang->execution->storyTitle = "Story Name"; +$lang->execution->all = "All {$lang->executionCommon}s"; +$lang->execution->undone = 'Unfinished '; +$lang->execution->unclosed = 'Unclosed'; +$lang->execution->typeDesc = "OPS {$lang->executionCommon} has no {$lang->SRCommon}, Bug, Build, or Test features."; +$lang->execution->mine = 'Mine: '; +$lang->execution->involved = 'Mine'; +$lang->execution->other = 'Others'; +$lang->execution->deleted = 'Deleted'; +$lang->execution->delayed = 'Delayed'; +$lang->execution->product = $lang->execution->products; +$lang->execution->readjustTime = "Adjust {$lang->executionCommon} Begin and End"; +$lang->execution->readjustTask = 'Adjust Task Begin and End'; +$lang->execution->effort = 'Effort'; +$lang->execution->storyEstimate = 'Story Estimate'; +$lang->execution->newEstimate = 'New Estimate'; +$lang->execution->reestimate = 'Reestimate'; +$lang->execution->selectRound = 'Select Round'; +$lang->execution->average = 'Average'; +$lang->execution->relatedMember = 'Team'; +$lang->execution->watermark = 'Exported by ZenTao'; +$lang->execution->burnXUnit = '(Date)'; +$lang->execution->burnYUnit = '(Hours)'; +$lang->execution->waitTasks = 'Waiting Tasks'; +$lang->execution->viewByUser = 'By User'; +$lang->execution->oneProduct = "Only one stage can be linked {$lang->productCommon}"; +$lang->execution->noLinkProduct = "Stage not linked {$lang->productCommon}"; +$lang->execution->recent = 'Recent visits: '; +$lang->execution->copyNoExecution = 'There are no ' . $lang->executionCommon . 'available to copy.'; +$lang->execution->noTeam = 'No team members at the moment'; +$lang->execution->or = ' or '; +$lang->execution->selectProject = 'Please select project'; +$lang->execution->editName = 'Edit Name'; +$lang->execution->setWIP = 'WIP Settings'; +$lang->execution->sortColumn = 'Kanban Card Sorting'; +$lang->execution->batchCreateStroy = "Batch create {$lang->SRCommon}"; +$lang->execution->batchCreateTask = 'Batch create task'; /* Fields of zt_team. */ $lang->execution->root = 'Root'; @@ -311,6 +318,7 @@ $lang->execution->byPeriod = 'By Time'; $lang->execution->byUser = 'By User'; $lang->execution->noExecution = "No {$lang->executionCommon}. "; $lang->execution->noExecutions = "No {$lang->execution->common}."; +$lang->execution->noPrintData = "No data can be printed."; $lang->execution->noMembers = 'No team members yet. '; $lang->execution->workloadTotal = "The cumulative workload ratio should not exceed 100, and the total workload under the current product is: %s"; // $lang->execution->linkProjectStoryTip = "(Link {$lang->SRCommon} comes from {$lang->SRCommon} linked under the execution)"; @@ -347,6 +355,10 @@ $lang->execution->unfinishedTask = "[%s] unfinished tasks. "; $lang->execution->unresolvedBug = "[%s] unresolved bugs. "; $lang->execution->projectNotEmpty = 'Project cannot be empty.'; $lang->execution->confirmStoryToTask = $lang->SRCommon . '%s are converted to tasks in the current. Do you want to convert them anyways?'; +$lang->execution->realBeganNotEmpty = 'Actual Begin should not be empty.'; +$lang->execution->realBeganNotFuture = 'Actual Begin should be < = today.'; +$lang->execution->realEndNotEmpty = 'Actual End should not be empty.'; +$lang->execution->realEndNotFuture = 'Actual End should be < = today.'; /* Statistics. */ $lang->execution->charts = new stdclass(); @@ -381,12 +393,18 @@ $lang->execution->kanban = "Kanban"; $lang->execution->kanbanSetting = "Settings"; $lang->execution->resetKanban = "Reset"; $lang->execution->printKanban = "Print"; +$lang->execution->fullScreen = "Full Screen"; $lang->execution->bugList = "Bugs"; $lang->execution->kanbanHideCols = 'Closed & Cancelled Columns'; $lang->execution->kanbanShowOption = 'Unfold'; $lang->execution->kanbanColsColor = 'Customize Column Color'; +$lang->execution->kanbanViewList['all'] = 'All'; +$lang->execution->kanbanViewList['story'] = "{$lang->SRCommon}"; +$lang->execution->kanbanViewList['bug'] = 'Bug'; +$lang->execution->kanbanViewList['task'] = 'Task'; + $lang->kanbanSetting = new stdclass(); $lang->kanbanSetting->noticeReset = 'Do you want to reset Kanban?'; $lang->kanbanSetting->optionList['0'] = 'Hide'; @@ -427,7 +445,7 @@ $lang->execution->doingProject = 'Ongoing Projects'; $lang->execution->kanbanColType['wait'] = $lang->execution->statusList['wait'] . ' ' . $lang->execution->common; $lang->execution->kanbanColType['doing'] = $lang->execution->statusList['doing'] . ' ' . $lang->execution->common; $lang->execution->kanbanColType['suspended'] = $lang->execution->statusList['suspended'] . ' ' . $lang->execution->common; -$lang->execution->kanbanColType['closed'] = $lang->execution->statusList['closed'] . ' ' . $lang->execution->common; +$lang->execution->kanbanColType['closed'] = $lang->execution->statusList['closed'] . ' ' . $lang->execution->common . '(The recent two executions)'; $lang->execution->treeLevel = array(); $lang->execution->treeLevel['all'] = 'Expand All'; diff --git a/module/execution/lang/fr.php b/module/execution/lang/fr.php index bb698b546b..5d769daf4f 100644 --- a/module/execution/lang/fr.php +++ b/module/execution/lang/fr.php @@ -376,6 +376,11 @@ $lang->execution->kanbanHideCols = 'Colonnes masquées'; $lang->execution->kanbanShowOption = 'Déplier'; $lang->execution->kanbanColsColor = 'Personnalisation Couleurs'; +$lang->execution->kanbanViewList['all'] = 'All'; +$lang->execution->kanbanViewList['story'] = "{$lang->SRCommon}"; +$lang->execution->kanbanViewList['bug'] = 'Bug'; +$lang->execution->kanbanViewList['task'] = 'Task'; + $lang->kanbanSetting = new stdclass(); $lang->kanbanSetting->noticeReset = 'Voulez-vous réinitialiser le tableau Kanban ?'; $lang->kanbanSetting->optionList['0'] = 'Masquer'; diff --git a/module/execution/lang/vi.php b/module/execution/lang/vi.php index c6bfcd7051..865839cfd3 100644 --- a/module/execution/lang/vi.php +++ b/module/execution/lang/vi.php @@ -377,6 +377,11 @@ $lang->execution->kanbanHideCols = 'Cột Đã đóng & đã hủy'; $lang->execution->kanbanShowOption = 'Mở ra'; $lang->execution->kanbanColsColor = 'Tùy biến màu cột'; +$lang->execution->kanbanViewList['all'] = 'All'; +$lang->execution->kanbanViewList['story'] = "{$lang->SRCommon}"; +$lang->execution->kanbanViewList['bug'] = 'Bug'; +$lang->execution->kanbanViewList['task'] = 'Task'; + $lang->kanbanSetting = new stdclass(); $lang->kanbanSetting->noticeReset = 'Bạn có muốn thiết lập lại Kanban?'; $lang->kanbanSetting->optionList['0'] = 'Ẩn'; diff --git a/module/execution/lang/zh-cn.php b/module/execution/lang/zh-cn.php index 4c4b5436eb..ff3f9d29b5 100644 --- a/module/execution/lang/zh-cn.php +++ b/module/execution/lang/zh-cn.php @@ -10,98 +10,105 @@ * @link http://www.zentao.net */ /* 字段列表。*/ -$lang->execution->allExecutions = '所有' . $lang->execution->common; -$lang->execution->allExecutionAB = "{$lang->execution->common}列表"; -$lang->execution->id = $lang->executionCommon . '编号'; -$lang->execution->type = $lang->executionCommon . '类型'; -$lang->execution->name = $lang->executionCommon . '名称'; -$lang->execution->code = $lang->executionCommon . '代号'; -$lang->execution->project = '所属项目'; -$lang->execution->execName = "{$lang->execution->common}名称"; -$lang->execution->execCode = "{$lang->execution->common}代号"; -$lang->execution->execType = "{$lang->execution->common}类型"; -$lang->execution->stage = '阶段'; -$lang->execution->pri = '优先级'; -$lang->execution->openedBy = '由谁创建'; -$lang->execution->openedDate = '创建日期'; -$lang->execution->closedBy = '由谁关闭'; -$lang->execution->closedDate = '关闭日期'; -$lang->execution->canceledBy = '由谁取消'; -$lang->execution->canceledDate = '取消日期'; -$lang->execution->begin = '开始日期'; -$lang->execution->end = '截止日期'; -$lang->execution->dateRange = '起始日期'; -$lang->execution->realBegan = '实际开始'; -$lang->execution->realEnd = '实际结束'; -$lang->execution->to = '至'; -$lang->execution->days = '可用工作日'; -$lang->execution->day = '天'; -$lang->execution->workHour = '工时'; -$lang->execution->workHourUnit = 'h'; -$lang->execution->totalHours = '可用工时'; -$lang->execution->totalDays = '可用工日'; -$lang->execution->status = $lang->executionCommon . '状态'; -$lang->execution->execStatus = "{$lang->execution->common}状态"; -$lang->execution->subStatus = '子状态'; -$lang->execution->desc = $lang->executionCommon . '描述'; -$lang->execution->execDesc = "{$lang->execution->common}描述"; -$lang->execution->owner = '负责人'; -$lang->execution->PO = $lang->productCommon . '负责人'; -$lang->execution->PM = $lang->executionCommon . '负责人'; -$lang->execution->execPM = "{$lang->execution->common}负责人"; -$lang->execution->QD = '测试负责人'; -$lang->execution->RD = '发布负责人'; -$lang->execution->release = '发布'; -$lang->execution->acl = '访问控制'; -$lang->execution->teamname = '团队名称'; -$lang->execution->updateOrder = '排序'; -$lang->execution->order = $lang->executionCommon . '排序'; -$lang->execution->orderAB = '排序'; -$lang->execution->products = '相关' . $lang->productCommon; -$lang->execution->whitelist = '白名单'; -$lang->execution->addWhitelist = '添加白名单'; -$lang->execution->unbindWhitelist = '删除白名单'; -$lang->execution->totalEstimate = '预计'; -$lang->execution->totalConsumed = '消耗'; -$lang->execution->totalLeft = '剩余'; -$lang->execution->progress = '进度'; -$lang->execution->hours = '预计 %s 消耗 %s 剩余 %s'; -$lang->execution->viewBug = '查看bug'; -$lang->execution->noProduct = "无{$lang->executionCommon}"; -$lang->execution->createStory = "提{$lang->SRCommon}"; -$lang->execution->storyTitle = "{$lang->SRCommon}名称"; -$lang->execution->all = '所有'; -$lang->execution->undone = '未完成'; -$lang->execution->unclosed = '未关闭'; -$lang->execution->typeDesc = "运维{$lang->executionCommon}没有{$lang->SRCommon}、bug、版本、测试功能。"; -$lang->execution->mine = '我负责:'; -$lang->execution->involved = '我参与'; -$lang->execution->other = '其他'; -$lang->execution->deleted = '已删除'; -$lang->execution->delayed = '已延期'; -$lang->execution->product = $lang->execution->products; -$lang->execution->readjustTime = "调整{$lang->executionCommon}起止时间"; -$lang->execution->readjustTask = '顺延任务的起止时间'; -$lang->execution->effort = '日志'; -$lang->execution->storyEstimate = '需求估算'; -$lang->execution->newEstimate = '新一轮估算'; -$lang->execution->reestimate = '重新估算'; -$lang->execution->selectRound = '选择轮次'; -$lang->execution->average = '平均值'; -$lang->execution->relatedMember = '相关成员'; -$lang->execution->watermark = '由禅道导出'; -$lang->execution->burnXUnit = '(日期)'; -$lang->execution->burnYUnit = '(工时)'; -$lang->execution->waitTasks = '待处理'; -$lang->execution->viewByUser = '按用户查看'; -$lang->execution->oneProduct = "阶段只能关联一个{$lang->productCommon}"; -$lang->execution->noLinkProduct = "阶段没有关联{$lang->productCommon}"; -$lang->execution->recent = '近期访问:'; -$lang->execution->copyNoExecution = '没有可用的' . $lang->executionCommon . '来复制'; -$lang->execution->noTeam = '暂时没有团队成员'; -$lang->execution->or = '或'; -$lang->execution->selectProject = '请选择项目'; -$lang->execution->unfoldClosed = '展开已结束'; +$lang->execution->allExecutions = '所有' . $lang->execution->common; +$lang->execution->allExecutionAB = "{$lang->execution->common}列表"; +$lang->execution->id = $lang->executionCommon . '编号'; +$lang->execution->type = $lang->executionCommon . '类型'; +$lang->execution->name = $lang->executionCommon . '名称'; +$lang->execution->code = $lang->executionCommon . '代号'; +$lang->execution->project = '所属项目'; +$lang->execution->execName = "{$lang->execution->common}名称"; +$lang->execution->execCode = "{$lang->execution->common}代号"; +$lang->execution->execType = "{$lang->execution->common}类型"; +$lang->execution->stage = '阶段'; +$lang->execution->pri = '优先级'; +$lang->execution->openedBy = '由谁创建'; +$lang->execution->openedDate = '创建日期'; +$lang->execution->closedBy = '由谁关闭'; +$lang->execution->closedDate = '关闭日期'; +$lang->execution->canceledBy = '由谁取消'; +$lang->execution->canceledDate = '取消日期'; +$lang->execution->begin = '计划开始'; +$lang->execution->end = '计划完成'; +$lang->execution->dateRange = '起始日期'; +$lang->execution->realBeganAB = '实际开始'; +$lang->execution->realEndAB = '实际完成'; +$lang->execution->realBegan = '实际开始日期'; +$lang->execution->realEnd = '实际完成日期'; +$lang->execution->to = '至'; +$lang->execution->days = '可用工作日'; +$lang->execution->day = '天'; +$lang->execution->workHour = '工时'; +$lang->execution->workHourUnit = 'h'; +$lang->execution->totalHours = '可用工时'; +$lang->execution->totalDays = '可用工日'; +$lang->execution->status = $lang->executionCommon . '状态'; +$lang->execution->execStatus = "{$lang->execution->common}状态"; +$lang->execution->subStatus = '子状态'; +$lang->execution->desc = $lang->executionCommon . '描述'; +$lang->execution->execDesc = "{$lang->execution->common}描述"; +$lang->execution->owner = '负责人'; +$lang->execution->PO = $lang->productCommon . '负责人'; +$lang->execution->PM = $lang->executionCommon . '负责人'; +$lang->execution->execPM = "{$lang->execution->common}负责人"; +$lang->execution->QD = '测试负责人'; +$lang->execution->RD = '发布负责人'; +$lang->execution->release = '发布'; +$lang->execution->acl = '访问控制'; +$lang->execution->teamname = '团队名称'; +$lang->execution->updateOrder = '排序'; +$lang->execution->order = $lang->executionCommon . '排序'; +$lang->execution->orderAB = '排序'; +$lang->execution->products = '相关' . $lang->productCommon; +$lang->execution->whitelist = '白名单'; +$lang->execution->addWhitelist = '添加白名单'; +$lang->execution->unbindWhitelist = '删除白名单'; +$lang->execution->totalEstimate = '预计'; +$lang->execution->totalConsumed = '消耗'; +$lang->execution->totalLeft = '剩余'; +$lang->execution->progress = '进度'; +$lang->execution->hours = '预计 %s 消耗 %s 剩余 %s'; +$lang->execution->viewBug = '查看bug'; +$lang->execution->noProduct = "无{$lang->executionCommon}"; +$lang->execution->createStory = "提{$lang->SRCommon}"; +$lang->execution->storyTitle = "{$lang->SRCommon}名称"; +$lang->execution->all = '所有'; +$lang->execution->undone = '未完成'; +$lang->execution->unclosed = '未关闭'; +$lang->execution->typeDesc = "运维{$lang->executionCommon}没有{$lang->SRCommon}、bug、版本、测试功能。"; +$lang->execution->mine = '我负责:'; +$lang->execution->involved = '我参与'; +$lang->execution->other = '其他'; +$lang->execution->deleted = '已删除'; +$lang->execution->delayed = '已延期'; +$lang->execution->product = $lang->execution->products; +$lang->execution->readjustTime = "调整{$lang->executionCommon}起止时间"; +$lang->execution->readjustTask = '顺延任务的起止时间'; +$lang->execution->effort = '日志'; +$lang->execution->storyEstimate = '需求估算'; +$lang->execution->newEstimate = '新一轮估算'; +$lang->execution->reestimate = '重新估算'; +$lang->execution->selectRound = '选择轮次'; +$lang->execution->average = '平均值'; +$lang->execution->relatedMember = '相关成员'; +$lang->execution->watermark = '由禅道导出'; +$lang->execution->burnXUnit = '(日期)'; +$lang->execution->burnYUnit = '(工时)'; +$lang->execution->waitTasks = '待处理'; +$lang->execution->viewByUser = '按用户查看'; +$lang->execution->oneProduct = "阶段只能关联一个{$lang->productCommon}"; +$lang->execution->noLinkProduct = "阶段没有关联{$lang->productCommon}"; +$lang->execution->recent = '近期访问:'; +$lang->execution->copyNoExecution = '没有可用的' . $lang->executionCommon . '来复制'; +$lang->execution->noTeam = '暂时没有团队成员'; +$lang->execution->or = '或'; +$lang->execution->selectProject = '请选择项目'; +$lang->execution->unfoldClosed = '展开已结束'; +$lang->execution->editName = '编辑名称'; +$lang->execution->setWIP = '在制品数量设置(WIP)'; +$lang->execution->sortColumn = '看板列卡片排序'; +$lang->execution->batchCreateStroy = "批量新建{$lang->SRCommon}"; +$lang->execution->batchCreateTask = '批量建任务'; /* Fields of zt_team. */ $lang->execution->root = '源ID'; @@ -312,6 +319,7 @@ $lang->execution->byPeriod = '按时间段'; $lang->execution->byUser = '按用户'; $lang->execution->noExecution = "暂时没有{$lang->executionCommon}。"; $lang->execution->noExecutions = "暂时没有{$lang->execution->common}。"; +$lang->execution->noPrintData = "暂无数据可打印"; $lang->execution->noMembers = '暂时没有团队成员。'; $lang->execution->workloadTotal = "工作量占比累计不应当超过100, 当前产品下的工作量之和为%s"; // $lang->execution->linkProjectStoryTip = "(关联{$lang->SRCommon}来源于项目下所关联的{$lang->SRCommon})"; @@ -348,6 +356,10 @@ $lang->execution->unfinishedTask = "[%s]个未完成的任务,"; $lang->execution->unresolvedBug = "[%s]个未解决的bug,"; $lang->execution->projectNotEmpty = '所属项目不能为空。'; $lang->execution->confirmStoryToTask = '%s' . $lang->SRCommon . '已经在当前' . $lang->execution->common . '中转了任务,请确认是否重复转任务。'; +$lang->execution->realBeganNotEmpty = "实际开始不能为空。"; +$lang->execution->realBeganNotFuture = "实际开始不能大于当前日期。"; +$lang->execution->realEndNotEmpty = "实际完成不能为空。"; +$lang->execution->realEndNotFuture = "实际完成不能大于当前日期。"; /* 统计。*/ $lang->execution->charts = new stdclass(); @@ -382,12 +394,18 @@ $lang->execution->kanban = "看板"; $lang->execution->kanbanSetting = "看板设置"; $lang->execution->resetKanban = "恢复默认"; $lang->execution->printKanban = "打印看板"; +$lang->execution->fullScreen = "看板全屏展示"; $lang->execution->bugList = "Bug列表"; $lang->execution->kanbanHideCols = '看板隐藏已关闭、已取消列'; $lang->execution->kanbanShowOption = '显示折叠信息'; $lang->execution->kanbanColsColor = '看板列自定义颜色'; +$lang->execution->kanbanViewList['all'] = '综合看板'; +$lang->execution->kanbanViewList['story'] = "{$lang->SRCommon}看板"; +$lang->execution->kanbanViewList['bug'] = 'Bug看板'; +$lang->execution->kanbanViewList['task'] = '任务看板'; + $lang->kanbanSetting = new stdclass(); $lang->kanbanSetting->noticeReset = '是否恢复看板默认设置?'; $lang->kanbanSetting->optionList['0'] = '隐藏'; @@ -428,7 +446,7 @@ $lang->execution->doingProject = '进行中的项目'; $lang->execution->kanbanColType['wait'] = $lang->execution->statusList['wait'] . '的' . $lang->execution->common; $lang->execution->kanbanColType['doing'] = $lang->execution->statusList['doing'] . '的' . $lang->execution->common; $lang->execution->kanbanColType['suspended'] = $lang->execution->statusList['suspended'] . '的' . $lang->execution->common; -$lang->execution->kanbanColType['closed'] = $lang->execution->statusList['closed'] . '的' . $lang->execution->common; +$lang->execution->kanbanColType['closed'] = $lang->execution->statusList['closed'] . '的' . $lang->execution->common . '(最近2期)'; $lang->execution->treeLevel = array(); $lang->execution->treeLevel['all'] = '全部展开'; diff --git a/module/execution/model.php b/module/execution/model.php index 48d3a3c477..bdbdca88a0 100644 --- a/module/execution/model.php +++ b/module/execution/model.php @@ -381,6 +381,8 @@ class executionModel extends model $creatorExists = false; $teamMembers = array(); + $this->loadModel('kanban')->createLanes($executionID); + /* Save order. */ $this->dao->update(TABLE_EXECUTION)->set('`order`')->eq($executionID * 5)->where('id')->eq($executionID)->exec(); $this->file->updateObjectID($this->post->uid, $executionID, 'execution'); @@ -710,14 +712,24 @@ class executionModel extends model $now = helper::now(); $execution = fixer::input('post') - ->add('realBegan', helper::today()) ->setDefault('status', 'doing') ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', $now) ->remove('comment')->get(); + + if($execution->realBegan == '') + { + dao::$errors['realBegan'] = $this->lang->execution->realBeganNotEmpty; + return false; + } + if($execution->realBegan > helper::today()) + { + dao::$errors['realBegan'] = $this->lang->execution->realBeganNotFuture; + return false; + } $this->dao->update(TABLE_EXECUTION)->data($execution)->autoCheck()->where('id')->eq((int)$executionID)->exec(); - + if(!dao::isError()) return common::createChanges($oldExecution, $execution); } @@ -789,6 +801,7 @@ class executionModel extends model $now = helper::now(); $execution = fixer::input('post') + ->setDefault('realEnd', '') ->setDefault('status', 'doing') ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', $now) @@ -858,18 +871,30 @@ class executionModel extends model $execution = fixer::input('post') ->setDefault('status', 'closed') - ->setDefault('realEnd', helper::today()) ->setDefault('closedBy', $this->app->user->account) ->setDefault('closedDate', $now) ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', $now) ->remove('comment') ->get(); + + if($execution->realEnd == '') + { + dao::$errors['realEnd'] = $this->lang->execution->realEndNotEmpty; + return false; + } + + if($execution->realEnd > helper::today()) + { + dao::$errors['realEnd'] = $this->lang->execution->realEndNotFuture; + return false; + } $this->dao->update(TABLE_EXECUTION)->data($execution) ->autoCheck() ->where('id')->eq((int)$executionID) ->exec(); + if(!dao::isError()) { $this->loadModel('score')->create('execution', 'close', $oldExecution); @@ -1340,6 +1365,10 @@ class executionModel extends model { $link = helper::createLink('doc', $method, "type=execution&objectID=%s&from=execution"); } + elseif(in_array($module, array('issue', 'risk', 'opportunity', 'pssp', 'auditplan', 'nc', 'meeting'))) + { + $link = helper::createLink($module, 'browse', "executionID=%s&from=execution"); + } else { $link = helper::createLink($module, $method, "executionID=%s"); @@ -1711,7 +1740,8 @@ class executionModel extends model */ public function buildStorySearchForm($products, $branchGroups, $modules, $queryID, $actionURL, $type = 'executionStory', $objectID = 0) { - $branchPairs = array(); + $this->app->loadLang('branch'); + $branchPairs = array(BRANCH_MAIN => $this->lang->branch->main); $productType = 'normal'; $productNum = count($products); $productPairs = array(0 => ''); @@ -1727,18 +1757,9 @@ class executionModel extends model foreach($branches[$product->id] as $branchID => $branch) { if(!isset($branchGroups[$product->id][$branchID])) continue; - $branchPairs[$branchID] = ((count($products) > 1) ? $product->name . '/' : '') . $branchGroups[$product->id][$branchID]; + if($branchID != BRANCH_MAIN) $branchPairs[$branchID] = ((count($products) > 1) ? $product->name . '/' : '') . $branchGroups[$product->id][$branchID]; } } - else - { - $productBranches = isset($branchGroups[$product->id]) ? $branchGroups[$product->id] : array(0); - if(count($products) > 1) - { - foreach($productBranches as $branchID => $branchName) $productBranches[$branchID] = $product->name . '/' . $branchName; - } - $branchPairs += $productBranches; - } } } @@ -1752,7 +1773,16 @@ class executionModel extends model $this->config->product->search['actionURL'] = $actionURL; $this->config->product->search['queryID'] = $queryID; $this->config->product->search['params']['product']['values'] = $productPairs + array('all' => $this->lang->product->allProductsOfProject); - $this->config->product->search['params']['plan']['values'] = $this->loadModel('productplan')->getForProducts($products); + + $this->loadModel('productplan'); + $plans = array(); + $planPairs = array('' => ''); + foreach($products as $productID => $product) + { + $plans = $this->productplan->getBranchPlanPairs($productID, array(BRANCH_MAIN) + $product->branches, true); + foreach($plans as $plan) $planPairs += $plan; + } + $this->config->product->search['params']['plan']['values'] = $planPairs; $this->config->product->search['params']['module']['values'] = $modules; if($productType == 'normal') { @@ -3186,7 +3216,7 @@ class executionModel extends model ->andWhere('t1.parent')->ge(0) ->orderBy($orderBy) ->page($pager) - ->fetchAll(); + ->fetchAll('id'); $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'task'); @@ -3657,14 +3687,15 @@ class executionModel extends model * Get plans by $productID. * * @param int|array $productID - * + * @param string $param withMainPlan|skipParent * @return mixed */ - public function getPlans($products) + public function getPlans($products, $param = '') { $this->loadModel('productplan'); - $branchIDList = array(); + $param = strtolower($param); + $branchIDList = strpos($param, 'withmainplan') !== false ? array(BRANCH_MAIN => BRANCH_MAIN) : array(); foreach($products as $product) { foreach($product->branches as $branchID) $branchIDList[$branchID] = $branchID; @@ -3674,6 +3705,7 @@ class executionModel extends model ->where('product')->in(array_keys($products)) ->andWhere('deleted')->eq(0) ->andWhere('branch')->in($branchIDList)->fi() + ->beginIF(strpos($param, 'skipparent') !== false)->andWhere('parent')->ne(-1)->fi() ->orderBy('begin desc') ->fetchAll('id'); diff --git a/module/execution/view/activate.html.php b/module/execution/view/activate.html.php index d8f34124cc..7cb5cd726b 100644 --- a/module/execution/view/activate.html.php +++ b/module/execution/view/activate.html.php @@ -5,7 +5,7 @@ * @copyright Copyright 2009-2015 青岛易软天创网络科技有限公司(QingDao Nature Easy Soft Network Technology Co,LTD, www.cnezsoft.com) * @license ZPL (http://zpl.pub/page/zplv12.html) * @author Chunsheng Wang - * @package execution + * @package execution * @version $Id: suspend.html.php 935 2013-01-16 07:49:24Z wwccss@gmail.com $ * @link http://www.zentao.net */ @@ -59,7 +59,7 @@ - goback, $this->session->taskList, 'self', '', 'btn btn-wide'); ?> + execution->activate . $lang->executionCommon) . html::linkButton($lang->goback, $this->session->taskList, 'self', '', 'btn btn-wide'); ?> diff --git a/module/execution/view/close.html.php b/module/execution/view/close.html.php index 9d9edf493d..11ebcd1ed4 100644 --- a/module/execution/view/close.html.php +++ b/module/execution/view/close.html.php @@ -5,7 +5,7 @@ * @copyright Copyright 2009-2015 青岛易软天创网络科技有限公司(QingDao Nature Easy Soft Network Technology Co,LTD, www.cnezsoft.com) * @license ZPL (http://zpl.pub/page/zplv12.html) * @author Chunsheng Wang - * @package execution + * @package execution * @version $Id: suspend.html.php 935 2013-01-16 07:49:24Z wwccss@gmail.com $ * @link http://www.zentao.net */ @@ -30,12 +30,20 @@ printExtendFields($execution, 'table');?> + + execution->realEnd;?> + +
    + realEnd) && $execution->realEnd != '0000-00-00' ? $execution->realEnd : date('Y-m-d')), "class='form-control form-date' required");?> +
    + + comment;?> - goback, $this->session->taskList, 'self', '', 'btn btn-wide'); ?> + execution->close . $lang->executionCommon) . html::linkButton($lang->goback, $this->session->taskList, 'self', '', 'btn btn-wide'); ?> diff --git a/module/execution/view/create.html.php b/module/execution/view/create.html.php index a8d70a5385..80f26d7161 100644 --- a/module/execution/view/create.html.php +++ b/module/execution/view/create.html.php @@ -90,9 +90,31 @@ systemMode == 'new')) ? $lang->execution->execType : $lang->execution->type;?> - execution->lifeTimeList, '', "class='form-control chosen' onchange='showLifeTimeTips()'"); ?> + + stage->typeList, '', "class='form-control chosen'"); + } + else + { + echo html::select('lifetime', $lang->execution->lifeTimeList, '', "class='form-control' onchange='showLifeTimeTips()'"); + } + ?> +
    execution->typeDesc;?>
    + + + stage->percent;?> + +
    + + % +
    + + + execution->status;?> @@ -132,7 +154,7 @@
    begin)):?> -
    product . "]", $productPlan, $plan->id, "class='form-control chosen'");?>
    +
    product}][{$plan->branch}]", $productPlan, $plan->id, "class='form-control chosen'");?>
    id)?> diff --git a/module/execution/view/edit.html.php b/module/execution/view/edit.html.php index 97ad55a0d4..eb78f42961 100644 --- a/module/execution/view/edit.html.php +++ b/module/execution/view/edit.html.php @@ -69,7 +69,16 @@ execution->type;?> - execution->lifeTimeList, $execution->lifetime, "class='form-control' onchange='showLifeTimeTips()'");?> + type != 'stage') + { + echo html::select('lifetime', $lang->execution->lifeTimeList, $execution->lifetime, "class='form-control' onchange='showLifeTimeTips()'"); + } + else + { + echo html::select('attribute', $lang->stage->typeList, $execution->attribute, "class='chosen form-control'"); + } + ?> @@ -109,6 +118,17 @@
    + type == 'stage'):?> + + stage->percent;?> + +
    + percent, "class='form-control'");?> + % +
    + + + execution->manageProducts;?> diff --git a/module/execution/view/executionkanban.html.php b/module/execution/view/executionkanban.html.php index a3a98c4559..121ab6fb94 100644 --- a/module/execution/view/executionkanban.html.php +++ b/module/execution/view/executionkanban.html.php @@ -9,94 +9,37 @@ */ ?> -
    - -
    -

    - execution->noExecutions;?> -

    + + +
    +

    execution->noExecutions;?>

    +
    + +
    +
    +
    +
    - - - - - - - execution->kanbanColType as $status => $colName):?> - - - - - - - $executionList):?> - - - - - - - - - -
    execution->doingProject . ' (' . $projectCount . ')';?>
    -
    -
    - execution->myExecutions : zget($projects, $projectID);?> - -
    -
    -
    -
    -
    - execution->kanbanColType as $colStatus => $colName):?> -
    -
    - - -
    status == 'doing' and isset($execution->delay)) echo "style='border-left: 3px solid red';";?>> -
    -
    - project) . ' / ' . $execution->name : $execution->name; - - if(common::hasPriv('execution', 'task')) - { - echo html::a($this->createLink('execution', 'task', "executionID=$execution->id"), $executionName, '', "title='{$executionName}'"); - } - else - { - echo "{$executionName}"; - } - ?> -
    - -
    -
    -
    -
    hours->progress);?>
    -
    -
    -
    - -
    -
    - - -
    -
    - -
    -
    -
    -
    - + diff --git a/module/execution/view/kanban.html.php b/module/execution/view/kanban.html.php index 5db8444efb..534de79bca 100644 --- a/module/execution/view/kanban.html.php +++ b/module/execution/view/kanban.html.php @@ -9,17 +9,25 @@ */ ?> - + - -
    - 0) - { - $hasTask = true; - break; - } - } - ?> - -
    -

    - task->noTask;?> - - createLink('task', 'create', "execution=$executionID" . (isset($moduleID) ? "&storyID=&moduleID=$moduleID" : '')), " " . $lang->task->create, '', "class='btn btn-info'");?> - -

    + +
    +
    +
    - - - - - - - - - - - - - $group):?> - - - - - - - -
    - -
    - - - -
    -
    - createLink('story', 'view', "storyID=$story->id", '', true), $story->title, '', 'class="kanbaniframe group-title" title="' . $story->title . '"'); - } - else - { - echo "{$story->title}"; - } - ?> - - - -
    -
    - #id?> - story->priList, $story->pri);?> - story->stageList[$story->stage];?> -
    estimate . 'h ';?>
    -
    -
    - -
    - - -
    -
    -
    - -
    - tasks[$col])):?> - tasks[$col] as $task):?> - -
    - parent > 0 ? "" . $lang->task->childrenAB . ' ' : ''; - if(common::hasPriv('task', 'view')) - { - echo html::a($this->createLink('task', 'view', "taskID=$task->id", '', true), "{$childrenAB}{$task->name}", '', 'class="title kanbaniframe" title="' . $task->name . '"'); - } - else - { - echo "{$childrenAB}{$task->name}"; - } - ?> -
    - " . zget($realnames, $task->assignedTo) . ""; - if(empty($task->assignedTo)) $assignedToRealName = "{$lang->task->noAssigned}"; - if(common::hasPriv('task', 'assignTo', $task)) - { - echo html::a($this->createLink('task', 'assignTo', "executionID={$task->execution}&taskID={$task->id}", '', true), ' ' . $assignedToRealName, '', 'class="btn btn-icon-left kanbaniframe task-assignedTo"'); - } - else - { - echo " {$assignedToRealName}"; - } - ?> - delay)):?> - task->delayed;?> - - left;?>h -
    -
    - - - bugs[$col])):?> - bugs[$col] as $bug):?> -
    - createLink('bug', 'view', "bugID=$bug->id", '', true), " #{$bug->id}{$bug->title}", '', 'class="title kanbaniframe" title="' . $bug->title . '"'); - } - else - { - echo " #{$bug->id}{$bug->title}"; - } - ?> -
    - " . zget($realnames, $bug->assignedTo) . ""; - if(empty($bug->assignedTo)) $assignedToRealName = "{$lang->task->noAssigned}"; - if(common::hasPriv('bug', 'assignTo', $bug)) - { - echo html::a($this->createLink('bug', 'assignTo', "bugID={$bug->id}", '', true), ' ' . $assignedToRealName, '', 'class="btn btn-icon-left kanbaniframe bug-assignedTo"'); - } - else - { - echo " {$assignedToRealName}"; - } - ?> - bug->statusList, $bug->status);?> -
    -
    - - -
    - -
    -
    -
    -
    - + + + + + + + + + common::hasPriv('kanban', 'setColumn'), + 'canSetWIP' => common::hasPriv('kanban', 'setWIP'), + 'canSetLane' => common::hasPriv('kanban', 'setLane'), + 'canMoveLane' => common::hasPriv('kanban', 'laneMove'), + 'canSortCards' => common::hasPriv('kanban', 'cardsSort'), + 'canCreateTask' => $canCreateTask, + 'canBatchCreateTask' => $canBatchCreateTask, + 'canCreateBug' => $canCreateBug, + 'canBatchCreateBug' => $canBatchCreateBug, + 'canCreateStory' => $canCreateStory, + 'canBatchCreateStory' => $canBatchCreateStory, + 'canLinkStory' => $canLinkStory, + 'canLinkStoryByPlane' => $canLinkStoryByPlane, + 'canAssignTask' => common::hasPriv('task', 'assignto'), + 'canAssignStory' => common::hasPriv('story', 'assignto'), + 'canAssignBug' => common::hasPriv('bug', 'assignto'), + 'canFinishTask' => common::hasPriv('task', 'finish'), + 'canPauseTask' => common::hasPriv('task', 'pause'), + 'canCancelTask' => common::hasPriv('task', 'cancel'), + 'canCloseTask' => common::hasPriv('task', 'close'), + 'canActivateTask' => common::hasPriv('task', 'activate'), + 'canStartTask' => common::hasPriv('task', 'start') + ) +); +?> +execution);?> +story);?> +task);?> +bug);?> +execution->editName);?> +execution->setWIP);?> +execution->sortColumn);?> +kanban);?> +task->deadlineAB);?> +task->noAssigned);?> + + diff --git a/module/execution/view/manageproducts.html.php b/module/execution/view/manageproducts.html.php index 775536c289..433deed8dd 100644 --- a/module/execution/view/manageproducts.html.php +++ b/module/execution/view/manageproducts.html.php @@ -38,11 +38,11 @@ ";?>
    - +
    - + diff --git a/module/execution/view/printkanban.html.php b/module/execution/view/printkanban.html.php index c25968ed51..371f13c44b 100644 --- a/module/execution/view/printkanban.html.php +++ b/module/execution/view/printkanban.html.php @@ -3,13 +3,14 @@ * The kanban view file of execution module of ZenTaoPMS. * * @copyright Copyright 2009-2012 青岛易软天创网络科技有限公司 (QingDao Nature Easy Soft Network Technology Co,LTD www.cnezsoft.com) - * @author Wang Yidong, Zhu Jinyong + * @author Wang Yidong, Zhu Jinyong * @package execution * @version $Id: kanban.html.php $ */ ?> +
    diff --git a/module/execution/view/start.html.php b/module/execution/view/start.html.php index 73137af125..8a67e8954b 100644 --- a/module/execution/view/start.html.php +++ b/module/execution/view/start.html.php @@ -5,7 +5,7 @@ * @copyright Copyright 2009-2015 青岛易软天创网络科技有限公司(QingDao Nature Easy Soft Network Technology Co,LTD, www.cnezsoft.com) * @license ZPL (http://zpl.pub/page/zplv12.html) * @author Chunsheng Wang - * @package execution + * @package execution * @version $Id: start.html.php 935 2013-01-16 07:49:24Z wwccss@gmail.com $ * @link http://www.zentao.net */ @@ -26,12 +26,20 @@ printExtendFields($execution, 'table', 'columns=2');?> + + + + - +
    execution->realBegan;?> +
    + realBeganDate) && $execution->realBegan != '0000-00-00' ? $execution->realBegan : date('Y-m-d')), "class='form-control form-date' required");?> +
    +
    comment;?>
    execution->start) . ' ' . html::linkButton($lang->goback, $this->session->taskList, 'self', '', 'btn btn-wide'); ?>execution->start . $lang->executionCommon) . ' ' . html::linkButton($lang->goback, $this->session->taskList, 'self', '', 'btn btn-wide'); ?>
    diff --git a/module/execution/view/story.html.php b/module/execution/view/story.html.php index 5361f817f5..0f9f4c76f9 100644 --- a/module/execution/view/story.html.php +++ b/module/execution/view/story.html.php @@ -220,7 +220,8 @@ pri?>' title='story->priList, $story->pri, $story->pri);?>'>story->priList, $story->pri, $story->pri);?> - product][$story->branch])) echo "" . $branchGroups[$story->product][$story->branch] . '';?> + config->execution->story->showBranch) ? $this->config->execution->story->showBranch : 1;?> + product][$story->branch]) and $showBranch) echo "" . $branchGroups[$story->product][$story->branch] . '';?> module) and isset($modulePairs[$story->module])) echo "{$modulePairs[$story->module]} ";?> parent > 0) echo "{$lang->story->childrenAB}";?> title, null, "style='color: $story->color' data-app='execution'");?> diff --git a/module/execution/view/suspend.html.php b/module/execution/view/suspend.html.php index 1581055615..b061a5e683 100644 --- a/module/execution/view/suspend.html.php +++ b/module/execution/view/suspend.html.php @@ -5,7 +5,7 @@ * @copyright Copyright 2009-2015 青岛易软天创网络科技有限公司(QingDao Nature Easy Soft Network Technology Co,LTD, www.cnezsoft.com) * @license ZPL (http://zpl.pub/page/zplv12.html) * @author Chunsheng Wang - * @package execution + * @package execution * @version $Id: suspend.html.php 935 2013-01-16 07:49:24Z wwccss@gmail.com $ * @link http://www.zentao.net */ @@ -34,11 +34,11 @@ - goback, $this->session->taskList, 'self', '', 'btn btn-wide'); ?> + execution->suspend . $lang->executionCommon) . html::linkButton($lang->goback, $this->session->taskList, 'self', '', 'btn btn-wide'); ?> -
    +
    diff --git a/module/execution/view/task.html.php b/module/execution/view/task.html.php index 6da2ab904f..b8254cc885 100644 --- a/module/execution/view/task.html.php +++ b/module/execution/view/task.html.php @@ -37,7 +37,6 @@ body {margin-bottom: 25px;} product->getById($productID); $removeLink = $browseType == 'byproduct' ? inlink('task', "executionID=$executionID&browseType=$status¶m=0&orderBy=$orderBy&recTotal=0&recPerPage={$pager->recPerPage}") : 'javascript:removeCookieByKey("productBrowseParam")'; $moduleName = $product->name; $html = $moduleName . html::a($removeLink, "", '', "class='text-muted'"); diff --git a/module/execution/view/view.html.php b/module/execution/view/view.html.php index c7d41c996e..ce6b3cf02b 100644 --- a/module/execution/view/view.html.php +++ b/module/execution/view/view.html.php @@ -242,24 +242,30 @@ execution->begin;?> begin;?> - execution->totalEstimate;?> - totalEstimate . $lang->execution->workHour;?> + execution->realBeganAB;?> + realBegan == '0000-00-00' ? '' : $execution->realBegan;?> execution->end;?> end;?> - execution->totalConsumed;?> - totalConsumed . $lang->execution->workHour;?> + execution->realEndAB;?> + realEnd == '0000-00-00' ? '' : $execution->realEnd;?> + execution->totalEstimate;?> + totalEstimate . $lang->execution->workHour;?> execution->totalDays;?> days;?> - execution->totalLeft;?> - totalLeft . $lang->execution->workHour;?> + execution->totalConsumed;?> + totalConsumed . $lang->execution->workHour;?> execution->totalHours;?> - totalHours . $lang->execution->workHour;?> + totalHours . $lang->execution->workHour;?> + + + execution->totalLeft;?> + totalLeft . $lang->execution->workHour;?> diff --git a/module/gitlab/model.php b/module/gitlab/model.php index 3fcdad0671..61aa1fbce0 100644 --- a/module/gitlab/model.php +++ b/module/gitlab/model.php @@ -386,7 +386,7 @@ class gitlabModel extends model if(is_numeric($host)) $host = $this->getApiRoot($host); if(strpos($host, 'http://') !== 0 and strpos($host, 'https://') !== 0) return false; - $url = sprintf($host, $api); + $url = sprintf($apiRoot, $api); return json_decode(commonModel::http($url, $data, $options)); } @@ -474,7 +474,7 @@ class gitlabModel extends model * * @param int $gitlabID * @access public - * @return array + * @return void */ public function apiGetProjects($gitlabID) { @@ -691,7 +691,6 @@ class gitlabModel extends model public function apiUpdateHook($gitlabID, $projectID, $hookID) { $apiRoot = $this->getApiRoot($gitlabID); - $url = sprintf($apiRoot, "/projects/{$projectID}/hooks/{$hookID}"); $postData = new stdclass; $postData->enable_ssl_verification = "false"; @@ -700,6 +699,8 @@ class gitlabModel extends model $postData->push_events = "true"; $postData->tag_push_events = "true"; $postData->note_events = "true"; + $postData->url = $url; + $postData->token = $token; $url = sprintf($apiRoot, "/projects/{$projectID}/hooks/{$hookID}"); return commonModel::http($url, $postData, $options = array(CURLOPT_CUSTOMREQUEST => 'PUT')); @@ -1013,7 +1014,7 @@ class gitlabModel extends model $type = zget($body, 'object_kind', ''); if(!$type or !is_callable(array($this, "webhookParse{$type}"))) return false; // fix php 8.0 bug. link: https://www.php.net/manual/zh/function.call-user-func-array.php#125953 - //return call_user_func_array(array($this, "webhookParse{$type}"), array('body' => $body, $gitlabID)); + return call_user_func_array(array($this, "webhookParse{$type}"), array($body, $gitlabID)); } @@ -1041,7 +1042,7 @@ class gitlabModel extends model $issue->issue->objectID = $object->id; /* Parse markdown description to html. */ - $issue->issue->description = $this->app->loadClass('hyperdown')->makeHtml($issue->issue->description); + $issue->issue->description = commonModel::processMarkdown($issue->issue->description); if(!isset($this->config->gitlab->maps->{$object->type})) return false; $issue->object = $this->issueToZentaoObject($issue->issue, $gitlabID, $body->changes); @@ -1248,9 +1249,9 @@ class gitlabModel extends model /** * Create webhook for zentao. * - * @param array $products - * @param int $gitlabID - * @param int $projectID + * @param int $products + * @param int $gitlabID + * @param int $projectID * @access public * @return bool */ @@ -1363,7 +1364,7 @@ class gitlabModel extends model if($value) $issue->$field = $value; } - if(isset($issue->assignee_id) and $issue->assignee_id == 'closed') unset($issue->assignee_id); + if($isset($issue->assignee_id) and $issue->assignee_id == 'closed') unset($issue->assignee_id); /* issue->state is null when creating it, we should put status_event when updating it. */ if(isset($issue->state) and $issue->state == 'closed') $issue->state_event = 'close'; diff --git a/module/gitlab/view/view.html.php b/module/gitlab/view/view.html.php index b1cb6b07d3..30fc445968 100644 --- a/module/gitlab/view/view.html.php +++ b/module/gitlab/view/view.html.php @@ -27,3 +27,4 @@
    + diff --git a/module/group/lang/resource.php b/module/group/lang/resource.php index 786d2cf8b8..ad2a85f961 100644 --- a/module/group/lang/resource.php +++ b/module/group/lang/resource.php @@ -25,8 +25,11 @@ $lang->moduleOrder[40] = 'release'; $lang->moduleOrder[45] = 'project'; $lang->moduleOrder[50] = 'projectstory'; $lang->moduleOrder[55] = 'execution'; +$lang->moduleOrder[56] = 'kanban'; +$lang->moduleOrder[57] = 'programplan'; $lang->moduleOrder[60] = 'task'; $lang->moduleOrder[65] = 'build'; +$lang->moduleOrder[66] = 'design'; $lang->moduleOrder[70] = 'qa'; $lang->moduleOrder[75] = 'bug'; @@ -46,6 +49,7 @@ $lang->moduleOrder[130] = 'group'; $lang->moduleOrder[135] = 'user'; $lang->moduleOrder[140] = 'admin'; +$lang->moduleOrder[142] = 'stage'; $lang->moduleOrder[145] = 'extension'; $lang->moduleOrder[150] = 'custom'; $lang->moduleOrder[155] = 'action'; @@ -160,6 +164,32 @@ if($config->systemMode == 'new') { $lang->resource->my->project = 'project'; + /* Design. */ + $lang->resource->design = new stdclass(); + $lang->resource->design->browse = 'browse'; + $lang->resource->design->view = 'view'; + $lang->resource->design->create = 'create'; + $lang->resource->design->batchCreate = 'batchCreate'; + $lang->resource->design->edit = 'edit'; + $lang->resource->design->assignTo = 'assignTo'; + $lang->resource->design->delete = 'delete'; + $lang->resource->design->linkCommit = 'linkCommit'; + $lang->resource->design->viewCommit = 'viewCommit'; + $lang->resource->design->unlinkCommit = 'unlinkCommit'; + $lang->resource->design->revision = 'revision'; + + $lang->design->methodOrder[5] = 'browse'; + $lang->design->methodOrder[10] = 'view'; + $lang->design->methodOrder[15] = 'create'; + $lang->design->methodOrder[20] = 'batchCreate'; + $lang->design->methodOrder[25] = 'edit'; + $lang->design->methodOrder[30] = 'assignTo'; + $lang->design->methodOrder[35] = 'delete'; + $lang->design->methodOrder[40] = 'linkCommit'; + $lang->design->methodOrder[45] = 'viewCommit'; + $lang->design->methodOrder[50] = 'unlinkCommit'; + $lang->design->methodOrder[55] = 'revision'; + /* Program. */ $lang->resource->program = new stdclass(); $lang->resource->program->browse = 'browse'; @@ -203,6 +233,16 @@ if($config->systemMode == 'new') $lang->program->methodOrder[95] = 'export'; $lang->program->methodOrder[100] = 'updateOrder'; + /* Program plan. */ + $lang->resource->programplan = new stdclass(); + $lang->resource->programplan->browse = 'browse'; + $lang->resource->programplan->create = 'create'; + $lang->resource->programplan->edit = 'edit'; + + $lang->programplan->methodOrder[5] = 'browse'; + $lang->programplan->methodOrder[10] = 'create'; + $lang->programplan->methodOrder[15] = 'edit'; + /* Project. */ $lang->resource->project = new stdclass(); $lang->resource->project->index = 'index'; @@ -335,6 +375,22 @@ if($config->systemMode == 'new') $lang->projectrelease->methodOrder[65] = 'batchUnlinkBug'; $lang->projectrelease->methodOrder[70] = 'changeStatus'; + /* Stage. */ + $lang->resource->stage = new stdclass(); + $lang->resource->stage->browse = 'browse'; + $lang->resource->stage->create = 'create'; + $lang->resource->stage->batchCreate = 'batchCreate'; + $lang->resource->stage->edit = 'edit'; + $lang->resource->stage->setType = 'setType'; + $lang->resource->stage->delete = 'delete'; + + $lang->stage->methodOrder[5] = 'browse'; + $lang->stage->methodOrder[10] = 'create'; + $lang->stage->methodOrder[15] = 'batchCreate'; + $lang->stage->methodOrder[20] = 'edit'; + $lang->stage->methodOrder[25] = 'setType'; + $lang->stage->methodOrder[30] = 'delete'; + /* Stakeholer. */ $lang->resource->stakeholder = new stdclass(); $lang->resource->stakeholder->browse = 'browse'; @@ -558,6 +614,20 @@ $lang->release->methodOrder[60] = 'unlinkBug'; $lang->release->methodOrder[65] = 'batchUnlinkBug'; $lang->release->methodOrder[70] = 'changeStatus'; +/* Kanban */ +$lang->resource->kanban = new stdclass(); +$lang->resource->kanban->setLane = 'setLane'; +$lang->resource->kanban->setColumn = 'setColumn'; +$lang->resource->kanban->setWIP = 'setWIP'; +$lang->resource->kanban->laneMove = 'laneMove'; +$lang->resource->kanban->cardsSort = 'cardsSort'; + +$lang->kanban->methodOrder[5] = 'setLane'; +$lang->kanban->methodOrder[10] = 'setColumn'; +$lang->kanban->methodOrder[15] = 'setWIP'; +$lang->kanban->methodOrder[20] = 'laneMove'; +$lang->kanban->methodOrder[25] = 'cardsSort'; + /* Execution. */ $lang->resource->execution = new stdclass(); $lang->resource->execution->view = 'view'; @@ -778,6 +848,7 @@ $lang->resource->bug->confirmStoryChange = 'confirmStoryChange'; $lang->resource->bug->delete = 'deleteAction'; $lang->resource->bug->batchChangeModule = 'batchChangeModule'; $lang->resource->bug->batchChangeBranch = 'batchChangeBranch'; +$lang->resource->bug->batchChangePlan = 'batchChangePlan'; $lang->bug->methodOrder[0] = 'index'; $lang->bug->methodOrder[5] = 'browse'; @@ -1303,18 +1374,20 @@ $lang->tree->methodOrder[30] = 'delete'; /* Report. */ $lang->resource->report = new stdclass(); -$lang->resource->report->index = 'index'; -$lang->resource->report->projectDeviation = 'projectDeviation'; -$lang->resource->report->productSummary = 'productSummary'; -$lang->resource->report->bugCreate = 'bugCreate'; -$lang->resource->report->bugAssign = 'bugAssign'; -$lang->resource->report->workload = 'workload'; +$lang->resource->report->index = 'index'; +$lang->resource->report->projectDeviation = 'projectDeviation'; +$lang->resource->report->productSummary = 'productSummary'; +$lang->resource->report->bugCreate = 'bugCreate'; +$lang->resource->report->bugAssign = 'bugAssign'; +$lang->resource->report->workload = 'workload'; +$lang->resource->report->annualData = 'annual'; $lang->report->methodOrder[0] = 'index'; $lang->report->methodOrder[5] = 'projectDeviation'; $lang->report->methodOrder[10] = 'productSummary'; $lang->report->methodOrder[15] = 'bugCreate'; $lang->report->methodOrder[20] = 'workload'; +$lang->report->methodOrder[25] = 'annual'; /* Search. */ $lang->resource->search = new stdclass(); diff --git a/module/holiday/config.php b/module/holiday/config.php new file mode 100644 index 0000000000..f46a285b1e --- /dev/null +++ b/module/holiday/config.php @@ -0,0 +1,5 @@ +holiday)) $config->holiday = new stdclass(); +$config->holiday->require = new stdclass(); +$config->holiday->require->create = 'name,begin,end'; +$config->holiday->require->edit = 'name,begin,end'; diff --git a/module/holiday/control.php b/module/holiday/control.php new file mode 100644 index 0000000000..e4dcf4fc8e --- /dev/null +++ b/module/holiday/control.php @@ -0,0 +1,117 @@ + + * @package holiday + * @version $Id + * @link http://www.zentao.net + */ +class holiday extends control +{ + /** + * Holiday list. + * + * @access public + * @return void + */ + public function index() + { + $this->locate(inlink('browse')); + } + + /** + * Holiday list. + * + * @param string $year + * @access public + * @return void + */ + public function browse($year = '') + { + $holidays = $this->holiday->getList($year); + $yearList = $this->holiday->getYearPairs(); + + $this->view->title = $this->lang->holiday->browse; + $this->view->holidays = $holidays; + $this->view->yearList = $yearList; + $this->view->currentYear = $year; + $this->display(); + } + + /** + * Create a holiday. + * + * @access public + * @return void + */ + public function create() + { + if($_POST) + { + $holidayID = $this->holiday->create(); + if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); + $actionID = $this->loadModel('action')->create('holiday', $holidayID, 'created'); + return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => 'parent')); + } + + $this->view->title = $this->lang->holiday->create; + $this->display(); + } + + /** + * Edit holiday. + * + * @param int $id + * @access public + * @return void + */ + public function edit($id) + { + $holiday = $this->holiday->getById($id); + if($_POST) + { + $this->holiday->update($id); + if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); + return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => 'parent')); + } + + $this->view->title = $this->lang->holiday->edit; + $this->view->holiday = $holiday; + $this->display(); + } + + /** + * Delete holiday. + * + * @param int $id + * @param int $confirm + * @access public + * @return void + */ + public function delete($id, $confirm = 'no') + { + if($confirm == 'no') + { + die(js::confirm($this->lang->holiday->confirmDelete, inLink('delete', "id=$id&confirm=yes"))); + } + else + { + $holidayInformation = $this->dao->select('begin, end')->from(TABLE_HOLIDAY)->where('id')->eq($id)->fetch(); + $this->dao->delete()->from(TABLE_HOLIDAY)->where('id')->eq($id)->exec(); + + /* Update project. */ + $this->holiday->updateProgramPlanDuration($holidayInformation->begin, $holidayInformation->end); + $this->holiday->updateProjectRealDuration($holidayInformation->begin, $holidayInformation->end); + + /* Update task. */ + $this->holiday->updateTaskPlanDuration($holidayInformation->begin, $holidayInformation->end); + $this->holiday->updateTaskRealDuration($holidayInformation->begin, $holidayInformation->end); + + if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); + die(js::reload('parent')); + } + } +} diff --git a/module/holiday/css/browse.css b/module/holiday/css/browse.css new file mode 100644 index 0000000000..50d38310c3 --- /dev/null +++ b/module/holiday/css/browse.css @@ -0,0 +1,3 @@ +.tree .active{font-weight: bold;} +.with-side .side {position: absolute; width: 130px;} +.with-side .main {padding-left: 145px; float: left;} diff --git a/module/holiday/css/index.html b/module/holiday/css/index.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/module/holiday/ext/model/zentaomax.php b/module/holiday/ext/model/zentaomax.php new file mode 100644 index 0000000000..90de78ecd8 --- /dev/null +++ b/module/holiday/ext/model/zentaomax.php @@ -0,0 +1,276 @@ +dao->select('*')->from(TABLE_HOLIDAY) + ->where('type')->eq('holiday') + ->andWhere('begin')->le($end) + ->andWhere('end')->ge($begin) + ->fetchAll('id'); + + $naturalDays = $this->getDaysBetween($begin, $end); + + $holidays = array(); + foreach($records as $record) + { + $dates = $this->getDaysBetween($record->begin, $record->end); + $holidays = array_merge($holidays, $dates); + } + + return array_intersect($naturalDays, $holidays); +} + +public function getWorkingDays($begin = '', $end = '') +{ + $records = $this->dao->select('*')->from(TABLE_HOLIDAY) + ->where('type')->eq('working') + ->andWhere('begin')->le($end) + ->andWhere('end')->ge($begin) + ->fetchAll('id'); + + $workingDays = array(); + foreach($records as $record) + { + $dates = $this->getDaysBetween($record->begin, $record->end); + $workingDays = array_merge($workingDays, $dates); + } + return $workingDays; +} + +public function getActualWorkingDays($begin, $end) +{ + if(empty($begin) or empty($end) or $begin == '0000-00-00' or $end == '0000-00-00') return array(); + + $actualDays = array(); + $currentDay = $begin; + + $holidays = $this->getHolidays($begin, $end); + $workingDays = $this->getWorkingDays($begin, $end); + $weekend = isset($this->config->project->weekend) ? $this->config->project->weekend : 2; + + /* When the start date and end date are the same. */ + if($begin == $end) + { + if(in_array($begin, $workingDays)) return $actualDays[] = $begin; + if(in_array($begin, $holidays)) return $actualDays; + + $w = date('w', strtotime($begin)); + if($weekend == 2) + { + if($w == 0 or $w == 6) return $actualDays; + } + else + { + if($w == 0) return $actualDays; + } + + $actualDays[] = $begin; + return $actualDays; + } + + for($i = 0; $currentDay < $end; $i ++) + { + $currentDay = date('Y-m-d', strtotime("$begin + $i days")); + $w = date('w', strtotime($currentDay)); + + if(in_array($currentDay, $workingDays)) + { + $actualDays[] = $currentDay; + continue; + } + + if(in_array($currentDay, $holidays)) continue; + if($weekend == 2) + { + if($w == 0 or $w == 6) continue; + } + else + { + if($w == 0) continue; + } + $actualDays[] = $currentDay; + } + + return $actualDays; +} + +public function getDaysBetween($begin, $end) +{ + $beginTime = strtotime($begin); + $endTime = strtotime($end); + $days = ($endTime - $beginTime) / 86400; + + $dateList = array(); + for($i = 0; $i <= $days; $i ++) $dateList[] = date('Y-m-d', strtotime("+$i days", $beginTime)); + + return $dateList; +} + +public function isHoliday($date) +{ + $record = $this->dao->select('*')->from(TABLE_HOLIDAY) + ->where('type')->eq('holiday') + ->andWhere('begin')->le($date) + ->andWhere('end')->ge($date) + ->fetch(); + return !empty($record); +} + +public function isWorkingDay($date) +{ + $record = $this->dao->select('*')->from(TABLE_HOLIDAY) + ->where('type')->eq('working') + ->andWhere('begin')->le($date) + ->andWhere('end')->ge($date) + ->fetch(); + return !empty($record); +} + +public function update($id) +{ + $result = parent::update($id); + + if($result) + { + $beginDate = $this->post->begin; + $endDate = $this->post->end; + + /* Update project. */ + $this->updateProgramPlanDuration($beginDate, $endDate); + $this->updateProjectRealDuration($beginDate, $endDate); + + /* Update task. */ + $this->updateTaskPlanDuration($beginDate, $endDate); + $this->updateTaskRealDuration($beginDate, $endDate); + } + + return $result; +} + +public function create() +{ + $lastInsertID = parent::create(); + + if($lastInsertID) + { + $beginDate = $this->post->begin; + $endDate = $this->post->end; + + /* Update project. */ + $this->updateProgramPlanDuration($beginDate, $endDate); + $this->updateProjectRealDuration($beginDate, $endDate); + + /* Update task. */ + $this->updateTaskPlanDuration($beginDate, $endDate); + $this->updateTaskRealDuration($beginDate, $endDate); + } + + return $lastInsertID; +} + +public function delete($id, $null = null) +{ + $holidayInformation = $this->dao->select('begin,end')->from(TABLE_HOLIDAY)->where('id')->eq($id)->fetch(); + + $result = parent::delete($id, $null = null); + if($result) + { + /* Update project. */ + $this->updateProgramPlanDuration($holidayInformation->begin, $holidayInformation->end); + $this->updateProjectRealDuration($holidayInformation->begin, $holidayInformation->end); + + /* Update task. */ + $this->updateTaskPlanDuration($holidayInformation->begin, $holidayInformation->end); + $this->updateTaskRealDuration($holidayInformation->begin, $holidayInformation->end); + } + + return $result; +} + +public function updateProgramPlanDuration($beginDate, $endDate) +{ + $updateProjectList = $this->dao->select('id, begin, end') + ->from(TABLE_PROJECT) + ->where('begin')->between($beginDate, $endDate) + ->orWhere('end')->between($beginDate, $endDate) + ->orWhere("(begin < '$beginDate' AND end > '$endDate')") + ->andWhere('status')->ne('done') + ->fetchAll(); + + foreach($updateProjectList as $project) + { + $realDuration = $this->getActualWorkingDays($project->begin, $project->end); + $realDuration = count($realDuration); + + $this->dao->update(TABLE_PROJECT) + ->set('planDuration')->eq($realDuration) + ->where('id')->eq($project->id) + ->exec(); + } +} + +public function updateProjectRealDuration($beginDate, $endDate) +{ + $updateProjectList = $this->dao->select('id, realBegan, realEnd') + ->from(TABLE_PROJECT) + ->where('realBegan')->between($beginDate, $endDate) + ->orWhere('realEnd')->between($beginDate, $endDate) + ->orWhere("(realBegan < '$beginDate' AND realEnd > '$endDate')") + ->andWhere('status')->ne('done') + ->fetchAll(); + + foreach($updateProjectList as $project) + { + $realDuration = $this->getActualWorkingDays($project->realBegan, $project->realEnd); + $realDuration = count($realDuration); + + $this->dao->update(TABLE_PROJECT) + ->set('realDuration')->eq($realDuration) + ->where('id')->eq($project->id) + ->exec(); + } +} + +public function updateTaskPlanDuration($beginDate, $endDate) +{ + $updateTaskList = $this->dao->select('id, estStarted, deadline') + ->from(TABLE_TASK) + ->where('estStarted')->between($beginDate, $endDate) + ->orWhere('deadline')->between($beginDate, $endDate) + ->orWhere("(estStarted < '$beginDate' AND deadline > '$endDate')") + ->andWhere('status') ->ne('done') + ->fetchAll(); + + foreach($updateTaskList as $task) + { + $planduration = $this->getActualWorkingDays($task->estStarted, $task->deadline); + $planduration = count($planduration); + + $this->dao->update(TABLE_TASK) + ->set('planduration')->eq($planduration) + ->where('id')->eq($task->id) + ->exec(); + } + +} + +public function updateTaskRealDuration($beginDate, $endDate) +{ + $updateTaskList = $this->dao->select('id, realStarted, finishedDate') + ->from(TABLE_TASK) + ->where('realStarted')->between($beginDate, $endDate) + ->orWhere("date_format(finishedDate,'%Y-%m-%d')")->between($beginDate, $endDate) + ->orWhere("(realStarted < '$beginDate' AND date_format(finishedDate,'%Y-%m-%d') > '$endDate')") + ->andWhere('status')->ne('done') + ->fetchAll(); + + foreach($updateTaskList as $task) + { + $realDuration = $this->getActualWorkingDays($task->realBegan, date('Y-m-d',strtotime($task->finishedDate))); + $realDuration = count($realDuration); + + $this->dao->update(TABLE_TASK) + ->set('realDuration')->eq($realDuration) + ->where('id')->eq($task->id) + ->exec(); + } +} diff --git a/module/holiday/lang/de.php b/module/holiday/lang/de.php new file mode 100644 index 0000000000..c922267c1c --- /dev/null +++ b/module/holiday/lang/de.php @@ -0,0 +1,27 @@ +holiday)) $lang->holiday = new stdclass(); +$lang->holiday->common = 'Holiday'; +$lang->holiday->browse = 'Browse'; +$lang->holiday->create = 'Create'; +$lang->holiday->edit = 'Edit'; +$lang->holiday->delete = 'Delete'; + +$lang->holiday->createAction = 'Create Holiday'; +$lang->holiday->editAction = 'Edit Holiday'; +$lang->holiday->deleteAction = 'Delete Holiday'; + +$lang->holiday->id = 'ID'; +$lang->holiday->name = 'Name'; +$lang->holiday->desc = 'Description'; +$lang->holiday->type = 'Type'; +$lang->holiday->begin = 'Begin'; +$lang->holiday->end = 'End'; +$lang->holiday->all = 'All'; + +$lang->holiday->holiday = 'Holiday'; + +$lang->holiday->typeList['holiday'] = 'Holiday'; +$lang->holiday->typeList['working'] = 'Working Day'; + +$lang->holiday->emptyTip = 'No Holiday'; +$lang->holiday->confirmDelete = 'Confirm removal of holidays?'; diff --git a/module/holiday/lang/en.php b/module/holiday/lang/en.php new file mode 100644 index 0000000000..c922267c1c --- /dev/null +++ b/module/holiday/lang/en.php @@ -0,0 +1,27 @@ +holiday)) $lang->holiday = new stdclass(); +$lang->holiday->common = 'Holiday'; +$lang->holiday->browse = 'Browse'; +$lang->holiday->create = 'Create'; +$lang->holiday->edit = 'Edit'; +$lang->holiday->delete = 'Delete'; + +$lang->holiday->createAction = 'Create Holiday'; +$lang->holiday->editAction = 'Edit Holiday'; +$lang->holiday->deleteAction = 'Delete Holiday'; + +$lang->holiday->id = 'ID'; +$lang->holiday->name = 'Name'; +$lang->holiday->desc = 'Description'; +$lang->holiday->type = 'Type'; +$lang->holiday->begin = 'Begin'; +$lang->holiday->end = 'End'; +$lang->holiday->all = 'All'; + +$lang->holiday->holiday = 'Holiday'; + +$lang->holiday->typeList['holiday'] = 'Holiday'; +$lang->holiday->typeList['working'] = 'Working Day'; + +$lang->holiday->emptyTip = 'No Holiday'; +$lang->holiday->confirmDelete = 'Confirm removal of holidays?'; diff --git a/module/holiday/lang/fr.php b/module/holiday/lang/fr.php new file mode 100644 index 0000000000..c922267c1c --- /dev/null +++ b/module/holiday/lang/fr.php @@ -0,0 +1,27 @@ +holiday)) $lang->holiday = new stdclass(); +$lang->holiday->common = 'Holiday'; +$lang->holiday->browse = 'Browse'; +$lang->holiday->create = 'Create'; +$lang->holiday->edit = 'Edit'; +$lang->holiday->delete = 'Delete'; + +$lang->holiday->createAction = 'Create Holiday'; +$lang->holiday->editAction = 'Edit Holiday'; +$lang->holiday->deleteAction = 'Delete Holiday'; + +$lang->holiday->id = 'ID'; +$lang->holiday->name = 'Name'; +$lang->holiday->desc = 'Description'; +$lang->holiday->type = 'Type'; +$lang->holiday->begin = 'Begin'; +$lang->holiday->end = 'End'; +$lang->holiday->all = 'All'; + +$lang->holiday->holiday = 'Holiday'; + +$lang->holiday->typeList['holiday'] = 'Holiday'; +$lang->holiday->typeList['working'] = 'Working Day'; + +$lang->holiday->emptyTip = 'No Holiday'; +$lang->holiday->confirmDelete = 'Confirm removal of holidays?'; diff --git a/module/holiday/lang/index.html b/module/holiday/lang/index.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/module/holiday/lang/vi.php b/module/holiday/lang/vi.php new file mode 100644 index 0000000000..a1560a635d --- /dev/null +++ b/module/holiday/lang/vi.php @@ -0,0 +1,27 @@ +holiday)) $lang->holiday = new stdclass(); +$lang->holiday->common = 'Ngày lễ'; +$lang->holiday->browse = 'Browse'; +$lang->holiday->create = 'Tạo'; +$lang->holiday->edit = 'Sửa'; +$lang->holiday->delete = 'Xóa'; + +$lang->holiday->createAction = 'Create Holiday'; +$lang->holiday->editAction = 'Edit Holiday'; +$lang->holiday->deleteAction = 'Delete Holiday'; + +$lang->holiday->id = 'ID'; +$lang->holiday->name = 'Tên'; +$lang->holiday->desc = 'Mô tả'; +$lang->holiday->type = 'Loại'; +$lang->holiday->begin = 'Bắt đầu'; +$lang->holiday->end = 'Kết thúc'; +$lang->holiday->all = 'All'; + +$lang->holiday->holiday = 'Ngày lễ'; + +$lang->holiday->typeList['holiday'] = 'Ngày lễ'; +$lang->holiday->typeList['working'] = 'Ngày làm việc'; + +$lang->holiday->emptyTip = 'No Holiday'; +$lang->holiday->confirmDelete = 'Confirm removal of holidays?'; diff --git a/module/holiday/lang/zh-cn.php b/module/holiday/lang/zh-cn.php new file mode 100644 index 0000000000..5f38ebbcda --- /dev/null +++ b/module/holiday/lang/zh-cn.php @@ -0,0 +1,27 @@ +holiday)) $lang->holiday = new stdclass(); +$lang->holiday->common = '节假日'; +$lang->holiday->browse = '浏览'; +$lang->holiday->create = '新建'; +$lang->holiday->edit = '编辑'; +$lang->holiday->delete = '删除'; + +$lang->holiday->createAction = '创建节假日'; +$lang->holiday->editAction = '编辑节假日'; +$lang->holiday->deleteAction = '删除节假日'; + +$lang->holiday->id = '编号'; +$lang->holiday->name = '名称'; +$lang->holiday->desc = '描述'; +$lang->holiday->type = '类型'; +$lang->holiday->begin = '开始日期'; +$lang->holiday->end = '结束日期'; +$lang->holiday->all = '所有'; + +$lang->holiday->holiday = '假期'; + +$lang->holiday->typeList['holiday'] = '假期'; +$lang->holiday->typeList['working'] = '补班'; + +$lang->holiday->emptyTip = '暂时没有节假日。'; +$lang->holiday->confirmDelete = '确认删除节假日?'; diff --git a/module/holiday/lang/zh-tw.php b/module/holiday/lang/zh-tw.php new file mode 100644 index 0000000000..43e0cd41ab --- /dev/null +++ b/module/holiday/lang/zh-tw.php @@ -0,0 +1,27 @@ +holiday)) $lang->holiday = new stdclass(); +$lang->holiday->common = '節假日'; +$lang->holiday->browse = '瀏覽'; +$lang->holiday->create = '新建'; +$lang->holiday->edit = '編輯'; +$lang->holiday->delete = '刪除'; + +$lang->holiday->createAction = '創建節假日'; +$lang->holiday->editAction = '編輯節假日'; +$lang->holiday->deleteAction = '刪除節假日'; + +$lang->holiday->id = '編號'; +$lang->holiday->name = '名稱'; +$lang->holiday->desc = '描述'; +$lang->holiday->type = '類型'; +$lang->holiday->begin = '開始日期'; +$lang->holiday->end = '結束日期'; +$lang->holiday->all = '所有'; + +$lang->holiday->holiday = '假期'; + +$lang->holiday->typeList['holiday'] = '假期'; +$lang->holiday->typeList['working'] = '補班'; + +$lang->holiday->emptyTip = '暫時沒有節假日。'; +$lang->holiday->confirmDelete = '確認刪除節假日?'; diff --git a/module/holiday/model.php b/module/holiday/model.php new file mode 100644 index 0000000000..004dcc41e7 --- /dev/null +++ b/module/holiday/model.php @@ -0,0 +1,410 @@ + + * @package holiday + * @version $Id + * @link http://www.zentao.net + */ +class holidayModel extends model +{ + /** + * Get holiday by id. + * + * @param int $id + * @access public + * @return object + */ + public function getById($id) + { + return $this->dao->select('*')->from(TABLE_HOLIDAY)->where('id')->eq($id)->fetch(); + } + + /** + * Get holiday list. + * + * @param string $year + * @param string $type + * @access public + * @return object + */ + public function getList($year = '', $type = 'all') + { + return $this->dao->select('*')->from(TABLE_HOLIDAY) + ->where('1') + ->beginIf(!empty($year)) + ->andWhere('year', true)->eq($year) + ->orWhere('begin')->like("$year-%") + ->orWhere('end')->like("$year-%") + ->markright(1) + ->fi() + ->beginIf($type != 'all' && $type)->andWhere('type')->eq($type)->fi() + ->fetchAll('id'); + } + + /** + * Get year pairs. + * + * @access public + * @return array + */ + public function getYearPairs() + { + return $this->dao->select('year,year')->from(TABLE_HOLIDAY)->groupBy('year')->orderBy('year_desc')->fetchPairs(); + } + + /** + * Create a holiday. + * + * @access public + * @return int + */ + public function create() + { + $holiday = fixer::input('post')->get(); + $holiday->year = substr($holiday->begin, 0, 4); + if(helper::isZeroDate($holiday->year)) return dao::$errors['begin'][] = sprintf($this->lang->error->date, $this->lang->holiday->begin); + if(helper::isZeroDate($holiday->end)) return dao::$errors['end'][] = sprintf($this->lang->error->date, $this->lang->holiday->end); + + $this->dao->insert(TABLE_HOLIDAY)->data($holiday) + ->autoCheck() + ->batchCheck($this->config->holiday->require->create, 'notempty') + ->check('end', 'ge', $holiday->begin) + ->exec(); + if(dao::isError()) return false; + + $beginDate = $this->post->begin; + $endDate = $this->post->end; + + /* Update project. */ + $this->updateProgramPlanDuration($beginDate, $endDate); + $this->updateProjectRealDuration($beginDate, $endDate); + + /* Update task. */ + $this->updateTaskPlanDuration($beginDate, $endDate); + $this->updateTaskRealDuration($beginDate, $endDate); + + return $this->dao->lastInsertID(); + } + + /** + * Edit holiday. + * + * @param int $id + * @access public + * @return bool + */ + public function update($id) + { + $holiday = fixer::input('post')->get(); + $holiday->year = substr($holiday->begin, 0, 4); + if(helper::isZeroDate($holiday->year)) return dao::$errors['begin'][] = sprintf($this->lang->error->date, $this->lang->holiday->begin); + if(helper::isZeroDate($holiday->end)) return dao::$errors['end'][] = sprintf($this->lang->error->date, $this->lang->holiday->end); + + $this->dao->update(TABLE_HOLIDAY) + ->data($holiday) + ->autoCheck() + ->batchCheck($this->config->holiday->require->edit, 'notempty') + ->check('end', 'ge', $holiday->begin) + ->where('id')->eq($id) + ->exec(); + + if(!dao::isError()) + { + $beginDate = $this->post->begin; + $endDate = $this->post->end; + + /* Update project. */ + $this->updateProgramPlanDuration($beginDate, $endDate); + $this->updateProjectRealDuration($beginDate, $endDate); + + /* Update task. */ + $this->updateTaskPlanDuration($beginDate, $endDate); + $this->updateTaskRealDuration($beginDate, $endDate); + } + return !dao::isError(); + } + + /** + * Get holidays by begin and end. + * + * @param string $begin + * @param string $end + * @access public + * @return array + */ + public function getHolidays($begin, $end) + { + $records = $this->dao->select('*')->from(TABLE_HOLIDAY) + ->where('type')->eq('holiday') + ->andWhere('begin')->le($end) + ->andWhere('end')->ge($begin) + ->fetchAll('id'); + + $naturalDays = $this->getDaysBetween($begin, $end); + + $holidays = array(); + foreach($records as $record) + { + $dates = $this->getDaysBetween($record->begin, $record->end); + $holidays = array_merge($holidays, $dates); + } + + return array_intersect($naturalDays, $holidays); + } + + /** + * Get working days. + * + * @param string $begin + * @param string $end + * @access public + * @return array + */ + public function getWorkingDays($begin = '', $end = '') + { + $records = $this->dao->select('*')->from(TABLE_HOLIDAY) + ->where('type')->eq('working') + ->andWhere('begin')->le($end) + ->andWhere('end')->ge($begin) + ->fetchAll('id'); + + $workingDays = array(); + foreach($records as $record) + { + $dates = $this->getDaysBetween($record->begin, $record->end); + $workingDays = array_merge($workingDays, $dates); + } + return $workingDays; + } + + /** + * Get actual working days. + * + * @param string $begin + * @param string $end + * @access public + * @return array + */ + public function getActualWorkingDays($begin, $end) + { + if(empty($begin) or empty($end) or $begin == '0000-00-00' or $end == '0000-00-00') return array(); + + $actualDays = array(); + $currentDay = $begin; + + $holidays = $this->getHolidays($begin, $end); + $workingDays = $this->getWorkingDays($begin, $end); + $weekend = isset($this->config->project->weekend) ? $this->config->project->weekend : 2; + + /* When the start date and end date are the same. */ + if($begin == $end) + { + if(in_array($begin, $workingDays)) return $actualDays[] = $begin; + if(in_array($begin, $holidays)) return $actualDays; + + $w = date('w', strtotime($begin)); + if($weekend == 2) + { + if($w == 0 or $w == 6) return $actualDays; + } + else + { + if($w == 0) return $actualDays; + } + + $actualDays[] = $begin; + return $actualDays; + } + + for($i = 0; $currentDay < $end; $i ++) + { + $currentDay = date('Y-m-d', strtotime("$begin + $i days")); + $w = date('w', strtotime($currentDay)); + + if(in_array($currentDay, $workingDays)) + { + $actualDays[] = $currentDay; + continue; + } + + if(in_array($currentDay, $holidays)) continue; + if($weekend == 2) + { + if($w == 0 or $w == 6) continue; + } + else + { + if($w == 0) continue; + } + $actualDays[] = $currentDay; + } + + return $actualDays; + } + + /** + * Get diff days. + * + * @param varchar $begin + * @param varchar $end + * @access public + * @return bool + */ + public function getDaysBetween($begin, $end) + { + $beginTime = strtotime($begin); + $endTime = strtotime($end); + $days = ($endTime - $beginTime) / 86400; + + $dateList = array(); + for($i = 0; $i <= $days; $i ++) $dateList[] = date('Y-m-d', strtotime("+$i days", $beginTime)); + + return $dateList; + } + + /** + * Judge if is holiday. + * + * @param string $date + * @access public + * @return bool + */ + public function isHoliday($date) + { + $record = $this->dao->select('*')->from(TABLE_HOLIDAY) + ->where('type')->eq('holiday') + ->andWhere('begin')->le($date) + ->andWhere('end')->ge($date) + ->fetch(); + return !empty($record); + } + + /** + * Judge if is working days. + * + * @param string $date + * @access public + * @return bool + */ + public function isWorkingDay($date) + { + $record = $this->dao->select('*')->from(TABLE_HOLIDAY) + ->where('type')->eq('working') + ->andWhere('begin')->le($date) + ->andWhere('end')->ge($date) + ->fetch(); + return !empty($record); + } + + /** + * Update project plan duration. + * + * @param string $beginDate + * @param string $endDate + * @access public + * @return void + */ + public function updateProgramPlanDuration($beginDate, $endDate) + { + $updateProjectList = $this->dao->select('id, begin, end') + ->from(TABLE_PROJECT) + ->where('begin')->between($beginDate, $endDate) + ->orWhere('end')->between($beginDate, $endDate) + ->orWhere("(begin < '$beginDate' AND end > '$endDate')") + ->andWhere('status')->ne('done') + ->fetchAll(); + + foreach($updateProjectList as $project) + { + $realDuration = $this->getActualWorkingDays($project->begin, $project->end); + $realDuration = count($realDuration); + + $this->dao->update(TABLE_PROJECT)->set('planDuration')->eq($realDuration)->where('id')->eq($project->id)->exec(); + } + } + + /** + * Update project real duration. + * + * @param string $beginDate + * @param string $endDate + * @access public + * @return void + */ + public function updateProjectRealDuration($beginDate, $endDate) + { + $updateProjectList = $this->dao->select('id, realBegan, realEnd') + ->from(TABLE_PROJECT) + ->where('realBegan')->between($beginDate, $endDate) + ->orWhere('realEnd')->between($beginDate, $endDate) + ->orWhere("(realBegan < '$beginDate' AND realEnd > '$endDate')") + ->andWhere('status')->ne('done') + ->fetchAll(); + + foreach($updateProjectList as $project) + { + $realDuration = $this->getActualWorkingDays($project->realBegan, $project->realEnd); + $realDuration = count($realDuration); + + $this->dao->update(TABLE_PROJECT)->set('realDuration')->eq($realDuration)->where('id')->eq($project->id)->exec(); + } + } + + /** + * Update task plan duration. + * + * @param string $beginDate + * @param string $endDate + * @access public + * @return void + */ + public function updateTaskPlanDuration($beginDate, $endDate) + { + $updateTaskList = $this->dao->select('id, estStarted, deadline') + ->from(TABLE_TASK) + ->where('estStarted')->between($beginDate, $endDate) + ->orWhere('deadline')->between($beginDate, $endDate) + ->orWhere("(estStarted < '$beginDate' AND deadline > '$endDate')") + ->andWhere('status') ->ne('done') + ->fetchAll(); + + foreach($updateTaskList as $task) + { + $planduration = $this->getActualWorkingDays($task->estStarted, $task->deadline); + $planduration = count($planduration); + + $this->dao->update(TABLE_TASK)->set('planduration')->eq($planduration)->where('id')->eq($task->id)->exec(); + } + } + + /** + * Update task real duration. + * + * @param string $beginDate + * @param string $endDate + * @access public + * @return void + */ + public function updateTaskRealDuration($beginDate, $endDate) + { + $updateTaskList = $this->dao->select('id, realStarted, finishedDate') + ->from(TABLE_TASK) + ->where('realStarted')->between($beginDate, $endDate) + ->orWhere("date_format(finishedDate,'%Y-%m-%d')")->between($beginDate, $endDate) + ->orWhere("(realStarted < '$beginDate' AND date_format(finishedDate,'%Y-%m-%d') > '$endDate')") + ->andWhere('status')->ne('done') + ->fetchAll(); + + foreach($updateTaskList as $task) + { + $realDuration = $this->getActualWorkingDays($task->realStarted, date('Y-m-d',strtotime($task->finishedDate))); + $realDuration = count($realDuration); + + $this->dao->update(TABLE_TASK)->set('realDuration')->eq($realDuration)->where('id')->eq($task->id)->exec(); + } + } +} \ No newline at end of file diff --git a/module/holiday/view/browse.html.php b/module/holiday/view/browse.html.php new file mode 100644 index 0000000000..9f59722738 --- /dev/null +++ b/module/holiday/view/browse.html.php @@ -0,0 +1,72 @@ + + * @package holiday + * @version $Id$ + * @link https://www.zentao.net + */ +?> + + +
    +
    +
    +
    +
      + +
    • '> + +
    • + +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + +
    holiday->name;?>holiday->holiday;?>holiday->type;?>holiday->desc;?>actions;?>
    name;?>begin, DT_DATE1) . ' ~ ' . formatTime($holiday->end, DT_DATE1);?>holiday->typeList, $holiday->type);?>desc;?> + id", $holiday, 'list', '', '', 'iframe', 'yes');?> + id", $holiday, 'list', '', 'hiddenwin');?> +
    + +
    +

    + holiday->emptyTip;?> +

    +
    + +
    +
    + diff --git a/module/holiday/view/create.html.php b/module/holiday/view/create.html.php new file mode 100644 index 0000000000..f764d14933 --- /dev/null +++ b/module/holiday/view/create.html.php @@ -0,0 +1,54 @@ + + * @package holiday + * @version $Id$ + * @link https://www.zentao.net + */ +?> + + +
    +
    +
    +

    + holiday->create;?> +

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    holiday->type;?>holiday->typeList, 'holiday');?>
    holiday->begin;?>
    holiday->end;?>
    holiday->name;?>
    holiday->desc?>
    +
    +
    +
    + diff --git a/module/holiday/view/edit.html.php b/module/holiday/view/edit.html.php new file mode 100644 index 0000000000..bcb7e739f6 --- /dev/null +++ b/module/holiday/view/edit.html.php @@ -0,0 +1,54 @@ + + * @package holiday + * @version $Id$ + * @link https://www.zentao.net + */ +?> + + +
    +
    +
    +

    + holiday->edit;?> +

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    holiday->type;?>holiday->typeList, $holiday->type);?>
    holiday->begin?>begin, "class='form-control form-date' required")?>
    holiday->end?>end, "class='form-control form-date' required")?>
    holiday->name?>name, "class='form-control' required")?>
    holiday->desc?>desc, "class='form-control'")?>
    +
    +
    +
    + diff --git a/module/holiday/view/index.html b/module/holiday/view/index.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/module/index/js/index.js b/module/index/js/index.js index af88f8f157..b78e3700f3 100644 --- a/module/index/js/index.js +++ b/module/index/js/index.js @@ -121,6 +121,11 @@ { return (link.params.from || link.params.$3) == 'project' ? 'project' : 'execution'; } + if(moduleName === 'issue' || moduleName === 'risk' || moduleName === 'opportunity' || moduleName === 'pssp' || moduleName === 'auditplan' || moduleName === 'meeting' || moduleName === 'nc') + { + if(link.params.$2 == 'project') return 'project'; + if(link.params.$2 == 'execution') return 'execution'; + } if(moduleName === 'product') { if(methodLowerCase === 'create' && (link.params.programID || link.params.$1)) return 'program'; @@ -148,7 +153,7 @@ if(methodLowerCase === 'browse') { var viewType = link.params.view || link.params.$2; - if(['bug', 'case', 'caselib'].includes(viewType)) return link.params.from === 'project' ? 'project' : 'qa'; + if(['bug', 'case', 'caselib'].includes(viewType)) return link.params.$5 === 'project' ? 'project' : 'qa'; if(viewType === 'doc' && (link.params.from === 'product' || link.params.$5 == 'product')) return 'product'; if(viewType === 'doc' && (link.params.from === 'project' || link.params.$5 == 'project')) return 'project'; @@ -859,3 +864,4 @@ function getLatestVersion() $('#globalSearchInput').click(); $('#upgradeContent').toggle(); } + diff --git a/module/job/control.php b/module/job/control.php index b17d81e552..a206bac9f7 100644 --- a/module/job/control.php +++ b/module/job/control.php @@ -120,7 +120,7 @@ class job extends control $this->view->repoTypes = $repoTypes; $this->view->products = array(0 => '') + $this->loadModel('product')->getProductPairsByProject($this->projectID); - $this->view->jenkinsServerList = array('' => '') + $this->loadModel('jenkins')->getPairs(); + $this->view->jenkinsServerList = $this->loadModel('jenkins')->getPairs(); $this->display(); } @@ -352,7 +352,7 @@ class job extends control if($productLeft == $productRight) $matchedProducts[$productName] = $productRight; } } - die(json_encode($matchedProducts)); + die(json_encode($matchedProduct)); } $productName = $this->loadModel('product')->getByID($repo->product)->name; diff --git a/module/job/model.php b/module/job/model.php index ecd520b333..c9344e89a4 100644 --- a/module/job/model.php +++ b/module/job/model.php @@ -364,7 +364,7 @@ class jobModel extends model if($job->triggerType == 'tag') { - $lastTag = $this->getLastTagByRepo($repo, $job); + $lastTag = $this->getLastTagByRepo($repo); if($lastTag) { $build->tag = $lastTag; @@ -430,7 +430,7 @@ class jobModel extends model /** * Exec gitlab pipeline. * - * @param object $job + * @param int $job * @access public * @return void */ @@ -473,11 +473,10 @@ class jobModel extends model * Get last tag of one repo. * * @param object $repo - * @param object $job * @access public * @return void */ - public function getLastTagByRepo($repo, $job) + public function getLastTagByRepo($repo) { if($repo->SCM == 'Subversion') { diff --git a/module/kanban/config.php b/module/kanban/config.php new file mode 100644 index 0000000000..f077fab4ef --- /dev/null +++ b/module/kanban/config.php @@ -0,0 +1,67 @@ +kanban = new stdclass(); + +$config->kanban->setwip = new stdclass(); +$config->kanban->setlane = new stdclass(); +$config->kanban->setlaneColumn = new stdclass(); +$config->kanban->setwip->requiredFields = 'limit'; +$config->kanban->setlane->requiredFields = 'name,type'; +$config->kanban->setlaneColumn->requiredFields = 'name'; + +$config->kanban->default = new stdclass(); +$config->kanban->default->story = new stdclass(); +$config->kanban->default->story->name = $lang->SRCommon; +$config->kanban->default->story->color = '#7ec5ff'; +$config->kanban->default->story->order = '5'; + +$config->kanban->default->bug = new stdclass(); +$config->kanban->default->bug->name = $lang->bug->common; +$config->kanban->default->bug->color = '#ba55d3'; +$config->kanban->default->bug->order = '10'; + +$config->kanban->default->task = new stdclass(); +$config->kanban->default->task->name = $lang->task->common; +$config->kanban->default->task->color = '#4169e1'; +$config->kanban->default->task->order = '15'; + +$config->kanban->storyColumnStageList = array(); +$config->kanban->storyColumnStageList['backlog'] = 'projected'; +$config->kanban->storyColumnStageList['ready'] = 'projected'; +$config->kanban->storyColumnStageList['developing'] = 'developing'; +$config->kanban->storyColumnStageList['developed'] = 'developed'; +$config->kanban->storyColumnStageList['testing'] = 'testing'; +$config->kanban->storyColumnStageList['tested'] = 'tested'; +$config->kanban->storyColumnStageList['verified'] = 'verified'; +$config->kanban->storyColumnStageList['released'] = 'released'; +$config->kanban->storyColumnStageList['closed'] = 'closed'; + +$config->kanban->storyColumnStatusList = array(); +$config->kanban->storyColumnStatusList['backlog'] = 'active'; +$config->kanban->storyColumnStatusList['ready'] = 'active'; +$config->kanban->storyColumnStatusList['developing'] = 'active'; +$config->kanban->storyColumnStatusList['developed'] = 'active'; +$config->kanban->storyColumnStatusList['testing'] = 'active'; +$config->kanban->storyColumnStatusList['tested'] = 'active'; +$config->kanban->storyColumnStatusList['verified'] = 'active'; +$config->kanban->storyColumnStatusList['released'] = 'active'; +$config->kanban->storyColumnStatusList['closed'] = 'closed'; + +$config->kanban->bugColumnStatusList = array(); +$config->kanban->bugColumnStatusList['unconfirmed'] = 'active'; +$config->kanban->bugColumnStatusList['confirmed'] = 'active'; +$config->kanban->bugColumnStatusList['fixing'] = 'active'; +$config->kanban->bugColumnStatusList['fixed'] = 'resolved'; +$config->kanban->bugColumnStatusList['testing'] = 'resolved'; +$config->kanban->bugColumnStatusList['tested'] = 'resolved'; +$config->kanban->bugColumnStatusList['closed'] = 'closed'; + +$config->kanban->taskColumnStatusList = array(); +$config->kanban->taskColumnStatusList['wait'] = 'wait'; +$config->kanban->taskColumnStatusList['developing'] = 'doing'; +$config->kanban->taskColumnStatusList['developed'] = 'done'; +$config->kanban->taskColumnStatusList['pause'] = 'pause'; +$config->kanban->taskColumnStatusList['canceled'] = 'cancel'; +$config->kanban->taskColumnStatusList['closed'] = 'closed'; + +$config->kanban->laneColorList = array('#7ec5ff', '#333', '#2b529c', '#e48600', '#d2323d', '#229f24', '#777', '#d2691e', '#008b8b', '#2e8b57', '#4169e1', '#4b0082', '#fa8072', '#ba55d3', '#2e8b57', '#6b8e23'); diff --git a/module/kanban/control.php b/module/kanban/control.php new file mode 100644 index 0000000000..73f7a3ccfd --- /dev/null +++ b/module/kanban/control.php @@ -0,0 +1,180 @@ + + * @package kanban + * @version $Id: control.php 4460 2021-10-26 11:03:02Z chencongzhi520@gmail.com $ + * @link https://www.zentao.net + */ +class kanban extends control +{ + /** + * Set WIP. + * + * @param int $columnID + * @param int $executionID + * @access public + * @return void + */ + public function setWIP($columnID, $executionID = 0) + { + if($_POST) + { + $this->kanban->setWIP($columnID); + if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); + + $this->loadModel('action')->create('kanbancolumn', $columnID, 'Edited', '', $executionID); + return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => 'parent')); + } + + $this->app->loadLang('story'); + + $column = $this->kanban->getColumnById($columnID); + if(!$column) die(js::error($this->lang->notFound) . js::locate($this->createLink('execution', 'kanban', "executionID=$executionID"))); + + $status = zget($this->config->kanban->{$column->laneType . 'ColumnStatusList'}, $column->type); + $title = isset($column->parentName) ? $column->parentName . '/' . $column->name : $column->name; + + $this->view->title = $title . $this->lang->colon . $this->lang->kanban->setWIP . '(' . $this->lang->kanban->WIP . ')'; + $this->view->column = $column; + $this->view->status = $status; + $this->display(); + } + + /** + * Set lane info. + * + * @param int $laneID + * @param int $executionID + * @access public + * @return void + */ + public function setLane($laneID, $executionID = 0) + { + if($_POST) + { + $this->kanban->setLane($laneID); + if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); + + $this->loadModel('action')->create('kanbanlane', $laneID, 'Edited', '', $executionID); + + return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => 'parent')); + } + + $lane = $this->kanban->getLaneById($laneID); + if(!$lane) die(js::error($this->lang->notFound) . js::locate($this->createLink('execution', 'kanban', "executionID=$executionID"))); + + $this->view->title = zget($this->lang->kanban->laneTypeList, $lane->type) . $this->lang->colon . $this->lang->kanban->setLane; + $this->view->lane = $lane; + + $this->display(); + } + + /** + * Set lane column info. + * + * @param int $columnID + * @param int $executionID + * @access public + * @return void + */ + public function setColumn($columnID, $executionID = 0) + { + $column = $this->kanban->getColumnById($columnID); + + if($_POST) + { + /* Check lane column name is unique. */ + $exist = $this->kanban->getColumnByName($this->post->name, $column->lane); + if($exist and $exist->id != $columnID) + { + return $this->sendError($this->lang->kanban->noColumnUniqueName); + } + + $changes = $this->kanban->updateLaneColumn($columnID, $column); + if(dao::isError()) return $this->sendError(dao::getError()); + if($changes) + { + $actionID = $this->loadModel('action')->create('kanbancolumn', $columnID, 'Edited', '', $executionID); + $this->action->logHistory($actionID, $changes); + } + + return $this->sendSuccess(array('locate' => 'parent')); + } + + $this->view->column = $column; + $this->view->title = $column->name . $this->lang->colon . $this->lang->kanban->setColumn; + $this->display(); + } + + /** + * AJAX: Update the cards sorting of the lane column. + * + * @param string $laneType story|bug|task + * @param int $columnID + * @param string $orderBy id_desc|id_asc|pri_desc|pri_asc|lastEditedDate_desc|lastEditedDate_asc|deadline_desc|deadline_asc|assignedTo_asc + * @access public + * @return void + */ + public function ajaxCardsSort($laneType, $columnID, $orderBy = 'id_desc') + { + $oldCards = array(); + $column = $this->dao->select('parent,cards')->from(TABLE_KANBANCOLUMN)->where('id')->eq($columnID)->fetch(); + + /* Get the cards of the kanban column. */ + if($column->parent == -1) + { + $childColumns = $this->dao->select('id,cards')->from(TABLE_KANBANCOLUMN)->where('parent')->eq($columnID)->fetchAll(); + foreach($childColumns as $childColumn) + { + $oldCards[$childColumn->id] = $childColumn->cards; + } + } + else + { + $oldCards[$columnID] = $column->cards; + } + + /* Update Kanban column card order. */ + $table = $this->config->objectTables[$laneType]; + foreach($oldCards as $colID => $cards) + { + if(empty($cards)) continue; + $objects = $this->dao->select('id')->from($table) + ->where('id')->in($cards) + ->orderBy($orderBy) + ->fetchPairs('id'); + + $objectIdList = ',' . implode(',', $objects) . ','; + $this->dao->update(TABLE_KANBANCOLUMN)->set('cards')->eq($objectIdList)->where('id')->eq($colID)->exec(); + } + echo true; + } + + /** + * Change the order through the lane move up and down. + * + * @param int $executionID + * @param string $currentType + * @param string $targetType + * @access public + * @return void + */ + public function laneMove($executionID, $currentType, $targetType) + { + if(empty($targetType)) return false; + + $this->kanban->updateLaneOrder($executionID, $currentType, $targetType); + + if(!dao::isError()) + { + $laneID = $this->dao->select('id')->from(TABLE_KANBANLANE)->where('execution')->eq($executionID)->andWhere('type')->eq($currentType)->fetch('id'); + $this->loadModel('action')->create('kanbanlane', $laneID, 'Moved'); + } + + die(js::locate($this->createLink('execution', 'kanban', 'executionID=' . $executionID . '&type=all'), 'parent')); + } +} diff --git a/module/kanban/css/setcolumn.css b/module/kanban/css/setcolumn.css new file mode 100644 index 0000000000..38087ed055 --- /dev/null +++ b/module/kanban/css/setcolumn.css @@ -0,0 +1,5 @@ +td>ul {padding-left: 0;} +li {display: block; float: left; padding: 5px; width: 28px; height: 28px;} +li>a { position: relative; display: block; width: 100%; height: 100%; padding: 0; margin: 0; font-family: ZentaoIcon; font-size: 14px; font-style: normal; font-weight: 400; font-variant: normal; line-height: 1; text-align: center; text-transform: none; border: 1px solid transparent; border-radius: 50%; speak: none; -webkit-font-smoothing: antialiased; } +li>a.active:before {font-size: 14px; content: "\e5ca"} + diff --git a/module/kanban/css/setlane.css b/module/kanban/css/setlane.css new file mode 100644 index 0000000000..1ae9a01187 --- /dev/null +++ b/module/kanban/css/setlane.css @@ -0,0 +1,4 @@ +td>ul {padding-left: 0;} +li {display: block; float: left; padding: 5px; width: 28px; height: 28px;} +li>a { position: relative; display: block; width: 100%; height: 100%; padding: 0; margin: 0; font-family: ZentaoIcon; font-size: 14px; font-style: normal; font-weight: 400; font-variant: normal; line-height: 1; text-align: center; text-transform: none; border: 1px solid transparent; border-radius: 50%; speak: none; -webkit-font-smoothing: antialiased; } +li>a.active:before {font-size: 14px; content: "\e5ca"} diff --git a/module/kanban/js/setcolumn.js b/module/kanban/js/setcolumn.js new file mode 100644 index 0000000000..a01570f547 --- /dev/null +++ b/module/kanban/js/setcolumn.js @@ -0,0 +1,13 @@ +/** + * Set lane color. + * + * @param string $color + * @access public + * @return void + */ +function setColor(color) +{ + $('.cp-tile').removeClass('active'); + $('.cp-tile[data-color="' + color + '"]').addClass('active'); + $('#color').val(color); +} diff --git a/module/kanban/js/setlane.js b/module/kanban/js/setlane.js new file mode 100644 index 0000000000..a01570f547 --- /dev/null +++ b/module/kanban/js/setlane.js @@ -0,0 +1,13 @@ +/** + * Set lane color. + * + * @param string $color + * @access public + * @return void + */ +function setColor(color) +{ + $('.cp-tile').removeClass('active'); + $('.cp-tile[data-color="' + color + '"]').addClass('active'); + $('#color').val(color); +} diff --git a/module/kanban/js/setwip.js b/module/kanban/js/setwip.js new file mode 100644 index 0000000000..cf7b4a0563 --- /dev/null +++ b/module/kanban/js/setwip.js @@ -0,0 +1,28 @@ +$(function() +{ + $('#noLimit').click(function() + { + if($(this).attr('checked') == 'checked') + { + $('#WIPCount').attr('disabled', true); + } + else + { + $('#WIPCount').removeAttr('disabled'); + } + }) +}) + +/** + * Set WIP count. + * + * @access public + * @return void + */ +function setWIPLimit() +{ + var count = $('#WIPCount').val(); + if($('#noLimit').attr('checked') == 'checked') count = -1;; + + $('#limit').val(count); +} diff --git a/module/kanban/lang/en.php b/module/kanban/lang/en.php new file mode 100644 index 0000000000..5f49e1f9b5 --- /dev/null +++ b/module/kanban/lang/en.php @@ -0,0 +1,105 @@ +kanban->type = array(); +$lang->kanban->type['all'] = "All KanBan"; +$lang->kanban->type['story'] = "Story KanBan"; +$lang->kanban->type['task'] = "Task KanBan"; +$lang->kanban->type['bug'] = "Bug KanBan"; + +$lang->kanban->group = new stdClass(); + +$lang->kanban->group->all = array(); +$lang->kanban->group->story = array(); +$lang->kanban->group->story['default'] = "Default"; +$lang->kanban->group->story['pri'] = "Story Priority"; +$lang->kanban->group->story['category'] = "Story Category"; +$lang->kanban->group->story['module'] = "Story Module"; +$lang->kanban->group->story['source'] = "Story Source"; +$lang->kanban->group->story['assignedTo'] = "Assigned To"; + +$lang->kanban->group->task = array(); +$lang->kanban->group->task['default'] = "Default"; +$lang->kanban->group->task['pri'] = "Task Priority"; +$lang->kanban->group->task['type'] = "Task Type"; +$lang->kanban->group->task['module'] = "Task Module"; +$lang->kanban->group->task['story'] = "Story"; +$lang->kanban->group->task['assignedTo'] = "Assigned To"; + +$lang->kanban->group->bug = array(); +$lang->kanban->group->bug['default'] = "Default"; +$lang->kanban->group->bug['pri'] = "Bug Priority"; +$lang->kanban->group->bug['type'] = "Bug Type"; +$lang->kanban->group->bug['module'] = "Bug Module"; +$lang->kanban->group->bug['severity'] = "Bug Severity"; +$lang->kanban->group->bug['assignedTo'] = "Assigned To"; + +$lang->kanban->WIP = 'WIP'; +$lang->kanban->setWIP = 'WIP Settings'; +$lang->kanban->WIPStatus = 'WIP Status'; +$lang->kanban->WIPStage = 'WIP Stage'; +$lang->kanban->WIPType = 'WIP Type'; +$lang->kanban->WIPCount = 'WIP Count'; +$lang->kanban->noLimit = 'No Limit ∞'; +$lang->kanban->setLane = 'Lane Settings'; +$lang->kanban->laneName = 'Lane Name'; +$lang->kanban->laneColor = 'Lane Color'; +$lang->kanban->setColumn = 'Column Settings'; +$lang->kanban->columnName = 'Column Name'; +$lang->kanban->columnColor = 'Column Color'; +$lang->kanban->noColumnUniqueName = 'The Kanban column name already exists.'; +$lang->kanban->moveUp = 'Swimlane Up'; +$lang->kanban->moveDown = 'Swimlane Down'; +$lang->kanban->laneMove = 'Swimlane Sorting'; +$lang->kanban->laneGroup = 'Lane Group'; +$lang->kanban->cardsSort = 'Cards Sortting'; +$lang->kanban->moreAction = 'More Action'; +$lang->kanban->noGroup = 'None'; + +$lang->kanban->error = new stdclass(); +$lang->kanban->error->mustBeInt = 'The WIPs must be positive integer.'; +$lang->kanban->error->parentLimitNote = 'The WIPs in the parent column cannot be < the sum of the WIPs in the child column.'; +$lang->kanban->error->childLimitNote = 'The sum of products in the child column cannot be > the number of products in the parent column.'; + +$this->lang->kanban->laneTypeList = array(); +$this->lang->kanban->laneTypeList['story'] = $lang->SRCommon; +$this->lang->kanban->laneTypeList['bug'] = 'Bug'; +$this->lang->kanban->laneTypeList['task'] = 'Task'; + +$lang->kanban->storyColumn = array(); +$lang->kanban->storyColumn['backlog'] = 'Backlog'; +$lang->kanban->storyColumn['ready'] = 'Ready'; +$lang->kanban->storyColumn['develop'] = 'Development'; +$lang->kanban->storyColumn['developing'] = 'Doing'; +$lang->kanban->storyColumn['developed'] = 'Done'; +$lang->kanban->storyColumn['test'] = 'Testing'; +$lang->kanban->storyColumn['testing'] = 'Doing'; +$lang->kanban->storyColumn['tested'] = 'Done'; +$lang->kanban->storyColumn['verified'] = 'Verified'; +$lang->kanban->storyColumn['released'] = 'Released'; +$lang->kanban->storyColumn['closed'] = 'Closed'; + +$lang->kanban->bugColumn = array(); +$lang->kanban->bugColumn['unconfirmed'] = 'Unconfirmed'; +$lang->kanban->bugColumn['confirmed'] = 'Confirmed'; +$lang->kanban->bugColumn['resolving'] = 'Resolving'; +$lang->kanban->bugColumn['fixing'] = 'Doing'; +$lang->kanban->bugColumn['fixed'] = 'Done'; +$lang->kanban->bugColumn['test'] = 'Test'; +$lang->kanban->bugColumn['testing'] = 'Doing'; +$lang->kanban->bugColumn['tested'] = 'Done'; +$lang->kanban->bugColumn['closed'] = 'Closed'; + +$lang->kanban->taskColumn = array(); +$lang->kanban->taskColumn['wait'] = 'Wait'; +$lang->kanban->taskColumn['develop'] = 'Develop'; +$lang->kanban->taskColumn['developing'] = 'Developing'; +$lang->kanban->taskColumn['developed'] = 'Developed'; +$lang->kanban->taskColumn['pause'] = 'Pause'; +$lang->kanban->taskColumn['canceled'] = 'Canceled'; +$lang->kanban->taskColumn['closed'] = 'Closed'; + +$lang->kanbancolumn = new stdclass(); +$lang->kanbancolumn->name = $lang->kanban->columnName; +$lang->kanbancolumn->limit = $lang->kanban->WIPCount; + +$lang->kanbanlane = new stdclass(); +$lang->kanbanlane->name = $lang->kanban->laneName; diff --git a/module/kanban/lang/zh-cn.php b/module/kanban/lang/zh-cn.php new file mode 100644 index 0000000000..2e99a20b90 --- /dev/null +++ b/module/kanban/lang/zh-cn.php @@ -0,0 +1,105 @@ +kanban->type = array(); +$lang->kanban->type['all'] = "综合看板"; +$lang->kanban->type['story'] = "{$lang->SRCommon}看板"; +$lang->kanban->type['task'] = "任务看板"; +$lang->kanban->type['bug'] = "Bug看板"; + +$lang->kanban->group = new stdClass(); + +$lang->kanban->group->all = array(); +$lang->kanban->group->story = array(); +$lang->kanban->group->story['default'] = "默认方式"; +$lang->kanban->group->story['pri'] = "需求优先级"; +$lang->kanban->group->story['category'] = "需求类别"; +$lang->kanban->group->story['module'] = "需求模块"; +$lang->kanban->group->story['source'] = "需求来源"; +$lang->kanban->group->story['assignedTo'] = "指派人员"; + +$lang->kanban->group->task = array(); +$lang->kanban->group->task['default'] = "默认方式"; +$lang->kanban->group->task['pri'] = "任务优先级"; +$lang->kanban->group->task['type'] = "任务类型"; +$lang->kanban->group->task['module'] = "任务所属模块"; +$lang->kanban->group->task['assignedTo'] = "指派人员"; +$lang->kanban->group->task['story'] = "{$lang->SRCommon}"; + +$lang->kanban->group->bug = array(); +$lang->kanban->group->bug['default'] = "默认方式"; +$lang->kanban->group->bug['pri'] = "Bug优先级"; +$lang->kanban->group->bug['severity'] = "Bug严重程度"; +$lang->kanban->group->bug['module'] = "Bug模块"; +$lang->kanban->group->bug['type'] = "Bug类型"; +$lang->kanban->group->bug['assignedTo'] = "指派人员"; + +$lang->kanban->WIP = 'WIP'; +$lang->kanban->setWIP = '在制品设置'; +$lang->kanban->WIPStatus = '在制品状态'; +$lang->kanban->WIPStage = '在制品阶段'; +$lang->kanban->WIPType = '在制品类型'; +$lang->kanban->WIPCount = '在制品数量'; +$lang->kanban->noLimit = '不限制∞'; +$lang->kanban->setLane = '泳道设置'; +$lang->kanban->laneName = '泳道名称'; +$lang->kanban->laneColor = '泳道颜色'; +$lang->kanban->setColumn = '看板列设置'; +$lang->kanban->columnName = '看板列名称'; +$lang->kanban->columnColor = '看板列颜色'; +$lang->kanban->noColumnUniqueName = '看板列名称已存在'; +$lang->kanban->moveUp = '泳道上移'; +$lang->kanban->moveDown = '泳道下移'; +$lang->kanban->laneMove = '泳道排序'; +$lang->kanban->laneGroup = '泳道分组'; +$lang->kanban->cardsSort = '卡片排序'; +$lang->kanban->moreAction = '更多操作'; +$lang->kanban->noGroup = '无'; + +$lang->kanban->error = new stdclass(); +$lang->kanban->error->mustBeInt = '在制品数量必须是正整数。'; +$lang->kanban->error->parentLimitNote = '父列的在制品数量不能小于子列在制品数量之和'; +$lang->kanban->error->childLimitNote = '子列在制品数量之和不能大于父列的在制品数量'; + +$this->lang->kanban->laneTypeList = array(); +$this->lang->kanban->laneTypeList['story'] = $lang->SRCommon; +$this->lang->kanban->laneTypeList['bug'] = 'Bug'; +$this->lang->kanban->laneTypeList['task'] = '任务'; + +$lang->kanban->storyColumn = array(); +$lang->kanban->storyColumn['backlog'] = 'Backlog'; +$lang->kanban->storyColumn['ready'] = '准备好'; +$lang->kanban->storyColumn['develop'] = '开发'; +$lang->kanban->storyColumn['developing'] = '进行中'; +$lang->kanban->storyColumn['developed'] = '完成'; +$lang->kanban->storyColumn['test'] = '测试'; +$lang->kanban->storyColumn['testing'] = '进行中'; +$lang->kanban->storyColumn['tested'] = '完成'; +$lang->kanban->storyColumn['verified'] = '已验收'; +$lang->kanban->storyColumn['released'] = '已发布'; +$lang->kanban->storyColumn['closed'] = '已关闭'; + +$lang->kanban->bugColumn = array(); +$lang->kanban->bugColumn['unconfirmed'] = '待确认'; +$lang->kanban->bugColumn['confirmed'] = '已确认'; +$lang->kanban->bugColumn['resolving'] = '解决中'; +$lang->kanban->bugColumn['fixing'] = '进行中'; +$lang->kanban->bugColumn['fixed'] = '完成'; +$lang->kanban->bugColumn['test'] = '测试'; +$lang->kanban->bugColumn['testing'] = '测试中'; +$lang->kanban->bugColumn['tested'] = '测试完毕'; +$lang->kanban->bugColumn['closed'] = '已关闭'; + +$lang->kanban->taskColumn = array(); +$lang->kanban->taskColumn['wait'] = '未开始'; +$lang->kanban->taskColumn['develop'] = '开发'; +$lang->kanban->taskColumn['developing'] = '研发中'; +$lang->kanban->taskColumn['developed'] = '研发完毕'; +$lang->kanban->taskColumn['pause'] = '已暂停'; +$lang->kanban->taskColumn['canceled'] = '已取消'; +$lang->kanban->taskColumn['closed'] = '已关闭'; + +$lang->kanbancolumn = new stdclass(); +$lang->kanbancolumn->name = $lang->kanban->columnName; +$lang->kanbancolumn->limit = $lang->kanban->WIPCount; + +$lang->kanbanlane = new stdclass(); +$lang->kanbanlane->name = $lang->kanban->laneName; diff --git a/module/kanban/model.php b/module/kanban/model.php new file mode 100644 index 0000000000..b9df8bd351 --- /dev/null +++ b/module/kanban/model.php @@ -0,0 +1,915 @@ + + * @package kanban + * @version $Id: model.php 5118 2021-10-22 10:18:41Z $ + * @link https://www.zentao.net + */ +?> +updateGroupLanes($executionID, $browseType, $groupBy); + + $lanes = $this->dao->select('*')->from(TABLE_KANBANLANE) + ->where('execution')->eq($executionID) + ->andWhere('deleted')->eq(0) + ->beginIF($browseType != 'all')->andWhere('type')->eq($browseType)->fi() + ->beginIF($groupBy == 'default')->andWhere('groupby')->eq('')->fi() + ->beginIF($groupBy != 'default')->andWhere('groupby')->eq($groupBy)->fi() + ->orderBy('order_asc') + ->fetchAll('id'); + + if(empty($lanes)) return array(); + + foreach($lanes as $lane) $this->updateCards($lane); + + $columns = $this->dao->select('*')->from(TABLE_KANBANCOLUMN) + ->where('deleted')->eq(0) + ->andWhere('lane')->in(array_keys($lanes)) + ->orderBy('id_asc') + ->fetchGroup('lane', 'id'); + + /* Get parent column type pairs. */ + $parentTypes = $this->dao->select('id, type')->from(TABLE_KANBANCOLUMN) + ->where('deleted')->eq(0) + ->andWhere('lane')->in(array_keys($lanes)) + ->andWhere('parent')->eq(-1) + ->fetchPairs('id', 'type'); + + /* Get group objects. */ + if($browseType == 'all' or $browseType == 'story') $objectGroup['story'] = $this->loadModel('story')->getExecutionStories($executionID); + if($browseType == 'all' or $browseType == 'bug') $objectGroup['bug'] = $this->loadModel('bug')->getExecutionBugs($executionID); + if($browseType == 'all' or $browseType == 'task') $objectGroup['task'] = $this->loadModel('execution')->getKanbanTasks($executionID, "id"); + + /* Get objects cards menus. */ + if($browseType == 'all' or $browseType == 'story') $storyCardMenu = $this->getKanbanCardMenu($executionID, $objectGroup['story'], 'story'); + if($browseType == 'all' or $browseType == 'bug') $bugCardMenu = $this->getKanbanCardMenu($executionID, $objectGroup['bug'], 'bug'); + if($browseType == 'all' or $browseType == 'task') $taskCardMenu = $this->getKanbanCardMenu($executionID, $objectGroup['task'], 'task'); + + /* Build kanban group data. */ + $kanbanGroup = array(); + foreach($lanes as $laneID => $lane) + { + $laneData = array(); + $columnData = array(); + $laneType = $groupBy == 'default' ? $lane->type : $lane->groupby; + + $laneData['id'] = $groupBy == 'default' ? $lane->type : $lane->groupby . '-' . $lane->extra; + $laneData['laneID'] = $laneID; + $laneData['name'] = $lane->name; + $laneData['color'] = $lane->color; + $laneData['order'] = $lane->order; + $laneData['defaultCardType'] = $lane->type; + + foreach($columns[$laneID] as $columnID => $column) + { + $columnData[$column->id]['id'] = $laneType . '-' . $column->type; + $columnData[$column->id]['columnID'] = $columnID; + $columnData[$column->id]['type'] = $column->type; + $columnData[$column->id]['name'] = $column->name; + $columnData[$column->id]['color'] = $column->color; + $columnData[$column->id]['limit'] = $column->limit; + $columnData[$column->id]['laneType'] = $lane->type; + $columnData[$column->id]['asParent'] = $column->parent == -1 ? true : false; + + if($column->parent > 0) + { + $columnData[$column->id]['parentType'] = zget($parentTypes, $column->parent, ''); + } + + $cardOrder = 1; + $cardIdList = array_filter(explode(',', $column->cards)); + foreach($cardIdList as $cardID) + { + $cardData = array(); + $objects = zget($objectGroup, $lane->type, array()); + $object = zget($objects, $cardID, array()); + + if(empty($object)) continue; + + $cardData['id'] = $object->id; + $cardData['order'] = $cardOrder; + $cardData['pri'] = $object->pri ? $object->pri : ''; + $cardData['estimate'] = $lane->type == 'bug' ? '' : $object->estimate; + $cardData['assignedTo'] = $object->assignedTo; + $cardData['deadline'] = $lane->type == 'story' ? '' : $object->deadline; + $cardData['severity'] = $lane->type == 'bug' ? $object->severity : ''; + + if($lane->type == 'task') + { + $cardData['name'] = $object->name; + } + else + { + $cardData['title'] = $object->title; + } + + if($lane->type == 'story') $cardData['menus'] = $storyCardMenu[$object->id]; + if($lane->type == 'bug') $cardData['menus'] = $bugCardMenu[$object->id]; + if($lane->type == 'task') $cardData['menus'] = $taskCardMenu[$object->id]; + + $laneData['cards'][$column->type][] = $cardData; + $cardOrder ++; + } + if(!isset($laneData['cards'][$column->type])) $laneData['cards'][$column->type] = array(); + } + + $kanbanGroup[$laneType]['id'] = $laneType; + $kanbanGroup[$laneType]['columns'] = array_values($columnData); + $kanbanGroup[$laneType]['lanes'][] = $laneData; + $kanbanGroup[$laneType]['defaultCardType'] = $lane->type; + } + + return $kanbanGroup; + } + + /** + * Add execution Kanban lanes and columns. + * + * @param int $executionID + * @param string $type all|story|bug|task + * @param string $groupBy default + * @access public + * @return void + */ + public function createLanes($executionID, $type = 'all', $groupBy = 'default') + { + if($groupBy == 'default' or $type == 'all') + { + foreach($this->config->kanban->default as $type => $lane) + { + $lane->type = $type; + $lane->execution = $executionID; + $this->dao->insert(TABLE_KANBANLANE)->data($lane)->exec(); + + $laneID = $this->dao->lastInsertId(); + $this->createColumns($laneID, $type, $executionID); + } + } + else + { + $this->loadModel($type); + $groupList = $this->getObjectGroup($executionID, $type, $groupBy); + + $objectPairs = array(); + if($groupBy == 'module') $objectPairs = $this->dao->select('id,name')->from(TABLE_MODULE)->where('type')->in('story,bug,task')->andWhere('deleted')->eq('0')->fetchPairs(); + if($groupBy == 'story') $objectPairs = $this->dao->select('id,title')->from(TABLE_STORY)->where('deleted')->eq(0)->fetchPairs(); + if($groupBy == 'assignedTo') $objectPairs = $this->loadModel('user')->getPairs('noletter'); + + $laneName = ''; + $laneOrder = 5; + $colorIndex = 0; + foreach($groupList as $groupKey) + { + if($groupKey) + { + if(strpos('module,story,assignedTo', $groupBy) !== false) + { + $laneName = zget($objectPairs, $groupKey); + } + else + { + $laneName = zget($this->lang->$type->{$groupBy . 'List'}, $groupKey); + } + } + else + { + $laneName = $this->lang->kanban->noGroup; + } + + $lane = new stdClass(); + $lane->execution = $executionID; + $lane->type = $type; + $lane->groupby = $groupBy; + $lane->extra = $groupKey; + $lane->name = $laneName; + $lane->color = $this->config->kanban->laneColorList[$colorIndex]; + $lane->order = $laneOrder; + + $laneOrder += 5; + $colorIndex += 1; + if($colorIndex == count($this->config->kanban->laneColorList) + 1) $colorIndex = 0; + $this->dao->insert(TABLE_KANBANLANE)->data($lane)->exec(); + + $laneID = $this->dao->lastInsertId(); + $this->createColumns($laneID, $type, $executionID, $groupBy, $groupKey); + } + } + } + + /** + * createColumn + * + * @param int $laneID + * @param string $type story|bug|task + * @param int $executionID + * @param string $groupBy + * @param string $groupValue + * @access public + * @return void + */ + public function createColumns($laneID, $type, $executionID, $groupBy = '', $groupValue = '') + { + $objects = array(); + + if($type == 'story') $objects = $this->loadModel('story')->getExecutionStories($executionID, 0, 0, 't2.id_desc'); + if($type == 'bug') $objects = $this->loadModel('bug')->getExecutionBugs($executionID); + if($type == 'task') $objects = $this->loadModel('execution')->getKanbanTasks($executionID); + + if(!empty($groupBy)) + { + foreach($objects as $objectID => $object) + { + if($object->$groupBy != $groupValue) unset($objects[$objectID]); + } + } + + $devColumnID = $testColumnID = $resolvingColumnID = 0; + if($type == 'story') + { + foreach($this->lang->kanban->storyColumn as $colType => $name) + { + $data = new stdClass(); + $data->lane = $laneID; + $data->name = $name; + $data->color = '#333'; + $data->type = $colType; + $data->cards = ''; + + if(strpos(',developing,developed,', $colType) !== false) $data->parent = $devColumnID; + if(strpos(',testing,tested,', $colType) !== false) $data->parent = $testColumnID; + if(strpos(',develop,test,', $colType) !== false) $data->parent = -1; + if(strpos(',ready,develop,test,', $colType) === false) + { + $storyStatus = $this->config->kanban->storyColumnStatusList[$colType]; + $storyStage = $this->config->kanban->storyColumnStageList[$colType]; + foreach($objects as $storyID => $story) + { + if($story->status == $storyStatus and $story->stage == $storyStage) $data->cards .= $storyID . ','; + } + if(!empty($data->cards)) $data->cards = ',' . $data->cards; + } + + $this->dao->insert(TABLE_KANBANCOLUMN)->data($data)->exec(); + if($colType == 'develop') $devColumnID = $this->dao->lastInsertId(); + if($colType == 'test') $testColumnID = $this->dao->lastInsertId(); + } + } + elseif($type == 'bug') + { + foreach($this->lang->kanban->bugColumn as $colType => $name) + { + $data = new stdClass(); + $data->lane = $laneID; + $data->name = $name; + $data->color = '#333'; + $data->type = $colType; + $data->cards = ''; + if(strpos(',fixing,fixed,', $colType) !== false) $data->parent = $resolvingColumnID; + if(strpos(',testing,tested,', $colType) !== false) $data->parent = $testColumnID; + if(strpos(',resolving,test,', $colType) !== false) $data->parent = -1; + if(strpos(',resolving,fixing,test,testing,tested,', $colType) === false) + { + $bugStatus = $this->config->kanban->bugColumnStatusList[$colType]; + foreach($objects as $bugID => $bug) + { + if($colType == 'unconfirmed' and $bug->status == $bugStatus and $bug->confirmed == 0) + { + $data->cards .= $bugID . ','; + } + elseif($colType == 'confirmed' and $bug->status == $bugStatus and $bug->confirmed == 1) + { + $data->cards .= $bugID . ','; + } + elseif(strpos(',unconfirmed,confirmed,', $colType) === false and $bug->status == $bugStatus) + { + $data->cards .= $bugID . ','; + } + } + if(!empty($data->cards)) $data->cards = ',' . $data->cards; + } + $this->dao->insert(TABLE_KANBANCOLUMN)->data($data)->exec(); + if($colType == 'resolving') $resolvingColumnID = $this->dao->lastInsertId(); + if($colType == 'test') $testColumnID = $this->dao->lastInsertId(); + } + } + elseif($type == 'task') + { + foreach($this->lang->kanban->taskColumn as $colType => $name) + { + $data = new stdClass(); + $data->lane = $laneID; + $data->name = $name; + $data->color = '#333'; + $data->type = $colType; + $data->cards = ''; + if(strpos(',developing,developed,', $colType) !== false) $data->parent = $devColumnID; + if($colType == 'develop') $data->parent = -1; + if(strpos(',develop,', $colType) === false) + { + $taskStatus = $this->config->kanban->taskColumnStatusList[$colType]; + foreach($objects as $taskID => $task) + { + if($task->status == $taskStatus) $data->cards .= $taskID . ','; + + } + if(!empty($data->cards)) $data->cards = ',' . $data->cards; + } + $this->dao->insert(TABLE_KANBANCOLUMN)->data($data)->exec(); + if($colType == 'develop') $devColumnID = $this->dao->lastInsertId(); + } + } + } + + /** + * Update kanban lane. + * + * @param int $executionID + * @param string $laneType + * @access public + * @return void + */ + public function updateLane($executionID, $laneType) + { + $lanes = $this->dao->select('*')->from(TABLE_KANBANLANE) + ->where('execution')->eq($executionID) + ->andWhere('type')->eq($laneType) + ->fetchAll('id'); + + foreach($lanes as $lane) $this->updateCards($lane); + } + + /** + * Update column cards. + * + * @param object $lane + * @access public + * @return void + */ + public function updateCards($lane) + { + $laneType = $lane->type; + $executionID = $lane->execution; + $cardPairs = $this->dao->select('*')->from(TABLE_KANBANCOLUMN) + ->where('deleted')->eq(0) + ->andWhere('lane')->eq($lane->id) + ->fetchPairs('type' ,'cards'); + + if($laneType == 'story') + { + $stories = $this->loadModel('story')->getExecutionStories($executionID); + foreach($stories as $storyID => $story) + { + foreach($this->config->kanban->storyColumnStageList as $colType => $stage) + { + if(strpos(',ready,develop,test,', $colType) !== false) continue; + + if($lane->groupby and $story->{$lane->groupby} != $lane->extra) + { + $cardPairs[$colType] = str_replace(",$storyID,", ',', $cardPairs[$colType]); + } + elseif($colType == 'backlog' and $story->stage == $stage and strpos($cardPairs['ready'], ",$storyID,") === false and strpos($cardPairs['backlog'], ",$storyID,") === false) + { + $cardPairs['backlog'] = empty($cardPairs['backlog']) ? ",$storyID," : ",$storyID" . $cardPairs['backlog']; + } + elseif($story->stage == $stage and strpos($cardPairs[$colType], ",$storyID,") === false) + { + $cardPairs[$colType] = empty($cardPairs[$colType]) ? ",$storyID," : ",$storyID" . $cardPairs[$colType]; + } + elseif($story->stage != $stage and strpos($cardPairs[$colType], ",$storyID,") !== false) + { + $cardPairs[$colType] = str_replace(",$storyID,", ',', $cardPairs[$colType]); + } + } + } + } + elseif($laneType == 'bug') + { + $bugs = $this->loadModel('bug')->getExecutionBugs($executionID); + foreach($bugs as $bugID => $bug) + { + foreach($this->config->kanban->bugColumnStatusList as $colType => $status) + { + if(strpos(',resolving,fixing,test,testing,tested,', $colType) !== false) continue; + + if($lane->groupby and $bug->{$lane->groupby} != $lane->extra) + { + $cardPairs[$colType] = str_replace(",$bugID,", ',', $cardPairs[$colType]); + } + elseif($colType == 'unconfirmed' and $bug->status == $status and $bug->confirmed == 0 and strpos($cardPairs['unconfirmed'], ",$bugID,") === false and strpos($cardPairs['fixing'], ",$bugID,") === false) + { + $cardPairs['unconfirmed'] = empty($cardPairs['unconfirmed']) ? ",$bugID," : ",$bugID" . $cardPairs['unconfirmed']; + if(strpos($cardPairs['closed'], ",$bugID,") !== false) $cardPairs['closed'] = str_replace(",$bugID,", ',', $cardPairs['closed']); + } + elseif($colType == 'confirmed' and $bug->status == $status and $bug->confirmed == 1 and strpos($cardPairs['confirmed'], ",$bugID,") === false and strpos($cardPairs['fixing'], ",$bugID,") === false) + { + $cardPairs['confirmed'] = empty($cardPairs['confirmed']) ? ",$bugID," : ",$bugID" . $cardPairs['confirmed']; + if(strpos($cardPairs['unconfirmed'], ",$bugID,") !== false) $cardPairs['unconfirmed'] = str_replace(",$bugID,", ',', $cardPairs['unconfirmed']); + } + elseif($colType == 'fixed' and $bug->status == $status and strpos($cardPairs['fixed'], ",$bugID,") === false and strpos($cardPairs['testing'], ",$bugID,") === false and strpos($cardPairs['tested'], ",$bugID,") === false) + { + $cardPairs['fixed'] = empty($cardPairs['fixed']) ? ",$bugID," : ",$bugID" . $cardPairs['fixed']; + } + elseif($colType == 'closed' and $bug->status == 'closed' and strpos($cardPairs[$colType], ",$bugID,") === false) + { + $cardPairs[$colType] = empty($cardPairs[$colType]) ? ",$bugID," : ",$bugID". $cardPairs[$colType]; + } + elseif($bug->status != $status and strpos($cardPairs[$colType], ",$bugID,") !== false) + { + $cardPairs[$colType] = str_replace(",$bugID,", ',', $cardPairs[$colType]); + } + } + } + } + elseif($laneType == 'task') + { + $tasks = $this->loadModel('execution')->getKanbanTasks($executionID); + foreach($tasks as $taskID => $task) + { + foreach($this->config->kanban->taskColumnStatusList as $colType => $status) + { + if($colType == 'develop') continue; + + if($lane->groupby and $task->{$lane->groupby} != $lane->extra) + { + $cardPairs[$colType] = str_replace(",$taskID,", ',', $cardPairs[$colType]); + } + elseif($task->status == $status and strpos($cardPairs[$colType], ",$taskID,") === false) + { + $cardPairs[$colType] = empty($cardPairs[$colType]) ? ",$taskID," : ",$taskID". $cardPairs[$colType]; + } + elseif($task->status != $status and strpos($cardPairs[$colType], ",$taskID,") !== false) + { + $cardPairs[$colType] = str_replace(",$taskID,", ',', $cardPairs[$colType]); + } + } + } + } + + foreach($cardPairs as $colType => $cards) + { + $this->dao->update(TABLE_KANBANCOLUMN)->set('cards')->eq($cards)->where('lane')->eq($lane->id)->andWhere('type')->eq($colType)->exec(); + } + + $this->dao->update(TABLE_KANBANLANE)->set('lastEditedTime')->eq(helper::now())->where('id')->eq($lane->id)->exec(); + } + + /** + * Update group lanes. + * + * @param int $executionID + * @param string $type + * @param string $groupBy + * @access public + * @return array + */ + public function updateGroupLanes($executionID, $type, $groupBy) + { + $this->loadModel($type); + + $lanes = $this->dao->select('*')->from(TABLE_KANBANLANE) + ->where('execution')->eq($executionID) + ->andWhere('deleted')->eq(0) + ->andWhere('type')->eq($type)->fi() + ->andWhere('groupby')->eq($groupBy)->fi() + ->orderBy('order_asc') + ->fetchAll('id'); + + /* Get old group list of kanban lane. */ + $oldGroupList = array(); + foreach($lanes as $lane) $oldGroupList[] = $lane->extra; + + /* Get new group list of kanban lane. */ + $groupList = $this->getObjectGroup($executionID, $type, $groupBy); + + $removeGroupList = array_diff($oldGroupList, $groupList); + $addGroupList = array_diff($groupList, $oldGroupList); + + $objectPairs = array(); + if($groupBy == 'module') $objectPairs = $this->dao->select('id,name')->from(TABLE_MODULE)->where('type')->in('story,bug,task')->andWhere('deleted')->eq('0')->fetchPairs(); + if($groupBy == 'story') $objectPairs = $this->dao->select('id,title')->from(TABLE_STORY)->where('deleted')->eq(0)->fetchPairs(); + if($groupBy == 'assignedTo') $objectPairs = $this->loadModel('user')->getPairs('noletter'); + + $colorIndex = 0; + + foreach($lanes as $laneID => $lane) + { + if(in_array($lane->extra, $removeGroupList)) + { + /* Remove lane and cloumns by laneID. */ + $this->dao->delete()->from(TABLE_KANBANLANE)->where('id')->eq($laneID)->exec(); + $this->dao->delete()->from(TABLE_KANBANCOLUMN)->where('lane')->eq($laneID)->exec(); + } + else + { + /* Update kanban lanes by group. */ + $laneName = $this->lang->$type->$groupBy . ': ' . $this->lang->kanban->noGroup; + if($lane->extra) + { + $namePairs = strpos('module,story,assignedTo', $groupBy) !== false ? $objectPairs : $this->lang->$type->{$groupBy . 'List'}; + $laneName = $this->lang->$type->$groupBy . ': ' . zget($namePairs, $lane->extra); + } + + $this->dao->update(TABLE_KANBANLANE)->set('name')->eq($laneName)->where('id')->eq($laneID)->exec(); + $this->updateCards($lane); + + $colorIndex += 1; + if($colorIndex == count($this->config->kanban->laneColorList)) $colorIndex = 0; + } + } + + /* Add new lanes by group. */ + foreach($addGroupList as $groupKey) + { + $laneName = $this->lang->kanban->noGroup; + if($groupKey) + { + $namePairs = strpos('module,story,assignedTo', $groupBy) !== false ? $objectPairs : $this->lang->$type->{$groupBy . 'List'}; + $laneName = zget($namePairs, $groupKey); + } + + $lane = new stdClass(); + $lane->execution = $executionID; + $lane->type = $type; + $lane->groupby = $groupBy; + $lane->extra = $groupKey; + $lane->name = $this->lang->$type->$groupBy . ": " . $laneName; + $lane->color = $this->config->kanban->laneColorList[$colorIndex]; + + $colorIndex += 1; + if($colorIndex == count($this->config->kanban->laneColorList)) $colorIndex = 0; + $this->dao->insert(TABLE_KANBANLANE)->data($lane)->exec(); + + $laneID = $this->dao->lastInsertId(); + $this->createColumns($laneID, $type, $executionID, $groupBy, $groupKey); + } + + $this->resetLaneOrder($executionID, $type, $groupBy); + } + + /** + * Update lane column. + * + * @param int $columnID + * @param object $column + * @access public + * @return array + */ + public function updateLaneColumn($columnID, $column) + { + $data = fixer::input('post')->get(); + + $this->dao->update(TABLE_KANBANCOLUMN)->data($data) + ->autoCheck() + ->batchcheck($this->config->kanban->setlaneColumn->requiredFields, 'notempty') + ->where('id')->eq($columnID) + ->exec(); + + if(dao::isError()) return; + + $changes = common::createChanges($column, $data); + return $changes; + } + + /** + * Change the order through the lane move up and down. + * + * @param int $executionID + * @param string $currentType + * @param string $targetType + * @access public + * @return void + */ + public function updateLaneOrder($executionID, $currentType, $targetType) + { + $orderList = $this->dao->select('id,type,`order`')->from(TABLE_KANBANLANE) + ->where('execution')->eq($executionID) + ->andWhere('type')->in(array($currentType, $targetType)) + ->andWhere('groupby')->eq('') + ->fetchAll('type'); + + $this->dao->update(TABLE_KANBANLANE)->set('`order`')->eq($orderList[$targetType]->order) + ->where('id')->eq($orderList[$currentType]->id) + ->andWhere('groupby')->eq('') + ->exec(); + + $this->dao->update(TABLE_KANBANLANE)->set('`order`')->eq($orderList[$currentType]->order) + ->where('id')->eq($orderList[$targetType]->id) + ->andWhere('groupby')->eq('') + ->exec(); + } + + /** + * Set WIP limit. + * + * @param int $columnID + * @access public + * @return bool + */ + public function setWIP($columnID) + { + $oldColumn = $this->getColumnById($columnID); + $column = fixer::input('post')->remove('WIPCount,noLimit')->get(); + if(!preg_match("/^-?\d+$/", $column->limit)) + { + dao::$errors['limit'] = $this->lang->kanban->error->mustBeInt; + return false; + } + $column->limit = (int)$column->limit; + + /* Check column limit. */ + $sumChildLimit = 0; + if($oldColumn->parent == -1 and $column->limit != -1) + { + $childColumns = $this->dao->select('id,`limit`')->from(TABLE_KANBANCOLUMN)->where('parent')->eq($columnID)->fetchAll(); + foreach($childColumns as $childColumn) + { + if($childColumn->limit == -1) + { + dao::$errors['limit'] = $this->lang->kanban->error->parentLimitNote; + return false; + } + + $sumChildLimit += $childColumn->limit; + } + + if($sumChildLimit > $column->limit) + { + dao::$errors['limit'] = $this->lang->kanban->error->parentLimitNote; + return false; + } + } + elseif($oldColumn->parent > 0) + { + $parentColumn = $this->getColumnByID($oldColumn->parent); + if($parentColumn->limit != -1) + { + $siblingLimit = $this->dao->select('`limit`')->from(TABLE_KANBANCOLUMN) + ->where('`parent`')->eq($oldColumn->parent) + ->andWhere('id')->ne($columnID) + ->fetch('limit'); + + $sumChildLimit = $siblingLimit + $column->limit; + + if($column->limit == -1 or $siblingLimit == -1 or $sumChildLimit > $parentColumn->limit) + { + dao::$errors['limit'] = $this->lang->kanban->error->childLimitNote; + return false; + } + } + } + + $this->dao->update(TABLE_KANBANCOLUMN)->data($column) + ->autoCheck() + ->checkIF($column->limit != -1, 'limit', 'gt', 0) + ->batchcheck($this->config->kanban->setwip->requiredFields, 'notempty') + ->where('id')->eq($columnID) + ->exec(); + + return dao::isError(); + } + + /** + * Set lane info. + * + * @param int $laneID + * @access public + * @return bool + */ + public function setLane($laneID) + { + $lane = fixer::input('post')->get(); + + $this->dao->update(TABLE_KANBANLANE)->data($lane) + ->autoCheck() + ->batchcheck($this->config->kanban->setlane->requiredFields, 'notempty') + ->where('id')->eq($laneID) + ->exec(); + + return dao::isError(); + } + + /** + * Reset order of lane. + * + * @param int $executionID + * @param int $type + * @param int $groupBy + * @access public + * @return void + */ + public function resetLaneOrder($executionID, $type, $groupBy) + { + $lanes = $this->dao->select('id,extra')->from(TABLE_KANBANLANE) + ->where('execution')->eq($executionID) + ->andWhere('type')->eq($type) + ->andWhere('groupBy')->eq($groupBy) + ->orderBy('extra_asc') + ->fetchPairs(); + + $laneOrder = 5; + $noExtra = 0; + + foreach($lanes as $laneID => $extra) + { + if(!$extra) + { + $noExtra = $laneID; + continue; + } + + $this->dao->update(TABLE_KANBANLANE)->set('order')->eq($laneOrder)->where('id')->eq($laneID)->exec(); + $laneOrder += 5; + } + + if($noExtra) $this->dao->update(TABLE_KANBANLANE)->set('order')->eq($laneOrder)->where('id')->eq($noExtra)->exec(); + } + + /** + * Get column by id. + * + * @param int $columnID + * @access public + * @return object + */ + public function getColumnById($columnID) + { + $column = $this->dao->select('t1.*, t2.type as laneType')->from(TABLE_KANBANCOLUMN)->alias('t1') + ->leftjoin(TABLE_KANBANLANE)->alias('t2')->on('t1.lane=t2.id') + ->where('t1.id')->eq($columnID) + ->andWhere('t1.deleted')->eq(0) + ->fetch(); + + if($column->parent > 0) $column->parentName = $this->dao->findById($column->parent)->from(TABLE_KANBANCOLUMN)->fetch('name'); + + return $column; + } + + /** + * Get Column by column name. + * + * @param string $name + * @param int $laneID + * @access public + * @return object + */ + public function getColumnByName($name, $laneID) + { + return $this->dao->select('*') + ->from(TABLE_KANBANCOLUMN) + ->where('name')->eq($name) + ->andWhere('lane')->eq($laneID) + ->fetch(); + } + + + /** + * Get lane by id. + * + * @param int $laneID + * @access public + * @return object + */ + public function getLaneById($laneID) + { + return $this->dao->findById($laneID)->from(TABLE_KANBANLANE)->fetch(); + } + + /** + * Get object group list. + * + * @param int $executionID + * @param string $type + * @param string $groupBy + * @access public + * @return array + */ + public function getObjectGroup($executionID, $type, $groupBy) + { + $table = zget($this->config->objectTables, $type); + + if($groupBy == 'story' or $type == 'story') + { + $selectField = $groupBy == 'story' ? "t1.$groupBy" : "t2.$groupBy"; + $groupList = $this->dao->select($selectField)->from(TABLE_PROJECTSTORY)->alias('t1') + ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story=t2.id') + ->where('t1.project')->eq($executionID) + ->andWhere('t2.deleted')->eq(0) + ->orderBy($groupBy . '_desc') + ->fetchPairs(); + + if($type == 'task') + { + $unlinkedTask = $this->dao->select('id')->from(TABLE_TASK) + ->where('execution')->eq($executionID) + ->andWhere('parent')->ge(0) + ->andWhere('story')->eq(0) + ->andWhere('deleted')->eq(0) + ->fetch('id'); + if($unlinkedTask) $groupList[0] = 0; + } + } + else + { + $groupList = $this->dao->select($groupBy)->from($table) + ->where('execution')->eq($executionID) + ->beginIF($type == 'task')->andWhere('parent')->ge(0)->fi() + ->andWhere('deleted')->eq(0) + ->orderBy($groupBy . '_desc') + ->fetchPairs(); + } + + return $groupList; + } + + /** + * Get Kanban cards menus by execution id. + * + * @param int $executionID + * @param array $objects + * @param string $objecType story|bug|task + * @access public + * @return array + */ + public function getKanbanCardMenu($executionID, $objects, $objecType) + { + $menus = array(); + switch ($objecType) + { + case 'story': + if(!isset($this->story)) $this->loadModel('story'); + + $objects = $this->story->mergeReviewer($objects); + foreach($objects as $story) + { + $menu = array(); + + $toTaskPriv = strpos('draft,closed', $story->status) !== false ? false : true; + if(common::hasPriv('story', 'edit') and $this->story->isClickable($story, 'edit')) $menu[] = array('label' => $this->lang->story->edit, 'icon' => 'edit', 'url' => helper::createLink('story', 'edit', "storyID=$story->id", '', true), 'size' => '95%'); + if(common::hasPriv('story', 'change') and $this->story->isClickable($story, 'change')) $menu[] = array('label' => $this->lang->story->change, 'icon' => 'alter', 'url' => helper::createLink('story', 'change', "storyID=$story->id", '', true), 'size' => '95%'); + if(common::hasPriv('story', 'review') and $this->story->isClickable($story, 'review')) $menu[] = array('label' => $this->lang->story->review, 'icon' => 'search', 'url' => helper::createLink('story', 'review', "storyID=$story->id", '', true), 'size' => '95%'); + if(common::hasPriv('task', 'create') and $toTaskPriv) $menu[] = array('label' => $this->lang->execution->wbs, 'icon' => 'plus', 'url' => helper::createLink('task', 'create', "executionID=$executionID&storyID=$story->id&moduleID=$story->module", '', true), 'size' => '95%'); + if(common::hasPriv('task', 'batchCreate') and $toTaskPriv) $menu[] = array('label' => $this->lang->execution->batchWBS, 'icon' => 'pluses', 'url' => helper::createLink('task', 'batchCreate', "executionID=$executionID&storyID=$story->id&moduleID=0&taskID=0&iframe=true", '', true), 'size' => '95%'); + if(common::hasPriv('story', 'activate') and $this->story->isClickable($story, 'activate')) $menu[] = array('label' => $this->lang->story->activate, 'icon' => 'magic', 'url' => helper::createLink('story', 'activate', "storyID=$story->id", '', true)); + if(common::hasPriv('execution', 'unlinkStory')) $menu[] = array('label' => $this->lang->execution->unlinkStory, 'icon' => 'unlink', 'url' => helper::createLink('execution', 'unlinkStory', "executionID=$executionID&storyID=$story->story&confirm=no", '', true)); + + $menus[$story->id] = $menu; + } + break; + case 'bug': + if(!isset($this->bug)) $this->loadModel('bug'); + + foreach($objects as $bug) + { + $menu = array(); + + if(common::hasPriv('bug', 'edit') and $this->bug->isClickable($bug, 'edit')) $menu[] = array('label' => $this->lang->bug->edit, 'icon' => 'edit', 'url' => helper::createLink('bug', 'edit', "bugID=$bug->id", '', true), 'size' => '95%'); + if(common::hasPriv('bug', 'confirmBug') and $this->bug->isClickable($bug, 'confirmBug')) $menu[] = array('label' => $this->lang->bug->confirmBug, 'icon' => 'ok', 'url' => helper::createLink('bug', 'confirmBug', "bugID=$bug->id", '', true)); + if(common::hasPriv('bug', 'resolve') and $this->bug->isClickable($bug, 'resolve')) $menu[] = array('label' => $this->lang->bug->resolve, 'icon' => 'checked', 'url' => helper::createLink('bug', 'resolve', "bugID=$bug->id", '', true)); + if(common::hasPriv('bug', 'close') and $this->bug->isClickable($bug, 'close')) $menu[] = array('label' => $this->lang->bug->close, 'icon' => 'plus', 'url' => helper::createLink('bug', 'close', "bugID=$bug->id", '', true)); + if(common::hasPriv('bug', 'create') and $this->bug->isClickable($bug, 'create')) $menu[] = array('label' => $this->lang->bug->copy, 'icon' => 'pluses', 'url' => helper::createLink('bug', 'create', "productID=$bug->product&branch=$bug->branch&extras=bugID=$bug->id", '', true), 'size' => '95%'); + if(common::hasPriv('bug', 'activate') and $this->bug->isClickable($bug, 'activate')) $menu[] = array('label' => $this->lang->bug->activate, 'icon' => 'magic', 'url' => helper::createLink('bug', 'activate', "bugID=$bug->id", '', true)); + if(common::hasPriv('story', 'create') and $bug->status != 'closed') $menu[] = array('label' => $this->lang->bug->toStory, 'icon' => 'unlink', 'url' => helper::createLink('story', 'create', "product=$bug->product&branch=$bug->branch&module=0&story=0&execution=0&bugID=$bug->id", '', true), 'size' => '95%'); + + $menus[$bug->id] = $menu; + } + break; + case 'task': + if(!isset($this->task)) $this->loadModel('task'); + + foreach($objects as $task) + { + $menu = array(); + + if(common::hasPriv('task', 'edit') and $this->task->isClickable($task, 'edit')) $menu[] = array('label' => $this->lang->task->edit, 'icon' => 'edit', 'url' => helper::createLink('task', 'edit', "taskID=$task->id", '', true), 'size' => '95%'); + if(common::hasPriv('task', 'pause') and $this->task->isClickable($task, 'pause')) $menu[] = array('label' => $this->lang->task->pause, 'icon' => 'ok', 'url' => helper::createLink('task', 'pause', "taskID=$task->id", '', true)); + if(common::hasPriv('task', 'restart') and $this->task->isClickable($task, 'restart')) $menu[] = array('label' => $this->lang->task->restart, 'icon' => 'play', 'url' => helper::createLink('task', 'restart', "taskID=$task->id", '', true)); + if(common::hasPriv('task', 'recordEstimate') and $this->task->isClickable($task, 'recordEstimate')) $menu[] = array('label' => $this->lang->task->recordEstimate, 'icon' => 'time', 'url' => helper::createLink('task', 'recordEstimate', "taskID=$task->id", '', true)); + if(common::hasPriv('task', 'activate') and $this->task->isClickable($task, 'activate')) $menu[] = array('label' => $this->lang->task->activate, 'icon' => 'magic', 'url' => helper::createLink('task', 'activate', "taskID=$task->id", '', true)); + if(common::hasPriv('task', 'batchCreate') and $this->task->isClickable($task, 'batchCreate')) $menu[] = array('label' => $this->lang->task->children, 'icon' => 'split', 'url' => helper::createLink('task', 'batchCreate', "execution=$task->execution&storyID=$task->story&moduleID=$task->module&taskID=$task->id", '', true), 'size' => '95%'); + if(common::hasPriv('task', 'create') and $this->task->isClickable($task, 'create')) $menu[] = array('label' => $this->lang->task->copy, 'icon' => 'copy', 'url' => helper::createLink('task', 'create', "projctID=$task->execution&storyID=$task->story&moduleID=$task->module&taskID=$task->id", '', true), 'size' => '95%'); + if(common::hasPriv('task', 'cancel') and $this->task->isClickable($task, 'cancel')) $menu[] = array('label' => $this->lang->task->cancel, 'icon' => 'ban-circle', 'url' => helper::createLink('task', 'cancel', "taskID=$task->id", '', true)); + + $menus[$task->id] = $menu; + } + break; + } + return $menus; + } +} diff --git a/module/kanban/view/setcolumn.html.php b/module/kanban/view/setcolumn.html.php new file mode 100644 index 0000000000..002ca7fef5 --- /dev/null +++ b/module/kanban/view/setcolumn.html.php @@ -0,0 +1,52 @@ + + * @package kanban + * @version $Id: setcolumn.html.php 935 2021-10-26 16:24:24Z xiawenlong@easycorp.ltd $ + * @link https://www.zentao.net + */ +?> + +
    +
    +
    +

    + " . $title . '';?> +

    +
    +
    + + + + + + + + + + + + + +
    kanban->columnName;?> + name, "class='form-control'");?> +
    kanban->columnColor;?> + color, "class='form-control'");?> + +
    + +
    +
    +
    +
    + diff --git a/module/kanban/view/setlane.html.php b/module/kanban/view/setlane.html.php new file mode 100644 index 0000000000..980c88f6f3 --- /dev/null +++ b/module/kanban/view/setlane.html.php @@ -0,0 +1,58 @@ + + * @package kanban + * @version $Id: setlane.html.php 935 2021-10-26 16:24:24Z liyuchun@easycorp.ltd $ + * @link https://www.zentao.net + */ +?> + +
    +
    +
    +

    + " . $title . '';?> +

    +
    +
    + + + + + + + + + + + + + + + + + +
    kanban->laneName;?> + name, "class='form-control'");?> +
    kanban->WIPType;?> + kanban->laneTypeList, $lane->type), "class='form-control' disabled");?> +
    kanban->laneColor;?> + color, "class='form-control'");?> + +
    + +
    +
    +
    +
    + diff --git a/module/kanban/view/setwip.html.php b/module/kanban/view/setwip.html.php new file mode 100644 index 0000000000..512a50c232 --- /dev/null +++ b/module/kanban/view/setwip.html.php @@ -0,0 +1,69 @@ + + * @package kanban + * @version $Id: setwip.html.php 935 2021-10-25 10:56:24Z liyuchun@easycorp.ltd $ + * @link https://www.zentao.net + */ +?> + + +
    +
    +
    +

    + " . $title . '';?> +

    +
    +
    + + parent != -1):?> + + + + + + laneType == 'story'):?> + + + + + + + + + + + + + +
    kanban->WIPStatus;?> + kanban->{$column->laneType . 'Column'}, $column->type, ''), "class='form-control' disabled");?> +
    kanban->WIPStage;?> + kanban->storyColumnStageList, $column->type);?> + story->stageList, $stage), "class='form-control' disabled");?> +
    kanban->WIPCount;?> +
    + limit == -1 ? 'disabled' : '';?> + limit != -1 ? $column->limit : '', "class='form-control' $attr");?> +
    +
    + limit, "class='form-control'");?> + +
    + limit == -1 ? 'checked' : '';?>/> + +
    +
    +
    +
    + +
    +
    +
    +
    + diff --git a/module/mail/model.php b/module/mail/model.php index 67ef56e317..4ed410325f 100644 --- a/module/mail/model.php +++ b/module/mail/model.php @@ -266,41 +266,47 @@ class mailModel extends model * @param string $body * @param array $ccList * @param bool $includeMe + * @param array $emails * @access public * @return void */ - public function send($toList, $subject, $body = '', $ccList = '', $includeMe = false) + public function send($toList, $subject, $body = '', $ccList = '', $includeMe = false, $emails = array()) { if(!$this->config->mail->turnon) return; if(!empty($this->config->mail->async)) return $this->addQueue($toList, $subject, $body, $ccList, $includeMe); ob_start(); - $toList = $toList ? explode(',', str_replace(' ', '', $toList)) : array(); - $ccList = $ccList ? explode(',', str_replace(' ', '', $ccList)) : array(); - /* Process toList and ccList, remove current user from them. If toList is empty, use the first cc as to. */ - if($includeMe == false) + if(empty($emails)) { - $account = isset($this->app->user->account) ? $this->app->user->account : ''; + $toList = $toList ? explode(',', str_replace(' ', '', $toList)) : array(); + $ccList = $ccList ? explode(',', str_replace(' ', '', $ccList)) : array(); - foreach($toList as $key => $to) if(trim($to) == $account or !trim($to)) unset($toList[$key]); - foreach($ccList as $key => $cc) if(trim($cc) == $account or !trim($cc)) unset($ccList[$key]); + /* Process toList and ccList, remove current user from them. If toList is empty, use the first cc as to. */ + if($includeMe == false) + { + $account = isset($this->app->user->account) ? $this->app->user->account : ''; + + foreach($toList as $key => $to) if(trim($to) == $account or !trim($to)) unset($toList[$key]); + foreach($ccList as $key => $cc) if(trim($cc) == $account or !trim($cc)) unset($ccList[$key]); + } + + /* Remove deleted users. */ + $users = $this->loadModel('user')->getPairs('nodeleted|all'); + $blockUsers = isset($this->config->message->blockUser) ? explode(',', $this->config->message->blockUser) : array(); + foreach($toList as $key => $to) if(!isset($users[trim($to)]) or in_array(trim($to), $blockUsers)) unset($toList[$key]); + foreach($ccList as $key => $cc) if(!isset($users[trim($cc)]) or in_array(trim($cc), $blockUsers)) unset($ccList[$key]); + + if(!$toList and !$ccList) return; + if(!$toList and $ccList) $toList = array(array_shift($ccList)); + $toList = join(',', $toList); + $ccList = join(',', $ccList); + + /* Get realname and email of users. */ + $this->loadModel('user'); + $emails = $this->user->getRealNameAndEmails(str_replace(' ', '', $toList . ',' . $ccList)); } - /* Remove deleted users. */ - $users = $this->loadModel('user')->getPairs('nodeleted|all'); - foreach($toList as $key => $to) if(!isset($users[trim($to)])) unset($toList[$key]); - foreach($ccList as $key => $cc) if(!isset($users[trim($cc)])) unset($ccList[$key]); - - if(!$toList and !$ccList) return; - if(!$toList and $ccList) $toList = array(array_shift($ccList)); - $toList = join(',', $toList); - $ccList = join(',', $ccList); - - /* Get realname and email of users. */ - $this->loadModel('user'); - $emails = $this->user->getRealNameAndEmails(str_replace(' ', '', $toList . ',' . $ccList)); - $this->clear(); /* Replace full webPath image for mail. */ @@ -691,7 +697,7 @@ class mailModel extends model { if($object->contentType == 'markdown') { - $object->content = $this->app->loadClass('hyperdown')->makeHtml($object->content); + $object->content = commonModel::processMarkdown($object->content); $object->content = str_replace("", "
    ", $object->content); $object->content = str_replace(" + + + + diff --git a/module/my/control.php b/module/my/control.php index 36e5c00177..949d479cca 100644 --- a/module/my/control.php +++ b/module/my/control.php @@ -147,8 +147,17 @@ class my extends control /* Append id for secend sort. */ $sort = $this->loadModel('common')->appendOrder($orderBy); + $todos = $this->loadModel('todo')->getList($type, $account, $status, 0, $pager, $sort); + $tasks = $this->loadModel('task')->getUserSuspendedTasks($account); + foreach($todos as $key => $todo) + { + if($todo->type == 'task' and isset($tasks[$todo->idvalue])) unset($todos[$key]); + } + + $pager->recTotal = count($todos); + /* Assign. */ - $this->view->todos = $this->loadModel('todo')->getList($type, $account, $status, 0, $pager, $sort); + $this->view->todos = $todos; $this->view->date = (int)$type == 0 ? date(DT_DATE1) : date(DT_DATE1, strtotime($type)); $this->view->type = $type; $this->view->recTotal = $recTotal; diff --git a/module/my/view/dynamic.html.php b/module/my/view/dynamic.html.php index de3a76491f..5546dd3f2b 100644 --- a/module/my/view/dynamic.html.php +++ b/module/my/view/dynamic.html.php @@ -60,7 +60,7 @@ objectType == 'meeting') $tab = $action->project ? "data-app='project'" : "data-app='my'";?> maxVersion) and strpos($config->action->assetType, $action->objectType) !== false) or empty($action->objectName)) + if((isset($config->maxVersion) and strpos($config->action->assetType, $action->objectType) !== false) and empty($action->objectName)) { echo '#' . $action->objectID; } diff --git a/module/my/view/execution.html.php b/module/my/view/execution.html.php index 59903afb1d..5fe12eb12b 100644 --- a/module/my/view/execution.html.php +++ b/module/my/view/execution.html.php @@ -63,7 +63,9 @@ ?> systemMode == 'new'):?> - + diff --git a/module/my/view/task.html.php b/module/my/view/task.html.php index dc4d1c5d5f..3fa6265c48 100644 --- a/module/my/view/task.html.php +++ b/module/my/view/task.html.php @@ -166,7 +166,7 @@ systemMode == 'new'):?> diff --git a/module/my/view/testcase.html.php b/module/my/view/testcase.html.php index 1fc94723f8..ca9fd9f131 100644 --- a/module/my/view/testcase.html.php +++ b/module/my/view/testcase.html.php @@ -116,7 +116,7 @@ createLink('testcase', 'batchEdit'); + $actionLink = $this->createLink('testcase', 'batchEdit', "productID=0&branch=all"); $misc = "data-form-action='$actionLink'"; echo html::commonButton($lang->edit, $misc); } diff --git a/module/personnel/model.php b/module/personnel/model.php index 6f0e94e0c0..da8114ed23 100644 --- a/module/personnel/model.php +++ b/module/personnel/model.php @@ -607,7 +607,7 @@ class personnelModel extends model if($oldWhitelist) $accounts = array_unique(array_merge($accounts, $oldWhitelist)); } } - $whitelist = ',' . implode(',', $accounts); + $whitelist = !empty($accounts) ? ',' . implode(',', $accounts) : ''; $this->dao->update($objectTable)->set('whitelist')->eq($whitelist)->where('id')->eq($objectID)->exec(); $deletedAccounts = array(); diff --git a/module/product/control.php b/module/product/control.php index 2c1e861032..c1d38e293b 100644 --- a/module/product/control.php +++ b/module/product/control.php @@ -218,10 +218,17 @@ class product extends control $this->lang->datatable->showBranch = sprintf($this->lang->datatable->showBranch, $this->lang->product->branchName[$product->type]); } - /* Get stories. */ + /* Get stories and branches. */ if($this->app->rawModule == 'projectstory') { - if(!empty($product)) $this->session->set('currentProductType', $product->type); + $branches = array(); + if(!empty($product)) + { + $this->session->set('currentProductType', $product->type); + $productBranches = $product->type != 'normal' ? $this->loadModel('execution')->getBranchByProduct($product->id, $projectID) : array(); + $branches = isset($productBranches[$product->id]) ? $productBranches[$product->id] : array(); + } + $this->products = $this->product->getProducts($projectID, 'all', '', false); $projectProducts = $this->product->getProducts($projectID); $productPlans = $this->execution->getPlans($projectProducts); @@ -231,7 +238,7 @@ class product extends control } else { - $branchID = $browseType == 'bymodule' ? 'all' : $branchID; + $branches = $this->loadModel('branch')->getPairs($productID); $stories = $this->product->getStories($productID, $branchID, $browseType, $queryID, $moduleID, $storyType, $sort, $pager); } @@ -301,7 +308,7 @@ class product extends control $this->view->moduleName = ($moduleID and $moduleID !== 'all') ? $this->tree->getById($moduleID)->name : $this->lang->tree->all; $this->view->branch = $branch; $this->view->branchID = $branchID; - $this->view->branches = $this->loadModel('branch')->getPairs($productID); + $this->view->branches = $branches; $this->view->storyStages = $this->product->batchGetStoryStage($stories); $this->view->setModule = true; $this->view->storyTasks = $storyTasks; @@ -727,6 +734,7 @@ class product extends control $this->view->users = $this->user->getPairs('noletter'); $this->view->groups = $this->loadModel('group')->getPairs(); $this->view->branches = $this->loadModel('branch')->getPairs($productID); + $this->view->reviewers = explode(',', $product->reviewer); $this->display(); } @@ -973,65 +981,25 @@ class product extends control * @param int $planID * @param bool $needCreate * @param string $expired - * @param string $from * @param string $param * @access public * @return void */ - public function ajaxGetPlans($productID, $branch = 0, $planID = 0, $fieldID = '', $needCreate = false, $expired = '', $from = '', $param = '') + public function ajaxGetPlans($productID, $branch = 0, $planID = 0, $fieldID = '', $needCreate = false, $expired = '', $param = '') { - $this->loadModel('productplan'); - if($from == 'story' and $branch == BRANCH_MAIN) + $param = strtolower($param); + $plans = $this->loadModel('productplan')->getPairs($productID, $branch, $expired, strpos($param, 'skipparent') !== false); + $field = $fieldID ? "plans[$fieldID]" : 'plan'; + $output = ''; + $output .= html::select($field, $plans, $planID, "class='form-control chosen'"); + if(count($plans) == 1 and $needCreate and $needCreate !== 'false') { - $plans = $this->productplan->getPairs($productID); - } - elseif($from == 'story' and $branch) - { - $plans = $this->productplan->getPairs($productID, 0); - $plans += $this->productplan->getPairs($productID, $branch); - } - else - { - $plans = $this->productplan->getPairs($productID, $branch, $expired); - } - - $field = $fieldID ? "plans[$fieldID]" : 'plan'; - - $output = ''; - if(strpos($param, 'batchEdit') !== false) - { - $output = "
    "; - $output .= "
    "; - $output .= html::select($field, $plans, $planID, "class='form-control chosen'"); - $output .= "
    "; - if(count($plans) == 1 and $needCreate) - { - $output .= "
    "; - $output .= "
    "; - $output .= html::a($this->createLink('productplan', 'create', "productID=$productID&branch=$branch", '', true), "", '', "class='btn btn-icon' data-toggle='modal' data-type='iframe' data-width='95%' title='{$this->lang->productplan->create}'"); - $output .= '
    '; - $output .= '
    '; - $output .= "
    "; - $output .= "
    "; - $output .= html::a("javascript:void(0)", "", '', "class='btn btn-icon refresh' data-toggle='tooltip' title='{$this->lang->refresh}' onclick='loadProductPlans($productID)'"); - $output .= '
    '; - $output .= '
    '; - } - $output .= "
    "; - } - else - { - $output .= html::select($field, $plans, $planID, "class='form-control chosen'"); - if(count($plans) == 1 and $needCreate) - { - $output .= "
    "; - $output .= html::a($this->createLink('productplan', 'create', "productID=$productID&branch=$branch", '', true), "", '', "class='btn btn-icon' data-toggle='modal' data-type='iframe' data-width='95%' title='{$this->lang->productplan->create}'"); - $output .= '
    '; - $output .= "
    "; - $output .= html::a("javascript:void(0)", "", '', "class='btn btn-icon refresh' data-toggle='tooltip' title='{$this->lang->refresh}' onclick='loadProductPlans($productID)'"); - $output .= '
    '; - } - + $output .= "
    "; + $output .= html::a($this->createLink('productplan', 'create', "productID=$productID&branch=$branch", '', true), "", '', "class='btn btn-icon' data-toggle='modal' data-type='iframe' data-width='95%' title='{$this->lang->productplan->create}'"); + $output .= '
    '; + $output .= "
    "; + $output .= html::a("javascript:void(0)", "", '', "class='btn btn-icon refresh' data-toggle='tooltip' title='{$this->lang->refresh}' onclick='loadProductPlans($productID)'"); + $output .= '
    '; } die($output); } @@ -1053,6 +1021,34 @@ class product extends control if(!$productID) die(html::select('line', array('' => '') + $lines, '', "class='form-control chosen'")); } + /** + * Ajax get reviewers. + * + * @param int $productID + * @param int $storyID + * @access public + * @return void + */ + public function ajaxGetReviewers($productID, $storyID = 0) + { + /* Get product reviewers. */ + $product = $this->product->getByID($productID); + $productReviewers = $product->reviewer; + if(!$productReviewers) $productReviewers = $this->loadModel('user')->getProductViewListUsers($product, '', '', ''); + + $storyReviewers = ''; + if($storyID) + { + $story = $this->loadModel('story')->getByID($storyID); + $storyReviewers = $this->story->getReviewerPairs($story->id, $story->version); + $storyReviewers = implode(',', array_keys($storyReviewers)); + } + + $reviewers = $this->loadModel('user')->getPairs('noclosed|nodeleted', $storyReviewers, 0, $productReviewers); + + die(html::select("reviewer[]", $reviewers, $storyReviewers, "class='form-control chosen' multiple")); + } + /** * Drop menu page. * @@ -1221,6 +1217,7 @@ class product extends control $kanbanGroup = $this->product->getStats4Kanban(); extract($kanbanGroup); + $programPairs = $this->loadModel('program')->getPairs(true); $myProducts = array(); $otherProducts = array(); foreach($productList as $productID => $product) @@ -1242,7 +1239,7 @@ class product extends control $this->view->title = $this->lang->product->kanban; $this->view->kanbanList = $kanbanList; - $this->view->programList = array(0 => $this->lang->product->emptyProgram) + $programList; + $this->view->programList = array(0 => $this->lang->product->emptyProgram) + $programPairs; $this->view->productList = $productList; $this->view->planList = $planList; $this->view->projectList = $projectList; @@ -1268,8 +1265,6 @@ class product extends control if($_POST) { $this->product->manageLine(); - if(dao::isError()) die(js::error(dao::getError())); - die(js::reload('parent')); } diff --git a/module/product/css/all.css b/module/product/css/all.css index 72833ae025..81340e23ba 100644 --- a/module/product/css/all.css +++ b/module/product/css/all.css @@ -1,3 +1,6 @@ +@media screen and (min-width: 2048px) {.container {max-width: 2000px!important;}} +@media screen and (min-width: 2560px) {.container {max-width: 2380px!important;}} + .side-col .detail-content {padding-left: 10px; margin-top: 0px;} .tree li.has-list.open:before {border-left: none;} #programTree li {padding: 0 0 0 8px;} @@ -34,23 +37,51 @@ .side-col {padding-right: 20px; width: 18%;} #sidebar>.cell {width: 100%;} -.main-table thead>tr:first-of-type>th {border-left: 1px solid #ddd;} -th.table-nest-title .nest-has-checkbox {margin-top: 7px; margin-left: 35px;} -th.table-nest-title .nest-none-checkbox {margin-top: 8px; margin-left: 5px;} -th.table-nest-title .header {margin-left: 5px;} -th.table-nest-title .sort-up {padding-left: 5px;} -th.table-nest-title .sort-down {padding-left: 5px;} -th.table-nest-title .table-nest-toggle {top: 12px; opacity: .6;} -th.table-nest-title .table-nest-toggle:hover {opacity: 1;} +th.c-name {width: 400px;} +th.c-requirement {width: 290px;} +th.c-story {width: 290px;} +th.c-bug {width: 200px;} +th.c-plan {width: 45px;} +th.c-release {width: 50px;} +th.c-actions {width: 60px;} +[lang='en'] .en-wrap-text {white-space: normal; height: 24px; line-height: 1; font-size: 12px;} -#productTableList .icon-product:before {content: '\e98f'; width: 22px; height: 22px; background: none; color: rgb(166, 170, 184); top: 0; line-height: 22px; margin-right: 2px; font-size: 14px;} -.table.has-sort-head thead>tr>th>a:after, .table.has-sort-head thead>tr>th>a:before {top: -3px;} -.main-table thead>tr>th {padding: 0px 8px; line-height: 24px;} +#productListForm thead>tr>th, +#productListForm tbody>tr>td {text-align: center; text-overflow: unset!important} +#productListForm thead>tr>th {padding: 0; line-height: 24px;} +#productListForm thead>tr:first-child>th {border-left: 1px solid #ddd;} +#productListForm thead>tr>th.c-checkbox {border-left: none; padding-left: 15px; width: 45px;} +#productListForm tbody>tr>td {padding: 2px 4px;} +#productListForm tbody>tr>td:first-child {text-align: left; padding-left: 15px;} +#productListForm th.table-nest-title .nest-has-checkbox {margin-top: 7px; margin-left: 35px;} +#productListForm th.table-nest-title .nest-none-checkbox {margin-top: 8px; margin-left: 5px;} +#productListForm th.table-nest-title .header {margin-left: 5px;} +#productListForm th.table-nest-title .sort-up {padding-left: 5px;} +#productListForm th.table-nest-title .sort-down {padding-left: 5px;} +#productListForm th.table-nest-title .table-nest-toggle {top: 12px; opacity: .6;} +#productListForm th.table-nest-title .table-nest-toggle:hover {opacity: 1;} +#productListForm .table.has-sort-head thead>tr>th>a:after, +#productListForm .table.has-sort-head thead>tr>th>a:before {top: -3px;} +#productListForm .icon-product:before {content: '\e98f'; width: 22px; height: 22px; background: none; color: rgb(166, 170, 184); top: 0; line-height: 22px; margin-right: 2px; font-size: 14px;} -.icon-move {color: #16a8f8;} -.c-name {text-overflow: unset !important; width: 110px !important;} -.c-checkbox {width: 45px;} -.c-story {width: 300px;} -.c-bug {width: 200px;} -.c-plan, .c-release {width: 60px;} +@media screen and (max-width: 1460px) +{ + th.c-name {width: auto;} + [lang^='zh-'] th.c-requirement {width: 260px;} + [lang^='zh-'] th.c-story {width: 260px;} +} +@media screen and (max-width: 1300px) +{ + [lang^='zh-'] th.c-requirement {width: 240px;} + [lang^='zh-'] th.c-story {width: 240px;} +} + +@media screen and (max-width: 1250px) +{ + #productListForm thead>tr>th {font-size: 12px;} + th.c-requirement {width: 265px;} + th.c-story {width: 265px;} + [lang^='zh-'] th.c-requirement {width: 220px;} + [lang^='zh-'] th.c-story {width: 220px;} +} diff --git a/module/product/css/view.css b/module/product/css/view.css index dd295ad7de..e5c7c817ad 100644 --- a/module/product/css/view.css +++ b/module/product/css/view.css @@ -1,10 +1,10 @@ .branch-list {padding-left: 0; margin: 0;} .branch-list > li {float: left; list-style: none; width: 50%; margin: 5px 0;} -.data-basic.table-data tbody > tr > th {width: 100px;} +.table-data tbody > tr > th {width: 110px;} .col-12 .cell table td {word-wrap: break-word; word-break: normal;} td.normal {color: #00da88;} td.acl {white-space: nowrap;} .row > .col-sm-12:first-child {padding-bottom: 20px;} .c-product ,.c-release ,.c-code {width: 100px !important;} -.c-openedBy ,.c-acl ,.c-prs ,.c-common {width:110px !important;} -.c-bugs ,.c-type {width:120px !important;} +.c-openedBy ,.c-acl ,.c-common {width:110px !important;} +.c-bugs {width:120px !important;} diff --git a/module/product/js/all.js b/module/product/js/all.js index f9b590b91e..aa33a70d54 100644 --- a/module/product/js/all.js +++ b/module/product/js/all.js @@ -2,14 +2,13 @@ $("#" + browseType + "Tab").addClass('btn-active-text'); $(function() { /* Init table sort. */ - var $list = $('#productTableList'); - $list.addClass('sortable').sortable( + $('#productTableList').addClass('sortable').sortable( { /* Init vars. */ reverse: orderBy === 'order_desc', selector: 'tr', dragCssClass: 'drag-row', - trigger: $list.find('.sort-handler').length ? '.sort-handler' : null, + trigger: '.sort-handler', /* Set movable conditions. */ canMoveHere: function($ele, $target) @@ -35,59 +34,53 @@ $(function() } }); - /* Update program checkboxes */ + /* Update parent checkbox */ + function updatePrarentCheckbox($parent) + { + var $row = $parent.closest('tr'); + var $checkbox = $row.find('.program-checkbox'); + var rowID = $row.data('id'); + var $subRows = $('#productTableList').children('.row-product[data-nest-path^="' + rowID + ',"],.row-product[data-nest-path*=",' + rowID + ',"]'); + var allCount = $subRows.length; + var selectedCount = $subRows.find('input:checkbox:checked').length; + var isAllChecked = allCount > 0 && allCount === selectedCount; + $checkbox.toggleClass('checked', isAllChecked) + .toggleClass('indeterminate', selectedCount > 0 && selectedCount < allCount); + $row.toggleClass('checked', isAllChecked); + } + + /* Update checkboxes */ function updateCheckboxes() { - var $tbody = $('#productTableList'); - $tbody.find('.program-checkbox').each(function() + $('#productTableList').children('.row-program,.row-line').each(function() { - var $checkbox = $(this); - var $tr = $checkbox.closest('tr'); - var rowID = $tr.data('id'); - if($tbody.find('tr[data-parent="' + rowID + '"] .program-checkbox').length > 0) - { - var notCheckedCount = 0; - $tbody.find('tr[data-parent="' + rowID + '"]').each(function() - { - if($(this).find('.program-checkbox').length > 0) - { - /* Get lines input length. */ - var lineRowID = $(this).data('id'); - notCheckedCount = $tbody.find('tr[data-parent="' + lineRowID + '"] input:checkbox:not(:checked)').length + notCheckedCount; - } - else - { - notCheckedCount = $(this).find('input:checkbox:not(:checked)').length + notCheckedCount; - } - }); - - var isAllRowChecked = !notCheckedCount; - } - else - { - var isAllRowChecked = !$tbody.find('tr[data-parent="' + rowID + '"] input:checkbox:not(:checked)').length; - } - $checkbox.toggleClass('checked', isAllRowChecked); + updatePrarentCheckbox($(this)) }); } - $('#productTableList').on('click', '.program-checkbox', function() + $('#productTableList').on('click', '.row-program,.row-line', function(e) { - var $checkbox = $(this).toggleClass('checked'); - var $tr = $checkbox.closest('tr'); - var rowID = $tr.data('id'); - var checked = $checkbox.hasClass('checked'); - $('#productTableList').children('tr').each(function() + if($(e.target).closest('.table-nest-toggle,a').length) return; + + var $row = $(this); + var $checkbox = $row.find('.program-checkbox').toggleClass('checked').removeClass('indeterminate'); + var isChecked = $checkbox.hasClass('checked'); + var rowID = $row.data('id'); + var $subRows = $('#productTableList').children('tr[data-nest-path^="' + rowID + ',"],tr[data-nest-path*=",' + rowID + ',"]'); + $row.toggleClass('checked', isChecked); + $subRows.toggleClass('checked', isChecked); + $subRows.find('input:checkbox').prop('checked', isChecked); + $subRows.find('.program-checkbox').toggleClass('checked', isChecked).removeClass('indeterminate'); + + var parentID = $row.attr('data-parent'); + if(parentID && parentID !== '0') { - var $tr = $(this); - var nestPath = $tr.attr('data-nest-path'); - if(!nestPath) return; - if(!nestPath.split(',').includes(rowID)) return; - var $checkbox = $tr.find('input:checkbox'); - if($checkbox.length) $checkbox.prop('checked', checked); - else $tr.find('.program-checkbox').toggleClass('checked', checked); - }); + updatePrarentCheckbox($('#productTableList>tr[data-id="' + parentID + '"]')); + } }); $('#productListForm').on('checkChange', updateCheckboxes); updateCheckboxes(); + + /* Disable animation for large rows */ + $('#productListForm').toggleClass('no-animation', $('#productTableList>tr').length > 40) }); diff --git a/module/product/js/browse.js b/module/product/js/browse.js index db8128724a..cbbc03ed83 100644 --- a/module/product/js/browse.js +++ b/module/product/js/browse.js @@ -111,6 +111,42 @@ $(function() $("#sidebarHeader").toggle("fast"); }); if($("main").is(".hide-sidebar")) $("#sidebarHeader").hide(); + + /* Shift key selection. */ + var isClickStoryToggle = false; + var lastStorySelected = ''; + $(".story-toggle").click(function() + { + isClickStoryToggle = true; + }); + $("#storyList tbody").on("click","tr",function(e) + { + var nowCheckbox = $(this)['context'].cells[0].childNodes[0].childNodes[0]; + if(e.shiftKey) + { + nowStorySelected = nowCheckbox.value; + if(lastStorySelected != '' && nowStorySelected != '' && lastStorySelected != nowStorySelected) + { + var isStartStorySelected = false; + var isEndStorySelected = false; + $("input[name^='storyIdList']").each(function() + { + isEndStorySelected = ((nowStorySelected == $(this).val() || lastStorySelected == $(this).val()) && isStartStorySelected ) || isEndStorySelected ? true : false; + + $(this)['context'].checked = $(this)['context'].checked || (isStartStorySelected && !isEndStorySelected) ? true : false; + + isStartStorySelected = nowStorySelected == $(this).val() || lastStorySelected == $(this).val() || isStartStorySelected ? true : false; + + if(isEndStorySelected) return; + }); + } + } + else if(!isClickStoryToggle) + { + lastStorySelected = nowCheckbox.value; + } + isClickStoryToggle = false; + }); }); /** diff --git a/module/product/js/create.js b/module/product/js/create.js index deb9e9a06a..ba883405c5 100644 --- a/module/product/js/create.js +++ b/module/product/js/create.js @@ -61,14 +61,12 @@ function toggleLine(obj) { $('form .line-no-exist').removeClass('hidden'); $('form .line-exist').addClass('hidden'); - $('#line_chosen').addClass('hidden'); $line.attr('disabled', 'disabled'); } else { - $(' #line').removeClass('hidden'); + $('form .line-exist').removeClass('hidden'); $('form .line-no-exist').addClass('hidden'); - $('#line_chosen').removeClass('hidden'); $line.removeAttr('disabled'); } } diff --git a/module/product/lang/de.php b/module/product/lang/de.php index ca0ee39ad8..305d9a7e97 100644 --- a/module/product/lang/de.php +++ b/module/product/lang/de.php @@ -26,7 +26,7 @@ $lang->product->other = 'Andere'; $lang->product->closed = 'Geschlossen'; $lang->product->updateOrder = 'Ranking'; $lang->product->orderAction = "Rank {$lang->productCommon}"; -$lang->product->all = 'Alle' . $lang->productCommon; +$lang->product->all = "{$lang->productCommon} List"; $lang->product->manageLine = "Manage {$lang->productCommon} Line"; $lang->product->newLine = "Create {$lang->productCommon} Line"; $lang->product->export = 'Exportiere Daten'; diff --git a/module/product/lang/en.php b/module/product/lang/en.php index 830906d142..386387d4d6 100644 --- a/module/product/lang/en.php +++ b/module/product/lang/en.php @@ -24,7 +24,7 @@ $lang->product->mine = 'Mine'; $lang->product->other = 'Others'; $lang->product->closed = 'Closed'; $lang->product->updateOrder = 'Order'; -$lang->product->all = 'All' . $lang->productCommon; +$lang->product->all = "{$lang->productCommon} List"; $lang->product->manageLine = "Manage {$lang->productCommon} Line"; $lang->product->newLine = "Create {$lang->productCommon} Line"; $lang->product->export = 'Export'; @@ -85,7 +85,6 @@ $lang->product->programChangeTip = "The projects linked with this {$lang->pr $lang->product->notChangeProgramTip = "The {$lang->SRCommon} of {$lang->productCommon} has been linked to the following projects, please cancel the link before proceeding"; $lang->product->confirmChangeProgram = "The projects linked with this {$lang->productCommon}: %s is also linked with other products, whether to transfer projects to the modified program set."; $lang->product->changeProgramError = "The {$lang->SRCommon} of this {$lang->productCommon} has been linked to the project, please unlink it before proceeding"; -$lang->product->programEmpty = 'Program cannot be empty.'; $lang->product->id = 'ID'; $lang->product->program = "Program"; @@ -106,6 +105,7 @@ $lang->product->QD = 'QA Manager'; $lang->product->RD = 'Release Manager'; $lang->product->feedback = 'Feedback Manger'; $lang->product->acl = 'Access Control'; +$lang->product->reviewer = 'Reviewer'; $lang->product->whitelist = 'Whitelist'; $lang->product->branch = '%s'; $lang->product->qa = 'Test'; diff --git a/module/product/lang/fr.php b/module/product/lang/fr.php index 0406aac1f5..ad123f6890 100644 --- a/module/product/lang/fr.php +++ b/module/product/lang/fr.php @@ -26,7 +26,7 @@ $lang->product->other = 'Autres'; $lang->product->closed = 'Fermés'; $lang->product->updateOrder = 'Ordre'; $lang->product->orderAction = "Rang {$lang->productCommon}"; -$lang->product->all = 'All' . $lang->productCommon; +$lang->product->all = "{$lang->productCommon} List"; $lang->product->manageLine = "Manage {$lang->productCommon} Line"; $lang->product->newLine = "Create {$lang->productCommon} Line"; $lang->product->export = 'Export'; diff --git a/module/product/lang/vi.php b/module/product/lang/vi.php index 69febd5fdc..92bc5c9e25 100644 --- a/module/product/lang/vi.php +++ b/module/product/lang/vi.php @@ -26,7 +26,7 @@ $lang->product->other = 'Khác'; $lang->product->closed = 'Đã đóng'; $lang->product->updateOrder = 'Sắp xếp'; $lang->product->orderAction = "Đánh giá {$lang->productCommon}"; -$lang->product->all = 'All' . $lang->productCommon; +$lang->product->all = "{$lang->productCommon} List"; $lang->product->manageLine = "Manage {$lang->productCommon} Line"; $lang->product->newLine = "Create {$lang->productCommon} Line"; $lang->product->export = 'Xuất'; diff --git a/module/product/lang/zh-cn.php b/module/product/lang/zh-cn.php index d693e6f335..7fc4525dd2 100644 --- a/module/product/lang/zh-cn.php +++ b/module/product/lang/zh-cn.php @@ -24,7 +24,7 @@ $lang->product->mine = '我负责'; $lang->product->other = '其他'; $lang->product->closed = '已关闭'; $lang->product->updateOrder = '排序'; -$lang->product->all = "所有{$lang->productCommon}"; +$lang->product->all = "{$lang->productCommon}列表"; $lang->product->manageLine = "维护{$lang->productCommon}线"; $lang->product->newLine = "新建{$lang->productCommon}线"; $lang->product->export = '导出数据'; @@ -87,7 +87,6 @@ $lang->product->programChangeTip = "如下项目只关联了该{$lang->produ $lang->product->notChangeProgramTip = "该{$lang->productCommon}的{$lang->SRCommon}已经关联到如下项目,请取消关联后再操作"; $lang->product->confirmChangeProgram = "如下项目既关联了该{$lang->productCommon}又关联了其他{$lang->productCommon},请确认是否继续关联该{$lang->productCommon},勾选后将取消与其他{$lang->productCommon}的关联关系,同时转移至新项目集下。"; $lang->product->changeProgramError = "该{$lang->productCommon}的{$lang->SRCommon}已经关联到项目,请取消关联后再操作"; -$lang->product->programEmpty = '项目集不能为空'; $lang->product->id = '编号'; $lang->product->program = "所属项目集"; @@ -108,6 +107,7 @@ $lang->product->QD = '测试负责人'; $lang->product->RD = '发布负责人'; $lang->product->feedback = '反馈负责人'; $lang->product->acl = '访问控制'; +$lang->product->reviewer = '评审人'; $lang->product->whitelist = '白名单'; $lang->product->branch = '所属%s'; $lang->product->qa = '测试'; diff --git a/module/product/lang/zh-tw.php b/module/product/lang/zh-tw.php index 3b0d84ae8d..af74229c88 100644 --- a/module/product/lang/zh-tw.php +++ b/module/product/lang/zh-tw.php @@ -24,7 +24,7 @@ $lang->product->mine = '我負責'; $lang->product->other = '其他'; $lang->product->closed = '已關閉'; $lang->product->updateOrder = '排序'; -$lang->product->all = "所有{$lang->productCommon}"; +$lang->product->all = "{$lang->productCommon}列表"; $lang->product->manageLine = "維護{$lang->productCommon}綫"; $lang->product->newLine = "新建{$lang->productCommon}綫"; $lang->product->export = '導出數據'; diff --git a/module/product/model.php b/module/product/model.php index 9cb4082884..dad456e69e 100644 --- a/module/product/model.php +++ b/module/product/model.php @@ -567,6 +567,7 @@ class productModel extends model ->setIF($this->post->acl == 'open', 'whitelist', '') ->stripTags($this->config->product->editor->create['id'], $this->config->allowedTags) ->join('whitelist', ',') + ->join('reviewer', ',') ->remove('uid,newLine,lineName') ->get(); @@ -641,6 +642,7 @@ class productModel extends model $product = fixer::input('post') ->setDefault('line', 0) ->join('whitelist', ',') + ->join('reviewer', ',') ->stripTags($this->config->product->editor->edit['id'], $this->config->allowedTags) ->remove('uid,changeProjects') ->get(); @@ -788,13 +790,7 @@ class productModel extends model $maxOrder = $maxOrder ? $maxOrder : 0; foreach($data->modules as $id => $name) { - if(empty($name)) continue; - if($this->config->systemMode == 'new' and empty($data->programs[$id])) - { - dao::$errors[] = $this->lang->product->programEmpty; - return false; - } - + if(!$name) continue; $line->name = strip_tags(trim($name)); $line->root = $data->programs[$id]; @@ -1437,7 +1433,7 @@ class productModel extends model * @access public * @return array */ - public function getStats($orderBy = 'order_desc', $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) { $this->loadModel('report'); $this->loadModel('story'); @@ -1962,6 +1958,7 @@ class productModel extends model } else if($module == 'doc') { + if($method == 'create' or $method == 'edit') $method = 'tableContents'; $link = helper::createLink('doc', $method, "type=product&objectID=%s&from=product"); } elseif($module == 'design') diff --git a/module/product/view/ajaxgetdropmenu.html.php b/module/product/view/ajaxgetdropmenu.html.php index 67954d3a17..fd55464514 100644 --- a/module/product/view/ajaxgetdropmenu.html.php +++ b/module/product/view/ajaxgetdropmenu.html.php @@ -30,6 +30,8 @@ a.productName:focus, a.productName:hover {background: #0c64eb; color: #fff !impo #swapper li > a {padding-top: 4px; padding-bottom: 4px;} #swapper li {padding-top: 0; padding-bottom: 0;} #swapper .tree li>.list-toggle {top: -1px;} + +#subHeader .tree ul {display: block;} -
    ", "", $object->content); $object->content = str_replace("", "", $object->content); @@ -741,7 +747,12 @@ class mailModel extends model } else { - $sendUsers = $this->{$objectType}->getToAndCcList($object); + $sendUsers = $this->{$objectType}->getToAndCcList($object); + } + + if($objectType == 'release' and strpos(",{$object->notify},", ',FB,') !== false) + { + $this->release->sendMail2Feedback($object, $subject); } if(!$sendUsers) return; diff --git a/module/message/control.php b/module/message/control.php index dc63edc9d6..f791f5e9aa 100644 --- a/module/message/control.php +++ b/module/message/control.php @@ -84,7 +84,9 @@ class message extends control { $data = fixer::input('post')->get(); $data->messageSetting = !empty($data->messageSetting) ? json_encode($data->messageSetting) : ''; + $data->blockUser = !empty($data->blockUser) ? implode(',', $data->blockUser) : ''; $this->loadModel('setting')->setItem('system.message.setting', $data->messageSetting); + $this->loadModel('setting')->setItem('system.message.blockUser', $data->blockUser); return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => 'reload')); } @@ -95,7 +97,7 @@ class message extends control $this->view->position[] = $this->lang->message->common; $this->view->position[] = $this->lang->message->setting; - $users = $this->loadModel('user')->getPairs('noletter'); + $users = $this->loadModel('user')->getPairs('noletter,noclosed'); unset($users['']); $this->view->users = $users; diff --git a/module/message/lang/de.php b/module/message/lang/de.php index 3363605bdb..2074f33b10 100644 --- a/module/message/lang/de.php +++ b/module/message/lang/de.php @@ -16,28 +16,3 @@ $lang->message->browserSetting->pollTimePlaceholder = 'Notify the time intervals $lang->message->browserSetting->turnonList[1] = 'On'; $lang->message->browserSetting->turnonList[0] = 'Off'; - -$lang->message->label = new stdclass(); -$lang->message->label->created = 'create'; -$lang->message->label->opened = 'open'; -$lang->message->label->changed = 'change'; -$lang->message->label->edited = 'edit'; -$lang->message->label->assigned = 'assign'; -$lang->message->label->closed = 'close'; -$lang->message->label->deleted = 'delete'; -$lang->message->label->undeleted = 'restore'; -$lang->message->label->commented = 'comment'; -$lang->message->label->activated = 'activate'; -$lang->message->label->resolved = 'resolve'; -$lang->message->label->reviewed = 'review'; -$lang->message->label->confirmed = 'confirm Story'; -$lang->message->label->frombug = 'convert from Bug'; -$lang->message->label->started = 'start'; -$lang->message->label->delayed = 'delay'; -$lang->message->label->suspended = 'suspend'; -$lang->message->label->finished = 'finish'; -$lang->message->label->paused = 'pause'; -$lang->message->label->canceled = 'cancel'; -$lang->message->label->restarted = 'continue'; -$lang->message->label->blocked = 'block'; -$lang->message->label->bugconfirmed = 'confirm'; diff --git a/module/message/lang/en.php b/module/message/lang/en.php index f3d02119da..88fa9bf34e 100644 --- a/module/message/lang/en.php +++ b/module/message/lang/en.php @@ -1,8 +1,9 @@ message->common = 'Notification'; -$lang->message->index = 'Home'; -$lang->message->setting = 'Settings'; -$lang->message->browser = 'Browser Notification'; +$lang->message->common = 'Notification'; +$lang->message->index = 'Home'; +$lang->message->setting = 'Settings'; +$lang->message->browser = 'Browser Notification'; +$lang->message->blockUser = 'Block User'; $lang->message->typeList['mail'] = 'Email'; $lang->message->typeList['message'] = 'Browser Notifications'; @@ -16,28 +17,3 @@ $lang->message->browserSetting->pollTimePlaceholder = 'Notify the time intervals $lang->message->browserSetting->turnonList[1] = 'On'; $lang->message->browserSetting->turnonList[0] = 'Off'; - -$lang->message->label = new stdclass(); -$lang->message->label->created = 'create'; -$lang->message->label->opened = 'open'; -$lang->message->label->changed = 'change'; -$lang->message->label->edited = 'edit'; -$lang->message->label->assigned = 'assign'; -$lang->message->label->closed = 'close'; -$lang->message->label->deleted = 'delete'; -$lang->message->label->undeleted = 'restore'; -$lang->message->label->commented = 'comment'; -$lang->message->label->activated = 'activate'; -$lang->message->label->resolved = 'resolve'; -$lang->message->label->reviewed = 'review'; -$lang->message->label->confirmed = 'confirm Story'; -$lang->message->label->frombug = 'convert from Bug'; -$lang->message->label->started = 'start'; -$lang->message->label->delayed = 'delay'; -$lang->message->label->suspended = 'suspend'; -$lang->message->label->finished = 'finish'; -$lang->message->label->paused = 'pause'; -$lang->message->label->canceled = 'cancel'; -$lang->message->label->restarted = 'continue'; -$lang->message->label->blocked = 'block'; -$lang->message->label->bugconfirmed = 'confirm'; diff --git a/module/message/lang/fr.php b/module/message/lang/fr.php index 87894cd7d4..6bb0757fb4 100644 --- a/module/message/lang/fr.php +++ b/module/message/lang/fr.php @@ -16,28 +16,3 @@ $lang->message->browserSetting->pollTimePlaceholder = 'Notifier les intervalles $lang->message->browserSetting->turnonList[1] = 'On'; $lang->message->browserSetting->turnonList[0] = 'Off'; - -$lang->message->label = new stdclass(); -$lang->message->label->created = 'create'; -$lang->message->label->opened = 'open'; -$lang->message->label->changed = 'change'; -$lang->message->label->edited = 'edit'; -$lang->message->label->assigned = 'assign'; -$lang->message->label->closed = 'close'; -$lang->message->label->deleted = 'delete'; -$lang->message->label->undeleted = 'restore'; -$lang->message->label->commented = 'comment'; -$lang->message->label->activated = 'activate'; -$lang->message->label->resolved = 'resolve'; -$lang->message->label->reviewed = 'review'; -$lang->message->label->confirmed = 'confirm Story'; -$lang->message->label->frombug = 'convert from Bug'; -$lang->message->label->started = 'start'; -$lang->message->label->delayed = 'delay'; -$lang->message->label->suspended = 'suspend'; -$lang->message->label->finished = 'finish'; -$lang->message->label->paused = 'pause'; -$lang->message->label->canceled = 'cancel'; -$lang->message->label->restarted = 'continue'; -$lang->message->label->blocked = 'block'; -$lang->message->label->bugconfirmed = 'confirm'; diff --git a/module/message/lang/vi.php b/module/message/lang/vi.php index c528464c99..2cd0c2e551 100644 --- a/module/message/lang/vi.php +++ b/module/message/lang/vi.php @@ -16,28 +16,3 @@ $lang->message->browserSetting->pollTimePlaceholder = 'Thông báo khoảng th $lang->message->browserSetting->turnonList[1] = 'On'; $lang->message->browserSetting->turnonList[0] = 'Off'; - -$lang->message->label = new stdclass(); -$lang->message->label->created = 'create'; -$lang->message->label->opened = 'open'; -$lang->message->label->changed = 'change'; -$lang->message->label->edited = 'edit'; -$lang->message->label->assigned = 'assign'; -$lang->message->label->closed = 'close'; -$lang->message->label->deleted = 'delete'; -$lang->message->label->undeleted = 'restore'; -$lang->message->label->commented = 'comment'; -$lang->message->label->activated = 'activate'; -$lang->message->label->resolved = 'resolve'; -$lang->message->label->reviewed = 'review'; -$lang->message->label->confirmed = 'confirm Story'; -$lang->message->label->frombug = 'convert from Bug'; -$lang->message->label->started = 'start'; -$lang->message->label->delayed = 'delay'; -$lang->message->label->suspended = 'suspend'; -$lang->message->label->finished = 'finish'; -$lang->message->label->paused = 'pause'; -$lang->message->label->canceled = 'cancel'; -$lang->message->label->restarted = 'continue'; -$lang->message->label->blocked = 'block'; -$lang->message->label->bugconfirmed = 'confirm'; diff --git a/module/message/lang/zh-cn.php b/module/message/lang/zh-cn.php index 3edad2389b..600d5ff70c 100644 --- a/module/message/lang/zh-cn.php +++ b/module/message/lang/zh-cn.php @@ -1,8 +1,9 @@ message->common = '消息'; -$lang->message->index = '首页'; -$lang->message->setting = '设置'; -$lang->message->browser = '浏览器通知'; +$lang->message->common = '消息'; +$lang->message->index = '首页'; +$lang->message->setting = '设置'; +$lang->message->browser = '浏览器通知'; +$lang->message->blockUser = '不通知人员'; $lang->message->typeList['mail'] = '邮件'; $lang->message->typeList['message'] = '浏览器通知'; @@ -16,28 +17,3 @@ $lang->message->browserSetting->pollTimePlaceholder = '通知的时间间隔, $lang->message->browserSetting->turnonList[1] = '打开'; $lang->message->browserSetting->turnonList[0] = '关闭'; - -$lang->message->label = new stdclass(); -$lang->message->label->created = '创建'; -$lang->message->label->opened = '创建'; -$lang->message->label->changed = '变更'; -$lang->message->label->edited = '编辑'; -$lang->message->label->assigned = '指派'; -$lang->message->label->closed = '关闭'; -$lang->message->label->deleted = '删除'; -$lang->message->label->undeleted = '还原'; -$lang->message->label->commented = '评论'; -$lang->message->label->activated = '激活'; -$lang->message->label->resolved = '解决'; -$lang->message->label->reviewed = '评审'; -$lang->message->label->confirmed = "确认{$lang->SRCommon}"; -$lang->message->label->frombug = "转{$lang->SRCommon}"; -$lang->message->label->started = '开始'; -$lang->message->label->delayed = '延期'; -$lang->message->label->suspended = '挂起'; -$lang->message->label->finished = '完成'; -$lang->message->label->paused = '暂停'; -$lang->message->label->canceled = '取消'; -$lang->message->label->restarted = '继续'; -$lang->message->label->blocked = '阻塞'; -$lang->message->label->bugconfirmed = '确认'; diff --git a/module/message/lang/zh-tw.php b/module/message/lang/zh-tw.php index eee89461a9..a3d30d6c0c 100644 --- a/module/message/lang/zh-tw.php +++ b/module/message/lang/zh-tw.php @@ -16,28 +16,3 @@ $lang->message->browserSetting->pollTimePlaceholder = '通知的時間間隔, $lang->message->browserSetting->turnonList[1] = '打開'; $lang->message->browserSetting->turnonList[0] = '關閉'; - -$lang->message->label = new stdclass(); -$lang->message->label->created = '創建'; -$lang->message->label->opened = '創建'; -$lang->message->label->changed = '變更'; -$lang->message->label->edited = '編輯'; -$lang->message->label->assigned = '指派'; -$lang->message->label->closed = '關閉'; -$lang->message->label->deleted = '刪除'; -$lang->message->label->undeleted = '還原'; -$lang->message->label->commented = '評論'; -$lang->message->label->activated = '激活'; -$lang->message->label->resolved = '解決'; -$lang->message->label->reviewed = '評審'; -$lang->message->label->confirmed = "確認{$lang->SRCommon}"; -$lang->message->label->frombug = "轉{$lang->SRCommon}"; -$lang->message->label->started = '開始'; -$lang->message->label->delayed = '延期'; -$lang->message->label->suspended = '掛起'; -$lang->message->label->finished = '完成'; -$lang->message->label->paused = '暫停'; -$lang->message->label->canceled = '取消'; -$lang->message->label->restarted = '繼續'; -$lang->message->label->blocked = '阻塞'; -$lang->message->label->bugconfirmed = '確認'; diff --git a/module/message/model.php b/module/message/model.php index cdb2e3a35e..bae3422d26 100644 --- a/module/message/model.php +++ b/module/message/model.php @@ -49,7 +49,7 @@ class messageModel extends model { foreach($actions as $action) { - $objectActions[$objectType][$action] = $this->lang->message->label->$action; + $objectActions[$objectType][$action] = str_replace($this->lang->webhook->trimWords, '', $this->lang->action->label->$action); } } return $objectActions; diff --git a/module/message/view/setting.html.php b/module/message/view/setting.html.php index 6b062ae12d..d059b229d7 100644 --- a/module/message/view/setting.html.php +++ b/module/message/view/setting.html.php @@ -74,6 +74,10 @@
    message->blockUser;?>message->blockUser) ? $config->message->blockUser: '', "class='form-control chosen' multiple");?>
    createLink('project', 'index', "id=$execution->project", '', '', $execution->project), $execution->projectName, '', "title='$execution->projectName'");?> + projectName) ? html::a($this->createLink('project', 'index', "id=$execution->project", '', '', $execution->project), $execution->projectName, '', "title='$execution->projectName'") : '';?> + begin;?> end;?>pri;?>' title='task->priList, $child->pri);?>'>task->priList, $child->pri);?> parent > 0) echo '' . $this->lang->task->childrenAB . ' ';?> - createLink('task', 'view', "taskID=$child->id", '', '', $child->project), $child->name, null, "style='color: $child->color' data-group='project'");?> + createLink('task', 'view', "taskID=$child->id", '', '', $child->project), $child->name, null, "style='color: $child->color'");?> createLink('project', 'view', "projectID=$child->project"), $child->projectName);?>
    +
    @@ -50,25 +50,25 @@ product->name);?> config->URAndSR):?> - + - + config->URAndSR):?> - + - + @@ -83,9 +83,9 @@ $trAttrs .= " class='$trClass'"; ?> systemMode == 'new'):?> - > + > - + > + > - + > + > - + diff --git a/module/product/view/browse.html.php b/module/product/view/browse.html.php index 195cbd8731..8419b8a322 100644 --- a/module/product/view/browse.html.php +++ b/module/product/view/browse.html.php @@ -65,7 +65,7 @@ $projectIDParam = $isProjectStory ? "projectID=$projectID&" : ''; echo "
  • " . html::a($this->createLink('projectstory', 'story', "projectID=$projectID"), $lang->product->all) . "
  • "; foreach($projectProducts as $product) { - echo "
  • " . html::a($this->createLink('projectstory', 'story', "projectID=$projectID&productID=$product->id&branch=0"), $product->name, '', "title='{$product->name}' class='text-ellipsis'") . "
  • "; + echo "
  • " . html::a($this->createLink('projectstory', 'story', "projectID=$projectID&productID=$product->id&branch=all"), $product->name, '', "title='{$product->name}' class='text-ellipsis'") . "
  • "; } ?> @@ -133,7 +133,7 @@ $projectIDParam = $isProjectStory ? "projectID=$projectID&" : ''; app->rawModule != 'projectstory') common::printIcon('story', 'report', "productID=$productID&branchID=$branch&storyType=$storyType&browseType=$browseType&moduleID=$moduleID&chartType=pie", '', 'button', 'bar-chart muted'); ?>
    -
    - + - + - + - + + + + + diff --git a/module/product/view/edit.html.php b/module/product/view/edit.html.php index 43f3070373..90a668e4da 100644 --- a/module/product/view/edit.html.php +++ b/module/product/view/edit.html.php @@ -64,6 +64,10 @@ + + + + diff --git a/module/product/view/project.html.php b/module/product/view/project.html.php index cc246c27e2..e7c3d1c04f 100644 --- a/module/product/view/project.html.php +++ b/module/product/view/project.html.php @@ -32,11 +32,9 @@ + systemMode == 'new'):?> - - - @@ -53,9 +51,6 @@ - systemMode == 'new'):?> - - + systemMode == 'new'):?> + + + bizVersion)):?> + + + + bizVersion)):?> + + + + +
    story->requirement;?>story->requirement;?> story->story;?> bug->common;?> product->plan;?> product->release;?>actions;?>actions;?>
    story->draft;?> story->activate;?> story->change;?>story->completeRate;?>
    story->completeRate;?>
    story->draft;?> story->activate;?> story->change;?>story->completeRate;?>
    story->completeRate;?>
    bug->activate;?> close;?> bug->fixedRate;?>
    @@ -127,9 +127,9 @@ $trAttrs .= " class='$trClass'"; } ?> -
    @@ -183,9 +183,9 @@ } $trAttrs .= " class='$trClass'"; ?> -
    id => '')) : '';?>id => ''));?> id", $product, 'list', 'edit');?> - +
    - + @@ -56,19 +56,23 @@
    product->code;?>
    product->PO;?>app->user->account, "class='form-control chosen'");?>app->user->account, "class='form-control chosen'");?>
    product->QD;?>
    product->RD;?>
    product->reviewer;?>
    product->type;?>product->RD;?> RD, "class='form-control chosen'");?>
    product->reviewer;?>reviewer, "class='form-control chosen' multiple");?>
    product->type;?> product->typeList, $product->type, "class='form-control'");?>
    idAB;?>project->name;?> program->common;?>project->name;?>execution->name;?> project->PM;?> project->begin;?>
    id);?>programName;?> systemMode == 'new') @@ -68,6 +63,9 @@ } ?> programName;?> PM]) ? $PMList[$project->PM]->id : ''?> PM)) echo html::a($this->createLink('user', 'profile', "userID=$userID", '', true), zget($users, $project->PM), '', "data-toggle='modal' data-type='iframe' data-width='800'");?> diff --git a/module/product/view/view.html.php b/module/product/view/view.html.php index 9d6950111f..37ce60d20f 100644 --- a/module/product/view/view.html.php +++ b/module/product/view/view.html.php @@ -52,7 +52,17 @@
    product->qa;?> QD);?> product->reviewer;?>
    product->reviewer;?>
    @@ -67,7 +77,7 @@ product->code;?> code;?> - product->type;?> + product->type;?> product->typeList, $product->type);?> story->openedBy?> @@ -75,10 +85,10 @@ code)):?> - product->type;?> + product->type;?> product->typeList, $product->type);?> - productCommon ." ". $lang->product->status;?> + productCommon . $lang->product->status;?> product->statusList, $product->status);?> story->openedDate?> @@ -86,7 +96,7 @@ code)):?> - productCommon ." ". $lang->product->status;?> + productCommon ." ". $lang->product->status;?> product->statusList, $product->status);?> product->acl;?> diff --git a/module/productplan/control.php b/module/productplan/control.php index 32cebc6bb3..aab7ee5ca2 100644 --- a/module/productplan/control.php +++ b/module/productplan/control.php @@ -269,8 +269,8 @@ class productplan extends control die(js::error($this->lang->notFound) . js::locate($this->createLink('product', 'index'))); } - $this->session->set('storyList', $this->app->getURI(true) . '&type=' . 'story', 'product'); - $this->session->set('bugList', $this->app->getURI(true) . '&type=' . 'bug', 'qa'); + $this->session->set('storyList', $this->createLink('productplan', 'view', "planID=$planID&type=story"), 'product'); + $this->session->set('bugList', $this->createLink('productplan', 'view', "planID=$planID&type=bug"), 'qa'); /* Determines whether an object is editable. */ $canBeChanged = common::canBeChanged('plan', $plan); @@ -278,6 +278,7 @@ class productplan extends control /* Load pager. */ $this->app->loadClass('pager', $static = true); if($this->app->getViewType() == 'mhtml') $recPerPage = 10; + if($this->app->getViewType() == 'xhtml') $recPerPage = 10; /* Append id for secend sort. */ $sort = $this->loadModel('common')->appendOrder($orderBy); @@ -331,26 +332,13 @@ class productplan extends control * @param int $productID * @param int $branch * @param string $number - * @param string $from + * @param string $expired * @access public * @return void */ - public function ajaxGetProductplans($productID, $branch = 0, $number = '', $from = '') + public function ajaxGetProductplans($productID, $branch = 0, $number = '', $expired = '') { - if($from == 'story' and $branch == BRANCH_MAIN) - { - $plans = $this->productplan->getPairs($productID); - } - elseif($from == 'story' and $branch) - { - $plans = $this->productplan->getPairs($productID, 0); - $plans += $this->productplan->getPairs($productID, $branch); - } - else - { - $plans = $this->productplan->getPairs($productID, $branch); - } - + $plans = $this->productplan->getPairs($productID, $branch, $expired, true); $planName = $number === '' ? 'plan' : "plan[$number]"; $plans = empty($plans) ? array('' => '') : $plans; die(html::select($planName, $plans, '', "class='form-control'")); @@ -375,6 +363,20 @@ class productplan extends control $this->loadModel('story')->sortStoriesOfPlan($planID, $storyIDList, $this->post->orderBy, $this->post->pageID, $this->post->recPerPage); } + /** + * Get projects by product id. + * + * @param int $productID + * @param int $branch + * @access public + * @return void + */ + public function ajaxGetProjects($productID, $branch = 0) + { + $projects = $this->loadModel('product')->getProjectPairsByProduct($productID, $branch); + die(html::select('project', $projects, '', "class='form-control chosen'")); + } + /** * Link stories. * @@ -417,8 +419,8 @@ class productplan extends control $this->config->product->search['queryID'] = $queryID; $this->config->product->search['style'] = 'simple'; $this->config->product->search['params']['product']['values'] = $products + array('all' => $this->lang->product->allProductsOfProject); - $this->config->product->search['params']['plan']['values'] = $this->productplan->getForProducts(array($plan->product => $plan->product)); - $this->config->product->search['params']['module']['values'] = $this->tree->getOptionMenu($plan->product, 'story', 0, 'all'); + $this->config->product->search['params']['plan']['values'] = $this->productplan->getPairsForStory($plan->product, $plan->branch, 'skipParent'); + $this->config->product->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($plan->product, 'story', 0, $plan->branch); $storyStatusList = $this->lang->story->statusList; unset($storyStatusList['closed']); $this->config->product->search['params']['status'] = array('operator' => '=', 'control' => 'select', 'values' => $storyStatusList); @@ -430,8 +432,8 @@ class productplan extends control else { $this->config->product->search['fields']['branch'] = $this->lang->product->branch; - $branches = array('' => '') + $this->loadModel('branch')->getPairs($plan->product); - if($plan->branch) $branches = array('' => '', $plan->branch => $branches[$plan->branch]); + $branchName = $this->loadModel('branch')->getById($plan->branch); + $branches = array('' => '', BRANCH_MAIN => $this->lang->branch->main, $plan->branch => $branchName); $this->config->product->search['params']['branch']['values'] = $branches; } $this->loadModel('search')->setSearchParams($this->config->product->search); @@ -440,7 +442,7 @@ class productplan extends control if($browseType == 'bySearch') { - $allStories = $this->story->getBySearch($plan->product, $plan->branch, $queryID, 'id', '', 'story', array_keys($planStories), $pager); + $allStories = $this->story->getBySearch($plan->product, "0,{$plan->branch}", $queryID, 'id', '', 'story', array_keys($planStories), $pager); } else { @@ -554,14 +556,16 @@ class productplan extends control $pager = new pager($recTotal, $recPerPage, $pageID); /* Build the search form. */ + if($this->config->systemMode == 'classic') unset($this->config->bug->search['fields']['project']); $this->config->bug->search['actionURL'] = $this->createLink('productplan', 'view', "planID=$planID&type=bug&orderBy=$orderBy&link=true¶m=" . helper::safe64Encode('&browseType=bySearch&queryID=myQueryID')); $this->config->bug->search['queryID'] = $queryID; $this->config->bug->search['style'] = 'simple'; - $this->config->bug->search['params']['plan']['values'] = $this->productplan->getForProducts(array($productID => $productID)); - $this->config->bug->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($productID, 'bug', 0, 'all'); - $this->config->bug->search['params']['project']['values'] = $this->product->getExecutionPairsByProduct($productID); + $this->config->bug->search['params']['plan']['values'] = $this->productplan->getPairsForStory($productID, $plan->branch, 'skipParent'); + $this->config->bug->search['params']['execution']['values'] = $this->loadModel('product')->getExecutionPairsByProduct($plan->product, $plan->branch); $this->config->bug->search['params']['openedBuild']['values'] = $this->loadModel('build')->getProductBuildPairs($productID, $branch = 0, $params = ''); $this->config->bug->search['params']['resolvedBuild']['values'] = $this->build->getProductBuildPairs($productID, $branch = 0, $params = ''); + $this->config->bug->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($plan->product, 'bug', 0, $plan->branch); + if($this->config->systemMode == 'new') $this->config->bug->search['params']['project']['values'] = $this->product->getProjectPairsByProduct($productID, $plan->branch); unset($this->config->bug->search['fields']['product']); if($this->session->currentProductType == 'normal') @@ -572,8 +576,8 @@ class productplan extends control else { $this->config->bug->search['fields']['branch'] = $this->lang->product->branch; - $branches = array('' => '') + $this->loadModel('branch')->getPairs($productID); - if($plan->branch) $branches = array('' => '', $plan->branch => $branches[$plan->branch]); + $branchName = $this->loadModel('branch')->getById($plan->branch); + $branches = array('' => '', BRANCH_MAIN => $this->lang->branch->main, $plan->branch => $branchName); $this->config->bug->search['params']['branch']['values'] = $branches; } $this->loadModel('search')->setSearchParams($this->config->bug->search); @@ -586,7 +590,7 @@ class productplan extends control } else { - $allBugs = $this->bug->getActiveBugs($this->view->product->id, $plan->branch, $executions, array_keys($planBugs), $pager); + $allBugs = $this->bug->getActiveBugs($productID, $plan->branch, $executions, array_keys($planBugs), $pager); } $this->view->allBugs = $allBugs; @@ -648,7 +652,7 @@ class productplan extends control */ public function batchUnlinkBug($planID, $orderBy = 'id_desc') { - foreach($this->post->unlinkBugs as $bugID) $this->productplan->unlinkBug($bugID); + foreach($this->post->bugIDList as $bugID) $this->productplan->unlinkBug($bugID); die(js::locate($this->createLink('productplan', 'view', "planID=$planID&type=bug&orderBy=$orderBy"), 'parent')); } diff --git a/module/productplan/css/browse.css b/module/productplan/css/browse.css index ad619f6ed8..9d99a5e3fa 100644 --- a/module/productplan/css/browse.css +++ b/module/productplan/css/browse.css @@ -18,5 +18,5 @@ td.c-branch {overflow: hidden; text-align: left !important; text-overflow: ellip .c-title {width: 160px;} .c-branch {width: 100px;} .c-story {width: 80px;} -.c-execution {width: 120px;} +.c-execution {width: 70px;} .c-bug, .c-hour {width: 60px;} diff --git a/module/productplan/css/x.view.css b/module/productplan/css/x.view.css new file mode 100644 index 0000000000..26bc171ab4 --- /dev/null +++ b/module/productplan/css/x.view.css @@ -0,0 +1,12 @@ +.main-content{padding: 0;} +.actions{display: none;} +#mainMenu{display: none;} +#main {min-width: unset;} +.pager-size-menu{display: none;} +.table-footer{position: fixed; width: 100%; bottom: 0;} +.tab-btn-container{width: 100%; background-color: #f2f2f2;} +.tab-btn-container ul{display: flex; justify-content: center;} +.tab-btn-container ul li{list-style: none;} +.plan-title{position: absolute; top: 0; left: 0; padding: 8px 15px; font-weight: bolder;} +.tab-btn-container ul li span{display: none;} +.tab-btn-container ul li.active span{display: inline-block;} \ No newline at end of file diff --git a/module/productplan/js/browse.js b/module/productplan/js/browse.js index 92e1bca5bd..407242bacb 100644 --- a/module/productplan/js/browse.js +++ b/module/productplan/js/browse.js @@ -24,15 +24,8 @@ $(function() { var projectID = $('#project').val(); var planID = $('#planID').val(); - if(!projectID) - { - alert(projectNotEmpty); - return false; - } - else - { - $.apps.open(createLink('execution', 'create', 'projectID=' + projectID + '&executionID=©ExecutionID=&planID=' + planID + '&confirm=&productID=' + productID), 'project') - } + $.apps.open(createLink('execution', 'create', 'projectID=' + projectID + '&executionID=©ExecutionID=&planID=' + planID + '&confirm=&productID=' + productID), 'project') + $('#projects').modal('hide'); }); }); $(document).on('click', 'td.content .more', function(e) @@ -58,11 +51,20 @@ $(document).on('click', 'td.content .more', function(e) * Get planID * * @param object $obj + * @param int $branch * @access public * @return void */ -function getPlanID(obj) +function getPlanID(obj, branch) { var planID = $(obj).attr("data-id"); $('#planID').val(planID); + + link = createLink('productplan', 'ajaxGetProjects', 'productID=' + productID + '&branch=' + branch); + $.get(link, function(projects) + { + $('#project').replaceWith(projects); + $("#project_chosen").remove(); + $("#project").chosen(); + }); } diff --git a/module/productplan/js/create.js b/module/productplan/js/create.js index 09617fe5d4..9991c7e47a 100644 --- a/module/productplan/js/create.js +++ b/module/productplan/js/create.js @@ -41,6 +41,7 @@ function computeEndDate(delta) $('#begin').on('change', function() { + $("#end").val(''); $("input:radio[name='delta']").attr("checked",false); }); @@ -53,13 +54,13 @@ $('#future').on('change', function() { if($(this).prop('checked')) { - $('#begin').attr('disabled', 'disabled'); - $('#end').attr('disabled', 'disabled').parents('tr').hide(); + $('#begin').val('').attr('disabled', 'disabled'); + $('#end').val('').parents('tr').hide(); } else { $('#begin').removeAttr('disabled'); - $('#end').removeAttr('disabled').parents('tr').show(); + $('#end').val('').parents('tr').show(); } }); diff --git a/module/productplan/js/edit.js b/module/productplan/js/edit.js index 6b4d8cc604..3acfaeb631 100644 --- a/module/productplan/js/edit.js +++ b/module/productplan/js/edit.js @@ -43,14 +43,11 @@ $('#future').on('change', function() { if($(this).prop('checked')) { - $('#begin').attr('disabled', 'disabled'); + $('#begin').val('').attr('disabled', 'disabled'); $('#end').val('').parents('tr').hide(); } else { - var begin = $('#begin').val(); - if(begin == '') $('#begin').val(today); - $('#begin').removeAttr('disabled'); $('#end').parents('tr').show(); } diff --git a/module/productplan/lang/de.php b/module/productplan/lang/de.php index 9e5d9f9d37..f2cc6ab8b5 100644 --- a/module/productplan/lang/de.php +++ b/module/productplan/lang/de.php @@ -30,7 +30,7 @@ $lang->productplan->linkedStories = 'Verknüpfte Storys'; $lang->productplan->unlinkedStories = 'Unverknüpfte Storys'; $lang->productplan->updateOrder = 'Sortierung'; $lang->productplan->createChildren = "Create Child Plans"; -$lang->productplan->createExecution = "Create {$lang->execution->common}"; +$lang->productplan->createExecution = "Create {$lang->executionCommon}"; $lang->productplan->linkBug = "Bug Verknüpfen"; $lang->productplan->unlinkBug = "Bug Verknpfung aufheben"; @@ -46,7 +46,6 @@ $lang->productplan->confirmUnlinkBug = "Möchten Sie diesen Bug löschen?"; $lang->productplan->noPlan = 'Kein Plan. '; $lang->productplan->cannotDeleteParent = 'Cannot delete parent plan'; $lang->productplan->selectProjects = "Please select the project"; -$lang->productplan->projectNotEmpty = 'Project cannot be empty.'; $lang->productplan->nextStep = "Next step"; $lang->productplan->id = 'ID'; @@ -61,7 +60,7 @@ $lang->productplan->future = 'Wartend'; $lang->productplan->stories = 'Storys'; $lang->productplan->bugs = 'Bugs'; $lang->productplan->hour = $lang->hourCommon; -$lang->productplan->execution = $lang->execution->common; +$lang->productplan->execution = $lang->executionCommon; $lang->productplan->parent = "Parent Plan"; $lang->productplan->parentAB = "Parent"; $lang->productplan->children = "Child Plan"; @@ -77,12 +76,10 @@ $lang->productplan->endList[93] = '3 Monate'; $lang->productplan->endList[186] = '6 Monate'; $lang->productplan->endList[365] = '1 Jahr'; -$lang->productplan->errorNoTitle = 'ID %s Titel darf nicht leer sein.'; -$lang->productplan->errorNoBegin = 'ID %s Start darf nicht leer sein.'; -$lang->productplan->errorNoEnd = 'ID %s Ende darf nicht leer sein.'; -$lang->productplan->beginGeEnd = 'ID %s Start darf nicht größer als Ende sein.'; -$lang->productplan->beginLetterParent = "Parent begin date: %s, begin date should be >= parent begin date."; -$lang->productplan->endGreaterParent = "Parent end date: %s, end date should be <= parent end date."; +$lang->productplan->errorNoTitle = 'ID %s Titel darf nicht leer sein.'; +$lang->productplan->errorNoBegin = 'ID %s Start darf nicht leer sein.'; +$lang->productplan->errorNoEnd = 'ID %s Ende darf nicht leer sein.'; +$lang->productplan->beginGeEnd = 'ID %s Start darf nicht größer als Ende sein.'; $lang->productplan->featureBar['browse']['all'] = 'Alle'; $lang->productplan->featureBar['browse']['unexpired'] = 'Nicht abgelaufen'; diff --git a/module/productplan/lang/en.php b/module/productplan/lang/en.php index 061e2f90a4..d4f1596ed2 100644 --- a/module/productplan/lang/en.php +++ b/module/productplan/lang/en.php @@ -20,8 +20,10 @@ $lang->productplan->bugSummary = "Total %s Bugs on this page."; $lang->productplan->basicInfo = 'Basic Info'; $lang->productplan->batchEdit = 'Batch Edit'; $lang->productplan->project = 'Project'; +$lang->productplan->plan = 'Plan'; $lang->productplan->batchUnlink = "Batch Unlink"; +$lang->productplan->unlinkAB = "Unlink"; $lang->productplan->linkStory = "Link Story"; $lang->productplan->unlinkStory = "Unlink Story"; $lang->productplan->unlinkStoryAB = "Unlink"; @@ -30,7 +32,7 @@ $lang->productplan->linkedStories = 'Linked Stories'; $lang->productplan->unlinkedStories = 'Unlinked Stories'; $lang->productplan->updateOrder = 'Order'; $lang->productplan->createChildren = "Create Child Plans"; -$lang->productplan->createExecution = "Create {$lang->execution->common}"; +$lang->productplan->createExecution = "Create {$lang->executionCommon}"; $lang->productplan->linkBug = "Link Bug"; $lang->productplan->unlinkBug = "Unlink Bug"; @@ -46,7 +48,6 @@ $lang->productplan->confirmUnlinkBug = "Do you want to unlink this bug?"; $lang->productplan->noPlan = 'No plans yet. '; $lang->productplan->cannotDeleteParent = 'Cannot delete parent plan'; $lang->productplan->selectProjects = "Please select the project"; -$lang->productplan->projectNotEmpty = 'Project cannot be empty.'; $lang->productplan->nextStep = "Next step"; $lang->productplan->id = 'ID'; @@ -61,7 +62,7 @@ $lang->productplan->future = 'TBD'; $lang->productplan->stories = 'Story'; $lang->productplan->bugs = 'Bug'; $lang->productplan->hour = $lang->hourCommon; -$lang->productplan->execution = $lang->execution->common; +$lang->productplan->execution = $lang->executionCommon; $lang->productplan->parent = "Parent Plan"; $lang->productplan->parentAB = "Parent"; $lang->productplan->children = "Child Plan"; @@ -78,12 +79,10 @@ $lang->productplan->endList[93] = '3 Months'; $lang->productplan->endList[186] = '6 Months'; $lang->productplan->endList[365] = '1 Year'; -$lang->productplan->errorNoTitle = 'ID %s title should not be empty.'; -$lang->productplan->errorNoBegin = 'ID %s begin time should not be empty.'; -$lang->productplan->errorNoEnd = 'ID %s end time should not be empty.'; -$lang->productplan->beginGeEnd = 'ID %s begin time should not be >= end time.'; -$lang->productplan->beginLetterParent = "Parent begin date: %s, begin date should be >= parent begin date."; -$lang->productplan->endGreaterParent = "Parent end date: %s, end date should be <= parent end date."; +$lang->productplan->errorNoTitle = 'ID %s title should not be empty.'; +$lang->productplan->errorNoBegin = 'ID %s begin time should not be empty.'; +$lang->productplan->errorNoEnd = 'ID %s end time should not be empty.'; +$lang->productplan->beginGeEnd = 'ID %s begin time should not be >= end time.'; $lang->productplan->featureBar['browse']['all'] = 'All'; $lang->productplan->featureBar['browse']['unexpired'] = 'Unexpired'; diff --git a/module/productplan/lang/fr.php b/module/productplan/lang/fr.php index 65632a71c8..c2ce442c2a 100644 --- a/module/productplan/lang/fr.php +++ b/module/productplan/lang/fr.php @@ -30,7 +30,7 @@ $lang->productplan->linkedStories = 'Stories Planifiées'; $lang->productplan->unlinkedStories = 'Stories non Planifiées'; $lang->productplan->updateOrder = 'Ordre'; $lang->productplan->createChildren = "Créer Sous-Plans"; -$lang->productplan->createExecution = "Create {$lang->execution->common}"; +$lang->productplan->createExecution = "Create {$lang->executionCommon}"; $lang->productplan->linkBug = "Planifier Bug"; $lang->productplan->unlinkBug = "Retirer Bug"; @@ -46,7 +46,6 @@ $lang->productplan->confirmUnlinkBug = "Voulez-vous retirer ce bug du plan ?"; $lang->productplan->noPlan = "Aucun plan pour l'instant. "; $lang->productplan->cannotDeleteParent = 'Impossible de supprimer le plan parent'; $lang->productplan->selectProjects = "Please select the project"; -$lang->productplan->projectNotEmpty = 'Project cannot be empty.'; $lang->productplan->nextStep = "Next step"; $lang->productplan->id = 'ID'; @@ -61,7 +60,7 @@ $lang->productplan->future = 'A Définir'; $lang->productplan->stories = 'Story'; $lang->productplan->bugs = 'Bug'; $lang->productplan->hour = $lang->hourCommon; -$lang->productplan->execution = $lang->execution->common; +$lang->productplan->execution = $lang->executionCommon; $lang->productplan->parent = "Plan Parent"; $lang->productplan->parentAB = "Parent"; $lang->productplan->children = "Sous-Plan"; @@ -77,12 +76,10 @@ $lang->productplan->endList[93] = '3 Mois'; $lang->productplan->endList[186] = '6 Mois'; $lang->productplan->endList[365] = '1 Année'; -$lang->productplan->errorNoTitle = 'ID %s titre ne doit pas être à blanc.'; -$lang->productplan->errorNoBegin = "ID %s l'heure de début devrait être renseignée."; -$lang->productplan->errorNoEnd = "ID %s l'heure de fin devrait être renseignée."; -$lang->productplan->beginGeEnd = "ID %s l'heure de début ne doit pas être >= à l'heure de fin."; -$lang->productplan->beginLetterParent = "Parent begin date: %s, begin date should be >= parent begin date."; -$lang->productplan->endGreaterParent = "Parent end date: %s, end date should be <= parent end date."; +$lang->productplan->errorNoTitle = 'ID %s titre ne doit pas être à blanc.'; +$lang->productplan->errorNoBegin = "ID %s l'heure de début devrait être renseignée."; +$lang->productplan->errorNoEnd = "ID %s l'heure de fin devrait être renseignée."; +$lang->productplan->beginGeEnd = "ID %s l'heure de début ne doit pas être >= à l'heure de fin."; $lang->productplan->featureBar['browse']['all'] = 'Tous'; $lang->productplan->featureBar['browse']['unexpired'] = 'Non échus'; diff --git a/module/productplan/lang/vi.php b/module/productplan/lang/vi.php index eed23f98a0..ec97bffbdf 100644 --- a/module/productplan/lang/vi.php +++ b/module/productplan/lang/vi.php @@ -30,7 +30,7 @@ $lang->productplan->linkedStories = 'Câu chuyện liên kết'; $lang->productplan->unlinkedStories = 'Câu chuyện chưa liên kết'; $lang->productplan->updateOrder = 'Sắp xếp'; $lang->productplan->createChildren = "Tạo kế hoạch con"; -$lang->productplan->createExecution = "Create {$lang->execution->common}"; +$lang->productplan->createExecution = "Create {$lang->executionCommon}"; $lang->productplan->linkBug = "Liên kết Bug"; $lang->productplan->unlinkBug = "Hủy liên kết Bug"; @@ -46,7 +46,6 @@ $lang->productplan->confirmUnlinkBug = "Bạn có muốn hủy liên kết bug $lang->productplan->noPlan = 'Không có kế hoạch nào'; $lang->productplan->cannotDeleteParent = 'Không thể xóa kế hoạch mẹ'; $lang->productplan->selectProjects = "Please select the project"; -$lang->productplan->projectNotEmpty = 'Project cannot be empty.'; $lang->productplan->nextStep = "Next step"; $lang->productplan->id = 'ID'; @@ -61,7 +60,7 @@ $lang->productplan->future = 'TBD'; $lang->productplan->stories = 'Câu chuyện'; $lang->productplan->bugs = 'Bug'; $lang->productplan->hour = $lang->hourCommon; -$lang->productplan->execution = $lang->execution->common; +$lang->productplan->execution = $lang->executionCommon; $lang->productplan->parent = "Kế hoạch mẹ"; $lang->productplan->parentAB = "Mẹ"; $lang->productplan->children = "Kế hoạch con"; @@ -77,12 +76,10 @@ $lang->productplan->endList[93] = '3 tháng'; $lang->productplan->endList[186] = '6 tháng'; $lang->productplan->endList[365] = '1 năm'; -$lang->productplan->errorNoTitle = 'Tiêu đề ID %s không nên trống.'; -$lang->productplan->errorNoBegin = 'Thời gian bắt đầu ID %s không nên trống.'; -$lang->productplan->errorNoEnd = 'Thời gian kết thúc ID %s không nên trống.'; -$lang->productplan->beginGeEnd = 'ID %s thời gian bắt đầu không nên >= thời gian kết thúc.'; -$lang->productplan->beginLetterParent = "Parent begin date: %s, begin date should be >= parent begin date."; -$lang->productplan->endGreaterParent = "Parent end date: %s, end date should be <= parent end date."; +$lang->productplan->errorNoTitle = 'Tiêu đề ID %s không nên trống.'; +$lang->productplan->errorNoBegin = 'Thời gian bắt đầu ID %s không nên trống.'; +$lang->productplan->errorNoEnd = 'Thời gian kết thúc ID %s không nên trống.'; +$lang->productplan->beginGeEnd = 'ID %s thời gian bắt đầu không nên >= thời gian kết thúc.'; $lang->productplan->featureBar['browse']['all'] = 'Tất cả'; $lang->productplan->featureBar['browse']['unexpired'] = 'Chưa quá hạn'; diff --git a/module/productplan/lang/zh-cn.php b/module/productplan/lang/zh-cn.php index d5548d9529..793df51c5a 100644 --- a/module/productplan/lang/zh-cn.php +++ b/module/productplan/lang/zh-cn.php @@ -20,8 +20,10 @@ $lang->productplan->bugSummary = "本页共 %s 个Bug"; $lang->productplan->basicInfo = '基本信息'; $lang->productplan->batchEdit = '批量编辑'; $lang->productplan->project = '项目'; +$lang->productplan->plan = '计划'; $lang->productplan->batchUnlink = "批量移除"; +$lang->productplan->unlinkAB = "移除"; $lang->productplan->linkStory = "关联{$lang->SRCommon}"; $lang->productplan->unlinkStory = "移除{$lang->SRCommon}"; $lang->productplan->unlinkStoryAB = "移除"; @@ -30,7 +32,7 @@ $lang->productplan->linkedStories = $lang->SRCommon; $lang->productplan->unlinkedStories = "未关联{$lang->SRCommon}"; $lang->productplan->updateOrder = '排序'; $lang->productplan->createChildren = "创建子计划"; -$lang->productplan->createExecution = "创建{$lang->execution->common}"; +$lang->productplan->createExecution = "创建{$lang->executionCommon}"; $lang->productplan->linkBug = "关联Bug"; $lang->productplan->unlinkBug = "移除Bug"; @@ -46,7 +48,6 @@ $lang->productplan->confirmUnlinkBug = "您确认移除该Bug吗?"; $lang->productplan->noPlan = "暂时没有计划。"; $lang->productplan->cannotDeleteParent = "不能删除父计划"; $lang->productplan->selectProjects = "请选择所属项目"; -$lang->productplan->projectNotEmpty = '所属项目不能为空。'; $lang->productplan->nextStep = "下一步"; $lang->productplan->id = '编号'; @@ -61,7 +62,7 @@ $lang->productplan->future = '待定'; $lang->productplan->stories = "{$lang->SRCommon}数"; $lang->productplan->bugs = 'Bug数'; $lang->productplan->hour = $lang->hourCommon; -$lang->productplan->execution = $lang->execution->common; +$lang->productplan->execution = $lang->executionCommon; $lang->productplan->parent = "父计划"; $lang->productplan->parentAB = "父"; $lang->productplan->children = "子计划"; diff --git a/module/productplan/lang/zh-tw.php b/module/productplan/lang/zh-tw.php index dac293d0d6..dbcdf1b06a 100644 --- a/module/productplan/lang/zh-tw.php +++ b/module/productplan/lang/zh-tw.php @@ -46,7 +46,6 @@ $lang->productplan->confirmUnlinkBug = "您確認移除該Bug嗎?"; $lang->productplan->noPlan = "暫時沒有計劃。"; $lang->productplan->cannotDeleteParent = "不能刪除父計劃"; $lang->productplan->selectProjects = "請選擇所屬項目"; -$lang->productplan->projectNotEmpty = '所屬項目不能為空。'; $lang->productplan->nextStep = "下一步"; $lang->productplan->id = '編號'; diff --git a/module/productplan/model.php b/module/productplan/model.php index 225f2c9116..d9ddf69197 100644 --- a/module/productplan/model.php +++ b/module/productplan/model.php @@ -171,21 +171,22 @@ class productplanModel extends model /** * Get plan pairs. * - * @param array|int $product - * @param int $branch - * @param string $expired - * @param bool $skipParent + * @param array|int $product + * @param int|string|array $branch + * @param string $expired + * @param bool $skipParent * @access public * @return array */ public function getPairs($product = 0, $branch = '', $expired = '', $skipParent = false) { $date = date('Y-m-d'); - $plans = $this->dao->select('t1.id,t1.title,t1.parent,t1.begin,t1.end,t2.name as branchName')->from(TABLE_PRODUCTPLAN)->alias('t1') + $plans = $this->dao->select('t1.id,t1.title,t1.parent,t1.begin,t1.end,t2.name as branchName,t3.type as productType')->from(TABLE_PRODUCTPLAN)->alias('t1') ->leftJoin(TABLE_BRANCH)->alias('t2')->on('t2.id=t1.branch') + ->leftJoin(TABLE_PRODUCT)->alias('t3')->on('t3.id=t1.product') ->where('t1.product')->in($product) ->andWhere('t1.deleted')->eq(0) - ->beginIF($branch !== '')->andWhere('t1.branch')->eq($branch)->fi() + ->beginIF($branch !== '')->andWhere('t1.branch')->in($branch)->fi() ->beginIF($expired == 'unexpired')->andWhere('t1.end')->ge($date)->fi() ->beginIF($skipParent)->andWhere('t1.parent')->ne(-1)->fi() ->orderBy('t1.begin desc') @@ -201,7 +202,7 @@ class productplanModel extends model if($plan->parent > 0 and isset($parentTitle[$plan->parent])) $plan->title = $parentTitle[$plan->parent] . ' /' . $plan->title; $planPairs[$plan->id] = $plan->title . " [{$plan->begin} ~ {$plan->end}]"; if($plan->begin == '2030-01-01' and $plan->end == '2030-01-01') $planPairs[$plan->id] = $plan->title . ' ' . $this->lang->productplan->future; - $planPairs[$plan->id] = ($plan->branchName ? $plan->branchName : $this->lang->branch->main) . ' / ' . $planPairs[$plan->id]; + if($plan->productType != 'normal') $planPairs[$plan->id] = ($plan->branchName ? $plan->branchName : $this->lang->branch->main) . ' / ' . $planPairs[$plan->id]; } return array('' => '') + $planPairs; } @@ -211,34 +212,24 @@ class productplanModel extends model * * @param array|int $product * @param int $branch - * @param bool $skipParent + * @param string $param skipParent|withMainPlan|unexpired * @access public * @return array */ - public function getPairsForStory($product = 0, $branch = 0, $skipParent = false) + public function getPairsForStory($product = 0, $branch = '', $param = '') { - $date = date('Y-m-d'); - $plans = $this->dao->select('id,title,parent,begin,end')->from(TABLE_PRODUCTPLAN) + $date = date('Y-m-d'); + $param = strtolower($param); + $branch = strpos($param, 'withmainplan') !== false ? "0,$branch" : $branch; + $plans = $this->dao->select('id,title,parent,begin,end')->from(TABLE_PRODUCTPLAN) ->where('product')->in($product) ->andWhere('deleted')->eq(0) - ->beginIF($branch)->andWhere("branch")->in("0,$branch")->fi() - ->beginIF($skipParent)->andWhere('parent')->ne(-1)->fi() + ->beginIF(strpos($param, 'unexpired') !== false)->andWhere('end')->ge($date)->fi() + ->beginIF($branch !== 'all' or $branch !== '')->andWhere("branch")->in($branch)->fi() + ->beginIF(strpos($param, 'skipparent') !== false)->andWhere('parent')->ne(-1)->fi() ->orderBy('begin desc') ->fetchAll('id'); - if(!$plans) - { - $plans = $this->dao->select('id,title,parent,begin,end')->from(TABLE_PRODUCTPLAN) - ->where('product')->in($product) - ->andWhere('deleted')->eq(0) - ->andWhere('end')->lt($date) - ->beginIF($branch)->andWhere("branch")->in("0,$branch")->fi() - ->beginIF($skipParent)->andWhere('parent')->ne(-1)->fi() - ->orderBy('begin desc') - ->limit(5) - ->fetchAll('id'); - } - $plans = $this->reorder4Children($plans); $planPairs = array(); $parentTitle = array(); @@ -284,14 +275,19 @@ class productplanModel extends model * Get plan group by product id list. * * @param string|array $products + * @param string $param skipParent|unexpired * @access public * @return array */ - public function getGroupByProduct($products = '') + public function getGroupByProduct($products = '', $param = '') { + $date = date('Y-m-d'); + $param = strtolower($param); $plans = $this->dao->select('id,title,parent,begin,end,product,branch')->from(TABLE_PRODUCTPLAN) ->where('deleted')->eq(0) ->beginIF($products)->andWhere('product')->in($products)->fi() + ->beginIF(strpos($param, 'skipparent') !== false)->andWhere('parent')->ne(-1)->fi() + ->beginIF(strpos($param, 'unexpired') !== false)->andWhere('end')->ge($date)->fi() ->orderBy('id_desc') ->fetchAll('id'); @@ -345,15 +341,17 @@ class productplanModel extends model * * @param int $productID * @param array $branches + * @param bool $skipParent * @access public * @return array */ - public function getBranchPlanPairs($productID, $branches = '') + public function getBranchPlanPairs($productID, $branches = '', $skipParent = false) { $plans = $this->dao->select('branch,id,title,begin,end')->from(TABLE_PRODUCTPLAN) ->where('product')->eq($productID) ->andWhere('deleted')->eq(0) ->beginIF(!empty($branches))->andWhere('branch')->in($branches)->fi() + ->beginIF($skipParent)->andWhere('parent')->ne(-1)->fi() ->fetchAll('id'); $planPairs = array(); @@ -377,18 +375,6 @@ class productplanModel extends model ->setIF($this->post->future || empty($_POST['end']), 'end', '2030-01-01') ->remove('delta,uid,future') ->get(); - - if(!empty($plan->parentBegin)) - { - if($plan->begin < $plan->parentBegin) dao::$errors['begin'] = sprintf($this->lang->productplan->beginLetterParent, $plan->parentBegin); - } - if(!empty($plan->parentEnd)) - { - if($plan->end !=='2030-01-01' and $plan->end > $plan->parentEnd) dao::$errors['end'] = sprintf($this->lang->productplan->endGreaterParent, $plan->parentEnd); - } - unset($plan->parentBegin); - unset($plan->parentEnd); - if(!$this->post->future and strpos($this->config->productplan->create->requiredFields, 'begin') !== false and empty($_POST['begin'])) { dao::$errors['begin'] = sprintf($this->lang->error->notempty, $this->lang->productplan->begin); @@ -411,7 +397,25 @@ class productplanModel extends model $planID = $this->dao->lastInsertID(); $this->file->updateObjectID($this->post->uid, $planID, 'plan'); $this->loadModel('score')->create('productplan', 'create', $planID); - if(!empty($plan->parent)) $this->dao->update(TABLE_PRODUCTPLAN)->set('parent')->eq('-1')->where('id')->eq($plan->parent)->andWhere('parent')->eq('0')->exec(); + if(!empty($plan->parent)) + { + $parentPlan = $this->getByID($plan->parent); + if($parentPlan->parent == '0') + { + $this->dao->update(TABLE_PRODUCTPLAN)->set('parent')->eq('-1')->where('id')->eq($plan->parent)->andWhere('parent')->eq('0')->exec(); + + /* Transfer stories and bugs linked with the parent plan to the child plan. */ + $this->dao->update(TABLE_PLANSTORY)->set('plan')->eq($planID)->where('plan')->eq($plan->parent)->exec(); + $this->dao->update(TABLE_BUG)->set('plan')->eq($planID)->where('plan')->eq($plan->parent)->exec(); + $stories = $this->dao->select('*')->from(TABLE_STORY)->where("CONCAT(',', plan, ',')")->like("%,{$plan->parent},%")->fetchAll('id'); + foreach($stories as $storyID => $story) + { + $storyPlan = trim($story->plan, ','); + $storyPlan = str_replace(",{$plan->parent},", ",$planID,", ",$storyPlan,"); + $this->dao->update(TABLE_STORY)->set('plan')->eq($storyPlan)->where('id')->eq($storyID)->exec(); + } + } + } return $planID; } } diff --git a/module/productplan/view/browse.html.php b/module/productplan/view/browse.html.php index 79e50de4c6..7ed055988c 100644 --- a/module/productplan/view/browse.html.php +++ b/module/productplan/view/browse.html.php @@ -128,12 +128,12 @@ " . $this->loadModel('flow')->getFieldValue($extendField, $plan) . "";?> parent >= 0) { $executionLink = $config->systemMode == 'new' ? '#projects' : $this->createLink('execution', 'create', "projectID=0&executionID=0©ExecutionID=0&plan=$plan->id&confirm=no&productID=$productID"); if($config->systemMode == 'new') { - echo html::a($executionLink, '', '', "data-toggle='modal' data-id='$plan->id' onclick='getPlanID(this)' class='btn' title='{$lang->productplan->createExecution}'"); + echo html::a($executionLink, '', '', "data-toggle='modal' data-id='$plan->id' onclick='getPlanID(this, $plan->branch)' class='btn' title='{$lang->productplan->createExecution}'"); } else { @@ -141,7 +141,7 @@ } } 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)) echo html::a(inlink('view', "planID=$plan->id&type=bug&orderBy=id_desc&link=true"), '', '', "class='btn' title='{$lang->productplan->linkBug}'"); + 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)) { @@ -188,7 +188,7 @@ - + printExtendFields($project, 'table');?> + + + + diff --git a/module/project/view/create.html.php b/module/project/view/create.html.php index 80565c63d5..750fd09193 100644 --- a/module/project/view/create.html.php +++ b/module/project/view/create.html.php @@ -25,7 +25,7 @@
    - config->maxVersion) ? $lang->project->create . ' - ' . zget($lang->project->modelList, $model, '') : $lang->project->create;?> + project->create . ' - ' . zget($lang->project->modelList, $model, '');?>

    diff --git a/module/project/view/kanban.html.php b/module/project/view/kanban.html.php index 30c267af11..a96ed7ee8d 100644 --- a/module/project/view/kanban.html.php +++ b/module/project/view/kanban.html.php @@ -24,7 +24,7 @@ project->typeList[$type];?>
    -
    +
    @@ -44,6 +44,13 @@ js::set('kanbanGroup', $kanbanGroup); js::set('latestExecutions', $latestExecutions); js::set('programPairs', $programPairs); js::set('doingText', $lang->project->statusList['doing']); +js::set('priv', + array( + 'canStart' => common::hasPriv('project', 'start'), + 'canClose' => common::hasPriv('project', 'close'), + 'canActivate' => common::hasPriv('project', 'activate'), + ) +); ?> diff --git a/module/project/view/managepriv.html.php b/module/project/view/managepriv.html.php index 6c8eae9057..c5b75fb85e 100644 --- a/module/project/view/managepriv.html.php +++ b/module/project/view/managepriv.html.php @@ -21,6 +21,7 @@ #mainMenu #groupName{line-height:33px; float: left} .checkbox-right{padding-left:0px !important;} +.thWidth {width: 160px;} td.menus {border-right: 0;padding-right: 0;width: 220px !important;} td.menus + td {border-left: 0;} .menus .checkbox-primary {float: left; width: 220px;} diff --git a/module/project/view/manageproducts.html.php b/module/project/view/manageproducts.html.php index 2c92d6e14b..e4b6ca5b82 100644 --- a/module/project/view/manageproducts.html.php +++ b/module/project/view/manageproducts.html.php @@ -37,11 +37,11 @@ ";?>
    - +
    - + diff --git a/module/project/view/view.html.php b/module/project/view/view.html.php index 656b9d99d6..82cc615f3c 100644 --- a/module/project/view/view.html.php +++ b/module/project/view/view.html.php @@ -182,25 +182,31 @@ - - + + - - + + + + - - + + + + + +
    productplan->project?>
    @@ -202,5 +202,4 @@ -productplan->projectNotEmpty)?> diff --git a/module/productplan/view/create.html.php b/module/productplan/view/create.html.php index 8c42ea6126..c131aa5f34 100644 --- a/module/productplan/view/create.html.php +++ b/module/productplan/view/create.html.php @@ -22,15 +22,12 @@

    productplan->createChildren : $lang->productplan->create;?>

    - +
    - + @@ -53,7 +50,7 @@ - + - + diff --git a/module/program/view/project.html.php b/module/program/view/project.html.php index 3b82bc8ecb..c408924192 100644 --- a/module/program/view/project.html.php +++ b/module/program/view/project.html.php @@ -30,11 +30,7 @@ js::set('browseType', $browseType); $lang->project->mine), '', $this->cookie->involved ? 'checked=checked' : '');?>
    - config->maxVersion)):?> ' . $lang->project->create, '', 'class="btn btn-primary" data-toggle="modal" data-target="#guideDialog"');?> - config->systemMode == 'new'):?> - ' . $lang->project->create, '', 'class="btn btn-primary"');?> -
    @@ -43,11 +39,7 @@ js::set('browseType', $browseType);

    project->empty;?> - config->maxVersion)):?> ' . $lang->project->create, '', 'class="btn btn-info btn-wide " data-toggle="modal" data-target="#guideDialog"');?> - config->systemMode == 'new'):?> - ' . $lang->project->create, '', 'class="btn btn-info btn-wide"');?> -

    diff --git a/module/program/view/stakeholder.html.php b/module/program/view/stakeholder.html.php index 05f4574d08..74b51fbcbf 100644 --- a/module/program/view/stakeholder.html.php +++ b/module/program/view/stakeholder.html.php @@ -17,7 +17,7 @@ createLink('program', 'stakeholder', "programID=$programID"), '' . $lang->program->stakeholder . '', '', 'class="btn btn-link btn-active-text"');?>
    - " . $lang->program->createStakeholder, '', "class='btn btn-primary'");?> + " . $lang->program->createStakeholder, '', "class='btn btn-primary'");?>
    diff --git a/module/programplan/config.php b/module/programplan/config.php new file mode 100644 index 0000000000..2710d95c98 --- /dev/null +++ b/module/programplan/config.php @@ -0,0 +1,54 @@ +programplan->create = new stdclass(); +$config->programplan->edit = new stdclass(); +$config->programplan->create->requiredFields = 'name,begin,end'; +$config->programplan->edit->requiredFields = 'name,begin,end'; + +$config->programplan->datatable = new stdclass(); +$config->programplan->datatable->defaultField = array('id', 'name', 'percent', 'attribute', 'begin', 'end', 'realBegan', 'realEnd', 'actions'); + +$config->programplan->datatable->fieldList['id']['title'] = 'idAB'; +$config->programplan->datatable->fieldList['id']['fixed'] = 'left'; +$config->programplan->datatable->fieldList['id']['width'] = '70'; +$config->programplan->datatable->fieldList['id']['required'] = 'yes'; + +$config->programplan->datatable->fieldList['name']['title'] = 'name'; +$config->programplan->datatable->fieldList['name']['fixed'] = 'left'; +$config->programplan->datatable->fieldList['name']['width'] = 'auto'; +$config->programplan->datatable->fieldList['name']['required'] = 'yes'; + +$config->programplan->datatable->fieldList['percent']['title'] = 'percent'; +$config->programplan->datatable->fieldList['percent']['fixed'] = 'no'; +$config->programplan->datatable->fieldList['percent']['width'] = '100'; +$config->programplan->datatable->fieldList['percent']['required'] = 'no'; + +$config->programplan->datatable->fieldList['attribute']['title'] = 'attribute'; +$config->programplan->datatable->fieldList['attribute']['fixed'] = 'no'; +$config->programplan->datatable->fieldList['attribute']['width'] = '90'; +$config->programplan->datatable->fieldList['attribute']['required'] = 'no'; + +$config->programplan->datatable->fieldList['begin']['title'] = 'begin'; +$config->programplan->datatable->fieldList['begin']['fixed'] = 'no'; +$config->programplan->datatable->fieldList['begin']['width'] = '90'; +$config->programplan->datatable->fieldList['begin']['required'] = 'no'; + +$config->programplan->datatable->fieldList['end']['title'] = 'end'; +$config->programplan->datatable->fieldList['end']['fixed'] = 'no'; +$config->programplan->datatable->fieldList['end']['width'] = '90'; +$config->programplan->datatable->fieldList['end']['required'] = 'no'; + +$config->programplan->datatable->fieldList['realBegan']['title'] = 'realBegan'; +$config->programplan->datatable->fieldList['realBegan']['fixed'] = 'no'; +$config->programplan->datatable->fieldList['realBegan']['width'] = '90'; +$config->programplan->datatable->fieldList['realBegan']['required'] = 'no'; + +$config->programplan->datatable->fieldList['realEnd']['title'] = 'realEnd'; +$config->programplan->datatable->fieldList['realEnd']['fixed'] = 'no'; +$config->programplan->datatable->fieldList['realEnd']['width'] = '90'; +$config->programplan->datatable->fieldList['realEnd']['required'] = 'no'; + +$config->programplan->datatable->fieldList['actions']['title'] = 'actions'; +$config->programplan->datatable->fieldList['actions']['fixed'] = 'right'; +$config->programplan->datatable->fieldList['actions']['width'] = '150'; +$config->programplan->datatable->fieldList['actions']['required'] = 'yes'; +$config->programplan->datatable->fieldList['actions']['sort'] = 'no'; diff --git a/module/programplan/control.php b/module/programplan/control.php new file mode 100644 index 0000000000..457fed7b61 --- /dev/null +++ b/module/programplan/control.php @@ -0,0 +1,201 @@ + + * @package programplan + * @version $Id: control.php 5107 2013-07-12 01:46:12Z chencongzhi520@gmail.com $ + * @link http://www.zentao.net + */ +class programplan extends control +{ + /** + * __construct + * + * @param string $moduleName + * @param string $methodName + * @access public + * @return void + */ + public function __construct($moduleName = '', $methodName = '') + { + parent::__construct($moduleName, $methodName); + } + + /** + * Common action. + * + * @param int $projectID + * @param int $productID + * @param string $extra + * @access public + * @return void + */ + public function commonAction($projectID, $productID = 0, $extra = '') + { + $products = $this->loadModel('product')->getProductPairsByProject($projectID); + $productID = $this->product->saveState($productID, $products); + + $this->productID = $productID; + $this->loadModel('project')->setMenu($projectID); + } + + /** + * Browse program plans. + * + * @param int $projectID + * @param int $productID + * @param string $type + * @param string $orderBy + * @param int $baselineID + * @access public + * @return void + */ + public function browse($projectID = 0, $productID = 0, $type = 'gantt', $orderBy = 'id_asc', $baselineID = 0) + { + $this->app->loadLang('stage'); + $this->commonAction($projectID, $productID, $type); + $this->session->set('projectPlanList', $this->app->getURI(true), 'project'); + + if(!defined('RUN_MODE') || RUN_MODE != 'api') $projectID = $this->project->saveState((int)$projectID, $this->project->getPairsByProgram()); + + $products = $this->loadModel('product')->getProducts($projectID); + $this->lang->modulePageNav = $this->product->select($products, $this->productID, 'programplan', 'browse', $type, 0, 0, '', false); + + if(common::hasPriv('programplan', 'create')) $this->lang->TRActions = html::a($this->createLink('programplan', 'create', "projectID=$projectID"), " " . $this->lang->programplan->create, '', "class='btn btn-primary'"); + + $selectCustom = 0; // Display date and task settings. + $dateDetails = 1; // Gantt chart detail date display. + if($type == 'gantt') + { + $owner = $this->app->user->account; + $module = 'programplan'; + $section = 'browse'; + $object = 'stageCustom'; + $selectCustom = $this->loadModel('setting')->getItem("owner={$owner}&module={$module}§ion={$section}&key={$object}"); + if(strpos($selectCustom, 'date') !== false) $dateDetails = 0; + + $plans = $this->programplan->getDataForGantt($projectID, $this->productID, $baselineID); + } + + if($type == 'lists') + { + $sort = $this->loadModel('common')->appendOrder($orderBy); + $this->loadModel('datatable'); + $plans = $this->programplan->getPlans($projectID, $this->productID, $sort); + } + + $this->view->title = $this->lang->programplan->browse; + $this->view->position[] = $this->lang->programplan->browse; + $this->view->projectID = $projectID; + $this->view->productID = $this->productID; + $this->view->type = $type; + $this->view->plans = $plans; + $this->view->orderBy = $orderBy; + $this->view->selectCustom = $selectCustom; + $this->view->dateDetails = $dateDetails; + $this->view->users = $this->loadModel('user')->getPairs('noletter'); + + $this->display(); + } + + /** + * Create a project plan. + * + * @param int $projectID + * @param int $productID + * @param int $planID + * @access public + * @return void + */ + public function create($projectID = 0, $productID = 0, $planID = 0) + { + $this->commonAction($projectID, $productID); + $this->app->loadLang('project'); + if($_POST) + { + $this->programplan->create($projectID, $this->productID, $planID); + if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); + + $locate = $this->session->projectPlanList ? $this->session->projectPlanList : $this->createLink('programplan', 'browse', "projectID=$projectID"); + return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => $locate)); + } + + $this->app->loadLang('stage'); + $project = $this->loadModel('project')->getById($projectID); + + $this->view->title = $this->lang->programplan->create . $this->lang->colon . $project->name; + $this->view->position[] = html::a($this->createLink('programplan', 'browse', "projectID=$projectID"), $project->name); + $this->view->position[] = $this->lang->programplan->create; + + $this->view->project = $project; + $this->view->stages = empty($planID) ? $this->loadModel('stage')->getStages('id_asc') : array(); + $this->view->programPlan = $this->project->getById($planID, 'stage'); + $this->view->plans = $this->programplan->getStage($planID ? $planID : $projectID, $this->productID, 'parent'); + $this->view->planID = $planID; + $this->view->type = 'lists'; + + $this->display(); + } + + /** + * Edit a project plan. + * + * @param int $planID + * @param int $projectID + * @access public + * @return void + */ + public function edit($planID = 0, $projectID = 0) + { + $this->app->loadLang('project'); + $plan = $this->programplan->getByID($planID); + if($_POST) + { + $changes = $this->programplan->update($planID, $projectID); + + if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); + if($changes) + { + $actionID = $this->loadModel('action')->create('execution', $planID, 'edited'); + $this->action->logHistory($actionID, $changes); + } + $locate = isonlybody() ? 'parent' : inlink('browse', "program=$plan->program&type=lists"); + return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => $locate)); + } + + $this->app->loadLang('stage'); + $this->view->title = $this->lang->programplan->edit; + $this->view->position[] = $this->lang->programplan->edit; + $this->view->parentStage = $this->programplan->getParentStageList($this->session->project, $planID, $plan->product); + $this->view->isCreateTask = $this->programplan->isCreateTask($planID); + $this->view->plan = $plan; + + $this->display(); + } + + /** + * Save custom settings via ajax. + * + * @access public + * @return void + */ + public function ajaxCustom() + { + $data = fixer::input('post')->get(); + $owner = $this->app->user->account; + $module = 'programplan'; + $section = 'browse'; + $object = 'stageCustom'; + $setting = $this->loadModel('setting'); + $custom = empty($data->stageCustom) ? '' : implode(',', $data->stageCustom); + $setting->setItem("$owner.$module.$section.$object", $custom); + + $response = array(); + $response['result'] = 'success'; + $response['message'] = ''; + return $this->send($response); + } +} diff --git a/module/programplan/js/browse.js b/module/programplan/js/browse.js new file mode 100644 index 0000000000..480420dc76 --- /dev/null +++ b/module/programplan/js/browse.js @@ -0,0 +1,34 @@ +$(function() +{ + setTimeout(function() + { + fixScroll(); + }, 500); +}) + +function fixScroll() +{ + var $scrollwrapper = $('div.datatable').first().find('.scroll-wrapper:first'); + if($scrollwrapper.size() == 0)return; + + var $tfoot = $('div.datatable').first().find('table tfoot:last'); + var scrollOffset = $scrollwrapper.offset().top + $scrollwrapper.find('.scroll-slide').height(); + if($tfoot.size() > 0) scrollOffset += $tfoot.height(); + if($('div.datatable.head-fixed').size() == 0) scrollOffset -= '29'; + var windowH = $(window).height(); + if(scrollOffset > windowH + $(window).scrollTop()) $scrollwrapper.css({'position': 'fixed', 'bottom': 50 + 'px'}); + $(window).scroll(function() + { + newBottom = $tfoot.hasClass('fixedTfootAction') ? 50 + $tfoot.height() : 50; + if(typeof(ssoRedirect) != "undefined") newBottom = 50; + if(scrollOffset <= windowH + $(window).scrollTop()) + { + $scrollwrapper.css({'position':'relative', 'bottom': '0px'}); + } + else if($scrollwrapper.css('position') != 'fixed') + { + $scrollwrapper.css({'position': 'fixed', 'bottom': newBottom + 'px'}); + bottom = newBottom; + } + }); +} diff --git a/module/programplan/js/create.js b/module/programplan/js/create.js new file mode 100644 index 0000000000..62a08dbd0e --- /dev/null +++ b/module/programplan/js/create.js @@ -0,0 +1,16 @@ +function addItem(obj) +{ + var item = $('#addItem').html().replace(/%i%/g, i); + $(obj).closest('tr').after('
    ' + item + ''); + var newItem = $('#names' + i).closest('tr'); + newItem.find('.form-date').datepicker(); + $("#output" + i).chosen(); + $("#output_i__chosen").remove(); + i ++; +} + +function deleteItem(obj) +{ + if($('#planForm .table tbody').children().length < 2) return false; + $(obj).closest('tr').remove(); +} diff --git a/module/programplan/lang/de.php b/module/programplan/lang/de.php new file mode 100644 index 0000000000..37a10b3e13 --- /dev/null +++ b/module/programplan/lang/de.php @@ -0,0 +1,73 @@ + + * @package programplan + * @version $Id: en.php 4729 2013-05-03 07:53:55Z chencongzhi520@gmail.com $ + * @link http://www.zentao.net + */ +$lang->programplan->common = 'Program Plan'; +$lang->programplan->browse = 'Program Plan'; +$lang->programplan->gantt = 'Gantt Chart'; +$lang->programplan->list = 'Stage List'; +$lang->programplan->create = 'Create'; +$lang->programplan->edit = 'Edit'; +$lang->programplan->delete = 'Delete Stage'; +$lang->programplan->createSubPlan = 'Create Sub Plan'; + +$lang->programplan->parent = 'Parent Stage'; +$lang->programplan->emptyParent = 'N/A'; +$lang->programplan->name = 'Stage Name'; +$lang->programplan->subStageName = 'Sub Stage Name'; +$lang->programplan->percent = 'Workload Ratio'; +$lang->programplan->percentAB = 'Workload Ratio'; +$lang->programplan->planPercent = 'Workload'; +$lang->programplan->attribute = 'Stage Type'; +$lang->programplan->milestone = 'Milestone'; +$lang->programplan->taskProgress = 'Task Progress'; +$lang->programplan->task = 'Task'; +$lang->programplan->begin = 'Begin'; +$lang->programplan->end = 'End'; +$lang->programplan->realBegan = 'Actual Started'; +$lang->programplan->realEnd = 'Actual End'; +$lang->programplan->ac = 'Actual cost'; +$lang->programplan->sv = 'Schedule Variance'; +$lang->programplan->cv = 'Cost Variance'; +$lang->programplan->planDateRange = 'Planned Start'; +$lang->programplan->realDateRange = 'Actual Start'; +$lang->programplan->output = 'Output'; +$lang->programplan->openedBy = 'Created By'; +$lang->programplan->openedDate = 'Created Date'; +$lang->programplan->editedBy = 'Edited By'; +$lang->programplan->editedDate = 'Edited Date'; +$lang->programplan->duration = 'Duration'; +$lang->programplan->version = 'Version'; +$lang->programplan->full = 'Full Screen'; +$lang->programplan->today = 'Today'; +$lang->programplan->exporting = 'Exporting'; +$lang->programplan->exportFail = 'Export failed'; +$lang->programplan->hideCriticalPath = 'Hide Critical Path'; +$lang->programplan->showCriticalPath = 'Show Critical Path'; + +$lang->programplan->milestoneList[1] = 'Yes'; +$lang->programplan->milestoneList[0] = 'No'; + +$lang->programplan->noData = 'No Data'; +$lang->programplan->children = 'Sub Plan'; +$lang->programplan->childrenAB = 'Child'; +$lang->programplan->confirmDelete = 'Do you want to delete the current plan?'; +$lang->programplan->workloadTips = 'The proportion of the sub stage workload is divided by 100%.'; + +$lang->programplan->stageCustom = new stdClass(); +$lang->programplan->stageCustom->date = 'Show Date'; +$lang->programplan->stageCustom->task = 'Show Task'; + +$lang->programplan->error = new stdclass(); +$lang->programplan->error->percentNumber = '"Workload %" must be digits.'; +$lang->programplan->error->planFinishSmall = 'The "End" date must be > the "Begin" date.'; +$lang->programplan->error->percentOver = 'The sum of "Workload %" cannot exceed 100%.'; +$lang->programplan->error->createdTask = 'The task has been decomposed. Sub phases cannot be added.'; +$lang->programplan->error->parentWorkload = 'The sum of the workload of the child phase cannot be greater than that of the parent phase: %s.'; diff --git a/module/programplan/lang/en.php b/module/programplan/lang/en.php new file mode 100644 index 0000000000..de70969dc4 --- /dev/null +++ b/module/programplan/lang/en.php @@ -0,0 +1,75 @@ + + * @package programplan + * @version $Id: en.php 4729 2013-05-03 07:53:55Z chencongzhi520@gmail.com $ + * @link http://www.zentao.net + */ +$lang->programplan->common = 'Program Plan'; +$lang->programplan->browse = 'Program Plan'; +$lang->programplan->gantt = 'Gantt Chart'; +$lang->programplan->list = 'Stage List'; +$lang->programplan->create = 'Create'; +$lang->programplan->edit = 'Edit'; +$lang->programplan->delete = 'Delete Stage'; +$lang->programplan->createSubPlan = 'Create Sub Stage'; + +$lang->programplan->parent = 'Parent Stage'; +$lang->programplan->emptyParent = 'N/A'; +$lang->programplan->name = 'Stage Name'; +$lang->programplan->subStageName = 'Sub Stage Name'; +$lang->programplan->percent = 'Workload Ratio'; +$lang->programplan->percentAB = 'Ratio'; +$lang->programplan->planPercent = 'Workload'; +$lang->programplan->attribute = 'Type'; +$lang->programplan->milestone = 'Milestone'; +$lang->programplan->taskProgress = 'Task Progress'; +$lang->programplan->task = 'Task'; +$lang->programplan->begin = 'Begin'; +$lang->programplan->end = 'End'; +$lang->programplan->realBegan = 'Actual Start'; +$lang->programplan->realEnd = 'Actual End'; +$lang->programplan->ac = 'Actual Cost'; +$lang->programplan->sv = 'Schedule Variance'; +$lang->programplan->cv = 'Cost Variance'; +$lang->programplan->planDateRange = 'Planned Start'; +$lang->programplan->realDateRange = 'Actual Start'; +$lang->programplan->output = 'Output'; +$lang->programplan->openedBy = 'Created By'; +$lang->programplan->openedDate = 'Created Date'; +$lang->programplan->editedBy = 'Edited By'; +$lang->programplan->editedDate = 'Edited Date'; +$lang->programplan->duration = 'Duration'; +$lang->programplan->version = 'Version'; +$lang->programplan->full = 'Full Screen'; +$lang->programplan->today = 'Today'; +$lang->programplan->exporting = 'Exporting'; +$lang->programplan->exportFail = 'Failed'; +$lang->programplan->hideCriticalPath = 'Hide Critical Path'; +$lang->programplan->showCriticalPath = 'Show Critical Path'; +$lang->programplan->errorEnd = "Project end date: %s, end date should be <= project end date."; +$lang->programplan->errorBegin = "Project begin date: %s, begin date should be >= project begin date."; + +$lang->programplan->milestoneList[1] = 'Yes'; +$lang->programplan->milestoneList[0] = 'No'; + +$lang->programplan->noData = 'No Data'; +$lang->programplan->children = 'Sub Plan'; +$lang->programplan->childrenAB = 'Child'; +$lang->programplan->confirmDelete = 'Do you want to delete the current plan?'; +$lang->programplan->workloadTips = 'The workload of the sub stage is divided by 100%.'; + +$lang->programplan->stageCustom = new stdClass(); +$lang->programplan->stageCustom->date = 'Show Date'; +$lang->programplan->stageCustom->task = 'Show Task'; + +$lang->programplan->error = new stdclass(); +$lang->programplan->error->percentNumber = '"Workload %" must be digits.'; +$lang->programplan->error->planFinishSmall = 'The "End" date must be > the "Begin" date.'; +$lang->programplan->error->percentOver = 'The sum of "Workload %" cannot exceed 100%.'; +$lang->programplan->error->createdTask = 'The task is decomposed. Sub stages cannot be added.'; +$lang->programplan->error->parentWorkload = 'The sum of the workload in the sub stage cannot be > that in the parent stage: %s.'; diff --git a/module/programplan/lang/fr.php b/module/programplan/lang/fr.php new file mode 100644 index 0000000000..37a10b3e13 --- /dev/null +++ b/module/programplan/lang/fr.php @@ -0,0 +1,73 @@ + + * @package programplan + * @version $Id: en.php 4729 2013-05-03 07:53:55Z chencongzhi520@gmail.com $ + * @link http://www.zentao.net + */ +$lang->programplan->common = 'Program Plan'; +$lang->programplan->browse = 'Program Plan'; +$lang->programplan->gantt = 'Gantt Chart'; +$lang->programplan->list = 'Stage List'; +$lang->programplan->create = 'Create'; +$lang->programplan->edit = 'Edit'; +$lang->programplan->delete = 'Delete Stage'; +$lang->programplan->createSubPlan = 'Create Sub Plan'; + +$lang->programplan->parent = 'Parent Stage'; +$lang->programplan->emptyParent = 'N/A'; +$lang->programplan->name = 'Stage Name'; +$lang->programplan->subStageName = 'Sub Stage Name'; +$lang->programplan->percent = 'Workload Ratio'; +$lang->programplan->percentAB = 'Workload Ratio'; +$lang->programplan->planPercent = 'Workload'; +$lang->programplan->attribute = 'Stage Type'; +$lang->programplan->milestone = 'Milestone'; +$lang->programplan->taskProgress = 'Task Progress'; +$lang->programplan->task = 'Task'; +$lang->programplan->begin = 'Begin'; +$lang->programplan->end = 'End'; +$lang->programplan->realBegan = 'Actual Started'; +$lang->programplan->realEnd = 'Actual End'; +$lang->programplan->ac = 'Actual cost'; +$lang->programplan->sv = 'Schedule Variance'; +$lang->programplan->cv = 'Cost Variance'; +$lang->programplan->planDateRange = 'Planned Start'; +$lang->programplan->realDateRange = 'Actual Start'; +$lang->programplan->output = 'Output'; +$lang->programplan->openedBy = 'Created By'; +$lang->programplan->openedDate = 'Created Date'; +$lang->programplan->editedBy = 'Edited By'; +$lang->programplan->editedDate = 'Edited Date'; +$lang->programplan->duration = 'Duration'; +$lang->programplan->version = 'Version'; +$lang->programplan->full = 'Full Screen'; +$lang->programplan->today = 'Today'; +$lang->programplan->exporting = 'Exporting'; +$lang->programplan->exportFail = 'Export failed'; +$lang->programplan->hideCriticalPath = 'Hide Critical Path'; +$lang->programplan->showCriticalPath = 'Show Critical Path'; + +$lang->programplan->milestoneList[1] = 'Yes'; +$lang->programplan->milestoneList[0] = 'No'; + +$lang->programplan->noData = 'No Data'; +$lang->programplan->children = 'Sub Plan'; +$lang->programplan->childrenAB = 'Child'; +$lang->programplan->confirmDelete = 'Do you want to delete the current plan?'; +$lang->programplan->workloadTips = 'The proportion of the sub stage workload is divided by 100%.'; + +$lang->programplan->stageCustom = new stdClass(); +$lang->programplan->stageCustom->date = 'Show Date'; +$lang->programplan->stageCustom->task = 'Show Task'; + +$lang->programplan->error = new stdclass(); +$lang->programplan->error->percentNumber = '"Workload %" must be digits.'; +$lang->programplan->error->planFinishSmall = 'The "End" date must be > the "Begin" date.'; +$lang->programplan->error->percentOver = 'The sum of "Workload %" cannot exceed 100%.'; +$lang->programplan->error->createdTask = 'The task has been decomposed. Sub phases cannot be added.'; +$lang->programplan->error->parentWorkload = 'The sum of the workload of the child phase cannot be greater than that of the parent phase: %s.'; diff --git a/module/programplan/lang/vi.php b/module/programplan/lang/vi.php new file mode 100644 index 0000000000..37a10b3e13 --- /dev/null +++ b/module/programplan/lang/vi.php @@ -0,0 +1,73 @@ + + * @package programplan + * @version $Id: en.php 4729 2013-05-03 07:53:55Z chencongzhi520@gmail.com $ + * @link http://www.zentao.net + */ +$lang->programplan->common = 'Program Plan'; +$lang->programplan->browse = 'Program Plan'; +$lang->programplan->gantt = 'Gantt Chart'; +$lang->programplan->list = 'Stage List'; +$lang->programplan->create = 'Create'; +$lang->programplan->edit = 'Edit'; +$lang->programplan->delete = 'Delete Stage'; +$lang->programplan->createSubPlan = 'Create Sub Plan'; + +$lang->programplan->parent = 'Parent Stage'; +$lang->programplan->emptyParent = 'N/A'; +$lang->programplan->name = 'Stage Name'; +$lang->programplan->subStageName = 'Sub Stage Name'; +$lang->programplan->percent = 'Workload Ratio'; +$lang->programplan->percentAB = 'Workload Ratio'; +$lang->programplan->planPercent = 'Workload'; +$lang->programplan->attribute = 'Stage Type'; +$lang->programplan->milestone = 'Milestone'; +$lang->programplan->taskProgress = 'Task Progress'; +$lang->programplan->task = 'Task'; +$lang->programplan->begin = 'Begin'; +$lang->programplan->end = 'End'; +$lang->programplan->realBegan = 'Actual Started'; +$lang->programplan->realEnd = 'Actual End'; +$lang->programplan->ac = 'Actual cost'; +$lang->programplan->sv = 'Schedule Variance'; +$lang->programplan->cv = 'Cost Variance'; +$lang->programplan->planDateRange = 'Planned Start'; +$lang->programplan->realDateRange = 'Actual Start'; +$lang->programplan->output = 'Output'; +$lang->programplan->openedBy = 'Created By'; +$lang->programplan->openedDate = 'Created Date'; +$lang->programplan->editedBy = 'Edited By'; +$lang->programplan->editedDate = 'Edited Date'; +$lang->programplan->duration = 'Duration'; +$lang->programplan->version = 'Version'; +$lang->programplan->full = 'Full Screen'; +$lang->programplan->today = 'Today'; +$lang->programplan->exporting = 'Exporting'; +$lang->programplan->exportFail = 'Export failed'; +$lang->programplan->hideCriticalPath = 'Hide Critical Path'; +$lang->programplan->showCriticalPath = 'Show Critical Path'; + +$lang->programplan->milestoneList[1] = 'Yes'; +$lang->programplan->milestoneList[0] = 'No'; + +$lang->programplan->noData = 'No Data'; +$lang->programplan->children = 'Sub Plan'; +$lang->programplan->childrenAB = 'Child'; +$lang->programplan->confirmDelete = 'Do you want to delete the current plan?'; +$lang->programplan->workloadTips = 'The proportion of the sub stage workload is divided by 100%.'; + +$lang->programplan->stageCustom = new stdClass(); +$lang->programplan->stageCustom->date = 'Show Date'; +$lang->programplan->stageCustom->task = 'Show Task'; + +$lang->programplan->error = new stdclass(); +$lang->programplan->error->percentNumber = '"Workload %" must be digits.'; +$lang->programplan->error->planFinishSmall = 'The "End" date must be > the "Begin" date.'; +$lang->programplan->error->percentOver = 'The sum of "Workload %" cannot exceed 100%.'; +$lang->programplan->error->createdTask = 'The task has been decomposed. Sub phases cannot be added.'; +$lang->programplan->error->parentWorkload = 'The sum of the workload of the child phase cannot be greater than that of the parent phase: %s.'; diff --git a/module/programplan/lang/zh-cn.php b/module/programplan/lang/zh-cn.php new file mode 100644 index 0000000000..8107a944f5 --- /dev/null +++ b/module/programplan/lang/zh-cn.php @@ -0,0 +1,76 @@ + + * @package programplan + * @version $Id: zh-cn.php 4729 2013-05-03 07:53:55Z chencongzhi520@gmail.com $ + * @link http://www.zentao.net + */ +$lang->programplan->common = '项目计划'; +$lang->programplan->browse = '浏览阶段计划'; +$lang->programplan->gantt = '甘特图'; +$lang->programplan->list = '阶段列表'; +$lang->programplan->create = '设置阶段'; +$lang->programplan->edit = '编辑'; +$lang->programplan->delete = '删除阶段'; +$lang->programplan->createSubPlan = '创建二级阶段'; + +$lang->programplan->parent = '父阶段'; +$lang->programplan->emptyParent = '无'; +$lang->programplan->name = '阶段名称'; +$lang->programplan->subStageName = '子阶段名称'; +$lang->programplan->percent = '工作量占比'; +$lang->programplan->percentAB = '工作量占比'; +$lang->programplan->planPercent = '工作量'; +$lang->programplan->attribute = '阶段类型'; +$lang->programplan->milestone = '里程碑'; +$lang->programplan->taskProgress = '任务进度'; +$lang->programplan->task = '任务'; +$lang->programplan->begin = '计划开始'; +$lang->programplan->end = '计划完成'; +$lang->programplan->realBegan = '实际开始'; +$lang->programplan->realEnd = '实际完成'; +$lang->programplan->ac = '实际花费'; +$lang->programplan->sv = '进度偏差率'; +$lang->programplan->cv = '成本偏差率'; +$lang->programplan->planDateRange = '计划起始日期'; +$lang->programplan->realDateRange = '实际起始日期'; +$lang->programplan->output = '输出'; +$lang->programplan->openedBy = '由谁创建'; +$lang->programplan->openedDate = '创建日期'; +$lang->programplan->editedBy = '由谁编辑'; +$lang->programplan->editedDate = '编辑日期'; +$lang->programplan->duration = '工期'; +$lang->programplan->version = '版本号'; +$lang->programplan->full = '全屏'; +$lang->programplan->today = '今天'; +$lang->programplan->exporting = '导出'; +$lang->programplan->exportFail = '导出失败'; +$lang->programplan->hideCriticalPath = '隐藏关键路径'; +$lang->programplan->showCriticalPath = '显示关键路径'; +$lang->programplan->errorBegin = '阶段的开始时间不能小于所属项目的开始时间%s'; +$lang->programplan->errorEnd = '阶段的结束时间不能大于所属项目的结束时间%s'; + +$lang->programplan->milestoneList[1] = '是'; +$lang->programplan->milestoneList[0] = '否'; + +$lang->programplan->noData = '暂无数据。'; +$lang->programplan->children = '二级计划'; +$lang->programplan->childrenAB = '子'; +$lang->programplan->confirmDelete = '确定要删除当前计划吗?'; +$lang->programplan->workloadTips = '子阶段工作量占比按百分百的比例进行拆分'; + +$lang->programplan->stageCustom = new stdClass(); +$lang->programplan->stageCustom->date = '显示日期'; +$lang->programplan->stageCustom->task = '显示任务'; + +$lang->programplan->error = new stdclass(); +$lang->programplan->error->percentNumber = '"工作量比例"必须为数字'; +$lang->programplan->error->planFinishSmall = '"计划完成时间"必须大于"计划开始时间"'; +$lang->programplan->error->percentOver = '工作量占比累计不应当超过100%'; +$lang->programplan->error->createdTask = '已分解任务,不可添加子阶段'; +$lang->programplan->error->parentWorkload = '子阶段的工作量之和不能大于父阶段的工作量:%s'; +$lang->programplan->error->parentDuration = '子阶段计划开始、计划完成不能超过父阶段'; diff --git a/module/programplan/lang/zh-tw.php b/module/programplan/lang/zh-tw.php new file mode 100644 index 0000000000..7afefe8a6f --- /dev/null +++ b/module/programplan/lang/zh-tw.php @@ -0,0 +1,73 @@ + + * @package programplan + * @version $Id: zh-tw.php 4729 2013-05-03 07:53:55Z chencongzhi520@gmail.com $ + * @link http://www.zentao.net + */ +$lang->programplan->common = '項目計劃'; +$lang->programplan->browse = '瀏覽階段計劃'; +$lang->programplan->gantt = '甘特圖'; +$lang->programplan->list = '階段列表'; +$lang->programplan->create = '設置階段'; +$lang->programplan->edit = '編輯'; +$lang->programplan->delete = '刪除'; +$lang->programplan->createSubPlan = '創建二級階段'; + +$lang->programplan->parent = '父階段'; +$lang->programplan->emptyParent = '無'; +$lang->programplan->name = '階段名稱'; +$lang->programplan->subStageName = '子階段名稱'; +$lang->programplan->percent = '工作量占比'; +$lang->programplan->percentAB = '工作量占比'; +$lang->programplan->planPercent = '工作量'; +$lang->programplan->attribute = '階段類型'; +$lang->programplan->milestone = '里程碑'; +$lang->programplan->taskProgress = '任務進度'; +$lang->programplan->task = '任務'; +$lang->programplan->begin = '計劃開始'; +$lang->programplan->end = '計劃完成'; +$lang->programplan->realBegan = '實際開始'; +$lang->programplan->realEnd = '實際完成'; +$lang->programplan->ac = '實際花費'; +$lang->programplan->sv = '進度偏差率'; +$lang->programplan->cv = '成本偏差率'; +$lang->programplan->planDateRange = '計划起始日期'; +$lang->programplan->realDateRange = '實際起始日期'; +$lang->programplan->output = '輸出'; +$lang->programplan->openedBy = '由誰創建'; +$lang->programplan->openedDate = '創建日期'; +$lang->programplan->editedBy = '由誰編輯'; +$lang->programplan->editedDate = '編輯日期'; +$lang->programplan->duration = '計劃工期'; +$lang->programplan->version = '版本號'; +$lang->programplan->full = '全屏'; +$lang->programplan->today = '今天'; +$lang->programplan->exporting = '導出'; +$lang->programplan->exportFail = '導出失敗'; +$lang->programplan->hideCriticalPath = '隱藏關鍵路徑'; +$lang->programplan->showCriticalPath = '顯示關鍵路徑'; + +$lang->programplan->milestoneList[1] = '是'; +$lang->programplan->milestoneList[0] = '否'; + +$lang->programplan->noData = '暫無數據。'; +$lang->programplan->children = '二級計劃'; +$lang->programplan->childrenAB = '子'; +$lang->programplan->confirmDelete = '確定要刪除當前計劃嗎?'; +$lang->programplan->workloadTips = '子階段工作量占比按百分百的比例進行拆分'; + +$lang->programplan->stageCustom = new stdClass(); +$lang->programplan->stageCustom->date = '顯示日期'; +$lang->programplan->stageCustom->task = '顯示任務'; + +$lang->programplan->error = new stdclass(); +$lang->programplan->error->percentNumber = '"工作量比例"必須為數字'; +$lang->programplan->error->planFinishSmall = '"計劃完成時間"必須大於"計劃開始時間"'; +$lang->programplan->error->percentOver = '工作量占比累計不應當超過100%'; +$lang->programplan->error->createdTask = '已分解任務,不可添加子階段'; +$lang->programplan->error->parentWorkload = '子階段的工作量之和不能大於父階段的工作量:%s'; diff --git a/module/programplan/model.php b/module/programplan/model.php new file mode 100644 index 0000000000..b471de16a7 --- /dev/null +++ b/module/programplan/model.php @@ -0,0 +1,941 @@ + + * @package programplan + * @version $Id: model.php 5079 2013-07-10 00:44:34Z chencongzhi520@gmail.com $ + * @link http://www.zentao.net + */ +?> +dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($planID)->fetch(); + + return $this->processPlan($plan); + } + + /** + * Get plans list. + * + * @param int $executionID + * @param int $productID + * @param string $browseType all|parent + * @param string $orderBy + * @access public + * @return array + */ + public function getStage($executionID = 0, $productID = 0, $browseType = 'all', $orderBy = 'id_asc') + { + if(empty($executionID) || empty($productID)) return array(); + + $plans = $this->dao->select('t2.*')->from(TABLE_PROJECTPRODUCT)->alias('t1') + ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id') + ->where('t1.product')->eq($productID) + ->andWhere('t2.type')->eq('stage') + ->beginIF($browseType == 'all')->andWhere('t2.project')->eq($executionID)->fi() + ->beginIF($browseType == 'parent')->andWhere('t2.parent')->eq($executionID)->fi() + ->beginIF(!$this->app->user->admin)->andWhere('t2.id')->in($this->app->user->view->sprints)->fi() + ->andWhere('t2.deleted')->eq('0') + ->orderBy($orderBy) + ->fetchAll('id'); + + return $this->processPlans($plans); + } + + /** + * Get plans by idList. + * + * @param array $idList + * @access public + * @return array + */ + public function getByList($idList = array()) + { + $plans = $this->dao->select('*')->from(TABLE_PROJECT) + ->where('id')->in($idList) + ->andWhere('type')->eq('project') + ->fetchAll('id'); + + return $this->processPlans($plans); + } + + /** + * Get plans. + * + * @param int $executionID + * @param int $productID + * @param string $orderBy + * @access public + * @return array + */ + public function getPlans($executionID = 0, $productID = 0, $orderBy = 'id_asc') + { + $plans = $this->getStage($executionID, $productID, 'all', $orderBy); + + $parents = array(); + $children = array(); + foreach($plans as $planID => $plan) + { + $plan->grade == 1 ? $parents[$planID] = $plan : $children[$plan->parent][] = $plan; + } + + foreach($parents as $planID => $plan) $parents[$planID]->children = isset($children[$planID]) ? $children[$planID] : array(); + + return $parents; + } + + /** + * Get pairs. + * + * @param int $executionID + * @param int $productID + * @param string $type + * @access public + * @return array + */ + public function getPairs($executionID, $productID = 0, $type = 'all') + { + $plans = $this->getPlans($executionID, $productID); + + $pairs = array(0 => ''); + foreach($plans as $plan) + { + $pairs[$plan->id] = $plan->name; + if(!empty($plan->children)) + { + foreach($plan->children as $child) $pairs[$child->id] = $plan->name . '/' . $child->name; + } + } + + return $pairs; + } + + /** + * Get gantt data. + * + * @param int $executionID + * @param int $productID + * @param int $baselineID + * @param string $selectCustom + * @param bool $returnJson + * @access public + * @return string + */ + public function getDataForGantt($executionID, $productID, $baselineID = 0, $selectCustom = '', $returnJson = true) + { + $this->loadModel('stage'); + + $plans = $this->getStage($executionID, $productID); + if($baselineID) + { + $baseline = $this->loadModel('cm')->getByID($baselineID); + $oldData = json_decode($baseline->data); + $oldPlans = $oldData->stage; + foreach($oldPlans as $id => $oldPlan) + { + if(!isset($plans[$id])) continue; + $plans[$id]->version = $oldPlan->version; + $plans[$id]->name = $oldPlan->name; + $plans[$id]->milestone = $oldPlan->milestone; + $plans[$id]->begin = $oldPlan->begin; + $plans[$id]->end = $oldPlan->end; + } + } + + $datas = array(); + $planIDList = array(); + $isMilestone = " "; + $stageIndex = array(); + foreach($plans as $plan) + { + $planIDList[$plan->id] = $plan->id; + + $start = helper::isZeroDate($plan->begin) ? '' : $plan->begin; + $end = helper::isZeroDate($plan->end) ? '' : $plan->end; + + $data = new stdclass(); + $data->id = $plan->id; + $data->type = 'plan'; + $data->text = empty($plan->milestone) ? $plan->name : $plan->name . $isMilestone ; + $data->percent = $plan->percent; + $data->attribute = zget($this->lang->stage->typeList, $plan->attribute); + $data->milestone = zget($this->lang->programplan->milestoneList, $plan->milestone); + $data->begin = $start; + $data->deadline = $end; + $data->realBegan = helper::isZeroDate($plan->realBegan) ? '' : substr($plan->realBegan, 0, 10); + $data->realEnd = helper::isZeroDate($plan->realEnd) ? '' : substr($plan->realEnd, 0, 10); + $data->parent = $plan->grade == 1 ? 0 :$plan->parent; + $data->open = true; + $data->start_date = $data->realBegan ? $data->realBegan : $data->begin; + $data->endDate = $data->realEnd ? $data->realEnd : $data->deadline; + $data->duration = 0; + + if($data->endDate > $data->start_date) $data->duration = helper::diffDate($data->endDate, $data->start_date) + 1; + if($data->start_date) $data->start_date = date('d-m-Y', strtotime($data->start_date)); + if($data->start_date == '' or $data->endDate == '') $data->duration = 0; + + $datas['data'][] = $data; + $stageIndex[] = array('planID' => $plan->id, 'progress' => array('totalConsumed' => 0, 'totalReal' => 0)); + } + + $taskSign = "[ T ] "; + $taskPri = "%s "; + + /* Judge whether to display tasks under the stage. */ + $owner = $this->app->user->account; + $module = 'programplan'; + $section = 'browse'; + $object = 'stageCustom'; + + if(empty($selectCustom)) $selectCustom = $this->loadModel('setting')->getItem("owner={$owner}&module={$module}§ion={$section}&key={$object}"); + + $tasks = array(); + if(strpos($selectCustom, 'task') !== false) + { + $tasks = $this->dao->select('*')->from(TABLE_TASK)->where('deleted')->eq(0)->andWhere('execution')->in($planIDList)->fetchAll('id'); + } + + if($baselineID) + { + $oldTasks = $oldData->task; + foreach($oldTasks as $id => $oldTask) + { + if(!isset($tasks->$id)) continue; + $tasks->$id->version = $oldTask->version; + $tasks->$id->name = $oldTask->name; + $tasks->$id->estStarted = $oldTask->estStarted; + $tasks->$id->deadline = $oldTask->deadline; + } + } + + foreach($tasks as $task) + { + $start = helper::isZeroDate($task->estStarted) ? '' : $task->estStarted; + $end = helper::isZeroDate($task->deadline) ? '' : $task->deadline; + + $realBegan = helper::isZeroDate($task->realStarted) ? '' : substr($task->realStarted, 0, 10); + $realEnd = helper::isZeroDate($task->finishedDate) ? '' : substr($task->finishedDate, 0, 10); + $priIcon = sprintf($taskPri, $task->pri, $task->pri, $task->pri); + + $data = new stdclass(); + $data->id = $task->execution . '-' . $task->id; + $data->type = 'task'; + $data->text = $taskSign . $priIcon . $task->name; + $data->percent = ''; + $data->attribute = ''; + $data->milestone = ''; + $data->begin = $start; + $data->deadline = $end; + $data->realBegan = $realBegan; + $data->realEnd = $realEnd; + $data->parent = $task->parent > 0 ? $task->execution . '-' . $task->parent : $task->execution; + $data->open = true; + $progress = $task->consumed ? round($task->consumed / ($task->left + $task->consumed), 3) * 100 : 0; + $data->taskProgress = $progress . '%'; + $data->start_date = $data->realBegan ? $data->realBegan : $data->begin; + $data->endDate = $data->realEnd ? $data->realEnd : $data->deadline; + $data->duration = 0; + + if($data->endDate > $data->start_date) $data->duration = helper::diffDate($data->endDate, $data->start_date) + 1; + if($data->start_date) $data->start_date = date('d-m-Y', strtotime($data->start_date)); + if($data->start_date == '' or $data->endDate == '') $data->duration = 0; + + $datas['data'][] = $data; + foreach($stageIndex as $index => $stage) + { + if($stage['planID'] == $task->execution) + { + $stageIndex[$index]['progress']['totalConsumed'] += $task->consumed; + $stageIndex[$index]['progress']['totalReal'] += ($task->left + $task->consumed); + } + } + } + + /* Calculate the progress of the phase. */ + foreach($stageIndex as $index => $stage) + { + $progress = empty($stage['progress']['totalConsumed']) ? 0 : round($stage['progress']['totalConsumed'] / $stage['progress']['totalReal'], 3) * 100; + $progress .= '%'; + $datas['data'][$index]->taskProgress = $progress; + } + + return $returnJson ? json_encode($datas) : $datas; + } + + /** + * Get total percent. + * + * @param object $stage + * @param object $parent + * @access public + * @return int + */ + public function getTotalPercent($stage, $parent = false) + { + /* When parent is equal to true, query the total workload of the subphase. */ + $executionID = $parent ? $stage->id : $stage->project; + $plans = $this->getStage($executionID, $stage->product, 'parent'); + + $totalPercent = 0; + $stageID = $stage->id; + foreach($plans as $id => $stage) + { + if($id == $stageID) continue; + $totalPercent += $stage->percent; + } + + return $totalPercent; + } + + /** + * Process plans. + * + * @param int $plans + * @access public + * @return object + */ + public function processPlans($plans) + { + foreach($plans as $planID => $plan) $plans[$planID] = $this->processPlan($plan); + return $plans; + } + + /** + * Process plan. + * + * @param int $plan + * @access public + * @return object + */ + public function processPlan($plan) + { + $plan->setMilestone = true; + + if($plan->parent) + { + $attribute = $this->dao->select('attribute')->from(TABLE_PROJECT)->where('id')->eq($plan->parent)->fetch('attribute'); + $plan->attribute = $attribute == 'develop' ? $attribute : $plan->attribute; + } + else + { + $milestones = $this->dao->select('count(*) AS count')->from(TABLE_PROJECT) + ->where('parent')->eq($plan->id) + ->andWhere('milestone')->eq(1) + ->andWhere('deleted')->eq(0) + ->fetch('count'); + if($milestones > 0) + { + $plan->milestone = 0; + $plan->setMilestone = false; + } + } + + $plan->begin = $plan->begin == '0000-00-00' ? '' : $plan->begin; + $plan->end = $plan->end == '0000-00-00' ? '' : $plan->end; + $plan->realBegan = $plan->realBegan == '0000-00-00' ? '' : $plan->realBegan; + $plan->realEnd = $plan->realEnd == '0000-00-00' ? '' : $plan->realEnd; + + $plan->product = $this->loadModel('product')->getProductIDByProject($plan->id); + $plan->productName = $this->dao->findByID($plan->product)->from(TABLE_PRODUCT)->fetch('name'); + + return $plan; + } + + /** + * Get duration. + * + * @param int $begin + * @param int $end + * @access public + * @return int + */ + public function getDuration($begin, $end) + { + $duration = $this->loadModel('holiday')->getActualWorkingDays($begin, $end); + return count($duration); + } + + /** + * Create a plan. + * + * @param int $projectID + * @param int $productID + * @param int $parentID + * @access public + * @return bool + */ + public function create($projectID = 0, $productID = 0, $parentID = 0) + { + $data = (array)fixer::input('post')->get(); + extract($data); + + /* Determine if a task has been created under the parent phase. */ + if(!$this->isCreateTask($parentID)) return dao::$errors['message'][] = $this->lang->programplan->error->createdTask; + + /* The child phase type setting is the same as the parent phase. */ + $parentAttribute = ''; + $parentPercent = 0; + if($parentID) + { + $parentStage = $this->getByID($parentID); + $parentAttribute = $parentStage->attribute; + $parentPercent = $parentStage->percent; + $parentACL = $parentStage->acl; + } + + $attributes = array_values($attributes); + $milestone = array_values($milestone); + $datas = array(); + foreach($names as $key => $name) + { + if(empty($name)) continue; + + $plan = new stdclass(); + $plan->id = isset($planIDList[$key]) ? $planIDList[$key] : ''; + $plan->type = 'stage'; + $plan->project = $projectID; + $plan->parent = $parentID ? $parentID : $projectID; + $plan->name = $names[$key]; + $plan->percent = $percents[$key]; + $plan->attribute = empty($parentID) ? $attributes[$key] : $parentAttribute; + $plan->milestone = $milestone[$key]; + $plan->begin = empty($begin[$key]) ? '0000-00-00' : $begin[$key]; + $plan->end = empty($end[$key]) ? '0000-00-00' : $end[$key]; + $plan->realBegan = empty($realBegan[$key]) ? '0000-00-00' : $realBegan[$key]; + $plan->realEnd = empty($realEnd[$key]) ? '0000-00-00' : $realEnd[$key]; + $plan->output = empty($output[$key]) ? '' : implode(',', $output[$key]); + $plan->acl = empty($parentID) ? $acl[$key] : $parentACL; + + $datas[] = $plan; + } + + $project = $this->loadModel('project')->getByID($projectID); + + $totalPercent = 0; + $totalDevType = 0; + $milestone = 0; + foreach($datas as $plan) + { + if($plan->percent and !preg_match("/^[0-9]+(.[0-9]{1,3})?$/", $plan->percent)) + { + dao::$errors['message'][] = $this->lang->programplan->error->percentNumber; + return false; + } + if($plan->end != '0000-00-00' and $plan->end < $plan->begin) + { + dao::$errors['message'][] = $this->lang->programplan->error->planFinishSmall; + return false; + } + if(isset($parentStage) and ($plan->end > $parentStage->end || $plan->begin < $parentStage->begin)) + { + dao::$errors['message'][] = $this->lang->programplan->error->parentDuration; + return false; + } + if($plan->begin < $project->begin) + { + dao::$errors['message'][] = sprintf($this->lang->programplan->errorBegin, $project->begin); + return false; + } + if($plan->end != '0000-00-00' and $plan->end > $project->end) + { + dao::$errors['message'][] = sprintf($this->lang->programplan->errorEnd, $project->end); + return false; + } + + if($plan->begin == '0000-00-00') $plan->begin = ''; + if($plan->end == '0000-00-00') $plan->end = ''; + foreach(explode(',', $this->config->programplan->create->requiredFields) as $field) + { + $field = trim($field); + if($field and empty($plan->$field)) + { + dao::$errors['message'][] = sprintf($this->lang->error->notempty, $this->lang->programplan->$field); + return false; + } + } + + $plan->percent = (float)$plan->percent; + $totalPercent += $plan->percent; + + if($plan->milestone) $milestone = 1; + } + + if($totalPercent > 100) return dao::$errors['message'][] = $this->lang->programplan->error->percentOver; + + $this->loadModel('action'); + $this->loadModel('user'); + $this->loadModel('execution'); + $this->app->loadLang('doc'); + $account = $this->app->user->account; + $now = helper::now(); + foreach($datas as $data) + { + /* Set planDuration and realDuration. */ + if(isset($this->config->maxVersion)) + { + $data->planDuration = $this->getDuration($data->begin, $data->end); + $data->realDuration = $this->getDuration($data->realBegan, $data->realEnd); + } + + $projectChanged = false; + $data->days = helper::diffDate($data->end, $data->begin) + 1; + if($data->id) + { + $stageID = $data->id; + unset($data->id); + + $oldStage = $this->getByID($stageID); + $planChanged = ($oldStage->name != $data->name || $oldStage->milestone != $data->milestone || $oldStage->begin != $data->begin || $oldStage->end != $data->end); + + if($planChanged) $data->version = $oldStage->version + 1; + $this->dao->update(TABLE_PROJECT)->data($data) + ->autoCheck() + ->batchCheck($this->config->programplan->edit->requiredFields, 'notempty') + ->checkIF($plan->percent != '', 'percent', 'float') + ->where('id')->eq($stageID) + ->exec(); + + if($data->acl != 'open') $this->user->updateUserView($stageID, 'sprint'); + + /* Record version change information. */ + if($planChanged) + { + $spec = new stdclass(); + $spec->project = $stageID; + $spec->version = $data->version; + $spec->name = $data->name; + $spec->milestone = $data->milestone; + $spec->begin = $data->begin; + $spec->end = $data->end; + $this->dao->insert(TABLE_PROJECTSPEC)->data($spec)->exec(); + } + + $changes = common::createChanges($oldStage, $data); + $actionID = $this->action->create('execution', $stageID, 'edited'); + $this->action->logHistory($actionID, $changes); + } + else + { + unset($data->id); + $data->status = 'wait'; + $data->version = 1; + $data->parentVersion = $data->parent == 0 ? 0 : $this->dao->findByID($data->parent)->from(TABLE_PROJECT)->fetch('version'); + $data->team = substr($data->name,0, 30); + $data->openedBy = $account; + $data->openedDate = $now; + $data->openedVersion = $this->config->version; + if(!isset($data->acl)) $data->acl = $this->dao->findByID($data->parent)->from(TABLE_PROJECT)->fetch('acl'); + $this->dao->insert(TABLE_PROJECT)->data($data) + ->autoCheck() + ->batchCheck($this->config->programplan->create->requiredFields, 'notempty') + ->checkIF($plan->percent != '', 'percent', 'float') + ->exec(); + + if(!dao::isError()) + { + $stageID = $this->dao->lastInsertID(); + + if($data->acl != 'open') $this->user->updateUserView($stageID, 'sprint'); + $this->dao->update(TABLE_PROJECT)->set('`order`')->eq($stageID * 5)->where('id')->eq($stageID)->exec(); + + /* Create doc lib. */ + $lib = new stdclass(); + $lib->execution = $stageID; + $lib->name = str_replace($this->lang->executionCommon, $this->lang->project->stage, $this->lang->doclib->main['execution']); + $lib->type = 'execution'; + $lib->main = '1'; + $lib->acl = 'default'; + $this->dao->insert(TABLE_DOCLIB)->data($lib)->exec(); + + /* Add creators to stage teams and execution teams. */ + $member = new stdclass(); + $member->root = $stageID; + $member->account = $account; + $member->role = $this->lang->user->roleList[$this->app->user->role]; + $member->join = $now; + $member->type = $data->type; + $member->days = $data->days; + $member->hours = $this->config->execution->defaultWorkhours; + $this->dao->insert(TABLE_TEAM)->data($member)->exec(); + $this->execution->addProjectMembers($data->project, array($member)); + + $this->setTreePath($stageID); + if($data->acl != 'open') $this->user->updateUserView($stageID, 'sprint'); + + $this->post->set('products', array(0 => $productID)); + $this->execution->updateProducts($stageID); + + /* Record version change information. */ + $spec = new stdclass(); + $spec->project = $stageID; + $spec->version = $data->version; + $spec->name = $data->name; + $spec->milestone = $data->milestone; + $spec->begin = $data->begin; + $spec->end = $data->end; + $this->dao->insert(TABLE_PROJECTSPEC)->data($spec)->exec(); + + $this->action->create('execution', $stageID, 'opened', '', join(',', $_POST['products'])); + } + } + + /* If child plans has milestone, update parent plan set milestone eq 0 . */ + if($parentID and $milestone) $this->dao->update(TABLE_PROJECT)->set('milestone')->eq(0)->where('id')->eq($parentID)->exec(); + + if(dao::isError()) die(js::error(dao::getError())); + } + } + + /** + * Set stage tree path. + * + * @param int $planID + * @access public + * @return bool + */ + public function setTreePath($planID) + { + $stage = $this->dao->select('id,type,parent,path,grade')->from(TABLE_PROJECT)->where('id')->eq($planID)->fetch(); + $parent = $this->dao->select('id,type,parent,path,grade')->from(TABLE_PROJECT)->where('id')->eq($stage->parent)->fetch(); + + if($parent->type == 'project') + { + $path['path'] = ",{$parent->id},{$stage->id},"; + $path['grade'] = 1; + } + elseif($parent->type == 'stage') + { + $path['path'] = $parent->path . "{$stage->id},"; + $path['grade'] = $parent->grade + 1; + } + $this->dao->update(TABLE_PROJECT)->set('path')->eq($path['path'])->set('grade')->eq($path['grade'])->where('id')->eq($stage->id)->exec(); + } + + /** + * Update a plan. + * + * @param int $planID + * @param int $projectID + * @access public + * @return bool|array + */ + public function update($planID = 0, $projectID = 0) + { + /* Get oldPlan and the data from the post. */ + $oldPlan = $this->getByID($planID); + $plan = fixer::input('post') + ->setDefault('begin', '0000-00-00') + ->setDefault('end', '0000-00-00') + ->setDefault('realBegan', '0000-00-00') + ->setDefault('realEnd', '0000-00-00') + ->join('output', ',') + ->get(); + + /* Judgment of required items. */ + if($plan->begin == '0000-00-00') dao::$errors['begin'][] = sprintf($this->lang->error->notempty, $this->lang->programplan->begin); + if($plan->end == '0000-00-00') dao::$errors['end'][] = sprintf($this->lang->error->notempty, $this->lang->programplan->end); + + $planChanged = ($oldPlan->name != $plan->name || $oldPlan->milestone != $plan->milestone || $oldPlan->begin != $plan->begin || $oldPlan->end != $plan->end); + + if($plan->parent > 0) + { + $parentStage = $this->getByID($plan->parent); + $plan->attribute = $parentStage->attribute; + $plan->acl = $parentStage->acl; + $parentPercent = $parentStage->percent; + + $childrenTotalPercent = $this->getTotalPercent($parentStage, true); + $childrenTotalPercent = $plan->parent == $oldPlan->parent ? ($childrenTotalPercent - $oldPlan->percent + $plan->percent) : ($childrenTotalPercent + $plan->percent); + if($childrenTotalPercent > 100) return dao::$errors['percent'][] = $this->lang->programplan->error->percentOver; + + /* If child plan has milestone, update parent plan set milestone eq 0 . */ + if($plan->milestone and $parentStage->milestone) $this->dao->update(TABLE_PROJECT)->set('milestone')->eq(0)->where('id')->eq($oldPlan->parent)->exec(); + } + else + { + /* Synchronously update sub-phase permissions. */ + $childrenIDList = $this->dao->select('*')->from(TABLE_PROJECT)->where('parent')->eq($oldPlan->id)->fetch('id'); + if(!empty($childrenIDList)) $this->dao->update(TABLE_PROJECT)->set('acl')->eq($plan->acl)->where('id')->in($childrenIDList)->exec(); + + /* The workload of the parent plan cannot exceed 100%. */ + $oldPlan->parent = $plan->parent; + $totalPercent = $this->getTotalPercent($oldPlan); + $totalPercent = $totalPercent + $plan->percent; + if($totalPercent > 100) return dao::$errors['percent'][] = $this->lang->programplan->error->percentOver; + } + + /* Set planDuration and realDuration. */ + if(isset($this->config->maxVersion)) + { + $plan->planDuration = $this->getDuration($plan->begin, $plan->end); + $plan->realDuration = $this->getDuration($plan->realBegan, $plan->realEnd); + } + + if($planChanged) $plan->version = $oldPlan->version + 1; + if(empty($plan->parent)) $plan->parent = $projectID; + + $this->dao->update(TABLE_PROJECT)->data($plan) + ->autoCheck() + ->batchCheck($this->config->programplan->edit->requiredFields, 'notempty') + ->checkIF($plan->end != '0000-00-00', 'end', 'ge', $plan->begin) + ->checkIF($plan->percent != false, 'percent', 'float') + ->where('id')->eq($planID) + ->exec(); + + if(dao::isError()) return false; + $this->setTreePath($planID); + if($plan->acl != 'open') $this->loadModel('user')->updateUserView($planID, 'sprint'); + + if($planChanged) + { + $spec = new stdclass(); + $spec->project = $planID; + $spec->version = $plan->version; + $spec->name = $plan->name; + $spec->milestone = $plan->milestone; + $spec->begin = $plan->begin; + $spec->end = $plan->end; + + $this->dao->insert(TABLE_PROJECTSPEC)->data($spec)->exec(); + } + + return common::createChanges($oldPlan, $plan); + } + + /** + * Print cell. + * + * @param int $col + * @param int $plan + * @param int $users + * @param int $projectID + * @access public + * @return string + */ + public function printCell($col, $plan, $users, $projectID) + { + $id = $col->id; + if($col->show) + { + $class = 'c-' . $id; + $title = ''; + $idList = array('id','name','output','percent','attribute','version','begin','end','realBegan','realEnd', 'openedBy', 'openedDate'); + if(in_array($id,$idList)) + { + $class .= ' text-left'; + $title = "title='{$plan->$id}'"; + if($id == 'output') $class .= ' text-ellipsis'; + if(!empty($plan->children)) $class .= ' has-child'; + } + else + { + $class .= ' text-center'; + } + if($id == 'actions') $class .= ' c-actions'; + + echo "'; + } + } + + /** + * Is create task. + * + * @param int $planID + * @access public + * @return bool + */ + public function isCreateTask($planID) + { + $task = $this->dao->select('*')->from(TABLE_TASK)->where('execution')->eq($planID)->limit(1)->fetch(); + return empty($task) ? true : false; + } + + /** + * Is clickable. + * + * @param int $plan + * @param int $action + * @static + * @access public + * @return bool + */ + public static function isClickable($plan, $action) + { + $action = strtolower($action); + + if($action == 'create' and $plan->grade > 1) return false; + + return true; + } + + /** + * Get the stage set to milestone. + * + * @param int $projectID + * @access public + * @return array + */ + public function getMilestones($projectID = 0) + { + return $this->dao->select('id, name')->from(TABLE_PROJECT) + ->where('project')->eq($projectID) + ->andWhere('type')->eq('stage') + ->andWhere('milestone')->eq(1) + ->andWhere('deleted')->eq(0) + ->orderBy('id_desc') + ->fetchPairs(); + } + + /** + * Get milestone by product. + * + * @param int $productID + * @access public + * @return object + */ + public function getMilestoneByProduct($productID) + { + return $this->dao->select('t1.id, t1.name')->from(TABLE_PROJECT)->alias('t1') + ->leftJoin(TABLE_PROJECTPRODUCT)->alias('t2')->on('t1.id=t2.project') + ->where('t2.product')->eq($productID) + ->andWhere('t1.type')->eq('stage') + ->andWhere('t1.milestone')->eq(1) + ->andWhere('t1.deleted')->eq(0) + ->orderBy('t1.begin asc') + ->fetchPairs(); + } + + /** + * Get parent stage list. + * + * @param int $executionID + * @param int $planID + * @param int $productID + * @access public + * @return array + */ + public function getParentStageList($executionID, $planID, $productID) + { + $parentStage = $this->dao->select('t2.id, t2.name')->from(TABLE_PROJECTPRODUCT) + ->alias('t1')->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id') + ->where('t1.product')->eq($productID) + ->andWhere('t2.project')->eq($executionID) + ->andWhere('t2.grade')->eq(1) + ->beginIF(!$this->app->user->admin)->andWhere('t2.id')->in($this->app->user->view->sprints)->fi() + ->orderBy('t2.id desc') + ->fetchPairs(); + + /* Remove the currently edited stage. */ + if(isset($parentStage[$planID])) unset($parentStage[$planID]); + + $plan = $this->getByID($planID); + foreach($parentStage as $key => $stage) + { + $isCreate = $this->isCreateTask($key); + if($isCreate === false and $key != $plan->parent) unset($parentStage[$key]); + } + $parentStage[0] = $this->lang->programplan->emptyParent; + ksort($parentStage); + + return $parentStage; + } +} diff --git a/module/programplan/view/browse.html.php b/module/programplan/view/browse.html.php new file mode 100644 index 0000000000..c732b2be1d --- /dev/null +++ b/module/programplan/view/browse.html.php @@ -0,0 +1,38 @@ + + * @package programplan + * @version $Id: browse.html.php 4903 2013-06-26 05:32:59Z wyd621@gmail.com $ + * @link http://www.zentao.net + */ +?> + + + +
    + +
    +

    + programplan->noData;?> + + createLink('programplan', 'create', "projectID=$projectID&productID=$productID"), " " . $lang->programplan->create, '', "class='btn btn-info'");?> + +

    +
    + + + + +
    + + diff --git a/module/programplan/view/create.html.php b/module/programplan/view/create.html.php new file mode 100644 index 0000000000..0dc384c14b --- /dev/null +++ b/module/programplan/view/create.html.php @@ -0,0 +1,184 @@ + + * @package programplan + * @version $Id: create.html.php 4903 2013-06-26 05:32:59Z wyd621@gmail.com $ + * @link http://www.zentao.net + */ +?> + + + + + + +programplan->name : $lang->programplan->subStageName;?> +
    + +
    productplan->parent;?>title;?> - begin);?> - end);?> - title;?>
    productplan->begin;?> -
    +
    diff --git a/module/productplan/view/edit.html.php b/module/productplan/view/edit.html.php index 67c25b8426..8688cacca7 100644 --- a/module/productplan/view/edit.html.php +++ b/module/productplan/view/edit.html.php @@ -14,7 +14,6 @@ execution->weekend);?> -
    diff --git a/module/productplan/view/linkstory.html.php b/module/productplan/view/linkstory.html.php index 704c820e86..76abdaf6e8 100644 --- a/module/productplan/view/linkstory.html.php +++ b/module/productplan/view/linkstory.html.php @@ -46,7 +46,7 @@ id => sprintf('%03d', $story->id)));?>
    pri;?>' title='story->priList, $story->pri, $story->pri)?>'>story->priList, $story->pri, $story->pri)?>planTitle;?>planTitle;?> module];?>
    + app->getViewType() == 'xhtml'):?> +
    name . ' ' . $plan->title ?>
    + +
    '> @@ -85,8 +108,7 @@ - - id&orderBy=$orderBy");?>"> + id&orderBy=$orderBy");?>"> + app->getViewType() == 'xhtml'):?> + + + + + + @@ -136,6 +167,23 @@ $totalEstimate += $story->estimate; ?> + app->getViewType() == 'xhtml'):?> + + + + + +
    + idAB);?> + priAB);?>story->title);?> statusAB);?>
    @@ -124,6 +154,7 @@
    statusAB);?> story->stageAB);?> actions?>
    + id);?> + pri;?>' title='story->priList, $story->pri, $story->pri);?>'>story->priList, $story->pri, $story->pri);?> + parent > 0) echo "story->children}>{$lang->story->childrenAB}"; + echo $story->title; + ?> + + + processStatus('story', $story);?> + + id => sprintf('%03d', $story->id)));?> @@ -170,13 +218,14 @@ } ?>
    + app->getViewType() != 'xhtml'):?>
    + app->rawParams['type'] = 'story'; $storyPager->show('right', 'pagerjs'); @@ -353,7 +404,7 @@
    '> - + parent >= 0):?>
    id, \"bug\")", ' ' . $lang->productplan->linkBug, '', "class='btn btn-primary'");?>
    @@ -361,10 +412,23 @@
    id&orderBy=$orderBy");?>"> - + id}&type=bug&orderBy=%s&link=$link¶m=$param"; ?> + app->getViewType() == 'xhtml'):?> + + + + + - + + + app->getViewType() == 'xhtml'):?> + + + + + +
    + idAB);?> + priAB);?>bug->title);?>bug->status);?>
    @@ -374,19 +438,32 @@ idAB);?>
    priAB);?>bug->title);?>bug->title);?> openedByAB);?> bug->assignedToAB);?> bug->status);?> actions?>
    + id);?> + bug->priList, $bug->pri, $bug->pri);?>title?> + + processStatus('bug', $bug);?> + + - id => sprintf('%03d', $bug->id)));?> + id => sprintf('%03d', $bug->id)));?> id);?> @@ -409,19 +486,56 @@ } ?>
    - config->maxVersion)):?> ' . $lang->project->create, '', 'class="btn btn-secondary" data-toggle="modal" data-target="#guideDialog"');?> - config->systemMode == 'new'):?> - ' . $lang->project->create, '', 'class="btn btn-secondary"');?> - pageActions)) echo $lang->pageActions;?>
    diff --git a/module/program/view/edit.html.php b/module/program/view/edit.html.php index e04d68b45e..06fe71d320 100644 --- a/module/program/view/edit.html.php +++ b/module/program/view/edit.html.php @@ -85,7 +85,7 @@
    project->realBegan;?>realBegan) ? '' : $program->realBegan, "class='form-control form-date'");?>realBegan, "class='form-control form-date'");?>
    program->desc;?>
    "; + if(isset($this->config->bizVersion)) $this->loadModel('flow')->printFlowCell('programplan', $plan, $id); + switch($id) + { + case 'id': + echo sprintf('%03d', $plan->id); + break; + case 'name': + $milestoneFlag = $plan->milestone ? " lang->programplan->milestone}'>" : ''; + if($plan->grade > 1) echo '' . $this->lang->programplan->childrenAB . ' '; + echo $plan->name . $milestoneFlag; + if(!empty($plan->children)) echo ''; + break; + case 'percent': + echo $plan->percent . '%'; + break; + case 'attribute': + echo zget($this->lang->stage->typeList, $plan->attribute, ''); + break; + case 'begin': + echo $plan->begin; + break; + case 'end': + echo $plan->end; + break; + case 'realBegan': + echo $plan->realBegan; + break; + case 'realEnd': + echo $plan->realEnd; + break; + case 'output': + echo $plan->output; + break; + case 'version': + echo $plan->version; + break; + case 'editedBy': + echo zget($users, $plan->editedBy); + break; + case 'editedDate': + echo substr($plan->editedDate, 5, 11); + break; + case 'openedBy': + echo zget($users, $plan->openedBy); + break; + case 'openedDate': + echo substr($plan->openedDate, 5, 11); + break; + case 'actions': + common::printIcon('execution', 'start', "executionID={$plan->id}", $plan, 'list', '', '', 'iframe', true); + $class = !empty($plan->children) ? 'disabled' : ''; + common::printIcon('task', 'create', "executionID={$plan->id}", $plan, 'list', '', '', $class, false, "data-app='execution'"); + + if($plan->grade == 1 && $this->isCreateTask($plan->id)) + { + common::printIcon('programplan', 'create', "program={$plan->parent}&productID=$plan->product&planID=$plan->id", $plan, 'list', 'split', '', '', '', '', $this->lang->programplan->createSubPlan); + } + else + { + $disabled = ($plan->grade == 2) ? ' disabled' : ''; + echo html::a('javascript:alert("' . $this->lang->programplan->error->createdTask . '");', '', '', 'class="btn ' . $disabled . '"'); + } + + common::printIcon('programplan', 'edit', "planID=$plan->id&projectID=$projectID", $plan, 'list', '', '', 'iframe', true); + + $disabled = !empty($plan->children) ? ' disabled' : ''; + if(common::hasPriv('execution', 'delete', $plan)) + { + common::printIcon('execution', 'delete', "planID=$plan->id&confirm=no", $plan, 'list', 'trash', 'hiddenwin' , $disabled, '', '', $this->lang->programplan->delete); + } + break; + } + echo '
    + + + + + + + + + + + + config->qcVersion)):?> + + + + + + + + + + + + + + + + + + + + config->qcVersion)):?> + + + + + + + + + + + setMilestone ? '' : "disabled='disabled'"?> + id);?> + + + + + + + + + + + config->qcVersion)):?> + output) ? 0 : explode(',', $plan->output);?> + + + + + + + + + + + + + + + + + + + + config->qcVersion)):?> + + + + + + + + + + + + + +
    + programplan->percent;?> + + + + programplan->attribute;?>project->acl;?>programplan->milestone;?>programplan->begin;?>programplan->end;?>programplan->realBegan;?>programplan->realEnd;?>programplan->output;?> actions;?>
    +
    + + % +
    +
    stage->typeList, $stage->type, "class='form-control'");?>project->aclList, 'open', "class='form-control' $class");?>programplan->milestoneList, 0);?> + + +
    +
    + + % +
    +
    stage->typeList, $plan->attribute, "class='form-control'");?>project->aclList, $plan->acl, "class='form-control' $class");?>programplan->milestoneList, $plan->milestone, $disabled);?> + +
    +
    + + % +
    +
    stage->typeList, '', "class='form-control'");?>project->aclList, 'open', "class='form-control' $class");?>programplan->milestoneList, 0);?> + + +
    + + + +
    + + + + + + + + + + + + + + config->qcVersion)):?> + + + + + +
    + + diff --git a/module/programplan/view/edit.html.php b/module/programplan/view/edit.html.php new file mode 100644 index 0000000000..adb92bb94f --- /dev/null +++ b/module/programplan/view/edit.html.php @@ -0,0 +1,107 @@ + + * @package programplan + * @version $Id: edit.html.php 4903 2013-06-26 05:32:59Z wyd621@gmail.com $ + * @link http://www.zentao.net + */ +?> + + +
    +
    +
    +

    programplan->edit;?>

    +
    +
    + + + + + + + + + + + + + + + " id="attributeType"> + + + + setMilestone):?> + + + + + + milestone);?> + + + + grade == 2 ? "disabled='disabled'" : '';?> + + + + + + + + + + + config->qcVersion)):?> + + + + + + + + + +
    programplan->parent;?>parent, "class='form-control chosen '");?>
    programplan->name;?> name, "class='form-control'");?>
    programplan->percent;?> +
    + percent, "class='form-control'");?> +
    % +
    +
    programplan->attribute;?> stage->typeList, $plan->attribute, "class='form-control'");?>
    programplan->milestone;?> programplan->milestoneList, $plan->milestone);?>
    project->acl;?> project->aclList, $plan->acl, "class='form-control' $class");?>
    programplan->planDateRange;?> +
    + begin, "class='form-control form-date'");?> + project->to;?> + end, "class='form-control form-date'");?> +
    +
    programplan->realDateRange;?> +
    + realBegan, "class='form-control form-date'");?> + project->to;?> + realEnd, "class='form-control form-date'");?> +
    +
    programplan->output;?> output, "class='form-control chosen ' multiple");?>
    +
    +
    +
    + + diff --git a/module/programplan/view/gantt.html.php b/module/programplan/view/gantt.html.php new file mode 100644 index 0000000000..41fda4a4b9 --- /dev/null +++ b/module/programplan/view/gantt.html.php @@ -0,0 +1,436 @@ + + * @package programplan + * @version $Id: gantt.html.php 4903 2013-06-26 05:32:59Z wyd621@gmail.com $ + * @link http://www.zentao.net + */ +?> + + +createLink('programplan', 'ajaxCustom'));?> + +rawModule);?> +rawMethod);?> + +
    +
    +
    + programplan->full, 'id="fullScreenBtn"', 'btn btn-primary btn-sm')?> + rawModule == 'review' and $app->rawMethod == 'assess') unset($lang->programplan->stageCustom->date); ?> + programplan->stageCustom, $selectCustom);?> +
    +
    +
    +
    +
    +
    + diff --git a/module/programplan/view/list.html.php b/module/programplan/view/list.html.php new file mode 100644 index 0000000000..4a2200b0b6 --- /dev/null +++ b/module/programplan/view/list.html.php @@ -0,0 +1,89 @@ + + * @package programplan + * @version $Id: list.html.php 4903 2013-06-26 05:32:59Z wyd621@gmail.com $ + * @link http://www.zentao.net + */ +?> +programplan->confirmDelete); +?> + +
    +
    + +
    + datatable->getSetting('programplan'); + $widths = $this->datatable->setFixedFieldWidth($setting); + $widths['leftWidth'] = 300; + $columns = 0; + ?> + ' data-fixed-right-width='' data-checkbox-name='programplanList[]'> + + + + + + + + + + + + + + + + + $value) $this->programplan->printCell($value, $plan, $users, $projectID);?> + + children)):?> + + children as $key => $child):?> + + children)) ? ' table-child-bottom' : '';?> + + $value) $this->programplan->printCell($value, $child, $users, $projectID);?> + + + + + + +
    idAB);?> programplan->name);?>programplan->percent);?>programplan->attribute);?>programplan->begin);?>programplan->end);?>programplan->realBegan);?>programplan->realEnd);?>actions;?>
    +
    + diff --git a/module/project/config.php b/module/project/config.php index 61afe4943a..0559cdf897 100644 --- a/module/project/config.php +++ b/module/project/config.php @@ -118,3 +118,7 @@ $config->project->datatable->fieldList['actions']['fixed'] = 'right'; $config->project->datatable->fieldList['actions']['width'] = '180'; $config->project->datatable->fieldList['actions']['required'] = 'yes'; $config->project->datatable->fieldList['actions']['pri'] = '1'; + +$config->project->removePriv['project'] = array('browse', 'kanban', 'create', 'batchEdit', 'qa', 'updateOrder', 'createGuide', 'programTitle'); +$config->project->removePriv['bug'] = array('batchChangePlan'); +$config->project->removePriv['doc'] = array('catalog', 'index'); diff --git a/module/project/control.php b/module/project/control.php index cae2b08481..0301af3a81 100644 --- a/module/project/control.php +++ b/module/project/control.php @@ -307,7 +307,7 @@ class project extends control $this->view->title = $this->lang->project->kanban; $this->view->kanbanGroup = array_filter($kanbanGroup); $this->view->latestExecutions = $latestExecutions; - $this->view->programPairs = array(0 => $this->lang->project->noProgram) + $this->loadModel('program')->getPairs(true); + $this->view->programPairs = array(0 => $this->lang->project->noProgram) + $this->loadModel('program')->getPairs(true, 'order_asc'); $this->display(); } @@ -448,6 +448,8 @@ class project extends control if($this->app->tab == 'doc') unset($this->lang->doc->menu->project['subMenu']); + $topProgramID = $this->program->getTopByID($programID); + $this->view->title = $this->lang->project->create; $this->view->position[] = $this->lang->project->create; @@ -460,7 +462,7 @@ class project extends control $this->view->productPlans = array('0' => '') + $productPlans; $this->view->branchGroups = $this->loadModel('branch')->getByProducts(array_keys($products), 'noclosed'); $this->view->programID = $programID; - $this->view->multiBranchProducts = $this->product->getMultiBranchPairs($programID); + $this->view->multiBranchProducts = $this->product->getMultiBranchPairs($topProgramID); $this->view->model = $model; $this->view->name = $name; $this->view->code = $code; @@ -540,7 +542,7 @@ class project extends control $linkedProducts = $this->loadModel('product')->getProducts($projectID); $parentProject = $this->program->getByID($project->parent); $branches = $this->project->getBranchesByProject($projectID); - $plans = $this->productplan->getGroupByProduct(array_keys($linkedProducts)); + $plans = $this->productplan->getGroupByProduct(array_keys($linkedProducts), 'skipParent'); $projectStories = $this->project->getStoriesByProject($projectID); $projectBranches = $this->project->getBranchGroupByProject($projectID, array_keys($linkedProducts)); @@ -554,7 +556,8 @@ class project extends control foreach($branches[$productID] as $branchID => $branch) { $linkedBranches[$productID][$branchID] = $branchID; - $productPlans[$productID][$branchID] = isset($plans[$productID][$branchID]) ? $plans[$productID][$branchID] : array(); + if($branch != BRANCH_MAIN) $productPlans[$productID][$branchID] = isset($plans[$productID][BRANCH_MAIN]) ? $plans[$productID][BRANCH_MAIN] : array(); + $productPlans[$productID][$branchID] += isset($plans[$productID][$branchID]) ? $plans[$productID][$branchID] : array(); if(!empty($projectStories[$productID][$branchID]) or !empty($projectBranches[$productID][$branchID])) { @@ -845,7 +848,7 @@ class project extends control * @access public * @return void */ - public function execution($status = 'all', $projectID = 0, $orderBy = 'id_desc', $productID = 0, $recTotal = 0, $recPerPage = 10, $pageID = 1) + public function execution($status = 'all', $projectID = 0, $orderBy = 'order_asc', $productID = 0, $recTotal = 0, $recPerPage = 10, $pageID = 1) { $uri = $this->app->getURI(true); $this->app->session->set('executionList', $uri, 'project'); @@ -964,6 +967,7 @@ class project extends control { $this->loadModel('product'); $this->session->set('bugList', $this->app->getURI(true), 'project'); + $products = array('0' => $this->lang->product->all) + $this->product->getProducts($projectID, 'all', '', false); $this->lang->modulePageNav = $this->product->select($products, $productID, 'project', 'testcase', '', $branch, 0, '', false); @@ -1182,9 +1186,21 @@ class project extends control /* Unset not project privs. */ $project = $this->project->getByID($group->project); - foreach($this->lang->resource as $method => $label) + foreach($this->lang->resource as $module => $methods) { - if(!in_array($method, $this->config->programPriv->{$project->model})) unset($this->lang->resource->$method); + if(!in_array($module, $this->config->programPriv->{$project->model})) + { + unset($this->lang->resource->$module); + } + else + { + if($project->model == 'scrum' and $module == 'projectstory') $this->config->project->removePriv[$module][] = 'track'; + + foreach($methods as $method => $label) + { + if(isset($this->config->project->removePriv[$module]) and in_array($method, $this->config->project->removePriv[$module])) unset($this->lang->resource->$module->$method); + } + } } } diff --git a/module/project/css/browse.css b/module/project/css/browse.css index a9b6f2eebe..fecfbfca43 100644 --- a/module/project/css/browse.css +++ b/module/project/css/browse.css @@ -11,7 +11,8 @@ td.c-PM {white-space: nowrap; overflow: hidden;} .icon-cards-view {padding-left: 7px; font-size: 16px;} .icon-list {padding-left: 7px;} .panel-actions {position: relative; padding: 0 0;} -@media screen and (max-width: 1460px) +th.c-name {width: 360px !important;} +@media screen and (min-width: 1460px) { - th.c-name {width: 360px !important;} + th.c-name {width: auto;} } diff --git a/module/project/css/kanban.css b/module/project/css/kanban.css index 555f60c248..24771aaf89 100644 --- a/module/project/css/kanban.css +++ b/module/project/css/kanban.css @@ -19,4 +19,6 @@ table th, td {border: 2px solid #fff !important;} -#kanban {padding-bottom: 10px} +.kanban-col[data-type='closedProject'] .count {display: none} +.kanban-item > a {cursor: move;} +.kanban-item.execution-item > a {cursor: pointer;} diff --git a/module/project/js/common.js b/module/project/js/common.js index 40cc7d5f5f..a7c18ec6f8 100644 --- a/module/project/js/common.js +++ b/module/project/js/common.js @@ -190,7 +190,7 @@ function loadPlans(product, branchID) if(typeof(planID) == 'undefined') planID = 0; planID = $("select#plans" + productID).val() != '' ? $("select#plans" + productID).val() : planID; - $.get(createLink('product', 'ajaxGetPlans', "productID=" + productID + '&branch=' + branchID + '&planID=' + planID + '&fieldID&needCreate=&expired=' + ((config.currentMethod == 'create' || config.currentMethod == 'edit') ? 'unexpired' : '')), function(data) + $.get(createLink('product', 'ajaxGetPlans', "productID=" + productID + '&branch=0,' + branchID + '&planID=' + planID + '&fieldID&needCreate=&expired=' + (config.currentMethod == 'create' ? 'unexpired' : '') + '¶m=skipParent'), function(data) { if(data) { diff --git a/module/project/js/create.js b/module/project/js/create.js index 5936af5f44..b08a1ba690 100644 --- a/module/project/js/create.js +++ b/module/project/js/create.js @@ -201,7 +201,7 @@ function loadPlans(product, branchID) { if(typeof(planID) == 'undefined') planID = 0; planID = $("select#plans" + productID).val() != '' ? $("select#plans" + productID).val() : planID; - $.get(createLink('product', 'ajaxGetPlans', "productID=" + productID + '&branch=' + branchID + '&planID=' + planID + '&fieldID&needCreate=&expired=' + ((config.currentMethod == 'create' || config.currentMethod == 'edit') ? 'unexpired' : '')), function(data) + $.get(createLink('product', 'ajaxGetPlans', "productID=" + productID + '&branch=0,' + branchID + '&planID=' + planID + '&fieldID&needCreate=&expired=unexpired¶m=skipParent'), function(data) { if(data) { diff --git a/module/project/js/kanban.js b/module/project/js/kanban.js index 50e416da18..147c67aad2 100644 --- a/module/project/js/kanban.js +++ b/module/project/js/kanban.js @@ -57,6 +57,111 @@ function processKanbanData(key, programGroup) return {id: kanbanId, columns: columns, lanes: lanes}; } +/* + * Find drop columns + * @param {JQuery} $element Drag element + * @param {JQuery} $root Dnd root element + */ +function findDropColumns($element, $root) +{ + var $col = $element.closest('.kanban-col'); + var col = $col.data(); + var lane = $col.closest('.kanban-lane').data(); + var kanbanID = $root.data('id'); + console.log('findDropColumns', {$element, $root, kanbanID, col, lane}); + var kanbanRules = window.kanbanDropRules ? window.kanbanDropRules[kanbanID] : null; + + if(!kanbanRules) return $root.find('.kanban-lane[data-id="' + lane.id + '"] .kanban-lane-col:not([data-type="doingExecution"],[data-type="' + col.type + '"])'); + + var colRules = kanbanRules[col.type]; + var lane = $col.closest('.kanban-lane').data('lane'); + return $root.find('.kanban-lane-col').filter(function() + { + if(!colRules) return false; + if(colRules === true) return true; + + var $newCol = $(this); + var newCol = $newCol.data(); + if(newCol.id === col.id) return false; + + var $newLane = $newCol.closest('.kanban-lane'); + var newLane = $newLane.data('lane'); + return colRules.indexOf(newCol.type) > -1 && newLane.id === lane.id; + }); +} + +/** + * Change column type for a card + * @param {Object} card Card object + * @param {String} fromColType The column type before change + * @param {String} toColType The column type after change + * @param {String} kanbanID Kanban ID + */ +function changeCardColType(card, fromColType, toColType, kanbanID) +{ + if(typeof card == 'undefined') return false; + var cardID = card.id; + var projectID = cardID.substr(cardID.indexOf("-") + 1);; + var showIframe = false; + + if(toColType == 'doingProject') + { + if(fromColType == 'waitProject' && priv.canStart) + { + var link = createLink('project', 'start', 'project=' + projectID, '', true); + showIframe = true; + } + if(fromColType == 'closedProject' && priv.canActivate) + { + var link = createLink('project', 'activate', 'projectID=' + projectID, '', true); + showIframe = true; + } + } + else if(toColType == 'closedProject') + { + if(priv.canClose) + { + var link = createLink('project', 'close', 'projectID=' + projectID, '', true); + showIframe = true; + } + } + + if(showIframe) + { + var modalTrigger = new $.zui.ModalTrigger({type: 'iframe', width: '80%', url: link}); + modalTrigger.show(); + } + + /* + // TODO: The server must return a updated kanban data 服务器返回更新后的看板数据 + + // 调用 updateKanban 更新看板数据 + updateKanban(kanbanID, newKanbanData); + */ +} + +/** + * Handle finish drop task + * @param {Object} event Event object + * @returns {void} + */ +function handleFinishDrop(event) +{ + var $card = $(event.element); // The drag card + var $dragCol = $card.closest('.kanban-lane-col'); + var $dropCol = $(event.target); + + /* Get d-n-d(drag and drop) infos. */ + var card = $card.data('item'); + var fromColType = $dragCol.data('type'); + var toColType = $dropCol.data('type'); + var kanbanID = $card.closest('.kanban').data('id'); + + if(fromColType == 'doingProject') card = $card.parent().parent().data('item'); + + changeCardColType(card, fromColType, toColType, kanbanID); +} + $(function() { /* Init all kanbans */ @@ -64,6 +169,17 @@ $(function() { var $kanban = $('#kanban-' + key); if(!$kanban.length) return; - $kanban.kanban({data: processKanbanData(key, programGroup), maxColHeight: 'auto'}); + $kanban.kanban( + { + data: processKanbanData(key, programGroup), + maxColHeight: 'auto', + droppable: + { + selector: '.kanban-item:not(.execution-item)', + target: findDropColumns, + finish: handleFinishDrop, + mouseButton: 'left' + }, + }); }); }); diff --git a/module/project/lang/de.php b/module/project/lang/de.php index 55a95073d0..3ea1073bff 100644 --- a/module/project/lang/de.php +++ b/module/project/lang/de.php @@ -229,3 +229,6 @@ $lang->project->endGreaterParent = "The end date of the parent project: %s. It $lang->project->beginGreateChild = "The minimum start date of the project set: %s. The start date of the project cannot be less than the minimum start date of the project set."; $lang->project->endLetterChild = "The maximum finish date for the project set: %s. The completion date of a project cannot be greater than the maximum completion date of the project set."; $lang->project->childLongTime = "There are long-term projects in the child project, and the parent project should also be a long-term project."; + +$lang->project->action = new stdclass(); +$lang->project->action->managed = '$date, managed by $actor. $extra' . "\n"; diff --git a/module/project/lang/en.php b/module/project/lang/en.php index 7c1a6a4d02..a8d9f27d7e 100644 --- a/module/project/lang/en.php +++ b/module/project/lang/en.php @@ -109,8 +109,10 @@ $lang->project->surplus = 'Left'; $lang->project->progress = 'Progress'; $lang->project->dateRange = 'Duration'; $lang->project->to = ' to '; +$lang->project->realBeganAB = 'Actual Begin'; +$lang->project->realEndAB = 'Actual End'; +$lang->project->realBegan = 'Actual Begin'; $lang->project->realEnd = 'Actual End'; -$lang->project->realBegan = 'Actual Began'; $lang->project->bygrid = 'Kanban'; $lang->project->bylist = 'List'; $lang->project->bycard = 'Card'; @@ -147,7 +149,7 @@ $lang->project->teamSumCount = '%s people in total'; $lang->project->longTime = 'Long-Term Program'; $lang->project->future = 'TBD'; $lang->project->moreProject = 'More Project'; -$lang->project->days = 'Available Days'; +$lang->project->days = 'Days'; $lang->project->mailto = 'Mailto'; $lang->project->etc = " , etc"; $lang->project->product = 'Product'; @@ -162,7 +164,7 @@ $lang->project->typeList['other'] = 'Other Projects'; $lang->project->waitProjects = 'Waiting Projects'; $lang->project->doingProjects = 'Ongoing Projects'; $lang->project->doingExecutions = 'Ongoing Executions'; -$lang->project->closedProjects = 'Closed Projects'; +$lang->project->closedProjects = 'Closed Projects(The recent two projects)'; $lang->project->noProgram = 'Independent Projects'; $lang->project->laneColorList = array('#32C5FF', '#006AF1', '#9D28B2', '#FF8F26', '#FFC20E', '#00A78E', '#7FBB00', '#424BAC', '#C0E9FF', '#EC2761'); @@ -170,7 +172,7 @@ $lang->project->laneColorList = array('#32C5FF', '#006AF1', '#9D28B2', '#FF8F26' $lang->project->productNotEmpty = 'Please link products or create products.'; $lang->project->existProductName = 'Product name already exists.'; $lang->project->changeProgram = '%s > Change project'; -$lang->project->changeProgramTip = 'Once the program is edited, the product that is linked to this program will be changed. Do you want to edit it?'; +$lang->project->changeProgramTip = 'After modifying the project set, the products linked with the project will also modify the project set to which it belongs. Please confirm whether to modify it.'; $lang->project->linkedProjectsTip = 'Linked projects are as follows'; $lang->project->multiLinkedProductsTip = 'The following products linked to this project are also linked to other projects, please unlink before proceeding.'; $lang->project->linkStoryByPlanTips = "This action will associate all {$lang->SRCommon} under the selected plan to this project"; @@ -178,6 +180,9 @@ $lang->project->createExecution = "There is no {$lang->executionCommon} u $lang->project->unlinkExecutionMember = "The user participated in %s executions such as %s%s. Do you want to remove the user from those executions as well? (The data related to this user will not be deleted.)"; $lang->project->unlinkExecutionMembers = "The team members you are removing are also in the execution team of this project. Do you want to remove them from the execution team too?"; +$lang->project->realEndNotEmpty = 'Actual End should not be empty.'; +$lang->project->realEndNotFuture = 'Actual End should be < = today.'; + $lang->project->tenThousand = ''; $lang->project->unitList['CNY'] = 'RMB'; @@ -289,3 +294,6 @@ $lang->project->beginGreateChild = "The minimum start date of the project set $lang->project->endLetterChild = "The maximum finish date for the project set: %s. The completion date of a project cannot be greater than the maximum completion date of the project set."; $lang->project->childLongTime = "There are long-term projects in the child project, and the parent project should also be a long-term project."; $lang->project->confirmUnlinkMember = "Do you want to remove this user from project?"; + +$lang->project->action = new stdclass(); +$lang->project->action->managed = '$date, managed by $actor. $extra' . "\n"; diff --git a/module/project/lang/fr.php b/module/project/lang/fr.php index ad8cc6e623..abe25e1d72 100644 --- a/module/project/lang/fr.php +++ b/module/project/lang/fr.php @@ -229,3 +229,6 @@ $lang->project->endGreaterParent = "The end date of the parent project: %s. It $lang->project->beginGreateChild = "The minimum start date of the project set: %s. The start date of the project cannot be less than the minimum start date of the project set."; $lang->project->endLetterChild = "The maximum finish date for the project set: %s. The completion date of a project cannot be greater than the maximum completion date of the project set."; $lang->project->childLongTime = "There are long-term projects in the child project, and the parent project should also be a long-term project."; + +$lang->project->action = new stdclass(); +$lang->project->action->managed = '$date, managed by $actor. $extra' . "\n"; diff --git a/module/project/lang/vi.php b/module/project/lang/vi.php index 78cf1f7963..023856e72f 100644 --- a/module/project/lang/vi.php +++ b/module/project/lang/vi.php @@ -229,3 +229,6 @@ $lang->project->endGreaterParent = "The end date of the parent project: %s. It $lang->project->beginGreateChild = "The minimum start date of the project set: %s. The start date of the project cannot be less than the minimum start date of the project set."; $lang->project->endLetterChild = "The maximum finish date for the project set: %s. The completion date of a project cannot be greater than the maximum completion date of the project set."; $lang->project->childLongTime = "There are long-term projects in the child project, and the parent project should also be a long-term project."; + +$lang->project->action = new stdclass(); +$lang->project->action->managed = '$date, managed by $actor. $extra' . "\n"; diff --git a/module/project/lang/zh-cn.php b/module/project/lang/zh-cn.php index f246c9be1c..4f4e07f18b 100644 --- a/module/project/lang/zh-cn.php +++ b/module/project/lang/zh-cn.php @@ -109,8 +109,10 @@ $lang->project->surplus = '剩余'; $lang->project->progress = '进度'; $lang->project->dateRange = '起止日期'; $lang->project->to = '至'; -$lang->project->realEnd = '实际完成日期'; +$lang->project->realBeganAB = '实际开始'; +$lang->project->realEndAB = '实际完成'; $lang->project->realBegan = '实际开始日期'; +$lang->project->realEnd = '实际完成日期'; $lang->project->bygrid = '看板'; $lang->project->bylist = '列表'; $lang->project->bycard = '卡片'; @@ -162,7 +164,7 @@ $lang->project->typeList['other'] = '其他项目'; $lang->project->waitProjects = '未开始的项目'; $lang->project->doingProjects = '进行中的项目'; $lang->project->doingExecutions = '进行中的执行'; -$lang->project->closedProjects = '已关闭的项目'; +$lang->project->closedProjects = '已关闭的项目(最近2个)'; $lang->project->noProgram = '无项目集归属项目'; $lang->project->laneColorList = array('#32C5FF', '#006AF1', '#9D28B2', '#FF8F26', '#FFC20E', '#00A78E', '#7FBB00', '#424BAC', '#C0E9FF', '#EC2761'); @@ -170,7 +172,7 @@ $lang->project->laneColorList = array('#32C5FF', '#006AF1', '#9D28B2', '#FF8F26' $lang->project->productNotEmpty = '请关联产品或创建产品。'; $lang->project->existProductName = '产品名称已存在。'; $lang->project->changeProgram = '%s > 修改项目集'; -$lang->project->changeProgramTip = '修改项目集后,该项目关联产品的项目集也会被修改,请确认是否修改。'; +$lang->project->changeProgramTip = '修改项目集后,该项目关联的产品也会同时修改所属项目集,请确认是否修改。'; $lang->project->linkedProjectsTip = '关联的项目如下'; $lang->project->multiLinkedProductsTip = '该项目关联的如下产品还关联了其他项目,请取消关联后再操作'; $lang->project->linkStoryByPlanTips = "此操作会将所选计划下面的{$lang->SRCommon}全部关联到此项目中"; @@ -178,6 +180,9 @@ $lang->project->createExecution = "该项目下没有{$lang->executionCom $lang->project->unlinkExecutionMember = "该用户参与了%s%s%s个{$lang->execution->common},是否同时将其移除?(该用户所产生的数据不会受影响。)"; $lang->project->unlinkExecutionMembers = "移除的团队成员还参与了项目下的执行,是否同步从执行团队中移除?"; +$lang->project->realEndNotEmpty = "实际完成不能为空。"; +$lang->project->realEndNotFuture = "实际完成不能大于当前日期。"; + $lang->project->tenThousand = '万'; $lang->project->unitList['CNY'] = '人民币'; @@ -289,3 +294,6 @@ $lang->project->beginGreateChild = "项目集的最小开始日期:%s,项 $lang->project->endLetterChild = "项目集的最大完成日期:%s,项目的完成日期不能大于项目集的最大完成日期"; $lang->project->childLongTime = "子项目中有长期项目,父项目也应该是长期项目"; $lang->project->confirmUnlinkMember = "您确定从该项目中移除该用户吗?"; + +$lang->project->action = new stdclass(); +$lang->project->action->managed = '$date, 由 $actor 维护。$extra' . "\n"; diff --git a/module/project/model.php b/module/project/model.php index ebf1b70f3e..4b49d23069 100644 --- a/module/project/model.php +++ b/module/project/model.php @@ -204,7 +204,7 @@ class projectModel extends model public function getInfoList($status = 'undone', $itemCounts = 30, $orderBy = 'order_desc', $pager = null) { /* Init vars. */ - $projects = $this->loadModel('program')->getProjectList(0, $status, 0, $orderBy, $pager); + $projects = $this->loadModel('program')->getProjectList(0, $status, 0, $orderBy, $pager, 0, 1); if(empty($projects)) return array(); $projectIdList = array_keys($projects); @@ -490,7 +490,7 @@ class projectModel extends model * @access public * @return object */ - public function getPairsByProgram($programID = 0, $status = 'all', $isQueryAll = false, $orderBy = 'id_desc') + public function getPairsByProgram($programID = 0, $status = 'all', $isQueryAll = false, $orderBy = 'order_asc') { if(defined('TUTORIAL')) return $this->loadModel('tutorial')->getProjectPairs(); return $this->dao->select('id, name')->from(TABLE_PROJECT) @@ -911,6 +911,7 @@ class projectModel extends model $this->dao->insert(TABLE_PRODUCT)->data($product)->exec(); $productID = $this->dao->lastInsertId(); + $this->loadModel('action')->create('product', $productID, 'opened'); $this->dao->update(TABLE_PRODUCT)->set('`order`')->eq($productID * 5)->where('id')->eq($productID)->exec(); if($product->acl != 'open') $this->loadModel('user')->updateUserView($productID, 'product'); @@ -1248,6 +1249,7 @@ class projectModel extends model $now = helper::now(); $project = fixer::input('post') + ->setDefault('realEnd','') ->setDefault('status', 'doing') ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', $now) @@ -1324,10 +1326,22 @@ class projectModel extends model ->remove('comment') ->get(); + if($project->realEnd == '') + { + dao::$errors['realEnd'] = $this->lang->project->realEndNotEmpty; + return false; + } + if($project->realEnd > helper::today()) + { + dao::$errors['realEnd'] = $this->lang->project->realEndNotFuture; + return false; + } + $this->dao->update(TABLE_PROJECT)->data($project) ->autoCheck() ->where('id')->eq((int)$projectID) ->exec(); + if(!dao::isError()) { $this->loadModel('score')->create('project', 'close', $oldProject); @@ -1469,9 +1483,9 @@ class projectModel extends model { $canOrder = common::hasPriv('project', 'updateOrder'); $canBatchEdit = common::hasPriv('project', 'batchEdit'); - $projectLink = $this->config->systemMode == 'new' ? helper::createLink('project', 'index', "projectID=$project->id", '', '', $project->id) : helper::createLink('execution', 'task', "projectID=$project->id"); $account = $this->app->user->account; $id = $col->id; + $projectLink = $this->config->systemMode == 'new' ? helper::createLink('project', 'index', "projectID=$project->id", '', '', $project->id) : helper::createLink('execution', 'task', "projectID=$project->id"); if($col->show) { @@ -1528,11 +1542,8 @@ class projectModel extends model case 'name': $prefix = ''; $suffix = ''; - if(isset($this->config->maxVersion)) - { - if($project->model === 'waterfall') $prefix = "{$this->lang->project->waterfall} "; - if($project->model === 'scrum') $prefix = "{$this->lang->project->scrum} "; - } + if($project->model === 'waterfall') $prefix = "{$this->lang->project->waterfall} "; + if($project->model === 'scrum') $prefix = "{$this->lang->project->scrum} "; if(isset($project->delay)) $suffix = "{$this->lang->project->statusList['delay']}"; if(!empty($suffix) || !empty($prefix)) echo '
    '; if(!empty($prefix)) echo $prefix; @@ -1672,8 +1683,11 @@ class projectModel extends model } /* Delete the execution linked products that is not linked with the execution. */ - $executions = $this->dao->select('id')->from(TABLE_EXECUTION)->where('project')->eq((int)$projectID)->fetchPairs('id'); - $this->dao->delete()->from(TABLE_PROJECTPRODUCT)->where('project')->in($executions)->andWhere('product')->notin($products)->exec(); + if($projectID) + { + $executions = $this->dao->select('id')->from(TABLE_EXECUTION)->where('project')->eq((int)$projectID)->fetchPairs('id'); + $this->dao->delete()->from(TABLE_PROJECTPRODUCT)->where('project')->in($executions)->andWhere('product')->notin($products)->exec(); + } $oldProductKeys = array_keys($oldProjectProducts); $needUpdate = array_merge(array_diff($oldProductKeys, $products), array_diff($products, $oldProductKeys)); @@ -1729,10 +1743,15 @@ class projectModel extends model */ public function getTeamMemberPairs($projectID) { - $project = $this->getByID($projectID); + $project = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($projectID)->fetch(); + if(empty($project)) return array(); - $type = $this->config->systemMode == 'new' ? $project->type : 'project'; + $type = 'project'; + if($this->config->systemMode == 'new') + { + if($project->type == 'sprint' or $project->type == 'stage') $type = 'execution'; + } $members = $this->dao->select("t1.account, if(t2.deleted='0', t2.realname, t1.account) as realname")->from(TABLE_TEAM)->alias('t1') ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account = t2.account') @@ -1901,7 +1920,7 @@ class projectModel extends model { $this->loadModel('program'); - $projects = $this->program->getProjectStats(0, 'all'); + $projects = $this->program->getProjectStats(0, 'all', 0, 'order_asc'); $executions = $this->getStats(0, 'doing'); $doingExecutions = array(); @@ -1915,6 +1934,7 @@ class projectModel extends model $myProjects = array(); $otherProjects = array(); + $closedGroup = array(); foreach($projects as $project) { if(strpos('wait,doing,closed', $project->status) === false) continue; @@ -1924,11 +1944,42 @@ class projectModel extends model if($project->PM == $this->app->user->account) { - $myProjects[$topProgram][$project->status][$project->id] = $project; + if($project->status != 'closed') + { + $myProjects[$topProgram][$project->status][] = $project; + } + else + { + $closedGroup['my'][$topProgram][$project->closedDate] = $project; + } } else { - $otherProjects[$topProgram][$project->status][$project->id] = $project; + if($project->status != 'closed') + { + $otherProjects[$topProgram][$project->status][] = $project; + } + else + { + $closedGroup['other'][$topProgram][$project->closedDate] = $project; + } + } + } + + /* Only display recent two closed projects. */ + foreach($closedGroup as $group => $closedProjects) + { + foreach($closedProjects as $topProgram => $projects) + { + krsort($projects); + if($group == 'my') + { + $myProjects[$topProgram]['closed'] = array_slice($projects, 0, 2); + } + else + { + $otherProjects[$topProgram]['closed'] = array_slice($projects, 0, 2); + } } } @@ -1993,6 +2044,15 @@ class projectModel extends model global $lang; $project = $this->getByID($objectID); + if(isset($project->model) and $project->model == 'waterfall') + { + global $lang; + $this->loadModel('execution'); + $lang->executionCommon = $lang->project->stage; + + include $this->app->getModulePath('', 'execution') . 'lang/' . $this->app->getClientLang() . '.php'; + } + $model = 'scrum'; if($project) $model = $project->model; diff --git a/module/project/view/ajaxgetdropmenu.html.php b/module/project/view/ajaxgetdropmenu.html.php index 7605f19000..4aa753f687 100644 --- a/module/project/view/ajaxgetdropmenu.html.php +++ b/module/project/view/ajaxgetdropmenu.html.php @@ -74,10 +74,7 @@ foreach($projects as $programID => $programProjects) { $selected = $project->id == $projectID ? 'selected' : ''; $link = helper::createLink('project', 'index', "projectID=%s", '', '', $project->id); - $projectName = $project->name; - - /* If this version is maxVersion, add the execution icon before execution name. */ - if(isset($this->config->maxVersion)) $projectName = $project->model == 'scrum' ? ' ' . $project->name : ' ' . $project->name; + $projectName = $project->model == 'scrum' ? ' ' . $project->name : ' ' . $project->name; if($project->status != 'done' and $project->status != 'closed' and $project->PM == $this->app->user->account) { diff --git a/module/project/view/browsebylist.html.php b/module/project/view/browsebylist.html.php index 554576f90d..0d36db58f8 100644 --- a/module/project/view/browsebylist.html.php +++ b/module/project/view/browsebylist.html.php @@ -47,7 +47,7 @@  ", '', "class='btn btn-icon' title='{$lang->project->bycard}' id='switchButton' data-type='bycard'");?>
    " . $lang->export, '', "class='btn btn-link export'")?> - config->maxVersion) and !defined('TUTORIAL')):?> + ' . $lang->project->create, '', 'class="btn btn-primary create-project-btn" data-toggle="modal"');?> ' . $lang->project->create, '', 'class="btn btn-primary create-project-btn"');?> @@ -71,7 +71,7 @@

    project->empty;?> - config->maxVersion) and !defined('TUTORIAL')):?> + ' . $lang->project->create, '', 'class="btn btn-info" data-toggle="modal"');?> config->systemMode == 'new'):?> ' . $lang->project->create, '', 'class="btn btn-info"');?> diff --git a/module/project/view/close.html.php b/module/project/view/close.html.php index 9cdb9c957e..0fd52dabdd 100644 --- a/module/project/view/close.html.php +++ b/module/project/view/close.html.php @@ -30,6 +30,14 @@

    project->realEnd;?> +
    + realEnd) && $project->realEnd != '0000-00-00' ? $project->realEnd : date('Y-m-d')), "class='form-control form-date' required");?> +
    +
    comment;?>
    project->begin;?> begin;?>execution->totalEstimate;?>totalEstimate . $lang->execution->workHour;?>project->realBeganAB;?>realBegan == '0000-00-00' ? '' : $project->realBegan;?>
    project->end;?> end;?>execution->totalConsumed;?>totalConsumed . $lang->execution->workHour;?>project->realEndAB;?>realEnd == '0000-00-00' ? '' : $project->realEnd;?>
    execution->totalEstimate;?>totalEstimate . $lang->execution->workHour;?> execution->totalDays;?> days;?>execution->totalLeft;?>totalLeft . $lang->execution->workHour;?>
    execution->totalConsumed;?>totalConsumed . $lang->execution->workHour;?> execution->totalHours;?> totalHours . $lang->execution->workHour;?>
    execution->totalLeft;?>totalLeft . $lang->execution->workHour;?>
    diff --git a/module/projectrelease/control.php b/module/projectrelease/control.php index 3cb2786f02..8ceb121e6d 100644 --- a/module/projectrelease/control.php +++ b/module/projectrelease/control.php @@ -470,8 +470,8 @@ class projectrelease extends control $this->config->product->search['actionURL'] = $this->createLink('projectrelease', 'view', "releaseID=$releaseID&type=story&link=true¶m=" . helper::safe64Encode('&browseType=bySearch&queryID=myQueryID')); $this->config->product->search['queryID'] = $queryID; $this->config->product->search['style'] = 'simple'; - $this->config->product->search['params']['plan']['values'] = $this->loadModel('productplan')->getForProducts(array($release->product => $release->product)); - $this->config->product->search['params']['module']['values'] = $this->tree->getOptionMenu($release->product, $viewType = 'story', $startModuleID = 0); + $this->config->product->search['params']['plan']['values'] = $this->loadModel('productplan')->getPairsForStory($release->product, $release->branch, 'skipParent'); + $this->config->product->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($release->product, 'story', 0, $release->branch);; $this->config->product->search['params']['status'] = array('operator' => '=', 'control' => 'select', 'values' => $this->lang->story->statusList); if($release->productType == 'normal') { @@ -481,8 +481,8 @@ class projectrelease extends control else { $this->config->product->search['fields']['branch'] = sprintf($this->lang->product->branch, $this->lang->product->branchName[$release->productType]); - $branches = array('' => '') + $this->loadModel('branch')->getPairs($release->product, 'noempty'); - if($release->branch) $branches = array('' => '', $release->branch => $branches[$release->branch]); + $branchName = $this->loadModel('branch')->getById($release->branch); + $branches = array('' => '', BRANCH_MAIN => $this->lang->branch->main, $release->branch => $branchName); $this->config->product->search['params']['branch']['values'] = $branches; } $this->loadModel('search')->setSearchParams($this->config->product->search); @@ -493,7 +493,7 @@ class projectrelease extends control } else { - $allStories = $this->story->getExecutionStories($build->execution, 0, 0, 't1.`order`_desc', 'byProduct', $release->product, 'story', $release->stories, $pager); + $allStories = $this->story->getExecutionStories($build->execution, $release->product, 0, 't1.`order`_desc', 'byBranch', $release->branch, 'story', $release->stories, $pager); } $this->view->allStories = $allStories; @@ -585,12 +585,13 @@ class projectrelease extends control $this->loadModel('bug'); $queryID = ($browseType == 'bysearch') ? (int)$param : 0; unset($this->config->bug->search['fields']['product']); + unset($this->config->bug->search['fields']['project']); $this->config->bug->search['actionURL'] = $this->createLink('projectrelease', 'view', "releaseID=$releaseID&type=$type&link=true¶m=" . helper::safe64Encode('&browseType=bySearch&queryID=myQueryID')); $this->config->bug->search['queryID'] = $queryID; $this->config->bug->search['style'] = 'simple'; - $this->config->bug->search['params']['plan']['values'] = $this->loadModel('productplan')->getForProducts(array($release->product => $release->product)); - $this->config->bug->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($release->product, $viewType = 'bug', $startModuleID = 0); - $this->config->bug->search['params']['execution']['values'] = $this->loadModel('product')->getExecutionPairsByProduct($release->product, 0, 'id_desc', $release->project); + $this->config->bug->search['params']['plan']['values'] = $this->loadModel('productplan')->getPairsForStory($release->product, $release->branch, 'skipParent'); + $this->config->bug->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($release->product, 'bug', 0, $release->branch); + $this->config->bug->search['params']['execution']['values'] = $this->loadModel('product')->getExecutionPairsByProduct($release->product, $release->branch, 'id_desc', $release->project); $this->config->bug->search['params']['openedBuild']['values'] = $this->loadModel('build')->getProductBuildPairs($release->product, $branch = 0, $params = ''); $this->config->bug->search['params']['resolvedBuild']['values'] = $this->config->bug->search['params']['openedBuild']['values']; if($release->productType == 'normal') @@ -601,8 +602,8 @@ class projectrelease extends control else { $this->config->bug->search['fields']['branch'] = sprintf($this->lang->product->branch, $this->lang->product->branchName[$release->productType]); - $branches = array('' => '') + $this->loadModel('branch')->getPairs($release->product, 'noempty'); - if($release->branch) $branches = array('' => '', $release->branch => $branches[$release->branch]); + $branchName = $this->loadModel('branch')->getById($release->branch); + $branches = array('' => '', BRANCH_MAIN => $this->lang->branch->main, $release->branch => $branchName); $this->config->bug->search['params']['branch']['values'] = $branches; } $this->loadModel('search')->setSearchParams($this->config->bug->search); diff --git a/module/release/control.php b/module/release/control.php index c0cf909c16..70a0da3b80 100644 --- a/module/release/control.php +++ b/module/release/control.php @@ -419,9 +419,9 @@ class release extends control $this->config->product->search['actionURL'] = $this->createLink('release', 'view', "releaseID=$releaseID&type=story&link=true¶m=" . helper::safe64Encode('&browseType=bySearch&queryID=myQueryID')); $this->config->product->search['queryID'] = $queryID; $this->config->product->search['style'] = 'simple'; - $this->config->product->search['params']['plan']['values'] = $this->loadModel('productplan')->getForProducts(array($release->product => $release->product)); - $this->config->product->search['params']['module']['values'] = $this->tree->getOptionMenu($release->product, $viewType = 'story', $startModuleID = 0); + $this->config->product->search['params']['plan']['values'] = $this->loadModel('productplan')->getPairsForStory($release->product, $release->branch, 'skipParent'); $this->config->product->search['params']['status'] = array('operator' => '=', 'control' => 'select', 'values' => $this->lang->story->statusList); + $this->config->product->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($release->product, 'story', 0, $release->branch);; if($this->session->currentProductType == 'normal') { unset($this->config->product->search['fields']['branch']); @@ -430,19 +430,19 @@ class release extends control else { $this->config->product->search['fields']['branch'] = $this->lang->product->branch; - $branches = array('' => '') + $this->loadModel('branch')->getPairs($release->product, 'noempty'); - if($release->branch) $branches = array('' => '', $release->branch => $branches[$release->branch]); + $branchName = $this->loadModel('branch')->getById($release->branch); + $branches = array('' => '', BRANCH_MAIN => $this->lang->branch->main, $release->branch => $branchName); $this->config->product->search['params']['branch']['values'] = $branches; } $this->loadModel('search')->setSearchParams($this->config->product->search); - if($browseType == 'bySearch') + if($browseType == 'bySearch' or $build->execution == 0) { - $allStories = $this->story->getBySearch($release->product, $release->branch, $queryID, 'id', $build->execution ? $build->execution : '', 'story', $release->stories, $pager); + $allStories = $this->story->getBySearch($release->product, "0,{$release->branch}", $queryID, 'id', $build->execution ? $build->execution : '', 'story', $release->stories, $pager); } else { - $allStories = $this->story->getExecutionStories($build->execution, 0, 0, 't1.`order`_desc', 'byProduct', $release->product, 'story', $release->stories, $pager); + $allStories = $this->story->getExecutionStories($build->execution, $build->product, 0, 't1.`order`_desc', 'byBranch', $release->branch, 'story', $release->stories, $pager); } $this->view->allStories = $allStories; @@ -533,14 +533,15 @@ class release extends control $this->loadModel('bug'); $queryID = ($browseType == 'bysearch') ? (int)$param : 0; unset($this->config->bug->search['fields']['product']); + unset($this->config->bug->search['fields']['project']); $this->config->bug->search['actionURL'] = $this->createLink('release', 'view', "releaseID=$releaseID&type=$type&link=true¶m=" . helper::safe64Encode('&browseType=bySearch&queryID=myQueryID')); $this->config->bug->search['queryID'] = $queryID; $this->config->bug->search['style'] = 'simple'; - $this->config->bug->search['params']['plan']['values'] = $this->loadModel('productplan')->getForProducts(array($release->product => $release->product)); - $this->config->bug->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($release->product, $viewType = 'bug', $startModuleID = 0); - $this->config->bug->search['params']['execution']['values'] = $this->loadModel('product')->getExecutionPairsByProduct($release->product); + $this->config->bug->search['params']['plan']['values'] = $this->loadModel('productplan')->getPairsForStory($release->product, $release->branch, 'skipParent'); + $this->config->bug->search['params']['execution']['values'] = $this->loadModel('product')->getExecutionPairsByProduct($release->product, $release->branch); $this->config->bug->search['params']['openedBuild']['values'] = $this->loadModel('build')->getProductBuildPairs($release->product, $branch = 0, $params = ''); $this->config->bug->search['params']['resolvedBuild']['values'] = $this->config->bug->search['params']['openedBuild']['values']; + $this->config->bug->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($release->product, 'bug', 0, $release->branch); if($this->session->currentProductType == 'normal') { unset($this->config->bug->search['fields']['branch']); @@ -549,15 +550,15 @@ class release extends control else { $this->config->bug->search['fields']['branch'] = $this->lang->product->branch; - $branches = array('' => '') + $this->loadModel('branch')->getPairs($release->product, 'noempty'); - if($release->branch) $branches = array('' => '', $release->branch => $branches[$release->branch]); + $branchName = $this->loadModel('branch')->getById($release->branch); + $branches = array('' => '', BRANCH_MAIN => $this->lang->branch->main, $release->branch => $branchName); $this->config->bug->search['params']['branch']['values'] = $branches; } $this->loadModel('search')->setSearchParams($this->config->bug->search); $allBugs = array(); $releaseBugs = $type == 'bug' ? $release->bugs : $release->leftBugs; - if($browseType == 'bySearch') + if($browseType == 'bySearch' or $build->execution == 0) { $allBugs = $this->bug->getBySearch($release->product, $release->branch, $queryID, 'id_desc', $releaseBugs, $pager); } diff --git a/module/release/lang/en.php b/module/release/lang/en.php index a77a316989..957247d949 100644 --- a/module/release/lang/en.php +++ b/module/release/lang/en.php @@ -58,6 +58,9 @@ $lang->release->yesterday = 'Released Yesterday'; $lang->release->all = 'All'; $lang->release->notify = 'Notify'; $lang->release->mailto = 'Mailto'; +$lang->release->mailContent = '

    Dear users,

    The following requirements and bugs you feedback have been released in the %s. Please contact your account manager to check the latest version.

    '; +$lang->release->storyList = '

    Story List:%s。

    '; +$lang->release->bugList = '

    Bug List:%s。

    '; $lang->release->filePath = 'Download : '; $lang->release->scmPath = 'SCM Path : '; @@ -77,6 +80,7 @@ $lang->release->changeStatusList['terminate'] = 'Terminated'; $lang->release->action = new stdclass(); $lang->release->action->changestatus = array('main' => '$date, $extra by $actor', 'extra' => 'changeStatusList'); +$lang->release->notifyList['FB'] = "Feedback By"; $lang->release->notifyList['PO'] = "{$lang->productCommon} Owner"; $lang->release->notifyList['QD'] = 'QA Manager'; $lang->release->notifyList['SC'] = 'Story Creator'; diff --git a/module/release/lang/zh-cn.php b/module/release/lang/zh-cn.php index bcdafcdb9c..616baa0c4d 100644 --- a/module/release/lang/zh-cn.php +++ b/module/release/lang/zh-cn.php @@ -58,6 +58,9 @@ $lang->release->yesterday = '昨日发布'; $lang->release->all = '所有'; $lang->release->notify = '通知人员'; $lang->release->mailto = '抄送给'; +$lang->release->mailContent = '

    尊敬的用户,您好!

    您反馈的如下需求和Bug已经在 %s版本中发布,请联系客户经理查看最新版本。

    '; +$lang->release->storyList = '

    需求列表:%s。

    '; +$lang->release->bugList = '

    Bug列表:%s。

    '; $lang->release->filePath = '下载地址:'; $lang->release->scmPath = '版本库地址:'; @@ -77,6 +80,7 @@ $lang->release->changeStatusList['terminate'] = '停止维护'; $lang->release->action = new stdclass(); $lang->release->action->changestatus = array('main' => '$date, 由 $actor $extra。', 'extra' => 'changeStatusList'); +$lang->release->notifyList['FB'] = "反馈者"; $lang->release->notifyList['PO'] = "{$lang->productCommon}负责人"; $lang->release->notifyList['QD'] = '测试负责人'; $lang->release->notifyList['SC'] = '需求提交人'; diff --git a/module/release/model.php b/module/release/model.php index fd64b43d8c..19152c8153 100644 --- a/module/release/model.php +++ b/module/release/model.php @@ -520,4 +520,82 @@ class releaseModel extends model return array($toList, $ccList); } + + /** + * Send mail to feedback user. + * + * @param object $release + * @param string $subject + * @access public + * @return void + */ + public function sendMail2Feedback($release, $subject) + { + $stories = $bugs = array(); + + $buildObjects = $this->dao->select('stories,bugs')->from(TABLE_BUILD)->where('id')->eq($release->build)->fetch(); + $releaseObjects = $this->dao->select('stories,bugs')->from(TABLE_RELEASE)->where('id')->eq($release->id)->fetch(); + + $stories = explode(',', trim($buildObjects->stories, ',')) + explode(',', trim($releaseObjects->stories, ',')); + $bugs = explode(',', trim($buildObjects->bugs, ',')) + explode(',', trim($releaseObjects->bugs, ',')); + + if(!empty($stories)) + { + $storyNotifyList = $this->dao->select('id,title,notifyEmail')->from(TABLE_STORY) + ->where('id')->in($stories) + ->andWhere('notifyEmail')->ne('') + ->fetchGroup('notifyEmail', 'id'); + + $bugNotifyList = $this->dao->select('id,title,notifyEmail')->from(TABLE_BUG) + ->where('id')->in($bugs) + ->andWhere('notifyEmail')->ne('') + ->fetchGroup('notifyEmail', 'id'); + + $toList = array(); + $emails = array(); + $storyNames = array(); + $bugNames = array(); + foreach($storyNotifyList as $storyList) + { + $email = new stdClass(); + foreach($storyList as $story) + { + $storyNames[] = $story->title; + + if(isset($email->account)) continue; + $email->account = $story->notifyEmail; + $email->email = $story->notifyEmail; + $email->realname = ''; + $emails[$story->notifyEmail] = $email; + $toList[$story->notifyEmail] = $story->notifyEmail; + } + } + foreach($bugNotifyList as $bugList) + { + $email = new stdClass(); + foreach($bugList as $bug) + { + $bugNames[] = $bug->title; + + if(isset($email->account)) continue; + $email->account = $bug->notifyEmail; + $email->email = $bug->notifyEmail; + $email->realname = ''; + $emails[$bug->notifyEmail] = $email; + $toList[$bug->notifyEmail] = $bug->notifyEmail; + } + } + + if(!empty($toList)) + { + $storyNames = implode(',', $storyNames); + $bugNames = implode(',', $bugNames); + $mailContent = sprintf($this->lang->release->mailContent, $release->name); + if($storyNames) $mailContent .= sprintf($this->lang->release->storyList, $storyNames); + if($bugNames) $mailContent .= sprintf($this->lang->release->bugList, $bugNames); + $this->loadModel('mail')->send(implode(',', $toList), $subject, $mailContent, '', false, $emails); + } + } + + } } diff --git a/module/release/view/create.html.php b/module/release/view/create.html.php index 6a14e4d930..2601dedcec 100644 --- a/module/release/view/create.html.php +++ b/module/release/view/create.html.php @@ -51,7 +51,7 @@ release->notify;?> - release->notifyList);?> + release->notifyList, 'FB');?> release->mailto;?> diff --git a/module/repo/lang/en.php b/module/repo/lang/en.php index da7c51712f..1c58036410 100644 --- a/module/repo/lang/en.php +++ b/module/repo/lang/en.php @@ -133,7 +133,7 @@ $lang->repo->scmList['Subversion'] = 'SVN'; $lang->repo->gitlabHost = 'Gitlab Host'; $lang->repo->gitlabToken = 'Gitlab Token'; -$lang->repo->gitlabProject = 'Projects'; +$lang->repo->gitlabProject = 'Project'; $lang->repo->placeholder = new stdclass; $lang->repo->placeholder->gitlabHost = 'Input url of gitlab'; diff --git a/module/report/control.php b/module/report/control.php index 4cc2e3322c..984a13d518 100644 --- a/module/report/control.php +++ b/module/report/control.php @@ -115,7 +115,7 @@ class report extends control $this->view->end = $end; $this->view->bugs = $this->report->getBugs($begin, $end, $product, $execution); $this->view->users = $this->loadModel('user')->getPairs('noletter|noclosed|nodeleted'); - $this->view->executions = array('' => '') + $this->loadModel('execution')->getPairs(); + $this->view->executions = array('' => '') + $this->report->getProjectExecutions(); $this->view->products = array('' => '') + $this->loadModel('product')->getPairs(); $this->view->execution = $execution; $this->view->product = $product; diff --git a/module/report/lang/en.php b/module/report/lang/en.php index 337a4eb07c..c7d84faebe 100644 --- a/module/report/lang/en.php +++ b/module/report/lang/en.php @@ -110,6 +110,8 @@ $lang->report->mailTitle->testTask = " Request (%s),"; $lang->report->deviationDesc = 'According to the Closed Execution Deviation Rate = ((Total Cost - Total Estimate) / Total Estimate), the Deviation Rate is n/a when the Total Estimate is 0.'; $lang->report->proVersion = 'Try ZenTao Pro for more!'; $lang->report->proVersionEn = 'Try ZenTao Pro for more!'; +$lang->report->workloadDesc = 'Workload = the total left hours of all tasks of the user / selected days * hours per day. +For example: the begin and end date is January 1st to January 7th, and the total work days is 5 days, 8 hours per day. The Work load is all unfinished tasks assigned to this user to be finished in 5 days, 8 hours per day.'; $lang->report->annualData = new stdclass(); $lang->report->annualData->title = "%s work summary in %s"; diff --git a/module/report/lang/zh-cn.php b/module/report/lang/zh-cn.php index b884bf3de9..8b5dc45830 100644 --- a/module/report/lang/zh-cn.php +++ b/module/report/lang/zh-cn.php @@ -110,6 +110,7 @@ $lang->report->mailTitle->testTask = " 测试版本(%s),"; $lang->report->deviationDesc = '按照已关闭执行统计偏差率(偏差率 = (总消耗 - 总预计) / 总预计),总预计为0时偏差率为n/a。'; $lang->report->proVersion = '更多精彩,尽在专业版!'; $lang->report->proVersionEn = 'Try ZenTao Pro for more!'; +$lang->report->workloadDesc = '工作负载=用户所有任务剩余工时之和/选择的时间天数*每天的工时。例如:起止时间设为1月1日~1月7日、工作日天数5天、每天工时8h,统计的是所有指派给该人员的未完成的任务,在5天内,每天8h的情况下的工作负载。'; $lang->report->annualData = new stdclass(); $lang->report->annualData->title = "%s %s年工作汇总"; diff --git a/module/report/model.php b/module/report/model.php index 668819e56c..7dfc100dac 100644 --- a/module/report/model.php +++ b/module/report/model.php @@ -296,20 +296,20 @@ class reportModel extends model ->andWhere("t1.account NOT IN(SELECT `assignedTo` FROM " . TABLE_TASK . " WHERE `execution` = t1.`root` AND `status` NOT IN('cancel, closed, done, pause') AND assignedTo != '' GROUP BY assignedTo)") ->fetchGroup('account', 'name'); - $workload = array(); + $workload = array(); if(!empty($members)) { foreach($members as $member => $executions) { - $project = array(); + $project = array(); if(!empty($executions)) { foreach($executions as $name => $execution) { - $project[$execution->projectname]['projectID'] = $execution->project; - $project[$execution->projectname]['execution'][$name]['executionID'] = $execution->root; - $project[$execution->projectname]['execution'][$name]['count'] = 0; - $project[$execution->projectname]['execution'][$name]['manhour'] = 0; + $project[$execution->projectname]['projectID'] = $execution->project; + $project[$execution->projectname]['execution'][$name]['executionID'] = $execution->root; + $project[$execution->projectname]['execution'][$name]['count'] = 0; + $project[$execution->projectname]['execution'][$name]['manhour'] = 0; $workload[$member]['total']['count'] = 0; $workload[$member]['total']['manhour'] = 0; @@ -377,22 +377,22 @@ class reportModel extends model { if($user) { - $project = array(); + $project = array(); foreach($userTasks as $task) { if(isset($parents[$task->id])) continue; - $project[$task->projectname]['projectID'] = isset($project[$task->projectname]['projectID']) ? $project[$task->projectname]['projectID'] : $task->project; - $project[$task->projectname]['execution'][$task->executionName]['executionID'] = isset($project[$task->projectname]['execution'][$task->executionName]['executionID']) ? $project[$task->projectname]['execution'][$task->executionName]['executionID'] : $task->execution; - $project[$task->projectname]['execution'][$task->executionName]['count'] = isset($project[$task->projectname]['execution'][$task->executionName]['count']) ? $project[$task->projectname]['execution'][$task->executionName]['count'] + 1 : 1; - $project[$task->projectname]['execution'][$task->executionName]['manhour'] = isset($project[$task->projectname]['execution'][$task->executionName]['manhour']) ? $project[$task->projectname]['execution'][$task->executionName]['manhour'] + $task->left : $task->left; + $project[$task->projectname]['projectID'] = isset($project[$task->projectname]['projectID']) ? $project[$task->projectname]['projectID'] : $task->project; + $project[$task->projectname]['execution'][$task->executionName]['executionID'] = isset($project[$task->projectname]['execution'][$task->executionName]['executionID']) ? $project[$task->projectname]['execution'][$task->executionName]['executionID'] : $task->execution; + $project[$task->projectname]['execution'][$task->executionName]['count'] = isset($project[$task->projectname]['execution'][$task->executionName]['count']) ? $project[$task->projectname]['execution'][$task->executionName]['count'] + 1 : 1; + $project[$task->projectname]['execution'][$task->executionName]['manhour'] = isset($project[$task->projectname]['execution'][$task->executionName]['manhour']) ? $project[$task->projectname]['execution'][$task->executionName]['manhour'] + $task->left : $task->left; $workload[$user]['total']['count'] = isset($workload[$user]['total']['count']) ? $workload[$user]['total']['count'] + 1 : 1; $workload[$user]['total']['manhour'] = isset($workload[$user]['total']['manhour']) ? $workload[$user]['total']['manhour'] + $task->left : $task->left; } - $workload[$user]['task']['project'] = $project; + $workload[$user]['task']['project'] = $project; } - } + } unset($workload['closed']); return $workload; } @@ -1162,6 +1162,30 @@ class reportModel extends model return $processedOutput; } + + /** + * Get project and execution name. + * + * @access public + * @return array + */ + public function getProjectExecutions() + { + $executions = $this->dao->select('t1.id, t1.name, t2.name as projectname, t1.status') + ->from(TABLE_EXECUTION)->alias('t1') + ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id') + ->where('t1.deleted')->eq(0) + ->andWhere('t1.type')->in('stage,sprint') + ->fetchAll(); + + $pairs = array(); + foreach($executions as $execution) + { + $pairs[$execution->id] = $this->config->systemMode == 'new' ? $execution->projectname . '/' .$execution->name : $execution->name; + } + + return $pairs; + } } /** diff --git a/module/report/view/bugcreate.html.php b/module/report/view/bugcreate.html.php index 6377bd9a87..aecdbb183f 100644 --- a/module/report/view/bugcreate.html.php +++ b/module/report/view/bugcreate.html.php @@ -27,7 +27,7 @@
    - executionCommon;?> + config->systemMode == 'classic' ? $lang->executionCommon : $lang->execution->common;?>
    diff --git a/module/report/view/workload.html.php b/module/report/view/workload.html.php index 6c0d607346..1928a1a320 100644 --- a/module/report/view/workload.html.php +++ b/module/report/view/workload.html.php @@ -62,7 +62,9 @@
    -
    +
    + +
    diff --git a/module/search/model.php b/module/search/model.php index 442883203c..a233ae3974 100644 --- a/module/search/model.php +++ b/module/search/model.php @@ -132,11 +132,27 @@ class searchModel extends model $condition = ''; if($operator == "include") { - $condition = ' LIKE ' . $this->dbh->quote("%$value%"); + if($this->post->$fieldName == 'module') + { + $allModules = $this->loadModel('tree')->getAllChildId($value); + if($allModules) $condition = helper::dbIN($allModules); + } + else + { + $condition = ' LIKE ' . $this->dbh->quote("%$value%"); + } } elseif($operator == "notinclude") { - $condition = ' NOT LIKE ' . $this->dbh->quote("%$value%"); + if($this->post->$fieldName == 'module') + { + $allModules = $this->loadModel('tree')->getAllChildId($value); + if($allModules) $condition = " NOT " . helper::dbIN($allModules); + } + else + { + $condition = ' NOT LIKE ' . $this->dbh->quote("%$value%"); + } } elseif($operator == 'belong') { diff --git a/module/search/view/buildform.html.php b/module/search/view/buildform.html.php index 7b66722154..90f2d9043b 100644 --- a/module/search/view/buildform.html.php +++ b/module/search/view/buildform.html.php @@ -51,7 +51,7 @@ $formId = 'searchForm-' . uniqid(''); #userQueries .label:hover {background-color: #aaa; color: #fff;} #userQueries .label > .icon-close {position: absolute; top: 2px; right: 2px; border-radius: 9px; font-size: 12px; line-height: 18px; width: 18px; display: inline-block;} #userQueries .label > .icon-close:hover {background-color: #ff5d5d; color: #fff;} -@media (max-width: 1150px) {#userQueries {display: none}} +@media (max-width: 1050px) {#userQueries {display: none}} # .form-actions {text-align: left; padding: 0!important; max-width: 200px; vertical-align: middle; width: 100px;} #queryBox.show {min-height: 66px;} diff --git a/module/stage/control.php b/module/stage/control.php new file mode 100644 index 0000000000..6fa1d394ae --- /dev/null +++ b/module/stage/control.php @@ -0,0 +1,185 @@ + + * @package stage + * @version $Id: control.php 5107 2013-07-12 01:46:12Z chencongzhi520@gmail.com $ + * @link http://www.zentao.net + */ +class stage extends control +{ + /** + * Browse stages. + * + * @param string $orderBy + * @access public + * @return void + */ + public function browse($orderBy = "id_asc") + { + $this->view->stages = $this->stage->getStages($orderBy); + $this->view->orderBy = $orderBy; + $this->view->title = $this->lang->stage->common . $this->lang->colon . $this->lang->stage->browse; + $this->view->position[] = $this->lang->stage->common; + $this->view->position[] = $this->lang->stage->browse; + + $this->display(); + } + + /** + * Create a stage. + * + * @access public + * @return void + */ + public function create() + { + if($_POST) + { + $stageID = $this->stage->create(); + + $response['result'] = 'success'; + $response['message'] = $this->lang->saveSuccess; + if(!$stageID) + { + $response['result'] = 'fail'; + $response['message'] = dao::getError(); + return $this->send($response); + } + + $this->loadModel('action')->create('stage', $stageID, 'Opened'); + $response['locate'] = inlink('browse'); + return $this->send($response); + } + + $this->view->title = $this->lang->stage->common . $this->lang->colon . $this->lang->stage->create; + $this->view->position[] = $this->lang->stage->common; + $this->view->position[] = $this->lang->stage->create; + + $this->display(); + } + + /** + * Batch create stages. + * + * @access public + * @return void + */ + public function batchCreate() + { + if($_POST) + { + $this->stage->batchCreate(); + + $response['result'] = 'success'; + $response['message'] = $this->lang->saveSuccess; + if(dao::isError()) + { + $response['result'] = 'fail'; + $response['message'] = dao::getError(); + return $this->send($response); + } + + $response['locate'] = inlink('browse'); + return $this->send($response); + } + + $this->view->title = $this->lang->stage->common . $this->lang->colon . $this->lang->stage->batchCreate; + $this->view->position[] = $this->lang->stage->common; + $this->view->position[] = $this->lang->stage->batchCreate; + + $this->display(); + } + + /** + * Edit a stage. + * + * @param int $stageID + * @access public + * @return void + */ + public function edit($stageID = 0) + { + $stage = $this->stage->getByID($stageID); + if($_POST) + { + $changes = $this->stage->update($stageID); + + $response['result'] = 'success'; + $response['message'] = $this->lang->saveSuccess; + if(dao::isError()) + { + $response['result'] = 'fail'; + $response['message'] = dao::getError(); + return $this->send($response); + } + + $actionID = $this->loadModel('action')->create('stage', $stageID, 'Edited'); + if(!empty($changes)) $this->action->logHistory($actionID, $changes); + $response['locate'] = inlink('browse'); + return $this->send($response); + } + + $this->view->title = $this->lang->stage->common . $this->lang->colon . $this->lang->stage->edit; + $this->view->position[] = $this->lang->stage->common; + $this->view->position[] = $this->lang->stage->edit; + $this->view->stage = $stage; + + $this->display(); + } + + /** + * Set type. + * + * @access public + * @return void + */ + public function setType() + { + $this->loadModel('custom'); + if($_POST) + { + $data = fixer::input('post')->get(); + $this->custom->deleteItems("lang=all&module=stage§ion=typeList"); + foreach($data->keys as $index => $key) + { + $value = $data->values[$index]; + if(!$value or !$key) continue; + $this->custom->setItem("all.stage.typeList.{$key}", $value); + } + return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => $this->createLink('stage', 'settype'))); + } + + $this->view->title = $this->lang->stage->common . $this->lang->colon . $this->lang->stage->setType; + $this->view->position[] = $this->lang->stage->common; + $this->view->position[] = $this->lang->stage->setType; + $this->display(); + } + + /** + * Delete a stage. + * + * @param int $stageID + * @param string $confirm + * @access public + * @return void + */ + public function delete($stageID, $confirm = 'no') + { + $stage = $this->stage->getById($stageID); + + if($confirm == 'no') + { + die(js::confirm($this->lang->stage->confirmDelete, inlink('delete', "stageID=$stageID&confirm=yes"))); + } + else + { + $this->stage->delete(TABLE_STAGE, $stageID); + + die(js::reload('parent')); + } + } +} diff --git a/module/stage/ext/view/create.cmmi.html.hook.php b/module/stage/ext/view/create.cmmi.html.hook.php new file mode 100644 index 0000000000..6120b9e1a2 --- /dev/null +++ b/module/stage/ext/view/create.cmmi.html.hook.php @@ -0,0 +1 @@ + diff --git a/module/stage/ext/view/edit.cmmi.html.hook.php b/module/stage/ext/view/edit.cmmi.html.hook.php new file mode 100644 index 0000000000..6120b9e1a2 --- /dev/null +++ b/module/stage/ext/view/edit.cmmi.html.hook.php @@ -0,0 +1 @@ + diff --git a/module/stage/lang/de.php b/module/stage/lang/de.php new file mode 100644 index 0000000000..f11a49dfd9 --- /dev/null +++ b/module/stage/lang/de.php @@ -0,0 +1,38 @@ + + * @package stage + * @version $Id: en.php 4729 2013-05-03 07:53:55Z chencongzhi520@gmail.com $ + * @link http://www.zentao.net + */ +/* Actions. */ +$lang->stage->browse = 'Stage List'; +$lang->stage->create = 'Create Stage'; +$lang->stage->batchCreate = 'Batch Create'; +$lang->stage->edit = 'Edit'; +$lang->stage->delete = 'Delete'; +$lang->stage->view = 'Details'; + +/* Fields. */ +$lang->stage->common = 'Stage'; +$lang->stage->id = 'ID'; +$lang->stage->name = 'Name'; +$lang->stage->type = 'Type'; +$lang->stage->percent = 'Workload %'; +$lang->stage->setType = 'Set Type'; + +$lang->stage->typeList['request'] = 'Story'; +$lang->stage->typeList['design'] = 'Design'; +$lang->stage->typeList['dev'] = 'Development'; +$lang->stage->typeList['qa'] = 'Test'; +$lang->stage->typeList['release'] = 'Release'; +$lang->stage->typeList['review'] = 'Review'; +$lang->stage->typeList['other'] = 'Other'; + +$lang->stage->viewList = 'Stage List'; +$lang->stage->noStage = 'No stage yet'; +$lang->stage->confirmDelete = 'Do you want to delete it?'; diff --git a/module/stage/lang/en.php b/module/stage/lang/en.php new file mode 100644 index 0000000000..f11a49dfd9 --- /dev/null +++ b/module/stage/lang/en.php @@ -0,0 +1,38 @@ + + * @package stage + * @version $Id: en.php 4729 2013-05-03 07:53:55Z chencongzhi520@gmail.com $ + * @link http://www.zentao.net + */ +/* Actions. */ +$lang->stage->browse = 'Stage List'; +$lang->stage->create = 'Create Stage'; +$lang->stage->batchCreate = 'Batch Create'; +$lang->stage->edit = 'Edit'; +$lang->stage->delete = 'Delete'; +$lang->stage->view = 'Details'; + +/* Fields. */ +$lang->stage->common = 'Stage'; +$lang->stage->id = 'ID'; +$lang->stage->name = 'Name'; +$lang->stage->type = 'Type'; +$lang->stage->percent = 'Workload %'; +$lang->stage->setType = 'Set Type'; + +$lang->stage->typeList['request'] = 'Story'; +$lang->stage->typeList['design'] = 'Design'; +$lang->stage->typeList['dev'] = 'Development'; +$lang->stage->typeList['qa'] = 'Test'; +$lang->stage->typeList['release'] = 'Release'; +$lang->stage->typeList['review'] = 'Review'; +$lang->stage->typeList['other'] = 'Other'; + +$lang->stage->viewList = 'Stage List'; +$lang->stage->noStage = 'No stage yet'; +$lang->stage->confirmDelete = 'Do you want to delete it?'; diff --git a/module/stage/lang/fr.php b/module/stage/lang/fr.php new file mode 100644 index 0000000000..f11a49dfd9 --- /dev/null +++ b/module/stage/lang/fr.php @@ -0,0 +1,38 @@ + + * @package stage + * @version $Id: en.php 4729 2013-05-03 07:53:55Z chencongzhi520@gmail.com $ + * @link http://www.zentao.net + */ +/* Actions. */ +$lang->stage->browse = 'Stage List'; +$lang->stage->create = 'Create Stage'; +$lang->stage->batchCreate = 'Batch Create'; +$lang->stage->edit = 'Edit'; +$lang->stage->delete = 'Delete'; +$lang->stage->view = 'Details'; + +/* Fields. */ +$lang->stage->common = 'Stage'; +$lang->stage->id = 'ID'; +$lang->stage->name = 'Name'; +$lang->stage->type = 'Type'; +$lang->stage->percent = 'Workload %'; +$lang->stage->setType = 'Set Type'; + +$lang->stage->typeList['request'] = 'Story'; +$lang->stage->typeList['design'] = 'Design'; +$lang->stage->typeList['dev'] = 'Development'; +$lang->stage->typeList['qa'] = 'Test'; +$lang->stage->typeList['release'] = 'Release'; +$lang->stage->typeList['review'] = 'Review'; +$lang->stage->typeList['other'] = 'Other'; + +$lang->stage->viewList = 'Stage List'; +$lang->stage->noStage = 'No stage yet'; +$lang->stage->confirmDelete = 'Do you want to delete it?'; diff --git a/module/stage/lang/vi.php b/module/stage/lang/vi.php new file mode 100644 index 0000000000..f11a49dfd9 --- /dev/null +++ b/module/stage/lang/vi.php @@ -0,0 +1,38 @@ + + * @package stage + * @version $Id: en.php 4729 2013-05-03 07:53:55Z chencongzhi520@gmail.com $ + * @link http://www.zentao.net + */ +/* Actions. */ +$lang->stage->browse = 'Stage List'; +$lang->stage->create = 'Create Stage'; +$lang->stage->batchCreate = 'Batch Create'; +$lang->stage->edit = 'Edit'; +$lang->stage->delete = 'Delete'; +$lang->stage->view = 'Details'; + +/* Fields. */ +$lang->stage->common = 'Stage'; +$lang->stage->id = 'ID'; +$lang->stage->name = 'Name'; +$lang->stage->type = 'Type'; +$lang->stage->percent = 'Workload %'; +$lang->stage->setType = 'Set Type'; + +$lang->stage->typeList['request'] = 'Story'; +$lang->stage->typeList['design'] = 'Design'; +$lang->stage->typeList['dev'] = 'Development'; +$lang->stage->typeList['qa'] = 'Test'; +$lang->stage->typeList['release'] = 'Release'; +$lang->stage->typeList['review'] = 'Review'; +$lang->stage->typeList['other'] = 'Other'; + +$lang->stage->viewList = 'Stage List'; +$lang->stage->noStage = 'No stage yet'; +$lang->stage->confirmDelete = 'Do you want to delete it?'; diff --git a/module/stage/lang/zh-cn.php b/module/stage/lang/zh-cn.php new file mode 100644 index 0000000000..15980382a7 --- /dev/null +++ b/module/stage/lang/zh-cn.php @@ -0,0 +1,38 @@ + + * @package stage + * @version $Id: zh-cn.php 4729 2013-05-03 07:53:55Z chencongzhi520@gmail.com $ + * @link http://www.zentao.net + */ +/* Actions. */ +$lang->stage->browse = '阶段列表'; +$lang->stage->create = '新建'; +$lang->stage->batchCreate = '批量新建'; +$lang->stage->edit = '编辑'; +$lang->stage->delete = '删除'; +$lang->stage->view = '阶段详情'; + +/* Fields. */ +$lang->stage->common = '阶段'; +$lang->stage->id = '编号'; +$lang->stage->name = '阶段名称'; +$lang->stage->type = '阶段分类'; +$lang->stage->percent = '工作量占比'; +$lang->stage->setType = '阶段类型'; + +$lang->stage->typeList['request'] = '需求'; +$lang->stage->typeList['design'] = '设计'; +$lang->stage->typeList['dev'] = '开发'; +$lang->stage->typeList['qa'] = '测试'; +$lang->stage->typeList['release'] = '发布'; +$lang->stage->typeList['review'] = '总结评审'; +$lang->stage->typeList['other'] = '其他'; + +$lang->stage->viewList = '浏览列表'; +$lang->stage->noStage = '暂时没有阶段'; +$lang->stage->confirmDelete = '您确定要执行删除操作吗?'; diff --git a/module/stage/lang/zh-tw.php b/module/stage/lang/zh-tw.php new file mode 100644 index 0000000000..e4e73d5f49 --- /dev/null +++ b/module/stage/lang/zh-tw.php @@ -0,0 +1,38 @@ + + * @package stage + * @version $Id: zh-tw.php 4729 2013-05-03 07:53:55Z chencongzhi520@gmail.com $ + * @link http://www.zentao.net + */ +/* Actions. */ +$lang->stage->browse = '階段列表'; +$lang->stage->create = '新建'; +$lang->stage->batchCreate = '批量新建'; +$lang->stage->edit = '編輯'; +$lang->stage->delete = '刪除'; +$lang->stage->view = '階段詳情'; + +/* Fields. */ +$lang->stage->common = '階段'; +$lang->stage->id = '編號'; +$lang->stage->name = '階段名稱'; +$lang->stage->type = '階段分類'; +$lang->stage->percent = '工作量占比'; +$lang->stage->setType = '階段類型'; + +$lang->stage->typeList['request'] = '需求'; +$lang->stage->typeList['design'] = '設計'; +$lang->stage->typeList['dev'] = '開發'; +$lang->stage->typeList['qa'] = '測試'; +$lang->stage->typeList['release'] = '發佈'; +$lang->stage->typeList['review'] = '總結評審'; +$lang->stage->typeList['other'] = '其他'; + +$lang->stage->viewList = '瀏覽列表'; +$lang->stage->noStage = '暫時沒有階段'; +$lang->stage->confirmDelete = '您確定要執行刪除操作嗎?'; diff --git a/module/stage/model.php b/module/stage/model.php new file mode 100644 index 0000000000..9d4bd1e366 --- /dev/null +++ b/module/stage/model.php @@ -0,0 +1,130 @@ + + * @package stage + * @version $Id: model.php 5079 2013-07-10 00:44:34Z chencongzhi520@gmail.com $ + * @link http://www.zentao.net + */ +?> +add('createdBy', $this->app->user->account) + ->add('createdDate', helper::today()) + ->get(); + + $this->dao->insert(TABLE_STAGE)->data($stage)->autoCheck()->exec(); + + if(!dao::isError()) return $this->dao->lastInsertID(); + return false; + } + + /** + * Batch create stages. + * + * @access public + * @return bool + */ + public function batchCreate() + { + $data = fixer::input('post')->get(); + + $this->loadModel('action'); + foreach($data->name as $i => $name) + { + if(!$name) continue; + + $stage = new stdclass(); + $stage->name = $name; + $stage->percent = $data->percent[$i]; + $stage->type = $data->type[$i]; + $stage->createdBy = $this->app->user->account; + $stage->createdDate = helper::today(); + + $this->dao->insert(TABLE_STAGE)->data($stage)->autoCheck()->exec(); + + $stageID = $this->dao->lastInsertID(); + $this->action->create('stage', $stageID, 'Opened'); + } + + return true; + } + + /** + * Update a stage. + * + * @param int $stageID + * @access public + * @return bool + */ + public function update($stageID) + { + $oldStage = $this->dao->select('*')->from(TABLE_STAGE)->where('id')->eq((int)$stageID)->fetch(); + + $stage = fixer::input('post') + ->add('editedBy', $this->app->user->account) + ->add('editedDate', helper::today()) + ->get(); + + $this->dao->update(TABLE_STAGE)->data($stage)->autoCheck()->where('id')->eq((int)$stageID)->exec(); + + if(!dao::isError()) return common::createChanges($oldStage, $stage); + return false; + } + + /** + * Get stages. + * + * @param string $orderBy + * @access public + * @return array + */ + public function getStages($orderBy = 'id_desc') + { + return $this->dao->select('*')->from(TABLE_STAGE)->where('deleted')->eq(0)->orderBy($orderBy)->fetchAll('id'); + } + + /** + * Get pairs of stage. + * + * @access public + * @return array + */ + public function getPairs() + { + $stages = $this->getStages(); + + $pairs = array(); + foreach($stages as $stageID => $stage) + { + $pairs[$stageID] = $stage->name; + } + + return $pairs; + } + + /** + * Get a stage by id. + * + * @param int $stageID + * @access public + * @return object + */ + public function getByID($stageID) + { + return $this->dao->select('*')->from(TABLE_STAGE)->where('deleted')->eq(0)->andWhere('id')->eq((int)$stageID)->fetch(); + } +} diff --git a/module/stage/view/batchcreate.html.php b/module/stage/view/batchcreate.html.php new file mode 100644 index 0000000000..6be8c8f996 --- /dev/null +++ b/module/stage/view/batchcreate.html.php @@ -0,0 +1,46 @@ + + * @package stage + * @version $Id: batchCreate.html.php 4903 2013-06-26 05:32:59Z wyd621@gmail.com $ + * @link http://www.zentao.net + */ +?> + +
    +
    +

    stage->batchCreate;?>

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + +
    stage->id;?>stage->name;?>stage->percent;?>stage->type;?>
    stage->typeList, '', "class='form-control chosen'");?>
    + +
    +
    +
    + diff --git a/module/stage/view/browse.html.php b/module/stage/view/browse.html.php new file mode 100644 index 0000000000..b60412afff --- /dev/null +++ b/module/stage/view/browse.html.php @@ -0,0 +1,73 @@ + + * @package stage + * @version $Id: browse.html.php 4903 2013-06-26 05:32:59Z wyd621@gmail.com $ + * @link http://www.zentao.net + */ +?> + + +
    + +
    +

    + stage->noStage;?> + + createLink('stage', 'create'), " " . $lang->stage->create, '', "class='btn btn-info'");?> + +

    +
    + +
    +
    +
    +
    + stage->setType);?> + stage->browse, '', "class='selected'");?> +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + +
    stage->id);?>stage->name);?>stage->percent);?>stage->type);?>actions;?>
    id;?>name;?>percent;?>stage->typeList, $stage->type);?> + id", "", "list"); + common::printIcon('stage', 'delete', "stageID=$stage->id", "", "list", '', 'hiddenwin'); + ?> +
    +
    + +
    + diff --git a/module/stage/view/create.html.php b/module/stage/view/create.html.php new file mode 100644 index 0000000000..3d53f89f39 --- /dev/null +++ b/module/stage/view/create.html.php @@ -0,0 +1,52 @@ + + * @package stage + * @version $Id: create.html.php 4903 2013-06-26 05:32:59Z wyd621@gmail.com $ + * @link http://www.zentao.net + */ +?> + +
    +
    +
    +

    stage->create;?>

    +
    +
    + + + + + + + + + + + + + + + + + + + + + +
    stage->name;?>
    stage->percent;?> +
    + + % +
    +
    stage->type;?>stage->typeList, '', "class='form-control chosen'");?>
    + +
    +
    +
    +
    + diff --git a/module/stage/view/edit.html.php b/module/stage/view/edit.html.php new file mode 100644 index 0000000000..05c393bdbb --- /dev/null +++ b/module/stage/view/edit.html.php @@ -0,0 +1,51 @@ + + * @package stage + * @version $Id: edit.html.php 4903 2013-06-26 05:32:59Z wyd621@gmail.com $ + * @link http://www.zentao.net + */ +?> + +
    +
    +
    +

    stage->edit;?>

    +
    +
    + + + + + + + + + + + + + + + + + + + + + +
    stage->name;?>name, "class='form-control'");?>
    stage->percent;?> +
    + percent, "class='form-control'");?> + % +
    +
    stage->type;?>stage->typeList, $stage->type, "class='form-control chosen'");?>
    + +
    +
    +
    + diff --git a/module/stage/view/settype.html.php b/module/stage/view/settype.html.php new file mode 100644 index 0000000000..75d234e5be --- /dev/null +++ b/module/stage/view/settype.html.php @@ -0,0 +1,86 @@ + + * @package stage + * @version $Id: setType.html.php 4903 2013-06-26 05:32:59Z wyd621@gmail.com $ + * @link http://www.zentao.net + */ +?> + + + + + + + + + + + + + +EOT; +?> + +
    +
    +
    +
    +
    + stage->setType, '', "class='selected'");?> + stage->browse);?> +
    +
    +
    +
    +
    +
    + + + + + + + + + + stage->typeList as $key => $value):?> + + + + + + + + + + + +
    custom->key;?>custom->value;?>
    + + + + + +
    +
    +
    +
    + + diff --git a/module/story/config.php b/module/story/config.php index 8dcaff837b..ce53769963 100644 --- a/module/story/config.php +++ b/module/story/config.php @@ -5,6 +5,7 @@ $config->story->batchCreate = 10; $config->story->affectedFixedNum = 7; $config->story->needReview = 1; $config->story->removeFields = 'objectTypeList,productList,executionList,execution'; +$config->story->feedbackSource = array('customer', 'user', 'market', 'service', 'operation', 'support', 'forum'); $config->story->batchClose = new stdclass(); $config->story->batchClose->columns = 10; @@ -165,6 +166,16 @@ $config->story->datatable->fieldList['lastEditedDate']['fixed'] = 'no'; $config->story->datatable->fieldList['lastEditedDate']['width'] = '90'; $config->story->datatable->fieldList['lastEditedDate']['required'] = 'no'; +$config->story->datatable->fieldList['feedbackBy']['title'] = 'feedbackBy'; +$config->story->datatable->fieldList['feedbackBy']['fixed'] = 'no'; +$config->story->datatable->fieldList['feedbackBy']['width'] = '100'; +$config->story->datatable->fieldList['feedbackBy']['required'] = 'no'; + +$config->story->datatable->fieldList['notifyEmail']['title'] = 'notifyEmail'; +$config->story->datatable->fieldList['notifyEmail']['fixed'] = 'no'; +$config->story->datatable->fieldList['notifyEmail']['width'] = '100'; +$config->story->datatable->fieldList['notifyEmail']['required'] = 'no'; + $config->story->datatable->fieldList['mailto']['title'] = 'mailto'; $config->story->datatable->fieldList['mailto']['fixed'] = 'no'; $config->story->datatable->fieldList['mailto']['width'] = '100'; diff --git a/module/story/control.php b/module/story/control.php index 5c3bc7350e..5b47f55529 100644 --- a/module/story/control.php +++ b/module/story/control.php @@ -136,8 +136,15 @@ class story extends control if($objectID != 0) { - if($objectID != $this->session->project) $this->loadModel('action')->create('story', $storyID, 'linked2execution', '', $objectID); - if($this->config->systemMode == 'new') $this->loadModel('action')->create('story', $storyID, 'linked2project', '', $this->session->project); + $object = $this->dao->findById((int)$objectID)->from(TABLE_PROJECT)->fetch(); + if($object->type != 'project') + { + $this->loadModel('action')->create('story', $storyID, 'linked2execution', '', $objectID); + } + else + { + $this->loadModel('action')->create('story', $storyID, 'linked2project', '', $objectID); + } } if($todoID > 0) @@ -296,6 +303,10 @@ class story extends control ->fetch('id'); } + /* Get reviewers. */ + $reviewers = $product->reviewer; + if(!$reviewers) $reviewers = $this->loadModel('user')->getProductViewListUsers($product, '', '', ''); + /* Set Custom. */ foreach(explode(',', $this->config->story->list->customCreateFields) as $field) $customFields[$field] = $this->lang->story->$field; $this->view->customFields = $customFields; @@ -310,7 +321,7 @@ class story extends control $this->view->users = $users; $this->view->moduleID = $moduleID ? $moduleID : (int)$this->cookie->lastStoryModule; $this->view->moduleOptionMenu = $moduleOptionMenu; - $this->view->plans = $this->loadModel('productplan')->getPairsForStory($productID, $branch, true); + $this->view->plans = $this->loadModel('productplan')->getPairsForStory($productID, $branch, 'skipParent|unexpired'); $this->view->planID = $planID; $this->view->source = $source; $this->view->sourceNote = $sourceNote; @@ -320,6 +331,7 @@ class story extends control $this->view->branches = $branches; $this->view->productID = $productID; $this->view->product = $product; + $this->view->reviewers = $this->user->getPairs('noclosed|nodeleted', '', 0, $reviewers); $this->view->objectID = $objectID; $this->view->estimate = $estimate; $this->view->storyTitle = $title; @@ -436,13 +448,29 @@ class story extends control } } - /* Set products and module. */ + /* Set branch and module. */ $product = $this->product->getById($productID); $products = $this->product->getPairs(); - $moduleOptionMenu = $this->tree->getOptionMenu($productID, $viewType = 'story', 0, $branch === 'all' ? 0 : $branch); - if($product) $this->lang->product->branch = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]); + if($executionID != 0) + { + $productBranches = $product->type != 'normal' ? $this->loadModel('execution')->getBranchByProduct($productID, $executionID) : array(); + $branches = isset($productBranches[$productID]) ? $productBranches[$productID] : array(); + $branch = key($branches); + } + else + { + $branches = $product->type != 'normal' ? $this->loadModel('branch')->getPairs($productID, 'active') : array(); + } + + $moduleOptionMenu = $this->tree->getOptionMenu($productID, $viewType = 'story', 0, $branch === 'all' ? 0 : $branch); + $moduleOptionMenu['ditto'] = $this->lang->story->ditto; + + /* Get reviewers. */ + $reviewers = $product->reviewer; + if(!$reviewers) $reviewers = $this->loadModel('user')->getProductViewListUsers($product, '', '', ''); + /* Init vars. */ $planID = $plan; $pri = 0; @@ -462,9 +490,7 @@ class story extends control $this->view->titles = $titles; } - $moduleOptionMenu['ditto'] = $this->lang->story->ditto; - - $plans = $this->loadModel('productplan')->getPairsForStory($productID, $branch === 'all' ? 0 : $branch, true); + $plans = $this->loadModel('productplan')->getPairsForStory($productID, $branch === 'all' ? 0 : $branch, 'skipParent|unexpired'); $plans['ditto'] = $this->lang->story->ditto; $priList = (array)$this->lang->story->priList; @@ -477,6 +503,7 @@ class story extends control foreach(explode(',', $this->config->story->list->customBatchCreateFields) as $field) { if($product->type != 'normal') $customFields[$product->type] = $this->lang->product->branchName[$product->type]; + if(isonlybody() and $field == 'plan') continue; $customFields[$field] = $this->lang->story->$field; } @@ -501,16 +528,6 @@ class story extends control $showFields = str_replace('plan', '', $showFields); } - if($executionID != 0) - { - $productBranches = $product->type != 'normal' ? $this->loadModel('execution')->getBranchByProduct($productID, $executionID) : array(); - $branches = isset($productBranches[$productID]) ? $productBranches[$productID] : array(); - } - else - { - $branches = $product->type != 'normal' ? $this->loadModel('branch')->getPairs($productID, 'active') : array(); - } - $this->view->customFields = $customFields; $this->view->showFields = $showFields; @@ -525,6 +542,7 @@ class story extends control $this->view->moduleID = $moduleID; $this->view->moduleOptionMenu = $moduleOptionMenu; $this->view->plans = $plans; + $this->view->reviewers = $this->user->getPairs('noclosed|nodeleted', '', 0, $reviewers); $this->view->users = $this->user->getPairs('pdfirst|noclosed|nodeleted'); $this->view->priList = $priList; $this->view->sourceList = $sourceList; @@ -611,6 +629,7 @@ class story extends control $this->executeHooks($storyID); + if(isonlybody()) die(js::reload('parent.parent')); if(defined('RUN_MODE') && RUN_MODE == 'api') return $this->send(array('status' => 'success', 'data' => $storyID)); die(js::locate($this->createLink($this->app->rawModule, 'view', "storyID=$storyID"), 'parent')); } @@ -628,6 +647,7 @@ class story extends control if($product->status == 'normal' and !($product->PO == $this->app->user->account)) $othersProducts[$product->id] = $product->name; if($product->status == 'closed') continue; } + $products = $myProducts + $othersProducts; /* Assign. */ $story = $this->story->getById($storyID, 0, true); @@ -649,15 +669,22 @@ class story extends control if($this->app->tab == 'project' or $this->app->tab == 'execution') { - $objectID = $this->app->tab == 'project' ? $this->session->project : $this->session->execution; + $objectID = $this->app->tab == 'project' ? $this->session->project : $this->session->execution; $productBranches = $product->type != 'normal' ? $this->loadModel('execution')->getBranchByProduct($story->product, $objectID) : array(); - $branches = isset($productBranches[$story->product]) ? $productBranches[$story->product] : array(); + $branches = isset($productBranches[$story->product]) ? array(BRANCH_MAIN => $this->lang->branch->main) + $productBranches[$story->product] : array(); + $products = $this->product->getProductPairsByProject($objectID); + + $this->view->objectID = $objectID; } else { $branches = $product->type != 'normal' ? $this->loadModel('branch')->getPairs($product->id, 'active') : array(); } + /* Get product reviewers. */ + $productReviewers = $product->reviewer; + if(!$productReviewers) $productReviewers = $this->loadModel('user')->getProductViewListUsers($product, '', '', ''); + $this->story->replaceURLang($story->type); $this->view->title = $this->lang->story->edit . "STORY" . $this->lang->colon . $this->view->story->title; @@ -666,11 +693,12 @@ class story extends control $this->view->stories = $stories; $this->view->users = $users; $this->view->product = $product; - $this->view->plans = $this->loadModel('productplan')->getPairsForStory($story->product, $story->branch, true); - $this->view->products = $myProducts + $othersProducts; + $this->view->plans = $this->loadModel('productplan')->getPairsForStory($story->product, $story->branch, 'skipParent'); + $this->view->products = $products; $this->view->branches = $branches; $this->view->reviewers = implode(',', $reviewerList); $this->view->reviewedReviewer = $reviewedReviewer; + $this->view->productReviewers = $this->user->getPairs('noclosed|nodeleted', $reviewerList, 0, $productReviewers); $this->view->isShowReviewer = $isShowReviewer; $this->display(); } @@ -709,7 +737,7 @@ class story extends control } else if($this->app->tab == 'my') { - $this->loadModel('my')->setMenu(); + $this->loadModel('my'); if($from == 'work') $this->lang->my->menu->work['subModule'] = 'story'; if($from == 'contribute') $this->lang->my->menu->contribute['subModule'] = 'story'; } @@ -749,7 +777,7 @@ class story extends control $product = $this->product->getByID($productID); $branchProduct = $product->type == 'normal' ? false : true; $modules = array($productID => $this->tree->getOptionMenu($productID, 'story', 0, array_keys($branches))); - $plans = array($productID => $this->productplan->getBranchPlanPairs($productID)); + $plans = array($productID => $this->productplan->getBranchPlanPairs($productID, '', true)); $products = array($productID => $product); $branches = array($productID => $branches); } @@ -763,9 +791,9 @@ class story extends control foreach($linkedProducts as $linkedProduct) { $branchList = $this->branch->getPairs($linkedProduct->id, '', $executionID); - $branches[$linkedProduct->id] = $branchList; + $branches[$linkedProduct->id] = array(BRANCH_MAIN => $this->lang->branch->main) + $branchList; $modules[$linkedProduct->id] = $this->tree->getOptionMenu($linkedProduct->id, 'story', 0, array_keys($branchList)); - $plans[$linkedProduct->id] = $this->productplan->getBranchPlanPairs($linkedProduct->id, array_keys($branchList)); + $plans[$linkedProduct->id] = $this->productplan->getBranchPlanPairs($linkedProduct->id, array_keys($branchList), true); if(empty($plans[$linkedProduct->id])) $plans[$linkedProduct->id][0] = $plans[$linkedProduct->id]; if($linkedProduct->type != 'normal') $branchProduct = true; @@ -795,7 +823,7 @@ class story extends control $modules[$storyProduct->id] = $this->tree->getOptionMenu($storyProduct->id, 'story', 0, $branchIdList); if($storyProduct->type == 'normal') $modules[$storyProduct->id][0] = $modules[$storyProduct->id]; - $plans[$storyProduct->id] = $this->productplan->getBranchPlanPairs($storyProduct->id, array_keys($branchList)); + $plans[$storyProduct->id] = $this->productplan->getBranchPlanPairs($storyProduct->id, array_keys($branchList), true); if(empty($plans[$storyProduct->id])) $plans[$storyProduct->id][0] = $plans[$storyProduct->id]; if($storyProduct->type != 'normal') $branchProduct = true; @@ -881,6 +909,7 @@ class story extends control $module = $this->app->tab == 'project' ? 'projectstory' : 'story'; + if(isonlybody()) die(js::reload('parent.parent')); if(defined('RUN_MODE') && RUN_MODE == 'api') return $this->send(array('status' => 'success')); die(js::locate($this->createLink($module, 'view', "storyID=$storyID"), 'parent')); } @@ -894,13 +923,19 @@ class story extends control $story = $this->story->getById($storyID); $reviewer = $this->story->getReviewerPairs($storyID, $story->version); + $product = $this->loadModel('product')->getByID($story->product); + + /* Get product reviewers. */ + $productReviewers = $product->reviewer; + if(!$productReviewers) $productReviewers = $this->loadModel('user')->getProductViewListUsers($product, '', '', ''); /* Assign. */ - $this->view->title = $this->lang->story->change . "STORY" . $this->lang->colon . $this->view->story->title; - $this->view->users = $this->user->getPairs('pofirst|nodeleted|noclosed', $this->view->story->assignedTo); - $this->view->position[] = $this->lang->story->change; - $this->view->needReview = ($this->app->user->account == $this->view->product->PO || $this->config->story->needReview == 0) ? "checked='checked'" : ""; - $this->view->reviewer = implode(',', array_keys($reviewer)); + $this->view->title = $this->lang->story->change . "STORY" . $this->lang->colon . $this->view->story->title; + $this->view->users = $this->user->getPairs('pofirst|nodeleted|noclosed', $this->view->story->assignedTo); + $this->view->position[] = $this->lang->story->change; + $this->view->needReview = ($this->app->user->account == $this->view->product->PO || $this->config->story->needReview == 0) ? "checked='checked'" : ""; + $this->view->reviewer = implode(',', array_keys($reviewer)); + $this->view->productReviewers = $this->user->getPairs('noclosed|nodeleted', $reviewer, 0, $productReviewers); $this->display(); } @@ -1450,7 +1485,7 @@ class story extends control { foreach($plans[$storyID] as $plan) { - if($plan->branch != BRANCH_MAIN and $plan->branch != $branchID) + if($plan->branch != $branchID) { $conflictStoryIdList .= '[' . $storyID . ']'; $conflictStoryArray[] = $storyID; @@ -1893,6 +1928,8 @@ class story extends control $stories = $this->story->getProductStoryPairs($productID, $branch, $moduleID, $storyStatus, 'id_desc', $limit, $type, 'story', $hasParent); } + if(empty($stories)) $stories = $this->story->getProductStoryPairs($productID, $branch, 0, $storyStatus, 'id_desc', $limit, $type, 'story', $hasParent); + $storyID = isset($stories[$storyID]) ? $storyID : 0; $select = html::select('story' . $number, empty($stories) ? array('' => '') : $stories, $storyID, "class='form-control'"); diff --git a/module/story/css/batchcreate.css b/module/story/css/batchcreate.css index 8ac7a5c628..4e5dc17db3 100644 --- a/module/story/css/batchcreate.css +++ b/module/story/css/batchcreate.css @@ -1,3 +1,5 @@ +#importLinesModal .modal-dialog {width: 80%;} + #batchCreateForm .c-id {width: 50px; text-align: center; color: #838A9D;} #batchCreateForm .c-module, #batchCreateForm .c-plan {width: 180px;} diff --git a/module/story/css/create.css b/module/story/css/create.css index b059c5ef9f..8cf063f7ef 100644 --- a/module/story/css/create.css +++ b/module/story/css/create.css @@ -3,3 +3,5 @@ .pri-selector > .btn {padding: 5px 8px !important; width: 100%;} .pri-selector > .dropdown-menu {padding: 10px;} + +#source_chosen {min-width: 50px !important;} diff --git a/module/story/css/x.view.css b/module/story/css/x.view.css index 7714760a97..60a94c41f8 100644 --- a/module/story/css/x.view.css +++ b/module/story/css/x.view.css @@ -2,7 +2,16 @@ #mainMenu .pull-left .divider {display: none;} #mainMenu .pull-right {display: none;} #mainContent .col-4 {display: none;} -#main {margin-bottom: 40px;} +#main {margin-bottom: 40px; min-width: unset;} .main-actions {display: none;} .col-8>.cell {min-height: 300px;} .modal-dialog {width: 90%;} +#mainMenu {position: fixed; width: 100%; z-index: 999;} +#scrollContent {margin-top: 35px; height: calc(100% - 70px); display: block; overflow: hidden;} +#scrollContent:hover{overflow: overlay;} +html, body, #main, .container { height: 100%;} +body * {font-size: 13px !important; line-height: 1.42857143; color: rgb(51, 51, 51);} +.page-title > .label-id {min-width: 25px; line-height: 13px;} +::-webkit-scrollbar {width: 10px; height: 10px;} +::-webkit-scrollbar-thumb:vertical {box-shadow: inset 1px 1px 0 rgb(0 0 0 / 10%), inset 0 -1px 0 rgb(0 0 0 / 7%); background-color: rgba(0, 0, 0, 0.2); border-radius: 10px; opacity: 0; transition: opacity 0.1s;} +.btn span{line-height: 16px; vertical-align: middle;} \ No newline at end of file diff --git a/module/story/js/batchcreate.js b/module/story/js/batchcreate.js index 98da6e91d8..9c513a278a 100644 --- a/module/story/js/batchcreate.js +++ b/module/story/js/batchcreate.js @@ -58,7 +58,7 @@ function setModuleAndPlan(branchID, productID, num) $("#module" + num).chosen(); }); - planLink = createLink('productPlan', 'ajaxGetProductPlans', 'productID=' + productID + '&branch=' + branchID + '&num=' + num + "&from=story"); + planLink = createLink('productPlan', 'ajaxGetProductPlans', 'productID=' + productID + '&branch=' + branchID + '&num=' + num + '&expired=unexpired'); $.get(planLink, function(plans) { if(!plans) plans = ''; diff --git a/module/story/js/batchedit.js b/module/story/js/batchedit.js index 835f2de2da..fd823b61f7 100644 --- a/module/story/js/batchedit.js +++ b/module/story/js/batchedit.js @@ -34,7 +34,7 @@ function loadBranches(product, branch, storyID) $('#modules' + storyID).parent('td').load(moduleLink, function(){$('#modules' + storyID).chosen();}); planID = $('#plans' + storyID).val(); - planLink = createLink('product', 'ajaxGetPlans', 'productID=' + product + '&branch=' + branch + '&planID=' + planID + '&fieldID=' + storyID + '&needCreate=false&expired=&from=story¶m=batchEdit'); + planLink = createLink('product', 'ajaxGetPlans', 'productID=' + product + '&branch=' + branch + '&planID=' + planID + '&fieldID=' + storyID + '&needCreate=false&expired=¶m=skipParent'); $('#plans' + storyID).parent('td').load(planLink, function(){$('#plans' + storyID).chosen();}); } diff --git a/module/story/js/create.js b/module/story/js/create.js index 473c38bd5f..26b48b29d0 100644 --- a/module/story/js/create.js +++ b/module/story/js/create.js @@ -24,6 +24,23 @@ $(function() var value = $select.val(); $selector.find('.pri-text').html('' + value + ''); }); + + $('#source').on('change', function() + { + if(storyType == 'requirement') return false; + + var source = $(this).val(); + if($.inArray(source, feedbackSource) != -1) + { + $('#feedbackBox').removeClass('hidden'); + $('#reviewerBox').attr('colspan', 2); + } + else + { + $('#feedbackBox').addClass('hidden'); + $('#reviewerBox').attr('colspan', 4); + } + }); }); function refreshPlan() diff --git a/module/story/js/edit.js b/module/story/js/edit.js index f124856088..f234363a3e 100644 --- a/module/story/js/edit.js +++ b/module/story/js/edit.js @@ -19,4 +19,17 @@ $(function() reviewers = $('#reviewer').val(); } }) + + $('#source').on('change', function() + { + var source = $(this).val(); + if($.inArray(source, feedbackSource) != -1) + { + $('.feedbackBox').removeClass('hidden'); + } + else + { + $('.feedbackBox').addClass('hidden'); + } + }); }) diff --git a/module/story/js/x.view.js b/module/story/js/x.view.js index 4bc8e30157..df0847aed9 100644 --- a/module/story/js/x.view.js +++ b/module/story/js/x.view.js @@ -29,7 +29,14 @@ $(function() xuanAction += "' + action + ""; }); - xuanAction += '
    '; - $('body').append(xuanAction); + if(xuanAction != "
    ") + { + xuanAction += '
    '; + $('body').append(xuanAction); + } + else + { + $('#scrollContent').css('height', 'calc(100% - 36px)'); + } $('.xuancard-actions a.iframe').modalTrigger(); }) diff --git a/module/story/lang/en.php b/module/story/lang/en.php index 75cfdb335c..d5da3bf802 100644 --- a/module/story/lang/en.php +++ b/module/story/lang/en.php @@ -106,7 +106,7 @@ $lang->story->spec = 'Description'; $lang->story->assign = 'Assign'; $lang->story->verify = 'Acceptance'; $lang->story->pri = 'Priority'; -$lang->story->estimate = "Estimates {$lang->hourCommon}"; +$lang->story->estimate = "Estimates"; $lang->story->estimateAB = 'Est.'; $lang->story->hour = $lang->hourCommon; $lang->story->status = 'Status'; @@ -129,13 +129,15 @@ $lang->story->reviewedBy = 'ReviewedBy'; $lang->story->reviewers = 'Reviewers'; $lang->story->reviewedDate = 'ReviewedDate'; $lang->story->version = 'Version'; +$lang->story->feedbackBy = 'From Name'; +$lang->story->notifyEmail = 'From Email'; $lang->story->plan = 'Linked Plan'; $lang->story->planAB = 'Plan'; $lang->story->comment = 'Comment'; $lang->story->children = "Child {$lang->SRCommon}"; $lang->story->childrenAB = "C"; -$lang->story->linkStories = 'Linked Stories'; -$lang->story->childStories = 'Decomposed Stories'; +$lang->story->linkStories = 'Linked Story'; +$lang->story->childStories = 'Decomposed Story'; $lang->story->duplicateStory = 'Duplicated Story ID'; $lang->story->reviewResult = 'Review Result'; $lang->story->preVersion = 'Last Version'; @@ -151,13 +153,13 @@ $lang->story->unclosed = 'Unclosed'; $lang->story->deleted = 'Deleted'; $lang->story->released = 'Released Stories'; $lang->story->URChanged = 'Requirement Changed'; -$lang->story->design = 'Designs'; +$lang->story->design = 'Design'; $lang->story->case = 'Cases'; $lang->story->bug = 'Bugs'; $lang->story->repoCommit = 'Commits'; $lang->story->noRequirement = 'No Requirements'; $lang->story->one = 'One'; -$lang->story->field = 'Synchronized fields'; +$lang->story->field = 'Sync Field'; $lang->story->completeRate = 'Completion Rate'; $lang->story->reviewed = 'Reviewed'; $lang->story->toBeReviewed = 'To Be Reviewed'; @@ -166,7 +168,7 @@ $lang->story->ditto = 'Ditto'; $lang->story->dittoNotice = 'This story is not linked to the same product as the last one is!'; $lang->story->needNotReviewList[0] = 'Need Review'; -$lang->story->needNotReviewList[1] = 'Need Not Review'; +$lang->story->needNotReviewList[1] = 'No Review'; $lang->story->useList[0] = 'Yes'; $lang->story->useList[1] = 'No'; @@ -261,7 +263,7 @@ $lang->story->affectedBugs = 'Bugs'; $lang->story->affectedCases = 'Cases'; $lang->story->specTemplate = "As a < type of user >, I want < some goal > so that < some reason >."; -$lang->story->needNotReview = 'No Review Required'; +$lang->story->needNotReview = 'No Review'; $lang->story->successSaved = "Story is saved!"; $lang->story->confirmDelete = "Do you want to delete this story?"; $lang->story->errorEmptyChildStory = '『Decomposed Stories』canot be blank.'; diff --git a/module/story/lang/zh-cn.php b/module/story/lang/zh-cn.php index 6c485b6e94..e0516d9098 100644 --- a/module/story/lang/zh-cn.php +++ b/module/story/lang/zh-cn.php @@ -129,10 +129,12 @@ $lang->story->reviewedBy = '由谁评审'; $lang->story->reviewers = '评审人员'; $lang->story->reviewedDate = '评审时间'; $lang->story->version = '版本号'; +$lang->story->feedbackBy = '反馈者'; +$lang->story->notifyEmail = '通知邮箱'; $lang->story->plan = "所属计划"; $lang->story->planAB = '计划'; $lang->story->comment = '备注'; -$lang->story->children = "子{$lang->SRCommon}"; +$lang->story->children = "子需求"; $lang->story->childrenAB = "子"; $lang->story->linkStories = "相关{$lang->SRCommon}"; $lang->story->childStories = "细分{$lang->SRCommon}"; @@ -235,7 +237,7 @@ $lang->story->changeList['no'] = '不变更'; $lang->story->changeList['yes'] = '变更'; $lang->story->legendBasicInfo = '基本信息'; -$lang->story->legendLifeTime = "{$lang->SRCommon}的一生"; +$lang->story->legendLifeTime = "需求的一生"; $lang->story->legendRelated = '相关信息'; $lang->story->legendMailto = '抄送给'; $lang->story->legendAttatch = '附件'; @@ -245,9 +247,10 @@ $lang->story->legendFromBug = '来源Bug'; $lang->story->legendCases = '相关用例'; $lang->story->legendLinkStories = "相关{$lang->SRCommon}"; $lang->story->legendChildStories = "细分{$lang->SRCommon}"; -$lang->story->legendSpec = "{$lang->SRCommon}描述"; +$lang->story->legendSpec = "需求描述"; $lang->story->legendVerify = '验收标准'; $lang->story->legendMisc = '其他相关'; +$lang->story->legendInformation = '需求信息'; $lang->story->lblChange = "变更{$lang->SRCommon}"; $lang->story->lblReview = "评审{$lang->SRCommon}"; diff --git a/module/story/model.php b/module/story/model.php index c81aeb462c..b178fa8cb2 100644 --- a/module/story/model.php +++ b/module/story/model.php @@ -207,6 +207,8 @@ class storyModel extends model ->setIF($this->post->needNotReview, 'status', 'active') ->setIF($this->post->plan > 0, 'stage', 'planned') ->setIF($this->post->estimate, 'estimate', (float)$this->post->estimate) + ->setIF(!in_array($this->post->source, $this->config->story->feedbackSource), 'feedbackBy', '') + ->setIF(!in_array($this->post->source, $this->config->story->feedbackSource), 'notifyEmail', '') ->setIF($executionID > 0, 'stage', 'projected') ->setIF($bugID > 0, 'fromBug', $bugID) ->join('mailto', ',') @@ -233,7 +235,11 @@ class storyModel extends model $requiredFields = trim($requiredFields, ','); - $this->dao->insert(TABLE_STORY)->data($story, 'spec,verify')->autoCheck()->batchCheck($requiredFields, 'notempty')->exec(); + $this->dao->insert(TABLE_STORY)->data($story, 'spec,verify') + ->autoCheck() + ->checkIF($story->notifyEmail, 'notifyEmail', 'email') + ->batchCheck($requiredFields, 'notempty') + ->exec(); if(!dao::isError()) { $storyID = $this->dao->lastInsertID(); @@ -274,6 +280,7 @@ class storyModel extends model { $this->linkStory($executionID, $this->post->product, $storyID); if($this->config->systemMode == 'new' and $executionID != $this->session->project) $this->linkStory($this->session->project, $this->post->product, $storyID); + $this->loadModel('kanban')->updateLane($executionID, 'story'); } if(is_array($this->post->URS)) @@ -402,16 +409,13 @@ class storyModel extends model public function batchCreate($productID = 0, $branch = 0, $type = 'story') { $forceReview = $this->checkForceReview(); - foreach($_POST['needReview'] as $index => $value) + foreach($_POST['title'] as $index => $value) { if($_POST['title'][$index] and isset($_POST['reviewer'][$index])) $_POST['reviewer'][$index] = array_filter($_POST['reviewer'][$index]); - if($_POST['title'][$index] and empty($_POST['reviewer'][$index])) + if($_POST['title'][$index] and empty($_POST['reviewer'][$index]) and $forceReview) { - if($value || $forceReview) - { - dao::$errors[] = $this->lang->story->errorEmptyReviewedBy; - return false; - } + dao::$errors[] = $this->lang->story->errorEmptyReviewedBy; + return false; } } @@ -460,7 +464,7 @@ class storyModel extends model $story->category = $stories->category[$i]; $story->pri = $stories->pri[$i]; $story->estimate = $stories->estimate[$i]; - $story->status = ($stories->needReview[$i] == 0 and !$forceReview) ? 'active' : 'draft'; + $story->status = (empty($stories->reviewer[$i]) and !$forceReview) ? 'active' : 'draft'; $story->stage = ($this->app->tab == 'project' or $this->app->tab == 'execution') ? 'projected' : 'wait'; $story->keywords = $stories->keywords[$i]; $story->sourceNote = $stories->sourceNote[$i]; @@ -747,6 +751,8 @@ class storyModel extends model ->setIF($this->post->closedReason != false and $oldStory->closedDate == '', 'closedDate', $now) ->setIF($this->post->closedBy != false or $this->post->closedReason != false, 'status', 'closed') ->setIF($this->post->closedReason != false and $this->post->closedBy == false, 'closedBy', $this->app->user->account) + ->setIF(!in_array($this->post->source, $this->config->story->feedbackSource), 'feedbackBy', '') + ->setIF(!in_array($this->post->source, $this->config->story->feedbackSource), 'notifyEmail', '') ->setIF(!empty($_POST['plan'][0]) and $oldStory->stage == 'wait', 'stage', 'planned') ->stripTags($this->config->story->editor->edit['id'], $this->config->allowedTags) ->join('reviewedBy', ',') @@ -823,6 +829,7 @@ class storyModel extends model ->checkIF(isset($story->closedBy), 'closedReason', 'notempty') ->checkIF(isset($story->closedReason) and $story->closedReason == 'done', 'stage', 'notempty') ->checkIF(isset($story->closedReason) and $story->closedReason == 'duplicate', 'duplicateStory', 'notempty') + ->checkIF($story->notifyEmail, 'notifyEmail', 'email') ->where('id')->eq((int)$storyID)->exec(); if(!dao::isError()) @@ -1682,9 +1689,17 @@ class storyModel extends model */ public function batchChangeBranch($storyIdList, $branchID, $confirm = '', $plans = array()) { - $now = helper::now(); - $allChanges = array(); - $oldStories = $this->getByList($storyIdList); + $now = helper::now(); + $allChanges = array(); + $oldStories = $this->getByList($storyIdList); + $story = current($oldStories); + $productID = $story->product; + $mainModules = $this->dao->select('id')->from(TABLE_MODULE) + ->where('root')->eq($productID) + ->andWhere('branch')->eq(0) + ->andWhere('type')->eq('story') + ->fetchPairs('id'); + foreach($storyIdList as $storyID) { $oldStory = $oldStories[$storyID]; @@ -1693,6 +1708,7 @@ class storyModel extends model $story->lastEditedBy = $this->app->user->account; $story->lastEditedDate = $now; $story->branch = $branchID; + $story->module = ($oldStory->branch != $branchID and !in_array($oldStory->module, $mainModules)) ? 0 : $oldStory->module; $this->dao->update(TABLE_STORY)->data($story)->autoCheck()->where('id')->eq((int)$storyID)->exec(); if(!dao::isError()) @@ -1701,12 +1717,13 @@ class storyModel extends model { $planIdList = ''; $conflictPlanIdList = ''; + /* Determine whether there is a conflict between the branch of the story and the linked plan. */ if($oldStory->branch != $branchID and $branchID != BRANCH_MAIN and isset($plans[$storyID])) { foreach($plans[$storyID] as $planID => $plan) { - if($plan->branch != BRANCH_MAIN and $plan->branch != $branchID) + if($plan->branch != $branchID) { $conflictPlanIdList .= $planID . ','; } @@ -1844,6 +1861,8 @@ class storyModel extends model $tasks[] = $taskID; $this->action->create('task', $taskID, 'Opened', ''); } + + $this->loadModel('kanban')->updateLane($executionID, 'task'); return $tasks; } @@ -2443,13 +2462,21 @@ class storyModel extends model $allBranch = "`branch` = 'all'"; if($executionID != '') { - $branches = array(); - foreach($products as $product) + $branches = array(BRANCH_MAIN => BRANCH_MAIN); + if($branch === '') { - foreach($product->branches as $branchID) $branches[$branchID] = $branchID; + foreach($products as $product) + { + foreach($product->branches as $branchID) $branches[$branchID] = $branchID; + } } + else + { + $branches[$branch] = $branch; + } + $branches = join(',', $branches); - if($branches) $storyQuery .= " AND `branch`" . helper::dbIN($branches); + $storyQuery .= " AND `branch`" . helper::dbIN($branches); if($this->app->moduleName == 'release' or $this->app->moduleName == 'build') { @@ -2466,7 +2493,7 @@ class storyModel extends model } elseif($branch) { - if($branch and strpos($storyQuery, '`branch` =') === false) $storyQuery .= " AND `branch` in('$branch')"; + if($branch and strpos($storyQuery, '`branch` =') === false) $storyQuery .= " AND `branch` in($branch)"; } $storyQuery = preg_replace("/`plan` +LIKE +'%([0-9]+)%'/i", "CONCAT(',', `plan`, ',') LIKE '%,$1,%'", $storyQuery); @@ -2590,9 +2617,15 @@ class storyModel extends model else { $productParam = ($type == 'byproduct' and $param) ? $param : $this->cookie->storyProductParam; - $branchParam = $branchID = ($type == 'bybranch' and $param !== '') ? $param : $this->cookie->storyBranchParam; - $moduleParam = ($type == 'bymodule' and $param) ? $param : $this->cookie->storyModuleParam; - $modules = (empty($moduleParam) or $type != 'bymodule') ? array() : $this->dao->select('*')->from(TABLE_MODULE)->where('path')->like("%,$moduleParam,%")->andWhere('type')->eq('story')->andWhere('deleted')->eq(0)->fetchPairs('id', 'id'); + $branchParam = ($type == 'bybranch' and $param !== '') ? $param : $this->cookie->storyBranchParam; + $moduleParam = ($type == 'bymodule' and $param !== '') ? $param : $this->cookie->storyModuleParam; + + $modules = array(); + if(!empty($moduleParam) or strpos('allstory,unclosed,bymodule', $type) !== false) + { + $modules = $this->dao->select('id')->from(TABLE_MODULE)->where('path')->like("%,$moduleParam,%")->andWhere('type')->eq('story')->andWhere('deleted')->eq(0)->fetchPairs(); + } + if(strpos($branchParam, ',') !== false) list($productParam, $branchParam) = explode(',', $branchParam); $unclosedStatus = $this->lang->story->statusList; @@ -2617,7 +2650,7 @@ class storyModel extends model ->beginIF($excludeStories)->andWhere('t2.id')->notIN($excludeStories)->fi() ->beginIF($execution->type == 'project') ->beginIF(!empty($productID))->andWhere('t1.product')->eq($productID) - ->beginIF($type == 'bybranch' and $branchParam !== '')->andWhere('t2.branch')->eq($branchParam)->fi() + ->beginIF($type == 'bybranch' and $branchParam !== '')->andWhere('t2.branch')->in("0,$branchParam")->fi() ->beginIF(strpos('changed|closed', $type) !== false)->andWhere('t2.status')->eq($type)->fi() ->beginIF($type == 'unclosed')->andWhere('t2.status')->in(array_keys($unclosedStatus))->fi() ->beginIF($type == 'linkedexecution')->andWhere('t2.id')->in($storyIdList)->fi() @@ -2628,7 +2661,6 @@ class storyModel extends model ->beginIF($this->session->executionStoryBrowseType and strpos('changed|', $this->session->executionStoryBrowseType) !== false)->andWhere('t2.status')->in(array_keys($unclosedStatus))->fi() ->fi() ->beginIF($this->session->storyBrowseType and strpos('changed|', $this->session->storyBrowseType) !== false)->andWhere('t2.status')->in(array_keys($unclosedStatus))->fi() - ->beginIF(!empty($branchParam))->andWhere('t2.branch')->eq($branchParam)->fi() ->beginIF($modules)->andWhere('t2.module')->in($modules)->fi() ->andWhere('t2.deleted')->eq(0) ->orderBy($orderBy) @@ -3426,7 +3458,7 @@ class storyModel extends model { $action = strtolower($action); - if($story->parent < 0 and $action != 'edit' and $action != 'batchcreate') return false; + if($story->parent < 0 and $action != 'edit' and $action != 'batchcreate' and $action != 'change') return false; global $app; @@ -3692,6 +3724,14 @@ class storyModel extends model } } } + else if($id == 'feedbackBy') + { + $title = $story->feedbackBy; + } + else if($id == 'notifyEmail') + { + $title = $story->notifyEmail; + } else if($id == 'actions') { $class .= ' text-center'; @@ -3717,7 +3757,14 @@ class storyModel extends model echo ""; break; case 'title': - $showBranch = isset($this->config->product->browse->showBranch) ? $this->config->product->browse->showBranch : 1; + if($this->app->tab == 'project') + { + $showBranch = isset($this->config->projectstory->story->showBranch) ? $this->config->projectstory->story->showBranch : 1; + } + else + { + $showBranch = isset($this->config->product->browse->showBranch) ? $this->config->product->browse->showBranch : 1; + } if($storyType == 'requirement') echo 'SR '; if($story->parent > 0 and isset($story->parentName)) $story->title = "{$story->parentName} / {$story->title}"; if(isset($branches[$story->branch]) and $showBranch) echo "{$branches[$story->branch]} "; @@ -3821,6 +3868,12 @@ class storyModel extends model case 'lastEditedDate': echo substr($story->lastEditedDate, 5, 11); break; + case 'feedbackBy': + echo $story->feedbackBy; + break; + case 'notifyEmail': + echo $story->notifyEmail; + break; case 'mailto': $mailto = explode(',', $story->mailto); foreach($mailto as $account) diff --git a/module/story/view/batchcreate.html.php b/module/story/view/batchcreate.html.php index 314f410482..f7c1943dc5 100644 --- a/module/story/view/batchcreate.html.php +++ b/module/story/view/batchcreate.html.php @@ -43,10 +43,11 @@ - + + @@ -55,7 +56,6 @@ - - + + - - - + control == 'select' or $extendField->control == 'multi-select') ? " style='overflow:visible'" : '') . ">" . $this->loadModel('flow')->getFieldControl($extendField, '', $extendField->field . '[$id]') . "";?> diff --git a/module/story/view/batchedit.html.php b/module/story/view/batchedit.html.php index 7a3eab3f8c..dfda3626ff 100644 --- a/module/story/view/batchedit.html.php +++ b/module/story/view/batchedit.html.php @@ -72,29 +72,14 @@ foreach(explode(',', $showFields) as $field) diff --git a/module/story/view/change.html.php b/module/story/view/change.html.php index 9b52368be0..4135f51cc9 100644 --- a/module/story/view/change.html.php +++ b/module/story/view/change.html.php @@ -26,7 +26,7 @@ - - - - - + + + + + config->URAndSR):?> diff --git a/module/story/view/edit.html.php b/module/story/view/edit.html.php index 7fba2b462d..f00c2eba62 100644 --- a/module/story/view/edit.html.php +++ b/module/story/view/edit.html.php @@ -19,6 +19,7 @@ story->module);?> story->notice->reviewerNotEmpty);?> +story->feedbackSource); ?>
    @@ -182,6 +183,14 @@
    + '> + + + + '> + + + @@ -211,7 +220,7 @@ - + status == 'closed'):?> @@ -278,4 +287,5 @@ type);?> + diff --git a/module/story/view/header.html.php b/module/story/view/header.html.php index d0dea42867..ec1e52aed1 100644 --- a/module/story/view/header.html.php +++ b/module/story/view/header.html.php @@ -1,6 +1,13 @@ diff --git a/module/story/view/view.html.php b/module/story/view/view.html.php index bbccd60e90..69772a6eed 100644 --- a/module/story/view/view.html.php +++ b/module/story/view/view.html.php @@ -62,6 +62,9 @@ +app->getViewType() == 'xhtml'):?> +
    +
    @@ -181,7 +184,9 @@
    printExtendFields($story, 'div', "position=left&inForm=0&inCell=1");?> + app->getViewType() != 'xhtml'):?>
    +
    @@ -348,6 +353,16 @@
    + source, $config->story->feedbackSource)):?> + + + + + + + + + @@ -521,6 +536,9 @@ printExtendFields($story, 'div', "position=right&inForm=0&inCell=1");?> +app->getViewType() == 'xhtml'):?> + + + + diff --git a/module/testcase/view/browse.html.php b/module/testcase/view/browse.html.php index dbb69bfa9e..bb574ce338 100644 --- a/module/testcase/view/browse.html.php +++ b/module/testcase/view/browse.html.php @@ -42,7 +42,7 @@ js::set('suiteID', $suiteID);
    - lang->navGroup->testcase}", $lang->tree->manage, '', "class='btn btn-info btn-wide'");?> + app->tab}", $lang->tree->manage, '', "class='btn btn-info btn-wide' data-app='{$this->app->tab}'");?>
    diff --git a/module/testcase/view/importfromlib.html.php b/module/testcase/view/importfromlib.html.php index 189705a9db..df10fd4979 100644 --- a/module/testcase/view/importfromlib.html.php +++ b/module/testcase/view/importfromlib.html.php @@ -33,7 +33,7 @@ idAB);?> - + type != 'normal'):?> @@ -54,8 +54,8 @@ id);?> - - + type != 'normal'):?> + diff --git a/module/testcase/view/view.html.php b/module/testcase/view/view.html.php index b87f82e4bf..db1001db61 100644 --- a/module/testcase/view/view.html.php +++ b/module/testcase/view/view.html.php @@ -131,7 +131,7 @@ id", $case, 'button', '', '', 'showinonlybody'); if(!$isLibCase and $case->auto != 'unit') common::printIcon('testcase', 'create', "productID=$case->product&branch=$case->branch&moduleID=$case->module&from=testcase¶m=$case->id", $case, 'button', 'copy'); - if($isLibCase and common::hasPriv('caselib', 'createCase')) echo html::a($this->createLink('caselib', 'createCase', "libID=$case->lib&moduleID=$case->module¶m=$case->id"), "", '', "class='btn' title='{$lang->testcase->copy}'"); + if($isLibCase and common::hasPriv('caselib', 'createCase')) echo html::a($this->createLink('caselib', 'createCase', "libID=$case->lib&moduleID=$case->module¶m=$case->id", $case), "", '', "class='btn' title='{$lang->testcase->copy}'"); common::printIcon('testcase', 'delete', "caseID=$case->id", $case, 'button', 'trash', 'hiddenwin', ''); ?> diff --git a/module/testreport/control.php b/module/testreport/control.php index 541ca1c4c6..b5ce1cfbe5 100644 --- a/module/testreport/control.php +++ b/module/testreport/control.php @@ -189,7 +189,7 @@ class testreport extends control $taskPairs = array(); $scopeAndStatus[0] = 'local'; $scopeAndStatus[1] = 'totalStatus'; - $tasks = $this->testtask->getProductTasks($productID, 0, 'id_desc', null, $scopeAndStatus); + $tasks = $this->testtask->getProductTasks($productID, $task->branch, 'id_desc', null, $scopeAndStatus); foreach($tasks as $testTask) { if($testTask->build == 'trunk') continue; diff --git a/module/testreport/lang/de.php b/module/testreport/lang/de.php index e9bccff69c..62ccfcc2e9 100644 --- a/module/testreport/lang/de.php +++ b/module/testreport/lang/de.php @@ -83,7 +83,6 @@ $lang->testreport->noTestTask = "No test requests for this {$lang->productCo $lang->testreport->noObjectID = "No test request or {$lang->executionCommon} is selected, so no report can be generated."; $lang->testreport->moreProduct = "Ein Testbericht kann nur innerhalb des selben Produkts erstellt werden."; $lang->testreport->hiddenCase = "Hide %s use cases"; -$lang->testreport->goalTip = "Descriptive information about the {$lang->execution->common} of this build"; $lang->testreport->bugSummary = <<%s Bug(s) in Summe erstellt , diff --git a/module/testreport/lang/en.php b/module/testreport/lang/en.php index 3297084c43..14dcbc879e 100644 --- a/module/testreport/lang/en.php +++ b/module/testreport/lang/en.php @@ -84,7 +84,6 @@ $lang->testreport->noTestTask = "No test requests for this {$lang->productCo $lang->testreport->noObjectID = "No test request or {$lang->executionCommon} is selected, so no report can be generated."; $lang->testreport->moreProduct = "Testing reports can only be generated for the same {$lang->productCommon}."; $lang->testreport->hiddenCase = "Hide %s use cases"; -$lang->testreport->goalTip = "Descriptive information about the {$lang->execution->common} of this build"; $lang->testreport->bugSummary = <<%s Bugs reported , diff --git a/module/testreport/lang/fr.php b/module/testreport/lang/fr.php index 3ac5dde6d3..088f77044f 100644 --- a/module/testreport/lang/fr.php +++ b/module/testreport/lang/fr.php @@ -83,7 +83,6 @@ $lang->testreport->noTestTask = "Pas de campagne de test pour ce {$lang->pro $lang->testreport->noObjectID = "Pas de campagne de test ou un {$lang->executionCommon} est sélectionné, aucun rapport ne peut être généré."; $lang->testreport->moreProduct = "Les rapports de test ne peuvent être produits que pour le même {$lang->productCommon}."; $lang->testreport->hiddenCase = "Hide %s use cases"; -$lang->testreport->goalTip = "Descriptive information about the {$lang->execution->common} of this build"; $lang->testreport->bugSummary = <<%s Bugs signalés , diff --git a/module/testreport/lang/vi.php b/module/testreport/lang/vi.php index 89e1e6d773..f5f273558e 100644 --- a/module/testreport/lang/vi.php +++ b/module/testreport/lang/vi.php @@ -83,7 +83,6 @@ $lang->testreport->noTestTask = "Không có yêu cầu thử nghiệm {$lang $lang->testreport->noObjectID = "Không có yêu cầu test hoặc {$lang->executionCommon} được chọn, bởi vậy không có báo cáo có thể được tạo."; $lang->testreport->moreProduct = "Báo cáo Test chỉ có thể được tạo cho cùng {$lang->productCommon}."; $lang->testreport->hiddenCase = "Hide %s use cases"; -$lang->testreport->goalTip = "Descriptive information about the {$lang->execution->common} of this build"; $lang->testreport->bugSummary = <<%s Bugs reported , diff --git a/module/testreport/lang/zh-cn.php b/module/testreport/lang/zh-cn.php index 6d94df07b5..6e609e0107 100644 --- a/module/testreport/lang/zh-cn.php +++ b/module/testreport/lang/zh-cn.php @@ -84,7 +84,6 @@ $lang->testreport->noTestTask = "该{$lang->productCommon}下还没有关联 $lang->testreport->noObjectID = "没有选定测试单或{$lang->executionCommon},无法创建测试报告!"; $lang->testreport->moreProduct = "只能对同一个{$lang->productCommon}生成测试报告。"; $lang->testreport->hiddenCase = "隐藏 %s 个用例"; -$lang->testreport->goalTip = "该版本所属{$lang->execution->common}的描述信息"; $lang->testreport->bugSummary = <<%s个Bug , diff --git a/module/testreport/view/create.html.php b/module/testreport/view/create.html.php index f5cadb3e90..df18c2bd6f 100644 --- a/module/testreport/view/create.html.php +++ b/module/testreport/view/create.html.php @@ -67,16 +67,11 @@ - desc)):?> - + - - desc)):?> - + - - desc)):?> + - + diff --git a/module/testsuite/control.php b/module/testsuite/control.php index 390877abcd..cfd2735440 100644 --- a/module/testsuite/control.php +++ b/module/testsuite/control.php @@ -190,7 +190,7 @@ class testsuite extends control $this->view->orderBy = $orderBy; $this->view->pager = $pager; $this->view->modules = $this->loadModel('tree')->getOptionMenu($suite->product, 'case', 0, 'all'); - $this->view->branches = $this->loadModel('branch')->getPairs($suite->product, 'noempty'); + $this->view->branches = $this->loadModel('branch')->getPairs($suite->product); $this->view->canBeChanged = common::canBeChanged('testsuite', $suite); $this->display(); diff --git a/module/testsuite/css/view.css b/module/testsuite/css/view.css new file mode 100644 index 0000000000..fae04dc5e2 --- /dev/null +++ b/module/testsuite/css/view.css @@ -0,0 +1 @@ +.caseModule {overflow: hidden; white-space: nowrap;} diff --git a/module/testsuite/view/view.html.php b/module/testsuite/view/view.html.php index 8942fc91b0..b8b2e3c7a2 100644 --- a/module/testsuite/view/view.html.php +++ b/module/testsuite/view/view.html.php @@ -84,9 +84,9 @@ - + diff --git a/module/testtask/control.php b/module/testtask/control.php index 8d63db5ddd..20cc2ad164 100644 --- a/module/testtask/control.php +++ b/module/testtask/control.php @@ -551,7 +551,7 @@ class testtask extends control $this->view->runs = $runs; $this->view->users = $this->loadModel('user')->getPairs('noclosed|qafirst|noletter'); $this->view->assignedToList = $assignedToList; - $this->view->moduleTree = $this->loadModel('tree')->getTreeMenu($productID, $viewType = 'case', $startModuleID = 0, array('treeModel', 'createTestTaskLink'), $extra = $taskID); + $this->view->moduleTree = $this->loadModel('tree')->getTreeMenu($productID, 'case', 0, array('treeModel', 'createTestTaskLink'), $taskID, $task->branch); $this->view->browseType = $browseType; $this->view->param = $param; $this->view->orderBy = $orderBy; @@ -1037,8 +1037,8 @@ class testtask extends control /* Build the search form. */ $this->loadModel('testcase'); - $this->config->testcase->search['params']['product']['values']= array($productID => $this->products[$productID]); - $this->config->testcase->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($productID, $viewType = 'case'); + $this->config->testcase->search['params']['product']['values'] = array($productID => $this->products[$productID]); + $this->config->testcase->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($productID, 'case', 0, $task->branch); $this->config->testcase->search['actionURL'] = inlink('linkcase', "taskID=$taskID&type=$type¶m=$param"); $this->config->testcase->search['style'] = 'simple'; if($task->productType == 'normal') @@ -1049,10 +1049,11 @@ class testtask extends control else { $this->config->testcase->search['fields']['branch'] = sprintf($this->lang->product->branch, $this->lang->product->branchName[$task->productType]); - $branches = array('' => '') + $this->loadModel('branch')->getPairs($task->product, 'noempty'); - if($task->branch) $branches = array('' => '', $task->branch => $branches[$task->branch]); + $branchName = $this->loadModel('branch')->getById($task->branch); + $branches = array('' => '', BRANCH_MAIN => $this->lang->branch->main, $task->branch => $branchName); $this->config->testcase->search['params']['branch']['values'] = $branches; } + if(!$this->config->testcase->needReview) unset($this->config->testcase->search['params']['status']['values']['wait']); $this->loadModel('search')->setSearchParams($this->config->testcase->search); @@ -1230,6 +1231,7 @@ class testtask extends control */ public function batchRun($productID, $orderBy = 'id_desc', $from = 'testcase', $taskID = 0) { + $this->loadModel('tree'); $url = $this->session->caseList ? $this->session->caseList : $this->createLink('testcase', 'browse', "productID=$productID"); if($this->post->results) { @@ -1257,7 +1259,7 @@ class testtask extends control { $this->loadModel('qa')->setMenu($this->products, $productID, $taskID); } - $this->view->moduleOptionMenu = $this->loadModel('tree')->getOptionMenu($productID, 'case', 0, 'all'); + $this->view->moduleOptionMenu = $this->tree->getOptionMenu($productID, 'case', 0, 'all'); $cases = $this->dao->select('*')->from(TABLE_CASE)->where('id')->in($caseIDList)->fetchAll('id'); } diff --git a/module/testtask/css/cases.css b/module/testtask/css/cases.css index f1bdbfe5d9..b556841989 100644 --- a/module/testtask/css/cases.css +++ b/module/testtask/css/cases.css @@ -16,3 +16,5 @@ td .warning {color: red;} .fail .result-fail, .blocked .result-blocked {color: #f00;} .pass .result-pass {color: green;} + +.tree li > a.active {color: #0c64eb;} diff --git a/module/testtask/model.php b/module/testtask/model.php index da81289a4a..c789571313 100644 --- a/module/testtask/model.php +++ b/module/testtask/model.php @@ -359,7 +359,7 @@ class testtaskModel extends model ->andWhere('id')->notIN($linkedCases) ->andWhere('status')->ne('wait') ->andWhere('type')->ne('unit') - ->beginIF($task->branch)->andWhere('branch')->in("0,$task->branch")->fi() + ->beginIF($task->branch !== '')->andWhere('branch')->in("0,$task->branch")->fi() ->andWhere('deleted')->eq(0) ->orderBy('id desc') ->page($pager) @@ -389,7 +389,7 @@ class testtaskModel extends model ->andWhere('product')->eq($productID) ->andWhere('status')->ne('wait') ->beginIF($linkedCases)->andWhere('id')->notIN($linkedCases)->fi() - ->beginIF($task->branch)->andWhere('branch')->in("0,$task->branch")->fi() + ->beginIF($task->branch !== '')->andWhere('branch')->in("0,$task->branch")->fi() ->andWhere('story')->in(trim($stories, ',')) ->andWhere('deleted')->eq(0) ->orderBy('id desc') @@ -422,7 +422,7 @@ class testtaskModel extends model ->andWhere('product')->eq($productID) ->andWhere('status')->ne('wait') ->beginIF($linkedCases)->andWhere('id')->notIN($linkedCases)->fi() - ->beginIF($task->branch)->andWhere('branch')->in("0,$task->branch")->fi() + ->beginIF($task->branch !== '')->andWhere('branch')->in("0,$task->branch")->fi() ->andWhere('fromBug')->in(trim($bugs, ',')) ->andWhere('deleted')->eq(0) ->orderBy('id desc') @@ -456,7 +456,7 @@ class testtaskModel extends model ->andWhere('t1.product')->eq($productID) ->andWhere('status')->ne('wait') ->beginIF($linkedCases)->andWhere('t1.id')->notIN($linkedCases)->fi() - ->beginIF($task->branch)->andWhere('t1.branch')->in("0,$task->branch")->fi() + ->beginIF($task->branch !== '')->andWhere('t1.branch')->in("0,$task->branch")->fi() ->andWhere('deleted')->eq(0) ->orderBy('id desc') ->page($pager) diff --git a/module/testtask/view/batchrun.html.php b/module/testtask/view/batchrun.html.php index bc99558c5b..75335c475a 100644 --- a/module/testtask/view/batchrun.html.php +++ b/module/testtask/view/batchrun.html.php @@ -28,13 +28,14 @@ + $case):?> status == 'wait') continue;?> id]", $caseID); - $moduleOptionMenu = $this->loadModel('tree')->getOptionMenu($case->product, $viewType = 'case', $startModuleID = 0); + if(!isset($moduleOptionMenu[$case->module])) $moduleOptionMenu += $this->tree->getOptionMenu($case->product, 'case', 0, $case->branch); } ?> diff --git a/module/testtask/view/cases.html.php b/module/testtask/view/cases.html.php index 13e6b8a7e9..e821bce3c3 100644 --- a/module/testtask/view/cases.html.php +++ b/module/testtask/view/cases.html.php @@ -84,7 +84,7 @@
    createLink('testcase', 'batchEdit', "productID=$productID&branch=$task->branch"); + $actionLink = $this->createLink('testcase', 'batchEdit', "productID=$productID&branch=all"); $misc = $canBatchEdit ? "onclick=\"setFormAction('$actionLink')\"" : "disabled='disabled'"; echo html::commonButton($lang->edit, $misc); ?> diff --git a/module/todo/model.php b/module/todo/model.php index 5473018c90..cb5f8782d3 100644 --- a/module/todo/model.php +++ b/module/todo/model.php @@ -515,8 +515,7 @@ class todoModel extends model $stmt = $this->dao->select('*')->from(TABLE_TODO) ->where('deleted')->eq('0') - ->beginIF($type == 'assignedtoother')->andWhere('account', true)->eq($account)->fi() - ->beginIF($type != 'assignedtoother')->andWhere('assignedTo', true)->eq($account)->fi() + ->andWhere('assignedTo', true)->eq($account) ->orWhere('finishedBy')->eq($account) ->markRight(1) ->beginIF($begin)->andWhere('date')->ge($begin)->fi() diff --git a/module/tree/control.php b/module/tree/control.php index 155a1d5fe4..0fd909702d 100644 --- a/module/tree/control.php +++ b/module/tree/control.php @@ -41,6 +41,9 @@ class tree extends control else if($this->app->tab == 'project') { $this->loadModel('project')->setMenu($this->session->project); + + $products = $this->product->getProducts($this->session->project, 'all', '', false); + if($viewType == 'case') $this->lang->modulePageNav = $this->product->select($products, $rootID, 'tree', 'browse', 'case', $branch); } /* According to the type, set the module root and modules. */ @@ -53,9 +56,10 @@ class tree extends control $branches = $this->loadModel('branch')->getPairs($product->id); if($currentModuleID) { - $branchName = $branches[$branch]; + $currentModuleBranch = $this->dao->select('branch')->from(TABLE_MODULE)->where('id')->eq($currentModuleID)->fetch('branch'); + $branchName = $branches[$currentModuleBranch]; unset($branches); - $branches[$branch] = $branchName; + $branches[$currentModuleBranch] = $branchName; } $this->view->branches = $branches; } diff --git a/module/tree/lang/en.php b/module/tree/lang/en.php index 2da5813be5..9b8e08da48 100644 --- a/module/tree/lang/en.php +++ b/module/tree/lang/en.php @@ -29,7 +29,7 @@ $lang->tree->updateOrder = 'Rank Module'; $lang->tree->manageChild = 'Manage Child Modules'; $lang->tree->manageStoryChild = 'Manage Child Modules'; $lang->tree->manageLineChild = "Manage {$lang->productCommon} Line"; -$lang->tree->manageBugChild = 'Manage Child Bugs'; +$lang->tree->manageBugChild = 'Manage Child Modules of Bugs'; $lang->tree->manageCaseChild = 'Manage Child Cases'; $lang->tree->manageCaselibChild = 'Manage Child Libraries'; $lang->tree->manageTaskChild = "Manage Child {$lang->executionCommon} Modules"; diff --git a/module/tree/model.php b/module/tree/model.php index c46956a1df..7041011faf 100644 --- a/module/tree/model.php +++ b/module/tree/model.php @@ -86,7 +86,7 @@ class treeModel extends model ->beginIF($type == 'task')->andWhere('type')->eq('task')->fi() ->beginIF($type != 'task')->andWhere('type')->in("story,$type")->fi() ->beginIF($startModulePath)->andWhere('path')->like($startModulePath)->fi() - ->beginIF($branch !== 'all') + ->beginIF($branch !== 'all' and $branch !== '' and $branch !== false) ->andWhere("(branch")->eq(0) ->orWhere('branch')->eq($branch) ->markRight(1) @@ -101,7 +101,7 @@ class treeModel extends model ->where('root')->eq((int)$rootID) ->andWhere('type')->eq($type) ->beginIF($startModulePath)->andWhere('path')->like($startModulePath)->fi() - ->beginIF($branch !== 'all') + ->beginIF($branch !== 'all' and $branch !== '' and $branch !== false) ->andWhere('(branch')->eq(0) ->orWhere('branch')->eq($branch) ->markRight(1) @@ -117,6 +117,7 @@ class treeModel extends model * @param int $rootID * @param string $type * @param int $startModule + * @param int $branch * @access public * @return string */ @@ -135,17 +136,14 @@ class treeModel extends model if($type == 'line') $rootID = 0; - $branches = array($branch => ''); + $branches = array(); if(strpos('story|bug|case', $type) !== false) { $product = $this->loadModel('product')->getById($rootID); if($product and $product->type != 'normal') { - $branchList = array('null' => '') + $this->loadModel('branch')->getPairs($rootID, 'all'); - - if(!$branch) $newBranches['null'] = ''; - $newBranches[$branch] = $branchList[$branch]; - $branches = $branch === 'all' ? $branchList : $newBranches; + $branchPairs = $this->loadModel('branch')->getPairs($rootID, 'all'); + $branches = $branch === 'all' ? $branchPairs : array($branch => $branchPairs[$branch]); } elseif($product and $product->type == 'normal') { @@ -156,11 +154,15 @@ class treeModel extends model $treeMenu = array(); foreach($branches as $branchID => $branch) { - $stmt = $this->dbh->query($this->buildMenuQuery($rootID, $type, $startModule, $branchID)); - $modules = array(); + $stmt = $this->dbh->query($this->buildMenuQuery($rootID, $type, $startModule, $branchID)); + $modules = array(); while($module = $stmt->fetch()) $modules[$module->id] = $module; - foreach($modules as $module) $this->buildTreeArray($treeMenu, $modules, $module, (empty($branch) or $branch == 'null') ? '/' : "/$branch/"); + foreach($modules as $module) + { + $branchName = ($product->type != 'normal' and $module->branch == BRANCH_MAIN) ? $this->lang->branch->main : $branch; + $this->buildTreeArray($treeMenu, $modules, $module, (empty($branchName)) ? '/' : "/$branchName/"); + } } ksort($treeMenu); @@ -388,18 +390,19 @@ class treeModel extends model if($type == 'line') $rootID = 0; $this->loadModel('branch'); - $projectID = zget($extra, 'projectID', 0); - $branches = array($branch => ''); - if($branch) + $projectID = zget($extra, 'projectID', 0); + $branches = array($branch => ''); + $executionModules = array(); + if($branch and empty($projectID)) { $branchName = $this->branch->getById($branch); $branches = array($branch => $branchName); - $extra = array('rootID' => $rootID, 'branch' => $branch); + $extra = $userFunc[1] == 'createTestTaskLink' ? $extra : array('rootID' => $rootID, 'branch' => $branch); } $manage = $userFunc[1] == 'createManageLink' ? true : false; $product = $this->loadModel('product')->getById($rootID); - if(strpos('story|bug|case', $type) !== false and $branch === 'all') + if(strpos('story|bug|case', $type) !== false and $branch === 'all' and empty($projectID)) { if($product->type != 'normal') $branches = array(BRANCH_MAIN => $this->lang->branch->main) + $this->loadModel('branch')->getPairs($rootID, 'noempty'); } @@ -407,17 +410,10 @@ class treeModel extends model { if($product->type != 'normal' and $projectID) { - $projectBranches = $this->dao->select('branch')->from(TABLE_PROJECTPRODUCT) - ->where('project')->eq($projectID) - ->andWhere('product')->eq($product->id) - ->fetchPairs(); - - $branches = array(); - if(isset($projectBranches[BRANCH_MAIN])) $branches = array(BRANCH_MAIN => $this->lang->branch->main); - $branches += $this->dao->select('id, name')->from(TABLE_BRANCH) - ->where('id')->in($projectBranches) - ->fetchPairs(); + $branches += $this->branch->getPairs($product->id, 'noempty', $projectID); } + + $executionModules = $this->getTaskTreeModules($projectID, true, $type); } /* Add for task #1945. check the module has case or no. */ @@ -425,8 +421,18 @@ class treeModel extends model $lastMenu = ''; $treeMenu = array(); - $stmt = $this->dbh->query($this->buildMenuQuery($rootID, $type, $startModule, $branch)); - while($module = $stmt->fetch()) $this->buildTree($treeMenu, $module, $type, $userFunc, $extra, $branch); + $stmt = $this->dbh->query($this->buildMenuQuery($rootID, $type, $startModule, $branch)); + while($module = $stmt->fetch()) + { + if(!$projectID) + { + $this->buildTree($treeMenu, $module, $type, $userFunc, $extra, $branch); + } + elseif(isset($executionModules[$module->id])) + { + $this->buildTree($treeMenu, $module, $type, $userFunc, $extra); + } + } ksort($treeMenu); $lastMenu .= array_shift($treeMenu); @@ -736,6 +742,8 @@ class treeModel extends model if($startModule) $startModulePath = $startModule->path . '%'; } + $executionModules = $this->getTaskTreeModules($rootID, true, 'case'); + /* Get module according to product. */ $productNum = count($products); $moduleName = $this->app->tab == 'project' ? 'project' : 'testcase'; @@ -761,9 +769,8 @@ class treeModel extends model $stmt = $this->dbh->query($query); while($module = $stmt->fetch()) { - $this->buildTree($treeMenu, $module, 'case', $userFunc, $extra); + if(isset($executionModules[$module->id])) $this->buildTree($treeMenu, $module, 'case', $userFunc, $extra); } - if(isset($treeMenu[0]) and $branch) $treeMenu[0] = "
  • $branchName
      {$treeMenu[0]}
  • "; $tree .= isset($treeMenu[0]) ? $treeMenu[0] : ''; } @@ -797,12 +804,7 @@ class treeModel extends model if($startModule) $startModulePath = $startModule->path . '%'; } - $executionModules = $this->getTaskTreeModules($rootID, true); - $executionBranches = $this->dao->select('DISTINCT t2.branch')->from(TABLE_PROJECTSTORY)->alias('t1') - ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id') - ->where('t1.project')->eq($rootID) - ->andWhere('t2.deleted')->eq(0) - ->fetchPairs(); + $executionModules = $this->getTaskTreeModules($rootID, true); /* Get module according to product. */ $products = $this->loadModel('product')->getProductPairsByProject($rootID); @@ -812,14 +814,14 @@ class treeModel extends model foreach($products as $id => $product) { $extra['productID'] = $id; - $projectProductLink = helper::createLink('projectstory', 'story', "projectID=$rootID&productID=$id"); + $projectProductLink = helper::createLink('projectstory', 'story', "projectID=$rootID&productID=$id&branch=all"); $executionProductLink = helper::createLink('execution', 'story', "executionID=$rootID&ordery=&status=byProduct&praram=$id"); $link = $this->app->rawModule == 'projectstory' ? $projectProductLink : $executionProductLink; if($productNum > 1) $menu .= "
  • " . html::a($link, $product, '_self', "id='product$id'"); /* tree menu. */ $tree = ''; - if(empty($branchGroups[$id])) $branchGroups[$id]['0'] = ''; + $branchGroups[$id][BRANCH_MAIN] = empty($branchGroups[$id]) ? '' : $this->lang->branch->main; foreach($branchGroups[$id] as $branch => $branchName) { $treeMenu = array(); @@ -835,16 +837,7 @@ class treeModel extends model while($module = $stmt->fetch()) { /* If not manage, ignore unused modules. */ - if(isset($executionModules[$module->id]) and $this->app->rawModule == 'execution') $this->buildTree($treeMenu, $module, 'task', $userFunc, $extra); - if($this->app->rawModule == 'projectstory') $this->buildTree($treeMenu, $module, 'task', $userFunc, $extra); - } - if((isset($treeMenu[0]) and $branch) or isset($executionBranches[$branch])) - { - $childMenu = isset($treeMenu[0]) ? "
      {$treeMenu[0]}
    " : ''; - $projectBranchLink = helper::createLink('projectstory', 'story', "projectID=$rootID&productID=$id&branch=" . (empty($branch) ? 0 : $branch) . "&browseType=byBranch"); - $executionBranchLink = helper::createLink('execution', 'story', "executionID=$rootID&ordery=&status=byBranch&praram=" . (empty($branch) ? "{$id},0" : $branch)); - $link = $this->app->rawModule == 'projectstory' ? $projectBranchLink : $executionBranchLink; - if($branchName) $treeMenu[0] = "
  • " . html::a($link, $branchName, '_self', "id='branch" . (empty($branch) ? "{$id}_0" : $branch) . "'") . "{$childMenu}
  • "; + if(isset($executionModules[$module->id])) $this->buildTree($treeMenu, $module, 'story', $userFunc, $extra); } $tree .= isset($treeMenu[0]) ? $treeMenu[0] : ''; } @@ -868,7 +861,7 @@ class treeModel extends model * @access public * @return void */ - public function buildTree(& $treeMenu, $module, $type, $userFunc, $extra, $branch = 0) + public function buildTree(& $treeMenu, $module, $type, $userFunc, $extra, $branch = 'all') { /* Add for task #1945. check the module has case or no. */ if((isset($extra['rootID']) and isset($extra['branch']) and $branch === 'null') or ($type == 'case' and is_numeric($extra))) @@ -940,20 +933,33 @@ class treeModel extends model * * @param int $executionID * @param bool $parent - * @param bool $linkStory + * @param string $linkObject * @access public * @return array */ - public function getTaskTreeModules($executionID, $parent = false, $linkStory = true) + public function getTaskTreeModules($executionID, $parent = false, $linkObject = 'story') { $executionModules = array(); $field = $parent ? 'path' : 'id'; - if($linkStory) + if($linkObject == 'story') { - /* Get story paths of this execution. */ - $paths = $this->dao->select('DISTINCT t3.' . $field)->from(TABLE_PROJECTSTORY)->alias('t1') - ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id') + $table1 = TABLE_PROJECTSTORY; + $table2 = TABLE_STORY; + } + if($linkObject == 'case') + { + $table1 = TABLE_PROJECTCASE; + $table2 = TABLE_CASE; + } + + if($linkObject) + { + if(strpos(',story,case,', ",$linkObject,") === false) return array(); + + /* Get object paths of this execution. */ + $paths = $this->dao->select('DISTINCT t3.' . $field)->from($table1)->alias('t1') + ->leftJoin($table2)->alias('t2')->on('t1.' . $linkObject . ' = t2.id') ->leftJoin(TABLE_MODULE)->alias('t3')->on('t2.module = t3.id') ->where('t1.project')->eq($executionID) ->andWhere('t3.deleted')->eq(0) @@ -1026,7 +1032,7 @@ class treeModel extends model } else { - return html::a(helper::createLink('product', 'browse', "root={$module->root}&branch=&type=byModule¶m={$module->id}"), $module->name, '_self', "id='module{$module->id}' title='{$module->name}'"); + return html::a(helper::createLink('product', 'browse', "root={$module->root}&branch={$extra['branchID']}&type=byModule¶m={$module->id}"), $module->name, '_self', "id='module{$module->id}' title='{$module->name}'"); } } @@ -1177,16 +1183,18 @@ class treeModel extends model /** * Create link of a test case. * - * @param object $module + * @param string $type + * @param object $module + * @param array $extra * @access public * @return string */ - public function createCaseLink($type, $module) + public function createCaseLink($type, $module, $extra = array()) { $moduleName = $this->app->tab == 'project' ? 'project' : 'testcase'; $methodName = $this->app->tab == 'project' ? 'testcase' : 'browse'; $projectParam = $this->app->tab == 'project' ? "projectID={$this->session->project}&" : ''; - return html::a(helper::createLink($moduleName, $methodName, $projectParam . "root={$module->root}&branch={$module->branch}&type=byModule¶m={$module->id}"), $module->name, '_self', "id='module{$module->id}' data-app='{$this->app->tab}' title='{$module->name}'"); + return html::a(helper::createLink($moduleName, $methodName, $projectParam . "root={$module->root}&branch={$extra['branchID']}&type=byModule¶m={$module->id}"), $module->name, '_self', "id='module{$module->id}' data-app='{$this->app->tab}' title='{$module->name}'"); } /** @@ -1289,6 +1297,7 @@ class treeModel extends model * @param int $rootID * @param int $moduleID * @param string $type + * @param int $branch * @access public * @return array */ @@ -1303,11 +1312,11 @@ class treeModel extends model ->where('root')->eq((int)$rootID) ->andWhere('parent')->eq((int)$moduleID) ->andWhere('type')->eq($type) - ->andWhere() - ->markLeft(1) - ->where("branch")->eq(0) - ->beginIF($branch != 0)->orWhere("branch")->eq((int)$branch)->fi() + ->beginIF($branch !== 'all') + ->andWhere("(branch")->eq(0) + ->orWhere("branch")->eq((int)$branch) ->markRight(1) + ->fi() ->andWhere('deleted')->eq(0) ->orderBy('`order`') ->fetchAll(); @@ -1928,10 +1937,9 @@ class treeModel extends model /** * Get full modules tree - * @param int $rootID + * @param object $stmt * @param string $viewType - * @param int $branch - * @param int $currentModuleID + * @param array $keepModules * @access public * @return array */ @@ -1960,7 +1968,7 @@ class treeModel extends model foreach($module->children as $children) { if($viewType == 'task' and isset($parentTypePairs[$children->parent]) and $parentTypePairs[$children->parent] != 'task') continue; - if($children->parent != 0) continue;//Filter project children modules. + if($children->parent != 0) continue; // Filter project children modules. $tree[] = $children; } } diff --git a/module/tree/view/browse.html.php b/module/tree/view/browse.html.php index 67a4684c2c..0721db9cd5 100644 --- a/module/tree/view/browse.html.php +++ b/module/tree/view/browse.html.php @@ -154,7 +154,9 @@ if($viewType == 'doc' or $viewType == 'api')
    -
    + +
    +
    tree->short}'");?>
    @@ -199,6 +201,7 @@ if($viewType == 'doc' or $viewType == 'api')
    app->tab);?> + "; + die; + } + foreach($projectIdList as $projectID) { $data->projectName = $projects[$projectID]->name; @@ -4486,7 +4509,7 @@ class upgradeModel extends model $this->dao->insert(TABLE_PROJECT)->data($project) ->batchcheck('name', 'notempty') - ->check('name', 'unique', "type='project'") + ->check('name', 'unique', "type='project' AND deleted=0") ->exec(); if(dao::isError()) return false; diff --git a/module/upgrade/view/createprogram.html.php b/module/upgrade/view/createprogram.html.php index 37fa143e4b..3824f3e72d 100644 --- a/module/upgrade/view/createprogram.html.php +++ b/module/upgrade/view/createprogram.html.php @@ -14,7 +14,6 @@
    diff --git a/module/upgrade/view/mergebyline.html.php b/module/upgrade/view/mergebyline.html.php index a623c75001..90c156aa0e 100644 --- a/module/upgrade/view/mergebyline.html.php +++ b/module/upgrade/view/mergebyline.html.php @@ -52,8 +52,18 @@
    - id][$productID]", array($sprint->id => $sprint->name), '', "title='{$sprint->name}' data-product='{$product->id}' data-line='{$line->id}' data-begin='{$sprint->begin}' data-end='{$sprint->end}' data-status='{$sprint->status}' data-pm='{$sprint->PM}' class='tile'");?> - id][$productID][$sprint->id]", $sprint->id);?> +
    + id][$productID]", array($sprint->id => $sprint->name), '', "title='{$sprint->name}' data-product='{$product->id}' data-line='{$line->id}' data-begin='{$sprint->begin}' data-end='{$sprint->end}' data-status='{$sprint->status}' data-pm='{$sprint->PM}' class='tile'");?> + id][$productID][$sprint->id]", $sprint->id);?> + +
    +
    diff --git a/module/upgrade/view/mergebyproduct.html.php b/module/upgrade/view/mergebyproduct.html.php index 3847d25860..933d1b04a3 100644 --- a/module/upgrade/view/mergebyproduct.html.php +++ b/module/upgrade/view/mergebyproduct.html.php @@ -29,8 +29,18 @@
    - id => $sprint->name), '', "data-product='{$productID}' data-begin='{$sprint->begin}' data-end='{$sprint->end}' data-status='{$sprint->status}' data-pm='{$sprint->PM}'");?> - id]", $sprint->id);?> +
    + id => $sprint->name), '', "data-product='{$productID}' data-begin='{$sprint->begin}' data-end='{$sprint->end}' data-status='{$sprint->status}' data-pm='{$sprint->PM}'");?> + id]", $sprint->id);?> + +
    +
    diff --git a/module/upgrade/view/mergebysprint.html.php b/module/upgrade/view/mergebysprint.html.php index 6ac1d2eb7a..9851a5c8b9 100644 --- a/module/upgrade/view/mergebysprint.html.php +++ b/module/upgrade/view/mergebysprint.html.php @@ -1,7 +1,8 @@
    upgrade->mergeSummary, $noMergedProductCount, $noMergedSprintCount); - if($type == 'moreLink') echo '
    ' . $lang->upgrade->mergeByProject; + if($type == 'sprint') echo '
    ' . $lang->upgrade->mergeByProject; + if($type == 'moreLink') echo '
    ' . $lang->upgrade->mergeByMoreLink; ?>
    @@ -14,7 +15,17 @@
    $sprint):?> - id => $sprint->name), '', "data-begin='{$sprint->begin}' data-end='{$sprint->end}' data-status='{$sprint->status}' data-pm='{$sprint->PM}'");?> +
    + id => $sprint->name), '', "data-begin='{$sprint->begin}' data-end='{$sprint->end}' data-status='{$sprint->status}' data-pm='{$sprint->PM}'");?> + +
    +
    diff --git a/module/upgrade/view/mergeprogram.html.php b/module/upgrade/view/mergeprogram.html.php index 3104e3d5b6..e2f8b73843 100644 --- a/module/upgrade/view/mergeprogram.html.php +++ b/module/upgrade/view/mergeprogram.html.php @@ -24,38 +24,8 @@ - + - -
    - upgrade->mergeSummary, $noMergedProductCount, $noMergedSprintCount); - echo '
    ' . $lang->upgrade->mergeByMoreLink; - ?> -
    -
    idAB;?> '>product->branch;?> '>story->module;?> '>story->plan;?> story->title;?> '>story->spec;?> '>story->source;?>story->category;?> '>story->pri;?> '>story->estimate;?>'>story->needReview;?> ' style="width: 200px !important">story->reviewedBy;?> '>story->keywords;?>
    $idPlus '> ' style='overflow:visible'>
    @@ -94,9 +95,7 @@
    story->categoryList, 'feature', "class='form-control chosen'");?> ' style='overflow:visible'> '>'>story->reviewList, $needReview, "class='form-control'");?>'>'> '>
    '> product]->type == 'normal' ? "disabled='disabled'" : '';?> + product]->type == 'normal') $branches[$story->product] = array();?> product], $story->branch, "class='form-control chosen' onchange='loadBranches($story->product, this.value, $storyID);' $disabled");?> '> - product][$story->branch], $story->module, "class='form-control chosen'");?> + product][$story->branch]) ? $modules[$story->product][$story->branch] : array('0' => '/'), $story->module, "class='form-control chosen'");?> '> - ''); - if($story->branch != BRANCH_MAIN) - { - if(isset($plans[$story->branch])) $productPlans += $plans[$story->branch]; - if(isset($plans[0])) $productPlans += $plans[0]; - } - else - { - foreach($plans as $branchPlan) $productPlans += $branchPlan; - } - } - ?> - session->currentProductType == 'normal') $productPlans = array('' => '', 'ditto' => $this->lang->story->ditto) + $productPlans;?> product][$story->branch]) ? array('' => '') + $plans[$story->product][$story->branch] : '', $story->plan, "class='form-control chosen'");?> @@ -163,7 +148,7 @@ foreach(explode(',', $showFields) as $field)
    - + app->tab == 'product' ? html::a($this->session->storyList, $lang->goback, '', "class='btn btn-back btn-wide'") : html::backButton();?>
    story->reviewedBy;?>
    - + story->checkForceReview()):?> story->needNotReview, '', "id='needNotReview' {$needReview}");?> diff --git a/module/story/view/create.html.php b/module/story/view/create.html.php index 44ccc77ded..3cce00ae90 100644 --- a/module/story/view/create.html.php +++ b/module/story/view/create.html.php @@ -17,6 +17,8 @@ story->placeholder); ?> +story->feedbackSource); ?> + @@ -90,7 +92,7 @@
    story->source;?>
    story->sourceList, $source, "class='form-control chosen'");?> story->sourceNote;?> - +
    @@ -100,11 +102,10 @@
    story->reviewedBy;?> id='reviewerBox'> + ' id='reviewerBox'>
    - PO : '', "class='form-control chosen' multiple");?> + PO : '', "class='form-control chosen' multiple");?>
    story->checkForceReview()):?>
    @@ -119,18 +120,29 @@
    + + +
    -
    -
    story->source;?>
    - story->sourceList, $source, "class='form-control chosen'");?> - story->sourceNote;?> - -
    +
    story->source;?>
    + story->sourceList, $source, "class='form-control chosen'");?> + story->sourceNote;?> +
    -
    story->estimate;?> parent >= 0 ? html::input('estimate', $story->estimate, "class='form-control'") : $story->estimate;?>
    story->feedbackBy;?>feedbackBy, "class='form-control'");?>
    story->notifyEmail;?>notifyEmail, "class='form-control'");?>
    story->keywords;?> keywords, "class='form-control'");?>
    story->reviewers;?>
    story->estimate;?> estimate . $config->hourUnit;?>
    story->feedbackBy;?>feedbackBy;?>
    story->notifyEmail;?>notifyEmail;?>
    story->keywords;?> keywords;?>
    task->afterSubmit;?> task->afterChoices, !empty($task->id) ? 'toTaskList' : 'continueAdding');?>
    diff --git a/module/task/view/view.html.php b/module/task/view/view.html.php index c13844f461..7b5b8be611 100644 --- a/module/task/view/view.html.php +++ b/module/task/view/view.html.php @@ -46,6 +46,9 @@ +app->getViewType() == 'xhtml'):?> +
    +
    @@ -150,7 +153,9 @@ ?>
    printExtendFields($task, 'div', "position=left&inForm=0&inCell=1");?> + app->getViewType() != 'xhtml'):?>
    +
    @@ -386,6 +391,9 @@ printExtendFields($task, 'div', "position=right&inForm=0&inCell=1");?>
    +app->getViewType() == 'xhtml'):?> +
    +
    diff --git a/module/testcase/control.php b/module/testcase/control.php index cd117ca4bb..0cf4106843 100644 --- a/module/testcase/control.php +++ b/module/testcase/control.php @@ -126,6 +126,7 @@ class testcase extends control if($this->app->tab == 'project') { $this->products = array('0' => $this->lang->product->all) + $this->product->getProducts($projectID, 'all', '', false); + $branch = 'all'; $this->loadModel('project')->setMenu($projectID); } else @@ -182,7 +183,7 @@ class testcase extends control } else { - $moduleTree = $this->tree->getTreeMenu($productID, 'case', 0, array('treeModel', 'createCaseLink'), array('projectID' => $projectID, 'productID' => $productID), $projectID ? 0 : $branch); + $moduleTree = $this->tree->getTreeMenu($productID, 'case', 0, array('treeModel', 'createCaseLink'), array('projectID' => $projectID, 'productID' => $productID), $projectID ? '' : $branch); } $product = $this->product->getById($productID); @@ -370,6 +371,20 @@ class testcase extends control $this->qa->setMenu($this->products, $productID, $branch); } + /* Set branch. */ + $product = $this->product->getById($productID); + if($this->app->tab == 'execution' or $this->app->tab == 'project') + { + $objectID = $this->app->tab == 'project' ? $this->session->project : $executionID; + $productBranches = (isset($product->type) and $product->type != 'normal') ? $this->execution->getBranchByProduct($productID, $objectID) : array(); + $branches = isset($productBranches[$productID]) ? $productBranches[$productID] : array(); + $branch = key($branches); + } + else + { + $branches = (isset($product->type) and $product->type != 'normal') ? $this->loadModel('branch')->getPairs($productID, 'active') : array(); + } + /* Init vars. */ $type = 'feature'; $stage = ''; @@ -455,18 +470,6 @@ class testcase extends control /* Set custom. */ foreach(explode(',', $this->config->testcase->customCreateFields) as $field) $customFields[$field] = $this->lang->testcase->$field; - $product = $this->product->getById($productID); - if($this->app->tab == 'execution' or $this->app->tab == 'project') - { - $objectID = $this->app->tab == 'project' ? $projectID : $executionID; - $productBranches = (isset($product->type) and $product->type != 'normal') ? $this->execution->getBranchByProduct($productID, $objectID) : array(); - $branches = isset($productBranches[$productID]) ? $productBranches[$productID] : array(); - $branch = key($branches); - } - else - { - $branches = (isset($product->type) and $product->type != 'normal') ? $this->loadModel('branch')->getPairs($productID, 'active') : array(); - } $this->view->customFields = $customFields; $this->view->showFields = $this->config->testcase->custom->createFields; @@ -1597,7 +1600,8 @@ class testcase extends control $fields['stageValue'] = $this->lang->testcase->lblStageValue; if($product->type != 'normal') $fields['branchValue'] = $this->lang->product->branchName[$product->type]; - $branches = $this->loadModel('branch')->getPairs($productID); + $projectID = $this->app->tab == 'project' ? $this->session->project : 0; + $branches = $this->loadModel('branch')->getPairs($productID, '' , $projectID); foreach($branches as $branchID => $branchName) $branches[$branchID] = $branchName . "(#$branchID)"; $modules = $this->loadModel('tree')->getOptionMenu($productID, 'case'); @@ -1716,6 +1720,11 @@ class testcase extends control { $browseType = strtolower($browseType); $queryID = (int)$queryID; + $product = $this->loadModel('product')->getById($productID); + $branches = array(); + + $this->loadModel('branch'); + if($product->type != 'normal') $branches = array(BRANCH_MAIN => $this->lang->branch->main) + $this->branch->getPairs($productID, '', $projectID); if($_POST) { @@ -1756,6 +1765,7 @@ class testcase extends control $this->view->libraries = $libraries; $this->view->libID = $libID; + $this->view->product = $product; $this->view->productID = $productID; $this->view->branch = $branch; $this->view->cases = $this->loadModel('testsuite')->getNotImportedCases($productID, $libID, $orderBy, $pager, $browseType, $queryID); @@ -1763,7 +1773,7 @@ class testcase extends control $this->view->libModules = $this->tree->getOptionMenu($libID, 'caselib'); $this->view->pager = $pager; $this->view->orderBy = $orderBy; - $this->view->branches = array('0' => $this->lang->branch->main) + $this->loadModel('branch')->getPairs($productID, '', $projectID); + $this->view->branches = $branches; $this->view->browseType = $browseType; $this->view->queryID = $queryID; diff --git a/module/testcase/js/common.js b/module/testcase/js/common.js index f5503151a3..9835f94fbd 100644 --- a/module/testcase/js/common.js +++ b/module/testcase/js/common.js @@ -22,8 +22,6 @@ var newRowID = 0; function loadAll(productID) { loadProductBranches(productID) - loadProductModules(productID); - setStories(); } /** @@ -58,6 +56,9 @@ function loadProductBranches(productID) $('#product').closest('.input-group').append(data); $('#branch').css('width', config.currentMethod == 'create' ? '120px' : '95px'); } + + loadProductModules(productID); + setStories(); }) } @@ -81,7 +82,7 @@ function loadModuleRelated() */ function loadProductModules(productID, branch) { - if(typeof(branch) == 'undefined') branch = 0; + if(typeof(branch) == 'undefined') branch = $('#branch').val(); if(!branch) branch = 0; link = createLink('tree', 'ajaxGetOptionMenu', 'productID=' + productID + '&viewtype=case&branch=' + branch + '&rootModuleID=0&returnType=html&fieldID=&needManage=true'); $('#moduleIdBox').load(link, function() @@ -126,7 +127,7 @@ function setStories() branch = $('#branch').val(); if(typeof(branch) == 'undefined') branch = 0; link = createLink('story', 'ajaxGetProductStories', 'productID=' + productID + '&branch=' + branch + '&moduleID=' + moduleID + '&storyID=0&onlyOption=false&status=noclosed&limit=50&type=full&hasParent=1&executionID=' + executionID); - + $.get(link, function(stories) { var value = $('#story').val(); diff --git a/module/testcase/js/importfromlib.js b/module/testcase/js/importfromlib.js index af09e8720c..a480ed0f69 100644 --- a/module/testcase/js/importfromlib.js +++ b/module/testcase/js/importfromlib.js @@ -22,8 +22,37 @@ $(function() }); }) +/** + * Reload. + * + * @param int $libID + * @access public + * @return void + */ function reload(libID) -{ +{ link = createLink('testcase','importFromLib','productID='+ productID + '&branch=' + branch + '&libID='+libID); location.href = link; } + +/** + * Load modules. + * + * @param int $productID + * @param int $branch + * @param int $caseID + * @access public + * @return void + */ +function loadModules(productID, branch, caseID) +{ + if(typeof(branch) == 'undefined') branch = 0; + + moduleLink = createLink('tree', 'ajaxGetOptionMenu', 'productID=' + productID + '&viewtype=case&branch=' + branch + '&rootModuleID=0&returnType=html&fieldID=&needManage=true'); + var $tr = $('#module' + caseID).closest('tr'); + $('#module' + caseID).parent('td').load(moduleLink, function(data) + { + $tr.find('#module').chosen(); + $tr.find('#module').attr({"id": 'module' + caseID, "name": 'module[' + caseID + ']'}); + }); +} diff --git a/module/testcase/model.php b/module/testcase/model.php index 18d3532409..720de4e798 100644 --- a/module/testcase/model.php +++ b/module/testcase/model.php @@ -1551,7 +1551,14 @@ class testcaseModel extends model echo ""; break; case 'title': - $showBranch = isset($this->config->testcase->browse->showBranch) ? $this->config->testcase->browse->showBranch : 1; + if($this->app->tab == 'project') + { + $showBranch = isset($this->config->project->testcase->showBranch) ? $this->config->project->testcase->showBranch : 1; + } + else + { + $showBranch = isset($this->config->testcase->browse->showBranch) ? $this->config->testcase->browse->showBranch : 1; + } if(isset($branches[$case->branch]) and $showBranch) echo "{$branches[$case->branch]} "; if($modulePairs and $case->module and isset($modulePairs[$case->module])) echo "{$modulePairs[$case->module]} "; echo $canView ? ($fromCaseID ? html::a($caseLink, $case->title, null, "style='color: $case->color' data-app='{$this->app->tab}'") . html::a(helper::createLink('testcase', 'view', "caseID=$fromCaseID"), "[#$fromCaseID]", '', "data-app='{$this->app->tab}'") : html::a($caseLink, $case->title, null, "style='color: $case->color' data-app='{$this->app->tab}'")) : "$case->title"; diff --git a/module/testcase/view/batchedit.html.php b/module/testcase/view/batchedit.html.php index 618903907c..46908c484f 100644 --- a/module/testcase/view/batchedit.html.php +++ b/module/testcase/view/batchedit.html.php @@ -69,10 +69,10 @@ branch) ? $cases[$caseID]->branch : 0; - if((!$productID and !$cases[$caseID]->lib) or $app->tab != 'qa') + if(!$productID and !$cases[$caseID]->lib) { $caseProductID = $cases[$caseID]->product; - $product = isset($product) ? $product : $products[$caseProductID]; + $product = $products[$caseProductID]; $branches = isset($branches) ? $branches : array('' => ''); if($product->type != 'normal') { @@ -103,7 +103,7 @@
    id;?> type == 'normal') ? "disabled='disabled'" : '';?> - branch, "class='form-control chosen' onchange='loadBranches($branchProductID, this.value, $caseID)', $disabled");?> + '') + $branches, $product->type == 'normal' ? '' : $cases[$caseID]->branch, "class='form-control chosen' onchange='loadBranches($branchProductID, this.value, $caseID)', $disabled");?> testcase->branch ?> priAB);?> id}]", $branches, $branch, "class='form-control'")?>id}]", $branches, $branch, "class='form-control' onchange='loadModules($productID, this.value, $case->id)'")?> pri;?>' title='testcase->priList, $case->pri, $case->pri);?>'>pri == '0' ? '' : zget($lang->testcase->priList, $case->pri, $case->pri);?> id", $case->title)) echo $case->title;?>
    testreport->goal?> - desc;?> - - desc) ? $execution->desc : '';?>
    testreport->profile?> diff --git a/module/testreport/view/edit.html.php b/module/testreport/view/edit.html.php index 2362b021d5..65d8589de0 100644 --- a/module/testreport/view/edit.html.php +++ b/module/testreport/view/edit.html.php @@ -55,16 +55,11 @@ title, "class='form-control'")?>
    testreport->goal?> - desc?> - - desc?>
    testreport->profile?> diff --git a/module/testreport/view/view.html.php b/module/testreport/view/view.html.php index cddbd9bbd5..98874cf91e 100644 --- a/module/testreport/view/view.html.php +++ b/module/testreport/view/view.html.php @@ -63,13 +63,10 @@ testreport->members?> members) as $member)echo zget($users, $member) . '   ';?>
    testreport->goal?> - desc?> - - desc?>
    testcase->priList, $case->pri, $case->pri)?>module];?>module];?> - branch) echo "{$branches[$case->branch]}"?> + {$branches[$case->branch]}"?> createLink('testcase', 'view', "caseID=$case->id&version=$case->caseVersion"), $case->title);?> testcase->typeList[$case->type];?>testcase->stepDesc . '/' . $lang->testcase->stepExpect?>
    -
    @@ -22,7 +21,6 @@
    -
    - - - - - - - - - $sprint):?> - - - - - - - - - - - - -
    projectCommon;?>upgrade->selectProject;?>
    name}" . html::hidden("sprints[]", $sprint->id);?>projects, '', "class='form-control chosen'");?>
    diff --git a/module/upgrade/view/renameobject.html.php b/module/upgrade/view/renameobject.html.php new file mode 100644 index 0000000000..952f8d44bd --- /dev/null +++ b/module/upgrade/view/renameobject.html.php @@ -0,0 +1,42 @@ + + * @package upgrade + * @version $Id: renameobject.html.php 4129 2021-11-30 13:07:14Z sunguangming $ + */ +?> + + +
    +
    +
    +

    upgrade->duplicateProject;?>

    +
    +
    + + + + + + + $objectList):?> + $object):?> + id == end($objectList)->id) echo "class='group-end'";?>> + + + + + + + + + +
    $type->id;?>$type->name;?>upgrade->editedName;?>
    id;?>name;?>id]", '', "class='form-control'");?>
    +
    +
    +
    + diff --git a/module/user/model.php b/module/user/model.php index fb6968e130..382b1f592d 100644 --- a/module/user/model.php +++ b/module/user/model.php @@ -66,13 +66,14 @@ class userModel extends model /** * Get the account=>realname pairs. * - * @param string $params noletter|noempty|nodeleted|noclosed|withguest|pofirst|devfirst|qafirst|pmfirst|realname|outside|inside|all, can be sets of theme - * @param string $usersToAppended account1,account2 - * @param int $maxCount + * @param string $params noletter|noempty|nodeleted|noclosed|withguest|pofirst|devfirst|qafirst|pmfirst|realname|outside|inside|all, can be sets of theme + * @param string $usersToAppended account1,account2 + * @param int $maxCount + * @param string|array $accounts * @access public * @return array */ - public function getPairs($params = '', $usersToAppended = '', $maxCount = 0) + public function getPairs($params = '', $usersToAppended = '', $maxCount = 0, $accounts = '') { if(defined('TUTORIAL')) return $this->loadModel('tutorial')->getUserPairs(); /* Set the query fields and orderBy condition. @@ -100,6 +101,7 @@ class userModel extends model ->where('1') ->beginIF(strpos($params, 'all') === false)->andWhere('type')->eq($type)->fi() ->beginIF(strpos($params, 'nodeleted') !== false or empty($this->config->user->showDeleted))->andWhere('deleted')->eq('0')->fi() + ->beginIF($accounts)->andWhere('account')->in($accounts)->fi() ->orderBy($orderBy) ->beginIF($maxCount)->limit($maxCount)->fi() ->fetchAll($keyField); @@ -2058,12 +2060,10 @@ class userModel extends model /* Get all groups for whiteList. */ $allGroups = $this->dao->select('account, `group`')->from(TABLE_USERGROUP)->fetchAll(); $userGroups = array(); - $groupUsers = array(); foreach($allGroups as $group) { if(!isset($userGroups[$group->account])) $userGroups[$group->account] = ''; $userGroups[$group->account] .= "{$group->group},"; - $groupUsers[$group->group][$group->account] = $group->account; } list($productTeams, $productStakeholders) = $this->getProductMembers($products); @@ -2086,17 +2086,14 @@ class userModel extends model $teams = zget($productTeams, $productID, array()); $stakeholders = zget($productStakeholders, $productID, array()); $whiteList = zget($whiteListGroup, $productID, array()); - $viewList += $this->getProductViewListUsers($product, $groupUsers, $teams, $stakeholders, $whiteList); + $viewList += $this->getProductViewListUsers($product, $teams, $stakeholders, $whiteList); } $users = $viewList; } $stmt = $this->dao->select("account,products")->from(TABLE_USERVIEW)->where('account')->in($users); - if($whiteList) - { - foreach($products as $productID => $product) $stmt->orWhere("CONCAT(',', products, ',')")->like("%,{$productID},%"); - } + foreach($products as $productID => $product) $stmt->orWhere("CONCAT(',', products, ',')")->like("%,{$productID},%"); $userViews = $stmt->fetchPairs('account', 'products'); /* Process user view. */ @@ -2412,14 +2409,13 @@ class userModel extends model * Get product view list users. * * @param object $product - * @param array $groupUsers * @param array $linkedProjects * @param array $teams * @param array $whiteList * @access public * @return array */ - public function getProductViewListUsers($product, $groupUsers, $teams, $stakeholders, $whiteList) + public function getProductViewListUsers($product, $teams, $stakeholders, $whiteList) { $users = array(); @@ -2431,6 +2427,21 @@ class userModel extends model $users[$product->createdBy] = $product->createdBy; if(isset($product->feedback)) $users[$product->feedback] = $product->feedback; + if($teams === '' and $stakeholders === '') + { + list($productTeams, $productStakeholders) = $this->getProductMembers(array($product->id => $product)); + $teams = isset($productTeams[$product->id]) ? $productTeams[$product->id] : array(); + $teams = isset($productStakeholders[$product->id]) ? $productStakeholders[$product->id] : array(); + } + + if($whiteList === '') + { + $whiteList = $this->dao->select('account')->from(TABLE_ACL) + ->where('objectType')->eq('product') + ->andWhere('objectID')->eq($product->id) + ->fetchPairs(); + } + $users += $teams ? $teams : array(); $users += $stakeholders ? $stakeholders : array(); $users += $whiteList ? $whiteList : array(); diff --git a/module/user/view/task.html.php b/module/user/view/task.html.php index 2eb5f672d4..1c6f51ab38 100644 --- a/module/user/view/task.html.php +++ b/module/user/view/task.html.php @@ -42,7 +42,7 @@ idAB;?> - priAB;?> + priAB);?> task->execution);?> task->name);?> task->estimateAB;?> @@ -56,7 +56,7 @@ createLink('task', 'view', "taskID=$task->id"), sprintf('%03d', $task->id));?> - task->priList, $task->pri, $task->pri);?>'>pri == '0' ? '' : zget($lang->task->priList, $task->pri, $task->pri)?> + task->priList, $task->pri, $task->pri);?> createLink('execution', 'browse', "executionID=$task->executionID"), $task->executionName);?> team)) echo '' . $this->lang->task->multipleAB . ' ';?> diff --git a/module/weekly/control.php b/module/weekly/control.php new file mode 100644 index 0000000000..2528af2a63 --- /dev/null +++ b/module/weekly/control.php @@ -0,0 +1,92 @@ + + * @package weekly + * @version $Id$ + * @link http://www.zentao.net + */ +class weekly extends control +{ + /** + * The construct function, load users. + * + * @access public + * @return void + */ + + public function __construct() + { + parent::__construct(); + $this->view->users = $this->loadModel('user')->getPairs('noletter'); + } + + /** + * Common action. + * + * @param int $projectID + * @access public + * @return void + */ + public function commonAction($projectID = 0) + { + $this->loadModel('project')->setMenu($projectID); + } + + /** + * Index + * + * @param int $projectID + * @param string $date + * @access public + * @return void + */ + public function index($projectID = 0, $date = '') + { + $this->commonAction($projectID); + if(!$date) $date = helper::today(); + $date = date('Y-m-d', strtotime($date)); + + $this->view->title = $this->lang->weekly->common; + + $this->view->pv = $this->weekly->getPV($projectID, $date); + $this->view->ev = $this->weekly->getEV($projectID, $date); + $this->view->ac = $this->weekly->getAC($projectID, $date); + $this->view->sv = $this->weekly->getSV($this->view->ev, $this->view->pv); + $this->view->cv = $this->weekly->getCV($this->view->ev, $this->view->ac); + + $this->view->project = $this->loadModel('project')->getByID($projectID); + $this->view->weekSN = $this->weekly->getWeekSN($this->view->project->begin, $date); + $this->view->monday = $this->weekly->getThisMonday($date); + $this->view->lastDay = $this->weekly->getLastDay($date); + $this->view->staff = $this->weekly->getStaff($projectID, $date); + $this->view->finished = $this->weekly->getFinished($projectID, $date); + $this->view->postponed = $this->weekly->getPostponed($projectID, $date); + $this->view->nextWeek = $this->weekly->getTasksOfNextWeek($projectID, $date); + $this->view->workload = $this->weekly->getWorkloadByType($projectID, $date); + $this->weekly->save($projectID, $date); + + $this->lang->modulePageNav = $this->weekly->getPageNav($this->view->project, $date); + $this->display(); + } + + /** + * ComputeWeekly + * + * @access public + * @return void + */ + public function computeWeekly() + { + $projects = $this->dao->select('id, name')->from(TABLE_PROJECT) + ->where('deleted')->eq(0) + ->andWhere('type')->eq('project') + ->fetchPairs(); + $date = helper::today(); + + foreach($projects as $projectID => $project) $this->weekly->save($projectID, $date); + } +} diff --git a/module/weekly/lang/de.php b/module/weekly/lang/de.php new file mode 100644 index 0000000000..f665865f2e --- /dev/null +++ b/module/weekly/lang/de.php @@ -0,0 +1,39 @@ + + * @package weekly + * @version $Id + * @link http://www.zentao.net + */ +$lang->weekly->common = 'Project Weekly'; +$lang->weekly->index = 'Weekly Overview'; +$lang->weekly->progress = 'Progress'; +$lang->weekly->workload = 'Workload'; +$lang->weekly->total = 'Total'; + +$lang->weekly->reportTtitle = 'Project: % s Weekly (Week % s)'; +$lang->weekly->summary = 'Project Progress'; +$lang->weekly->finished = 'Work finished this week (100% completed work)'; +$lang->weekly->postponed = 'Work unfinished this week'; +$lang->weekly->nextWeek = 'Work planned for next week'; +$lang->weekly->workloadByType = 'Workload Summary'; + +$lang->weekly->term = 'Reporting Cycle'; +$lang->weekly->project = 'Project Name'; +$lang->weekly->master = 'Project Manager '; +$lang->weekly->staff = 'The number of men in this week'; + +$lang->weekly->weekDesc = 'Week % s (% s ~% s)'; +$lang->weekly->progress = 'Progress of the project'; +$lang->weekly->analysisResult = 'Analysis'; +$lang->weekly->cost = 'Project Cost'; + +$lang->weekly->pv = 'Planned Value(PV)'; +$lang->weekly->ev = 'Earned Value(EV)'; +$lang->weekly->ac = 'Actual Cost(AC)'; +$lang->weekly->sv = 'Schedule Variance(SV%)'; +$lang->weekly->cv = 'Cost Variance(CV%)'; diff --git a/module/weekly/lang/en.php b/module/weekly/lang/en.php new file mode 100644 index 0000000000..f665865f2e --- /dev/null +++ b/module/weekly/lang/en.php @@ -0,0 +1,39 @@ + + * @package weekly + * @version $Id + * @link http://www.zentao.net + */ +$lang->weekly->common = 'Project Weekly'; +$lang->weekly->index = 'Weekly Overview'; +$lang->weekly->progress = 'Progress'; +$lang->weekly->workload = 'Workload'; +$lang->weekly->total = 'Total'; + +$lang->weekly->reportTtitle = 'Project: % s Weekly (Week % s)'; +$lang->weekly->summary = 'Project Progress'; +$lang->weekly->finished = 'Work finished this week (100% completed work)'; +$lang->weekly->postponed = 'Work unfinished this week'; +$lang->weekly->nextWeek = 'Work planned for next week'; +$lang->weekly->workloadByType = 'Workload Summary'; + +$lang->weekly->term = 'Reporting Cycle'; +$lang->weekly->project = 'Project Name'; +$lang->weekly->master = 'Project Manager '; +$lang->weekly->staff = 'The number of men in this week'; + +$lang->weekly->weekDesc = 'Week % s (% s ~% s)'; +$lang->weekly->progress = 'Progress of the project'; +$lang->weekly->analysisResult = 'Analysis'; +$lang->weekly->cost = 'Project Cost'; + +$lang->weekly->pv = 'Planned Value(PV)'; +$lang->weekly->ev = 'Earned Value(EV)'; +$lang->weekly->ac = 'Actual Cost(AC)'; +$lang->weekly->sv = 'Schedule Variance(SV%)'; +$lang->weekly->cv = 'Cost Variance(CV%)'; diff --git a/module/weekly/lang/fr.php b/module/weekly/lang/fr.php new file mode 100644 index 0000000000..f665865f2e --- /dev/null +++ b/module/weekly/lang/fr.php @@ -0,0 +1,39 @@ + + * @package weekly + * @version $Id + * @link http://www.zentao.net + */ +$lang->weekly->common = 'Project Weekly'; +$lang->weekly->index = 'Weekly Overview'; +$lang->weekly->progress = 'Progress'; +$lang->weekly->workload = 'Workload'; +$lang->weekly->total = 'Total'; + +$lang->weekly->reportTtitle = 'Project: % s Weekly (Week % s)'; +$lang->weekly->summary = 'Project Progress'; +$lang->weekly->finished = 'Work finished this week (100% completed work)'; +$lang->weekly->postponed = 'Work unfinished this week'; +$lang->weekly->nextWeek = 'Work planned for next week'; +$lang->weekly->workloadByType = 'Workload Summary'; + +$lang->weekly->term = 'Reporting Cycle'; +$lang->weekly->project = 'Project Name'; +$lang->weekly->master = 'Project Manager '; +$lang->weekly->staff = 'The number of men in this week'; + +$lang->weekly->weekDesc = 'Week % s (% s ~% s)'; +$lang->weekly->progress = 'Progress of the project'; +$lang->weekly->analysisResult = 'Analysis'; +$lang->weekly->cost = 'Project Cost'; + +$lang->weekly->pv = 'Planned Value(PV)'; +$lang->weekly->ev = 'Earned Value(EV)'; +$lang->weekly->ac = 'Actual Cost(AC)'; +$lang->weekly->sv = 'Schedule Variance(SV%)'; +$lang->weekly->cv = 'Cost Variance(CV%)'; diff --git a/module/weekly/lang/vi.php b/module/weekly/lang/vi.php new file mode 100644 index 0000000000..f665865f2e --- /dev/null +++ b/module/weekly/lang/vi.php @@ -0,0 +1,39 @@ + + * @package weekly + * @version $Id + * @link http://www.zentao.net + */ +$lang->weekly->common = 'Project Weekly'; +$lang->weekly->index = 'Weekly Overview'; +$lang->weekly->progress = 'Progress'; +$lang->weekly->workload = 'Workload'; +$lang->weekly->total = 'Total'; + +$lang->weekly->reportTtitle = 'Project: % s Weekly (Week % s)'; +$lang->weekly->summary = 'Project Progress'; +$lang->weekly->finished = 'Work finished this week (100% completed work)'; +$lang->weekly->postponed = 'Work unfinished this week'; +$lang->weekly->nextWeek = 'Work planned for next week'; +$lang->weekly->workloadByType = 'Workload Summary'; + +$lang->weekly->term = 'Reporting Cycle'; +$lang->weekly->project = 'Project Name'; +$lang->weekly->master = 'Project Manager '; +$lang->weekly->staff = 'The number of men in this week'; + +$lang->weekly->weekDesc = 'Week % s (% s ~% s)'; +$lang->weekly->progress = 'Progress of the project'; +$lang->weekly->analysisResult = 'Analysis'; +$lang->weekly->cost = 'Project Cost'; + +$lang->weekly->pv = 'Planned Value(PV)'; +$lang->weekly->ev = 'Earned Value(EV)'; +$lang->weekly->ac = 'Actual Cost(AC)'; +$lang->weekly->sv = 'Schedule Variance(SV%)'; +$lang->weekly->cv = 'Cost Variance(CV%)'; diff --git a/module/weekly/lang/zh-cn.php b/module/weekly/lang/zh-cn.php new file mode 100644 index 0000000000..d0fc86ef86 --- /dev/null +++ b/module/weekly/lang/zh-cn.php @@ -0,0 +1,39 @@ + + * @package weekly + * @version $Id + * @link http://www.zentao.net + */ +$lang->weekly->common = '项目周报'; +$lang->weekly->index = '周报总览'; +$lang->weekly->progress = '完成百分比'; +$lang->weekly->workload = '工作量'; +$lang->weekly->total = '合计'; + +$lang->weekly->reportTtitle = '项目: %s 周报(第 %s 周)'; +$lang->weekly->summary = '项目进展状况'; +$lang->weekly->finished = '本周工作完成情况(100%完成的工作)'; +$lang->weekly->postponed = '本周未完成工作'; +$lang->weekly->nextWeek = '下周工作计划'; +$lang->weekly->workloadByType = '工作量统计'; + +$lang->weekly->term = '报告周期'; +$lang->weekly->project = '项目名称'; +$lang->weekly->master = '项目经理 '; +$lang->weekly->staff = '本周投入人数'; + +$lang->weekly->weekDesc = '第 %s 周( %s ~ %s)'; +$lang->weekly->progress = '项目当前进展状况'; +$lang->weekly->analysisResult = '分析结果'; +$lang->weekly->cost = '项目成本'; + +$lang->weekly->pv = '计划完成的工作(PV)'; +$lang->weekly->ev = '实际完成的工作(EV)'; +$lang->weekly->ac = '实际花费的成本(AC)'; +$lang->weekly->sv = '进度偏差率(SV%)'; +$lang->weekly->cv = '成本偏差率(CV%)'; diff --git a/module/weekly/lang/zh-tw.php b/module/weekly/lang/zh-tw.php new file mode 100644 index 0000000000..20f44ed51f --- /dev/null +++ b/module/weekly/lang/zh-tw.php @@ -0,0 +1,39 @@ + + * @package weekly + * @version $Id + * @link http://www.zentao.net + */ +$lang->weekly->common = '項目周報'; +$lang->weekly->index = '周報總覽'; +$lang->weekly->progress = '完成百分比'; +$lang->weekly->workload = '工作量'; +$lang->weekly->total = '合計'; + +$lang->weekly->reportTtitle = '項目: %s 周報(第 %s 周)'; +$lang->weekly->summary = '項目進展狀況'; +$lang->weekly->finished = '本週工作完成情況(100%完成的工作)'; +$lang->weekly->postponed = '本週未完成工作'; +$lang->weekly->nextWeek = '下周工作計劃'; +$lang->weekly->workloadByType = '工作量統計'; + +$lang->weekly->term = '報告周期'; +$lang->weekly->project = '項目名稱'; +$lang->weekly->master = '項目經理 '; +$lang->weekly->staff = '本週投入人數'; + +$lang->weekly->weekDesc = '第 %s 周( %s ~ %s)'; +$lang->weekly->progress = '項目當前進展狀況'; +$lang->weekly->analysisResult = '分析結果'; +$lang->weekly->cost = '項目成本'; + +$lang->weekly->pv = '計劃完成的工作(PV)'; +$lang->weekly->ev = '實際完成的工作(EV)'; +$lang->weekly->ac = '實際花費的成本(AC)'; +$lang->weekly->sv = '進度偏差率(SV%)'; +$lang->weekly->cv = '成本偏差率(CV%)'; diff --git a/module/weekly/model.php b/module/weekly/model.php new file mode 100644 index 0000000000..349231ae93 --- /dev/null +++ b/module/weekly/model.php @@ -0,0 +1,524 @@ + + * @package weekly + * @version $Id$ + * @link http://www.zentao.net + */ +class weeklyModel extends model +{ + /** + * GetPageNav + * + * @param int $project + * @param int $date + * @access public + * @return string + */ + public function getPageNav($project, $date) + { + $date = date('Ymd', strtotime($this->getThisMonday($date))); + $begin = $project->begin; + $weeks = $this->getWeekPairs($begin); + $current = zget($weeks, $date, ''); + $selectHtml = "
    "; + $selectHtml .= html::a('###', $this->lang->weekly->common . $this->lang->colon . $project->name, '', "class='btn'"); + $selectHtml .= '
    '; + + $selectHtml .= "
    "; + $selectHtml .= "
    "; + $selectHtml .= "" . $current . " "; + $selectHtml .= "
    '; + return $selectHtml; + + } + + /** + * GetWeekPairs + * + * @param int $begin + * @access public + * @return array + */ + public function getWeekPairs($begin) + { + $sn = $this->getWeekSN($begin, date('Y-m-d')); + $weeks = array(); + for($i = 0; $i <= $sn; $i++) + { + $monday = $this->getThisMonday($begin); + $sunday = $this->getThisSunday($begin); + $begin = date('Y-m-d', strtotime("$begin +7 days")); + $key = date('Ymd', strtotime($monday)); + $weeks[$key] = sprintf($this->lang->weekly->weekDesc, $i + 1, $monday, $sunday); + } + krsort($weeks); + return $weeks; + } + + /** + * GetFromDB + * + * @param int $project + * @param int $date + * @access public + * @return object + */ + public function getFromDB($project, $date) + { + $monday = $this->getThisMonday($date); + return $this->dao->select('*') + ->from(TABLE_WEEKLYREPORT) + ->where('weekStart')->eq($monday) + ->andWhere('project')->eq($project) + ->fetch(); + } + + /** + * Save data. + * + * @param int $project + * @param int $date + * @access public + * @return void + */ + public function save($project, $date) + { + $report = new stdclass; + $report->pv = $this->getPV($project, $date); + $report->ev = $this->getEV($project, $date); + $report->ac = $this->getAC($project, $date); + $report->sv = $this->getSV($report->ev, $report->pv); + $report->cv = $this->getCV($report->ev, $report->ac); + $report->project = $project; + $report->weekStart = $this->getThisMonday($date); + $report->staff = $this->getStaff($project); + $report->workload = json_encode($this->getWorkloadByType($project, $date)); + $this->dao->replace(TABLE_WEEKLYREPORT)->data($report)->exec(); + } + + /** + * GetWeekSN + * + * @param int $begin + * @param int $date + * @access public + * @return int + */ + public function getWeekSN($begin, $date) + { + return ceil((strtotime($date) - strtotime($begin)) / 7 / 86400); + } + + /** + * Get monday for a date. + * + * @param int $date + * @access public + * @return date + */ + public function getThisMonday($date) + { + $day = date('w', strtotime($date)); + if($day == 0) $day = 7; + $days = $day - 1; + return date('Y-m-d', strtotime("$date - $days days")); + } + + /** + * GetThisSunday + * + * @param int $date + * @access public + * @return date + */ + public function getThisSunday($date) + { + $monday = $this->getThisMonday($date); + return date('Y-m-d', strtotime("$monday +6 days")); + } + + /** + * GetLastDay + * + * @param int $date + * @access public + * @return string + */ + public function getLastDay($date) + { + $this->loadModel('project'); + $weekend = zget($this->config->project, 'weekend', 2); + $monday = $this->getThisMonday($date); + $sunday = $this->getThisSunday($date); + $workdays = $this->loadModel('holiday')->getActualWorkingDays($monday, $sunday); + return end($workdays); + } + + /** + * GetStaff + * + * @param int $project + * @param string $date + * @access public + * @return array + */ + public function getStaff($project, $date = '') + { + if(!$date) $date = date('Y-m-d'); + $monday = $this->getThisMonday($date); + $sunday = $this->getThisSunday($date); + $executions = $this->loadModel('execution')->getList($project, 'all', 'all', 0, 0, 0); + $executionIdList = array_keys($executions); + return $this->dao->select('count(distinct account) as count') + ->from(TABLE_EFFORT) + ->where('objectType')->eq('task') + ->andWhere('execution')->in($executionIdList) + ->andWhere('date')->ge($monday) + ->andWhere('date')->le($sunday) + ->fetch('count'); + } + + /** + * GetFinished + * + * @param int $project + * @param string $date + * @param int $pager + * @access public + * @return void + */ + public function getFinished($project, $date = '', $pager = null) + { + if(!$date) $date = date('Y-m-d'); + $monday = $this->getThisMonday($date); + $sunday = $this->getThisSunday($date); + + $executions = $this->loadModel('execution')->getList($project, 'all', $status = 'all', $limit = 0, $productID = 0, $branch = 0); + $executionIdList = array_keys($executions); + + $tasks = $this->dao->select('*') + ->from(TABLE_TASK) + ->where('execution')->in($executionIdList) + ->andWhere("(status='done' or closedReason= 'done')") + ->andWhere('finishedDate')->ge($monday) + ->andWhere('finishedDate')->le($sunday) + ->fetchAll(); + return $this->loadModel('task')->processTasks($tasks); + } + + /** + * GetPostponed + * + * @param int $project + * @param string $date + * @access public + * @return void + */ + public function getPostponed($project, $date = '') + { + if(!$date) $date = date('Y-m-d'); + $monday = $this->getThisMonday($date); + $sunday = $this->getThisSunday($date); + $nextMonday = date('Y-m-d', strtotime("$sunday +1 days")); + + $executions = $this->loadModel('execution')->getList($project, 'all', $status = 'all', $limit = 0, $productID = 0, $branch = 0); + $executionIdList = array_keys($executions); + $unFinished = $this->dao->select('*') + ->from(TABLE_TASK) + ->where('execution')->in($executionIdList) + ->andWhere('status')->in('wait,doing,pause') + ->andWhere('deadline')->ge($monday) + ->andWhere('deadline')->le($sunday) + ->fetchAll('id'); + + $postponed = $this->dao->select('*') + ->from(TABLE_TASK) + ->where('execution')->in($executionIdList) + ->andWhere('finishedDate')->gt($nextMonday) + ->andWhere('deadline')->ge($monday) + ->andWhere('deadline')->lt($nextMonday) + ->fetchAll('id'); + + $tasks = array_merge($unFinished, $postponed); + return $this->loadModel('task')->processTasks($tasks); + } + + /** + * GetTasksOfNextWeek + * + * @param int $project + * @param string $date + * @access public + * @return void + */ + public function getTasksOfNextWeek($project, $date = '') + { + if(!$date) $date = date('Y-m-d'); + $sunday = $this->getThisSunday($date); + $nextMonday = date('Y-m-d', strtotime("$sunday +1 days")); + $sencondMondy = date('Y-m-d', strtotime("$sunday +8 days")); + + $executions = $this->loadModel('execution')->getList($project, 'all', $status = 'all', $limit = 0, $productID = 0, $branch = 0); + $executionIdList = array_keys($executions); + + $tasks = $this->dao->select('*') + ->from(TABLE_TASK) + ->where('execution')->in($executionIdList) + ->andWhere("((deadline > '$nextMonday' and deadline < '$sencondMondy') or (estStarted > '$nextMonday' and estStarted < '$sencondMondy'))") + ->fetchAll('id'); + + return $this->loadModel('task')->processTasks($tasks); + } + + /** + * GetWorkloadByType + * + * @param int $project + * @param string $date + * @access public + * @return object + */ + public function getWorkloadByType($project, $date = '') + { + if(!$date) $date = date('Y-m-d'); + + $sunday = $this->getThisSunday($date); + $nextMonday = date('Y-m-d', strtotime("$sunday +1 days")); + $sencondMondy = date('Y-m-d', strtotime("$sunday +8 days")); + + $executions = $this->loadModel('execution')->getList($project, 'all', $status = 'all', $limit = 0, $productID = 0, $branch = 0); + $executionIdList = array_keys($executions); + + return $this->dao->select('type, sum(cast(estimate as decimal(10,2))) as workload') + ->from(TABLE_TASK) + ->where('execution')->in($executionIdList) + ->groupBy('type') + ->fetchPairs(); + } + + /** + * GetPlanedTaskByWeek + * + * @param int $project + * @param string $date + * @access public + * @return array + */ + public function getPlanedTaskByWeek($project, $date = '') + { + if(!$date) $date = date('Y-m-d'); + $monday = $this->getThisMonday($date); + $nextMonday = date('Y-m-d', strtotime("$monday +7 days")); + + $executions = $this->loadModel('execution')->getList($status = 'all', $limit = 0, $productID = 0, $branch = 0, $project); + $executionIdList = array_keys($executions); + + return $this->dao->select('*') + ->from(TABLE_TASK) + ->where('execution')->in($executionIdList) + ->andWhere('deadline')->ge($monday) + ->fetchAll('id'); + } + + /** + * GetPV + * + * @param int $project + * @param string $date + * @access public + * @return int + */ + public function getPV($projectID, $date = '') + { + $report = $this->getFromDB($projectID, $date); + if(!empty($report)) return $report->pv; + + if(!$date) $date = date('Y-m-d'); + $monday = $this->getThisMonday($date); + $sunday = $this->getThisSunday($date); + $lastDay = $this->getLastDay($date); + $nextMonday = date('Y-m-d', strtotime("$sunday +1 days")); + $workdays = $this->loadModel('holiday')->getActualWorkingDays($monday, $sunday); + + $executions = $this->loadModel('execution')->getList($projectID); + $executionIdList = array_keys($executions); + + $tasks = $this->dao->select('*')->from(TABLE_TASK) + ->where('execution')->in($executionIdList) + ->andWhere("(estStarted < '$nextMonday' or estStarted='0000-00-00')") + ->fetchAll('id'); + + $PV = 0; + foreach($tasks as $task) + { + if($task->estStarted == '0000-00-00') $task->estStarted = date('Y-m-d', strtotime($task->openedDate)); + if($task->deadline < $nextMonday) + { + $PV += $task->estimate; + continue; + } + + $fullDays = $this->loadModel('holiday')->getActualWorkingDays($task->estStarted, $task->deadline); + $passedDays = $this->loadModel('holiday')->getActualWorkingDays($task->estStarted, $sunday); + + if(empty($fullDays) or empty($passedDays) or empty($task->estimate)) continue; + $PV += count($passedDays) * $task->estimate / count($fullDays); + } + + return round($PV, 2); + } + + /** + * Get EV data. + * + * @param int $projectID + * @param string $date + * @access public + * @return int + */ + public function getEV($projectID, $date = '') + { + $report = $this->getFromDB($projectID, $date); + if(!empty($report)) return $report->ev; + + $executions = $this->loadModel('execution')->getList($projectID); + $executionIdList = array_keys($executions); + + if(!$date) $date = date('Y-m-d'); + $monday = $this->getThisMonday($date); + $sunday = $this->getThisSunday($date); + $lastDay = $this->getLastDay($date); + $nextMonday = date('Y-m-d', strtotime("$sunday +1 days")); + + $tasks = $this->dao->select('*') + ->from(TABLE_TASK) + ->where('execution')->in($executionIdList) + ->andWhere('consumed')->gt(0) + ->andWhere('status')->ne('cancel') + ->fetchAll('id'); + + $EV = 0; + foreach($tasks as $task) + { + if($task->status == 'done' or $task->closedReason == 'done') + { + $EV += $task->estimate; + } + else + { + $task->progress = round($task->consumed / ($task->consumed + $task->left), 2) * 100; + $EV += $task->estimate * $task->progress / 100; + } + } + return round($EV, 2); + } + + /** + * Get AC data. + * + * @param int $project + * @param string $date + * @access public + * @return int + */ + public function getAC($project, $date = '') + { + $report = $this->getFromDB($project, $date); + if(!empty($report)) return $report->ac; + + if(!$date) $date = date('Y-m-d'); + + $monday = $this->getThisMonday($date); + $nextMonday = date('Y-m-d', strtotime("$monday +7 days")); + $executions = $this->loadModel('execution')->getList($project, 'all', 'all', 0, 0, 0); + $executionIdList = array_keys($executions); + + if(isset($this->config->proVersion)) + { + $AC = $this->dao->select('sum(consumed) as consumed') + ->from(TABLE_EFFORT) + ->where('objectType')->eq('task') + ->andWhere('execution')->in($executionIdList) + ->andWhere('date')->ge($monday) + ->andWhere('date')->lt($nextMonday) + ->fetch('consumed'); + } + else + { + $taskIdList = $this->dao->select('id')->from(TABLE_TASK)->where('execution')->in($executionIdList)->fetchPairs(); + $AC = $this->dao->select('sum(consumed) as consumed') + ->from(TABLE_TASKESTIMATE) + ->where('task')->in($taskIdList) + ->andWhere('date')->ge($monday) + ->andWhere('date')->lt($nextMonday) + ->fetch('consumed'); + } + + return round($AC, 2); + } + + /** + * Get SV data. + * + * @param int $ev + * @param int $pv + * @access public + * @return int + */ + public function getSV($ev, $pv) + { + if($pv == 0) return 0; + $sv = -1 * (1- ($ev / $pv)); + return number_format($sv * 100, 2); + } + + /** + * GetCV + * + * @param int $ev + * @param int $ac + * @access public + * @return int + */ + public function getCV($ev, $ac) + { + if($ac == 0) return 0; + $cv = -1 * (1 - ($ev / $ac)); + return number_format($cv * 100, 2); + } + + /** + * GetTips + * + * @param string $type + * @param int $data + * @access public + * @return string + */ + public function getTips($type = 'progress', $data = 0) + { + $this->app->loadConfig('custom'); + if($type == 'progress') $tipsConfig = isset($this->config->custom->SV->progressTip) ? $this->config->custom->SV->progressTip : ''; + if($type == 'cost') $tipsConfig = isset($this->config->custom->CV->costTip) ? $this->config->custom->CV->costTip : ''; + + if(empty($tipsConfig)) return ''; + + $tipsConfig = json_decode($tipsConfig); + foreach($tipsConfig as $tipConfig) + { + if($tipConfig->min <= $data and $tipConfig->max >= $data) return $tipConfig->tip; + } + + return ''; + } +} diff --git a/module/weekly/view/index.html.php b/module/weekly/view/index.html.php new file mode 100644 index 0000000000..8158c426a0 --- /dev/null +++ b/module/weekly/view/index.html.php @@ -0,0 +1,186 @@ + + * @package ZenTaoPMS + * @version $Id: index.html.php 5094 2013-07-10 08:46:15Z chencongzhi520@gmail.com $ + */ +?> + +',n={zh_cn:{errorTip:"不是有效的颜色值"},zh_tw:{errorTip:"不是有效的顏色值"},en:{errorTip:"Not a valid color value"}},o=function(i,n){this.name=e,this.$=t(i),this.getOptions(n),this.init()};o.prototype.init=function(){var e=this,n=e.options,o=e.$,a=o.parent(),s=!1;a.hasClass("colorpicker")?e.$picker=a:(e.$picker=t(n.template||i),s=!0),e.$picker.addClass(n.wrapper).find(".cp-title").toggle(void 0!==n.title).text(n.title),e.$menu=e.$picker.find(".dropdown-menu").toggleClass("pull-right",n.pullMenuRight),e.$btn=e.$picker.find(".btn.dropdown-toggle"),e.$btn.find(".ic").addClass("icon-"+n.icon),n.btnTip&&e.$picker.attr("data-toggle","tooltip").tooltip({title:n.btnTip,placement:n.tooltip,container:"body"}),o.attr("data-provide",null),s&&o.after(e.$picker),e.colors={},t.each(n.colors,function(i,n){if(t.zui.Color.isColor(n)){var o=new t.zui.Color(n);e.colors[o.toCssStr()]=o}}),e.updateColors(),e.$picker.on("click",".cp-tile",function(){e.setValue(t(this).data("color"))});var r=function(){var i=o.val(),a=t.zui.Color.isColor(i);o.parent().toggleClass("has-error",!(a||n.optional&&""===i)),a?e.setValue(i,!0):n.optional&&""===i?o.tooltip("hide"):o.is(":focus")||o.tooltip("show",n.errorTip)};o.is("input:not([type=hidden])")?(n.tooltip&&o.attr("data-toggle","tooltip").tooltip({trigger:"manual",placement:n.tooltip,tipClass:"tooltip-danger",container:"body"}),o.on("keyup paste input change",r)):o.appendTo(e.$picker),r()},o.prototype.addColor=function(e){e instanceof t.zui.Color||(e=new t.zui.Color(e));var i=e.toCssStr(),n=this.options;this.colors[i]||(this.colors[i]=e);var o=t('',{titile:e}).data("color",e).css({color:e.contrast().toCssStr(),background:i,"border-color":e.luma()>.43?"#ccc":"transparent"}).attr("data-color",i);this.$menu.append(t("
  • ").css({width:n.tileSize,height:n.tileSize}).append(o)),n.optional&&this.$menu.find(".cp-tile.empty").parent().detach().appendTo(this.$menu)},o.prototype.updateColors=function(e){var i=this.$menu,n=this.options,e=e||this.colors,o=this,a=0;if(i.children("li:not(.heading)").remove(),t.each(e,function(t,e){o.addColor(e),a++}),n.optional){var s=t('
  • ').css({width:n.tileSize,height:n.tileSize});this.$menu.append(s),a++}i.css("width",Math.min(a,n.lineCount)*n.tileSize+6)},o.prototype.setValue=function(e,i){var n=this,o=n.options,a=n.$btn,s="";n.$menu.find(".cp-tile.active").removeClass("active");var r=o.updateBtn;if("auto"===r){var l=a.find(".color-bar");r=!l.length||function(t){l.css("background",t||"")}}if(e){var h=new t.zui.Color(e);s=h.toCssStr().toLowerCase(),r&&("function"==typeof r?r(s,a,n):a.css({background:s,color:h.contrast().toCssStr(),borderColor:h.luma()>.43?"#ccc":s})),n.colors[s]||n.addColor(h),i||n.$.val().toLowerCase()===s||n.$.val(s).trigger("change"),n.$menu.find('.cp-tile[data-color="'+s+'"]').addClass("active"),n.$.tooltip("hide"),n.$.trigger("colorchange",h)}else r&&("function"==typeof r?r(null,a,n):a.attr("style",null)),i||""===n.$.val()||n.$.val(s).trigger("change"),o.optional&&n.$.tooltip("hide"),n.$menu.find(".cp-tile.empty").addClass("active"),n.$.trigger("colorchange",null);o.updateBorder&&t(o.updateBorder).css("border-color",s),o.updateBackground&&t(o.updateBackground).css("background-color",s),o.updateColor&&t(o.updateColor).css("color",s),o.updateText&&t(o.updateText).text(s)},o.prototype.getOptions=function(i){var a=t.extend({},o.DEFAULTS,this.$.data(),i);"string"==typeof a.colors&&(a.colors=a.colors.split(","));var s=a.lang||t.zui.clientLang(),r=this.lang=t.zui.getLangData?t.zui.getLangData(e,s,n):n[s]||n.en;a.errorTip||(a.errorTip=r.errorTip),t.fn.tooltip||(a.btnTip=!1),this.options=a},o.DEFAULTS={colors:["#00BCD4","#388E3C","#3280fc","#3F51B5","#9C27B0","#795548","#F57C00","#F44336","#E91E63"],pullMenuRight:!0,wrapper:"btn-wrapper",tileSize:30,lineCount:5,optional:!0,tooltip:"top",icon:"caret-down",updateBtn:"auto"},o.LANG=n,t.fn.colorPicker=function(e){return this.each(function(){var i=t(this),n=i.data(name),a="object"==typeof e&&e;n||i.data(name,n=new o(this,a)),"string"==typeof e&&n[e]()})},t.fn.colorPicker.Constructor=o,t(function(){t('[data-provide="colorpicker"]').colorPicker()})}(jQuery),function(t,e){function i(t){return t===e&&(t=o+=1),a[t%a.length]}function n(e,i){var n=t(e);i=t.extend({percent:0,size:20,backColor:"#eee",color:"#00da88",borderColor:"#ccc",borderSize:1,rotate:-90,doughnut:8},n.data(),i);var o=i.percent,a=i.size;"string"==typeof o&&(o=Number.parseFloat(o,10)),"string"==typeof a&&(a=Number.parseFloat(a,10)),a=Math.floor(a);var s=a/2,r=3.14*s,l="http://www.w3.org/2000/svg",h=document.createElementNS(l,"svg"),c=document.createElementNS(l,"circle"),d=document.createElementNS(l,"circle"),u=document.createElementNS(l,"circle");d.setAttribute("r",s),d.setAttribute("cx",s),d.setAttribute("cy",s),d.setAttribute("fill",i.backColor),d.setAttribute("stroke",i.borderColor),d.setAttribute("stroke-width",i.borderSize),u.setAttribute("r",i.doughnut),u.setAttribute("cx",s),u.setAttribute("cy",s),u.setAttribute("fill",i.backColor),c.setAttribute("r",s/2),c.setAttribute("cx",s),c.setAttribute("cy",s),c.setAttribute("fill","transparent"),c.setAttribute("stroke-dasharray",(o*r/100).toFixed(1)+" "+r),c.setAttribute("stroke-width",s),c.setAttribute("stroke",i.color),c.setAttribute("transform","rotate(-90) translate(-"+a+")"),h.setAttribute("viewBox","0 0 "+a+" "+a),h.setAttribute("width",a),h.setAttribute("height",a),h.setAttribute("transform","rotate(180)"),h.appendChild(d),h.appendChild(c),h.appendChild(u),n[0].appendChild(h);var f={width:a,height:a};"inline"===n.css("display")&&(f.display="inline-block",f.verticalAlign="middle"),n.css(f)}var o=0,a=["#00a9fc","#ff5d5d","#fdc137","#00da88","#7ec5ff","#8666b8","#bd7b46","#ff9100","#ff3d00","#f57f17","#00e5ff","#00b0ff","#2979ff","#3d5afe","#651fff","#d500f9","#f50057","#ff1744"];jQuery.fn.tableChart=function(){t(this).each(function(){var e=t(this),n=e.data(),o=n.chart||"pie",a=t(n.target);if(a.length){var s=null;if("pie"===o){n=t.extend({scaleShowLabels:!0,scaleLabel:"<%=label%>: <%=value%>"},n);var r=[],l=e.find("tbody > tr").each(function(e){var n=t(this),o=i();n.attr("data-id",e).find(".chart-color-dot").css("background",o),r.push({label:n.find(".chart-label").text(),value:parseFloat(n.data("value")||n.find(".chart-value").text()),color:o,id:e})});r.length>1?n.scaleLabelPlacement="outside":1===r.length&&(n.scaleLabelPlacement="inside",r.push({label:"",value:r[0].value/2e3,color:"#fff",showLabel:!1})),s=a.pieChart(r,n),a.on("mousemove",function(t){var e=s.getSegmentsAtEvent(t);l.removeClass("active"),e.length&&l.filter('[data-id="'+e[0].id+'"]').addClass("active")})}else if("bar"===o){var h=i(),c=[],d={label:e.find("thead .chart-label").text(),color:h,data:[]},l=e.find("tbody > tr").each(function(e){var i=t(this);c.push(i.find(".chart-label").text()),d.data.push(i.data("value")||parseFloat(i.find(".chart-value").text())),i.find(".chart-color-dot").css("background",h)}),r={labels:c,datasets:[d]};c.length&&(n.barValueSpacing=5),s=a.barChart(r,n)}else if("line"===o){var h=i(),c=[],d={label:e.find("thead .chart-label").text(),color:h,data:[]},l=e.find("tbody > tr").each(function(e){var i=t(this);c.push(i.find(".chart-label").text()),d.data.push(parseInt(i.find(".chart-value").text())),i.find(".chart-color-dot").css("background",h)}),r={labels:c,datasets:[d]};c.length&&(n.barValueSpacing=5),s=a.lineChart(r,n)}null!==s&&e.data("zui.chart",s)}})},t(".table-chart").tableChart();var s=function(i,n){var o=t(i);if(!o.data("pieChart")){var a=o.is("canvas")?o:o.find("canvas"),s=t.extend({value:0,color:t.getThemeColor("primary")||"#006af1",backColor:t.getThemeColor("pale")||"#E9F2FB",doughnut:!0,doughnutSize:85,width:20,height:20,showTip:!1,name:"",tipTemplate:"<%=value%>%",animation:"auto",realValue:parseFloat(o.find(".progress-value").text())},n,o.data()),r=a.length;r||(a=t("").appendTo(o)),a.attr("width")!==e?s.width=a.width():a.attr("width",s.width),a.attr("height")!==e?s.height=a.height():a.attr("height",s.height),r||8!=t.zui.browser.ie||G_vmlCanvasManager.initElement(a[0]),"auto"===s.animation&&(s.animation=s.width>30),o.addClass("progress-pie-"+s.width).css({width:s.width,height:s.height}),s.value=Math.max(0,Math.min(100,s.value));var l=[{value:s.value,label:s.name,color:s.color,circleBeginEnd:!0},{value:100-s.value,label:"",color:s.backColor}],h=a[s.doughnut?"doughnutChart":"pieChart"](l,t.extend({segmentShowStroke:!1,animation:s.animation,showTooltips:s.showTip,tooltipTemplate:s.tipTemplate,percentageInnerCutout:s.doughnutSize,reverseDrawOrder:!0,animationEasing:"easeInOutQuart",onAnimationProgress:s.realValue?function(t){o.find(".progress-value").text(Math.floor(s.realValue*t))}:e,onAnimationComplete:s.realValue?function(t){o.find(".progress-value").text(s.realValue)}:e},s.chartOptions));o.data("pieChart",h)}};jQuery.fn.progressPie=function(e){t(this).each(function(){var i=t(this);if(!i.is(":hidden")&&!i.closest(".hidden,.datatable-origin").length){var n=i.closest(".tab-pane");n.length&&!n.hasClass("active")?t('[data-toggle="tab"][data-target="#'+n.attr("id")+'"]').one("shown.zui.tab",function(){s(i,e)}):s(this,e)}})},t.fn.pieIcon=function(e){t(this).each(function(){n(this,e)})},t(function(){t(".table-chart").tableChart();var e=t(".progress-pie:visible");e.length<100&&t(".progress-pie:visible").progressPie(),setTimeout(function(){t(".progress-pie:visible").progressPie()},e.length>100?1e3:50),t(".pie-icon:visible").pieIcon()})}(jQuery,void 0),function(t){jQuery.fn.sparkline=function(e){t(this).each(function(){var i=t(this),n=t.extend({values:i.attr("values"),width:i.width()-4,height:i.height()-4},i.data(),e),o=n.height,a=[],s=n.width,r=n.values.split(","),l=0;for(var h in r){var c=parseFloat(r[h]);NaN!=c&&(a.push(c),l=Math.max(c,l))}var d=(Math.min(l,30),Math.min(s,Math.max(10,a.length*s/30))),u=i.children("canvas");u.length||(i.append(''),u=i.children("canvas")),u.attr("width",d).attr("height",o);var f={labels:a,datasets:[{fillColor:t.getThemeColor("pale")||"rgba(0,0,255,0.05)",strokeColor:t.getThemeColor("primary")||"#0054EC",pointColor:t.getThemeColor("secondary")||"rgba(255,136,0,1)",pointStrokeColor:"#fff",data:a}]},p={animation:!0,scaleOverride:!0,scaleStepWidth:Math.ceil(l/10),scaleSteps:10,scaleStartValue:0,showScale:!1,showTooltips:!1,pointDot:!1,scaleShowGridLines:!1,datasetStrokeWidth:1},g=t(u).lineChart(f,p);i.data("sparklineChart",g)})},t(function(){t(".sparkline").sparkline()})}(jQuery),function(t){t.fn.fixedDate=function(){return t(this).each(function(){var e=t(this).attr("autocomplete","off");"0000-00-00"==e.val()&&e.focus(function(){"0000-00-00"==e.val()&&e.val("").datetimepicker("update")}).blur(function(){""==e.val()&&e.val("0000-00-00")})})},window.datepickerOptions={language:t("html").attr("lang"),weekStart:1,todayBtn:1,autoclose:1,todayHighlight:1,startView:2,forceParse:0,showMeridian:1,format:"yyyy-mm-dd hh:ii",startDate:"1970-1-1"},t.extend(t.fn.datetimepicker.defaults,window.datepickerOptions),t(function(){var e={minView:2,format:"yyyy-mm-dd"},i={startView:1,minView:0,maxView:1,format:"hh:ii"},n={minView:3,startView:3,format:"yyyy-mm"};t(".datepicker-wrapper").click(function(){t(this).find(".form-date, .form-datetime, .form-time, .form-month").datetimepicker("show").focus()}),t.fn.datepicker=function(i){return this.datetimepicker(t.extend({},e,i))},t.fn.timepicker=function(e){return this.datetimepicker(t.extend({},i,e))},t.fn.monthpicker=function(e){return this.datetimepicker(t.extend({},n,e))},t.fn.datepickerAll=function(){return this.find(".form-datetime").fixedDate().datetimepicker(),this.find(".form-date").fixedDate().datepicker(),this.find(".form-time").fixedDate().timepicker(),this.find(".form-month").fixedDate().monthpicker(),this},t("body").datepickerAll()})}(jQuery),function(t){var e=function(e,i){i=t.extend({idStart:0,idEnd:9,chosen:!0,datetimepicker:!0,colorPicker:!0,hotkeys:!0},i,e.data());var n=e.find(".template");!n.length&&i.template&&(n=t(i.template));var o=0,a=0,s=function(t){t.is("select.chosen")?t.next(".chosen-container").find("input").focus():t.focus()},r=function(t){var i=e.find("[data-ctrl-index]:focus,.chosen-container-active").first();if(i.length){if(i.is(".chosen-container-active")){if(i.hasClass("chosen-with-drop")&&("down"===t||"up"===t))return;i=i.prev("select.chosen")}var n=i.data("ctrlIndex"),r=i.closest("tr").data("row");"down"===t?r0?r-=1:r=a-1:"left"===t?n>0?n-=1:n=o-1:"right"===t&&(n").html(s);return r.attr("data-row",e).addClass(n.attr("class")).removeClass("template"),i.rowCreator&&i.rowCreator(r,e,i),o?o.after(r):h.append(r),c(r),r};t.extend(l,{createRow:u,template:d});for(var f=i.idStart;f<=i.idEnd;++f)u(f)}else c(e);e.on("click",".btn-copy",function(){var e=t(this),i=t(e.data("copyFrom")).val(),n=t(e.data("copyTo")).val(i).addClass("highlight");setTimeout(function(){n.removeClass("highlight")},2e3)}),i.hotkeys&&t(document).on("keydown",function(t){var e={"Ctrl+#37":"left","Ctrl+#39":"right","#38":"up","#40":"down","Ctrl+#38":"up","Ctrl+#40":"down"},i=[];t.ctrlKey&&i.push("Ctrl"),i.push("#"+t.keyCode);var n=e[i.join("+")];n&&(r(n),t.ctrlKey&&(t.stopPropagation(),t.preventDefault()))}),e.data("zui.batchActionForm",l),setTimeout(t.fixTableResponsive,0)};t.fn.batchActionForm=function(i){return this.each(function(){e(t(this),i)})}}(jQuery),function(t,e){"use strict";var i="zui.table",n={zh_cn:{selectedItems:"已选择 {0} 项",attrTotal:"{0}总计 {1}"},zh_tw:{selectedItems:"已选择 {0} 项",attrTotal:"{0}总计 {1}"},en:{selectedItems:"Seleted {0} items",attrTotal:"{0} total {1}"},de:{selectedItems:"{0} ausgewählt",attrTotal:"{0} insgesamt {1}"},fr:{selectedItems:"{0} sélectionnés",attrTotal:"{0} total {1}"}},o=/^((?!chrome|android).)*safari/i.test(navigator.userAgent),a=t.zui.browser.isIE(),s=a?200:100,r=function(e,o){var a=this;a.name=i;var s=a.$=t(e);o=a.options=t.extend({},r.DEFAULTS,this.$.data(),o),a.langName=o.lang||t.zui.clientLang(),a.lang=t.zui.getLangData(i,a.langName,n),a.id=s.attr("id"),a.id||(a.id=o.id||"table-"+t.zui.uuid(),a.noID=!0,s.attr("id",a.id),o.hot&&console.warn("ZUI: table hot replace id not defined, the element id attribute should be set.")),s.attr("data-ride")||s.attr("data-ride","table");var l=a.getTable();if(l.length){l.find("thead>tr>th").each(function(){var e=t(this);if(!e.attr("title")){var i=t.trim(e.find("a:first").text()||e.text()||"");i.length&&e.attr("title",i)}}),o.nested&&a.initNestedList(),o.checkable&&(s.on("click",".check-all",function(){a.checkAll(!t(this).hasClass("checked"))}),o.checkOnClickRow&&s.on("click","tbody>tr",function(e){t(e.target).closest('.btn,a,.not-check,.form-control,input[type="text"],.chosen-container').length||a.checkRow(t(this))}),s.on("click",'tbody input[type="checkbox"],tbody label[for]',function(e){e.stopPropagation();var i=t(this);i.is("label")&&(i=i.closest(".checkbox-primary").find('input[type="checkbox"]')),a.checkRow(i.closest("tr"),i.is(":checked"))}),o.selectable&&s.selectable(t.extend({},{selector:a.isDataTable?".fixed-left tbody>tr":"tbody>tr",selectClass:"",trigger:"td.c-id",clickBehavior:"multi",listenClick:!1,start:function(){this.syncSelectionsFromClass()},select:function(e){a.checkRow(e.target,!0),t.cookie("ajax_dragSelected")||(t.cookie("ajax_dragSelected","on",{expires:config.cookieLife,path:config.webRoot}),t.ajaxSendScore("dragSelected"))},unselect:function(t){a.checkRow(t.target,!1)},rangeStyle:{border:"1px solid #006af1",backgroundColor:"rgba(50,128,252,0.2)",borderRadius:"2px"}},t.isPlainObject(o.selectable)?o.selectable:null)));var h=a.$form=s.is("form")?s:s.find("form");h.length&&(o.ajaxForm?h.ajaxForm(t.isPlainObject(o.ajaxForm)?o.ajaxForm:null):h.on("click","[data-form-action]",function(){h.attr("action",t(this).data("formAction")).submit()})),(o.fixFooter||o.fixHeader)&&(a.pageFooterHeight=t("#footer").outerHeight(),a.updateFixUI(!0),t(window).on("scroll resize",function(){a.updateFixUI()}).on("sidebar.toggle",function(){setTimeout(function(){a.updateFixUI()},200)})),o.group&&(s.on("click",".group-toggle",function(){a.toggleRowGroup(t(this).closest("tr").data("id"))}),t(document).on("click",".group-collapse-all",function(){a.toggleGroups(!1)}).on("click",".group-expand-all",function(){a.toggleGroups(!0)})),a.defaultStatistic=s.find(".table-statistic").html(),a.updateStatistic(),a.initModals(),a.checkItems={},a.updateCheckUI()}};r.prototype.initNestedList=function(){var e=this,i=e.options,n=e.getTable().addClass("table-nested"),o=n.find("tbody"),a=o.children("tr[data-id]"),s={},r=[],l=i.preserveNested,h=i.enableEmptyNestedRow;n.toggleClass("disable-empty-nest-row",!h),a.each(function(e){var i=t(this),n=i.data();n.realOrder=e,n.$row=i,n.nestPath?(n.nestPathList=n.nestPath.split(","),n.nestPathList[0]||n.nestPathList.shift(),n.nestPathList[n.nestPathList.length-1]||n.nestPathList.pop(),n.nestPathLevel=n.nestPathList.length,i.attr("data-level",n.nestPathLevel)):(n.nestPath=","+n.id+",",n.nestPathList=[n.id],n.nestPathLevel=1),s[n.id]=n,delete n.children,r.push(n)});var c=[];t.each(r,function(t,n){return n.nestParent&&(n.parent=s[n.nestParent],n.parent)?(n.parent.children?n.parent.children.push(n):n.parent.children=[n],void(i.expandNestChild||e._initedNestedList||(n.$row.addClass("table-nest-hide"),n.parent.$row.addClass("table-nest-child-hide")))):(n.$row.removeClass("table-nest-hide"),void c.push(n))}),e.nestRowsMap=s,e.$nestBody=o;var d=function(e){for(var n=0;n').prependTo(s)),r.toggleClass("table-nest-toggle",!!a.children||h&&a.nested).css("marginLeft",(a.nestPathLevel-1)*i.nestLevelIndent),delete a.$row}};if(d(c),e.nestConfigStoreName="/"+window.config.currentModule+"/"+window.config.currentMethod+"/table."+e.id+".nestConfig",e.nestConfig=l?t.zui.store.get(e.nestConfigStoreName,{}):{},l)for(var u=0;u
    ");i.$.addClass("load-indicator loading"),s.load(window.location.href+" #"+o,function(r){if(a===o)i.$.empty().html(s.children().html()),i.$.find('[data-ride="pager"]').pager();else{i.$.find("#"+o).empty().html(s.children().html());try{var l=t(r),h=l.find("#"+o).closest('[data-ride="table"],#'+a);if(h.length){var c=h.find(".table-statistic");c.length&&(i.defaultStatistic=c.html());var d=i.$.find('[data-ride="pager"]').data("zui.pager"),u=h.find('[data-ride="pager"]');d&&u.length&&d.set(u.data())}}catch(f){console.error(f)}}i.$.removeClass("load-indicator loading").trigger("beforeTableReload"),delete i.defaultStatistic,i.updateStatistic(),i.initModals(),i.$.datepickerAll();var p=i.$.find("tbody>tr"),g=!1;t.each(i.checkItems,function(t,e){e&&(i.checkRow(p.filter('[data-id="'+t+'"]'),!0,!0),g=!0)}),g&&i.updateCheckUI(),n.nested&&i.initNestedList(),i.$.trigger("tableReload");var m=t("#mainMenu>.btn-toolbar>.btn-active-text>.label");if(m.length){var u=i.$.find(".pager[data-rec-total]"),v=u.length?u.attr("data-rec-total"):i.getTable().find("tbody:first>tr:not(.table-children)").length;m.text(v)}e&&e(),n.afterReload&&n.afterReload()})},r.prototype.initModals=function(){var e=this,i=e.options,n=e.$.find(i.iframeModalTrigger);if(n.length){var o={type:"iframe",onHide:i.replaceId?function(){var n=t.cookie("selfClose");(1==n||i.hot)&&(t("#triggerModal").data("cancel-reload",1),e.reload(function(){t.cookie("selfClose",0)}))}:null};n.modalTrigger(o)}},r.prototype.getTable=function(){var t=this.$;if(this.isDataTable)return t.find("div.datatable");var e=t.is("table")?t:t.find("table:not(.fixed-header-copy)").first();return e.is(".datatable")&&(this.isDataTable=!0,e.data("zui.datatable")||window.initDatatable(e),e=t.find("div.datatable")),e},r.prototype.toggleGroups=function(e){var i=this,n={};i.$.find("tbody>tr").each(function(){var o=t(this).closest("tr").data("id");n[o]||i.toggleRowGroup(o,e); })},r.prototype.toggleRowGroup=function(i,n){var o=this.$.find('tbody>tr[data-id="'+i+'"]'),a=o.filter(".group-summary"),s=n===e?!a.hasClass("hidden"):!!n;o.not(".group-summary").toggleClass("hidden",!s),a.toggleClass("hidden",s),t("body").toggleClass("table-group-collapsed",!this.$.find("tbody>tr.group-summary.hidden").length)},r.prototype.updateStatistic=function(){var i=this,n=i.$.find(".table-statistic");if(n.length){if(i.defaultStatistic===e&&(i.defaultStatistic=n.html()),i.options.statisticCreator)return void n.html(i.options.statisticCreator(i)||i.defaultStatistic);var o=i.statisticCols;if(!o&&o!==!1){o={};var a=!1;i.getTable().find("thead th").each(function(e){var i=t(this),n=i.data("statistic");n&&(a=!0,o[e]={format:n,name:i.text()})}),i.statisticCols=!!a&&o}var s=0;o&&t.each(o,function(t){o[t].total=0,o[t].checkedTotal=0}),i.$.find(i.isDataTable?".fixed-left tbody>tr":"tbody>tr").each(function(){var e=t(this),i=e.hasClass("checked"),n=e.children("td");i&&s++,o&&t.each(o,function(t){var e=parseFloat(n.eq(t).text());isNaN(e)&&(e=0),o[t].total+=e,i&&(o[t].checkedTotal+=e)})});var r=[];if(s)r.push(i.lang.selectedItems.format(s));else if(i.defaultStatistic)return void n.html(i.defaultStatistic);o&&t.each(o,function(t){var e=o[t],n=e[s?"checkedTotal":"total"];e.format&&(n=e.format.format(n)),r.push(i.lang.attrTotal.format(e.name,n))}),n.html(r.join(", "))}},r.prototype.updateFixUI=function(e){var i=this,n=(new Date).getTime();if(!e&&(i.lastUpdateCall&&clearTimeout(i.lastUpdateCall),!i.lastUpdateTime||n-i.lastUpdateTime
    ').append(t('
    ').addClass(i.attr("class")).append(n.clone())).insertAfter(i)),c){var d=h[0].getBoundingClientRect();l.css({left:d.left,width:h.width(),overflow:"hidden"}),l.find(".fixed-header-copy").css({left:o.left-d.left,position:"relative",minWidth:i.width()}),a||h.data("fixHeaderScroll")||(h.data("fixHeaderScroll",1),i.width()>h.width()&&h.on("scroll",function(){e.fixHeader()}))}else l.css({left:o.left,width:o.width});var u=l.find("th");n.find("th").each(function(e){u.eq(e).css("width",t(this).outerWidth())})}else l.remove()},r.prototype.fixFooter=function(){var e,i=this,n=i.getTable(),o=i.$.find(".table-footer");if(i.isDataTable)e=n[0].getBoundingClientRect();else{var a=n.find("tbody");if(!a.length)return;e=a[0].getBoundingClientRect()}var s=i.options.fixFooter;o.toggleClass("fixed-footer",!!r);var r="function"==typeof s?s(e,o):e.bottom>window.innerHeight-50-("number"==typeof s?s:i.pageFooterHeight||5);o.toggleClass("fixed-footer",!!r),n.toggleClass("with-footer-fixed",!!r),n.trigger("fixFooter",r);var l=t("body"),h=l.hasClass("body-modal");if(r){var c=n.parent(),d=c.is(".table-responsive");o.css({bottom:i.pageFooterHeight||0,left:d?c[0].getBoundingClientRect().left:e.left,width:d?c.width():e.width}),h&&l.css("padding-bottom",40)}else o.css({width:"",left:0,bottom:0}),h&&l.css("padding-bottom",0)},r.prototype.checkAll=function(e){var i=this,n=i.$.find(i.isDataTable?".fixed-left tbody>tr":"tbody>tr");n.each(function(){i.checkRow(t(this),e,!0)}),i.updateCheckUI()},r.prototype.checkRow=function(t,i,n){var o=this;o.isDataTable&&!t.is(".datatable-row-left")&&(t=o.getTable().find('.datatable-row-left[data-index="'+t.data("index")+'"]'));var a=t.find('input[type="checkbox"]');a.length&&!a.is(":disabled")&&(i===e&&(i=!a.is(":checked")),o.isDataTable?o.getTable().find('.datatable-row[data-index="'+t.data("index")+'"]').toggleClass("checked",i):t.toggleClass("checked",i),this.checkItems[t.data("id")]=i,a.prop("checked",i).trigger("change"),n||o.updateCheckUI())},r.prototype.updateCheckUI=function(){var e=this,i=e.getTable(),n=i.find(e.isDataTable?".fixed-left tbody>tr":"tbody>tr").not(".group-summary"),o=!1,a=null,s=0,r=!1,l=n.length;n.each(function(n){var h=t(this),c=h.find('input[type="checkbox"]');if(!c.length)return void l--;r=c.is(":checked");var d=e.isDataTable?i.find('.datatable-row[data-index="'+h.data("index")+'"]'):h;d.toggleClass("checked",r),d.toggleClass("row-check-begin",r&&!o),a&&a.toggleClass("row-check-end",!r&&o),r&&(s+=1),a=d,o=r,l===n+1&&d.toggleClass("row-check-end",r)}),e.$.toggleClass("has-row-checked",s>0).find(".check-all").toggleClass("checked",!(!l||s!==l)),e.updateStatistic(),e.options.onCheckChange&&e.options.onCheckChange(),i.trigger("checkChange")},r.DEFAULTS={checkable:!0,checkOnClickRow:!0,ajaxForm:!1,selectable:!0,fixHeader:!a,fixFooter:!a,iframeWidth:900,replaceId:"self",nestLevelIndent:18,nested:!1,preserveNested:!0,hot:!1,iframeModalTrigger:".iframe"},t.fn.table=function(e){return this.each(function(){var n=t(this),o=n.data(i),a="object"==typeof e&&e;o||n.data(i,o=new r(this,a)),"string"==typeof e&&o[e]()})},r.NAME=i,t.fn.table.Constructor=r,t(function(){t('[data-ride="table"]').table()})}(jQuery,void 0),function(t,e,i){t.fn._ajaxForm=t.fn.ajaxForm;var n={timeout:e.config?e.config.timeout:0,dataType:"json",method:"post"},o="";t.fn.enableForm=function(e,n,o){return e===i&&(e=!0),this.each(function(){var i=t(this);n||i.find('[type="submit"]').attr("disabled",e?null:"disabled"),!o&&i.hasClass("load-indicator")&&i.toggleClass("loading",!e),i.toggleClass("form-disabled",!e)})},t.enableForm=function(e,i,n,o){"string"==typeof e||e instanceof t?e=t(e):(o=n,n=i,i=e,e=t("form")),e.enableForm(i!==!1,n,o)},t.disableForm=function(e,i,n){t.enableForm(e,!1,i,n)};var a=function(e,i,n){"string"==typeof i&&(n=i,i=null),n=n||"show",t.zui.messager?t.zui.messager[n](e,i):alert(e)};t.ajaxForm=function(s,r){var l=t(s);if(l.length>1)return l.each(function(){t.ajaxForm(this,r)});"function"==typeof r&&(r={complete:r}),r=t.extend({},n,l.data(),r);var h=r.beforeSubmit,c=r.error,d=r.success,u=r.finish;delete r.finish,delete r.success,delete r.onError,delete r.beforeSubmit,r=t.extend({beforeSubmit:function(n,a,s){if(l.removeClass("form-watched").enableForm(!1),(h&&h(n,a,s))!==!1){var r={},c=a.find('[type="file"]');r.fileapi=c.length&&c[0].files!==i,r.formdata=e.FormData!==i;var d=r.fileapi&&a.find('input[type="file"]:enabled').filter(function(){return""!==t(this).val()}),u=d.length,f="multipart/form-data",p=a.attr("enctype")==f||a.attr("encoding")==f,g=r.fileapi&&r.formdata,m=u&&!g||p&&!r.formdata;m&&(""==o&&(o=s.url),s.url!=o&&(s.url=o),s.url=s.url.indexOf("&")>=0?s.url+"&HTTP_X_REQUESTED_WITH=XMLHttpRequest":s.url+"?HTTP_X_REQUESTED_WITH=XMLHttpRequest")}},success:function(i,n,o){if((d&&d(i,n,o,l))!==!1){try{"string"==typeof i&&(i=JSON.parse(i))}catch(s){}if(null===i||"object"!=typeof i)return i?alert(i):a("No response.","danger");var h=r.responser?t(r.responser):l.find(".form-responser");h.length||(h=t("#responser"));var c=i.message,f=function(){var n=i.callback;if(n){var o=n.indexOf("("),a=(o>0?n.substr(0,o):n).split("."),s=e,r=a[0];a.length>1&&(r=a[1],"top"===a[0]?s=e.top:"parent"===a[0]&&(s=e.parent));var h=s[r];if("function"==typeof h){var c=[];return o>0&&")"==n[n.length-1]&&(c=t.parseJSON("["+n.substring(o+1,n.length-1)+"]")),c.push(i),h.apply(l,c)}}};if("success"===i.result){var p=r.locate||i.locate,g=r.closeModal||i.closeModal,m=r.ajaxReload||i.ajaxReload;if(l.enableForm(!0,!!(p||g||m)),c){var v=l.find('[type="submit"]').first(),y=!1;v.length&&(v.popover({container:"body",trigger:"manual",content:c,tipClass:"popover-in-modal popover-success popover-form-result",placement:i.placement||v.data("placement")||r.popoverPlacement||"right"}).popover("show"),setTimeout(function(){v.popover("destroy")},r.popoverTime||2e3),y=!0),h.length&&(h.html(''+c+"").show().delay(3e3).fadeOut(100),y=!0),y||a(c,"success")}if(u)return u(i,!0,l);if(g&&setTimeout(t.zui.closeModal,r.closeModalTime||2e3),f()===!1)return;if(p)if("loadInModal"==p){var b=t(".modal");setTimeout(function(){b.load(b.attr("ref"),function(){t(this).find(".modal-dialog").css("width",t(this).data("width")),t.zui.ajustModalPosition()})},1e3)}else"parent"===p||"top"===p?e[p]&&setTimeout(function(){e[p].location.reload()},1200):"reload"===p?setTimeout(function(){e.location.href=e.location.href},1200):setTimeout(function(){t.tabs?t.tabs.open(p):e.location.href=p},1200);if(m){var w=t(m);w.length&&w.load(e.location.href+" "+m,function(){w.find('[data-toggle="modal"]').modalTrigger()})}}else{if(l.enableForm(),"string"==typeof c)h.length?h.html(''+c+"").show().delay(3e3).fadeOut(100):a(c,"danger");else if("object"==typeof c){var x=!1,C=[];t.each(c,function(e,i){var n=t.isArray(i)?i.join(""):i,o=t("#"+e);if(!o.length)return void C.push(n);var a=e+"Label",s=t("#"+a);if(!s.length){var r=o.closest(".input-group").length,l=o.closest("td").length;s=t('
    ').appendTo(l?o.closest("td"):r?o.closest(".input-group").parent():o.parent())}s.empty().append(n),o.addClass("has-error");var h=function(){var e=t("#"+a);if(e.length)return e.remove(),o.removeClass("has-error"),!0};o.on("change input mousedown",h);var c=t("#"+e+"_chosen");if(c.length&&c.find(".chosen-single,.chosen-choices").addClass("has-error").on("mousedown",function(){h()===!0&&t(this).removeClass("has-error")}),!x){if(o.hasClass("chosen"))o.trigger("chosen:activate");else if(o.is("textarea")&&o.data("keditor")){var d=o.data("keditor");d.focus(),d.edit.doc.body.focus()}else o.focus();x=!0}}),C.length&&a(C.join(";"),"danger")}if(u)return u(i,!1,l);if(f()===!1)return}}},error:function(t,i,n){if((c&&c(t,i,n,l))!==!1){l.enableForm();var o="timeout"==i||"error"==i?e.lang?e.lang.timeout:i:t.responseText+i+n;a(o,"danger")}}},r),l._ajaxForm(r).data("zui.ajaxform",!0),l.on("click","[data-form-action]",function(){l.attr("action",t(this).data("formAction")).submit()})},t.setAjaxForm=function(e,i,n){t.ajaxForm(e,t.isPlainObject(i)?i:{finish:i,beforeSubmit:n})},t.fn.ajaxForm=function(e){return this.each(function(){t.ajaxForm(this,e)})},t.fn.setInputRequired=function(){return this.each(function(){var e=t(this),i=e.parent();i.is(".input-control,td")?i.addClass("required"):e.is(".chosen")?e.attr("required",null).next(".chosen-container").addClass("required"):i.addClass("required"),e.attr("required",null);var n=i.closest(".input-group");n.length&&1===n.find(".required,input[required],select[required]").length&&n.addClass("required")})},t(function(){t('.form-ajax,form[data-type="ajax"]').ajaxForm(),setTimeout(function(){var i=e.config.requiredFields,n=t("form");i&&(i=i.split(",")),i&&i.length&&t.each(i,function(t,e){n.find("#"+e).attr("required","required")}),n.find("input[required],select[required],textarea[required]").setInputRequired()},400),t('form[target="hiddenwin"]').on("submit",function(){var e=t(this);e.data("zui.ajaxform")||e.enableForm(!1).data("disabledTime",(new Date).getTime())}).on("click",function(){var e=t(this),i=e.data("disabledTime");i&&(new Date).getTime()-i>1e4&&e.enableForm(!0).data("disabledTime",null)})})}(jQuery,window,void 0),function(t){"use strict";var e="zui.searchList",i=function(t,e){if(t&&t.length)for(var i=0;i
    ').append(s)),i.$menu.append(s),i.$menu.removeClass("loading"),i.isLoaded=!0,e&&e(!0)},error:function(){i.$menu.removeClass("loading").append('
    '+(n.errorText||window.lang&&window.lang.timeout)+"
    "),e&&e(!1)}},n.ajax))},n.prototype.scrollTo=function(t){t.length&&t[0].scrollIntoViewIfNeeded&&t[0].scrollIntoViewIfNeeded({behavior:"smooth"})},n.prototype.getItems=function(){return this.$.find(this.options.selector).addClass("search-list-item")},n.prototype.getActiveItem=function(){return this.getItems().filter(".active:first")},n.prototype.search=function(e){var n=this,o=void 0===e||null===e||""===e;n.$.toggleClass("has-search-text",!o);var a=n.getItems().removeClass("active");if(o)a.removeClass("hidden");else{var s=t.trim(e).split(" ");a.each(function(){var e=t(this),n=e.text()+" "+(e.data("key")||e.data("filter"));e.toggleClass("hidden",!i(s,n))})}n.scrollTo(a.not(".hidden").first().addClass("active"))},n.DEFAULTS={selector:".list-group a:not(.not-list-item)",searchBox:".search-box",onSelectItem:null},t.fn.searchList=function(i){return this.each(function(){var o=t(this),a=o.data(e),s="object"==typeof i&&i;a||o.data(e,a=new n(this,s)),"string"==typeof i&&a[i]()})},n.NAME=e,t.fn.searchList.Constructor=n,t(function(){t('[data-ride="searchList"]').searchList()})}(jQuery),function(t){"use strict";var e="zui.labelSelector",i=function(n,o){var a=this;a.name=e,a.$=t(n),o=a.options=t.extend({},i.DEFAULTS,this.$.data(),o),a.$.hide(),a.update()};i.prototype.select=function(t){t+="",this.$wrapper.find(".label.active").removeClass("active"),this.$wrapper.find('.label[data-value="'+t+'"]').addClass("active"),this.$.val(t).trigger("change")},i.prototype.update=function(){var e=this,i=e.options,n=e.$wrapper;if(!n){if(i.wrapper)n=t(i.wrapper);else{var o=e.$.next();n=o.hasClass(".label-selector")?o:t('
    ')}n.parent().length||e.$.after(n),e.$wrapper=n,n.on("click",".label",function(i){var n=e.$.val(),o=t(this).data("value");e.hasEmptyValue!==!1&&o==n&&(o=e.hasEmptyValue),e.select(o),i.preventDefault()})}n.empty();var a=e.$.val();e.hasEmptyValue=!1,e.$.children("option").each(function(){var e=t(this),o={label:e.text(),value:e.val()},s=""===o.value||"0"===o.value,r=t(i.labelTemplate||'');i.labelClass&&!s&&r.addClass(i.labelClass),i.labelCreator?r=i.labelCreator(r):(r.data("option",o).attr("data-value",o.value),s&&!o.label?r.addClass("empty").append(''):r.text(o.label).toggleClass("active",a===o.value)),n.append(r)})},i.DEFAULTS={},t.fn.labelSelector=function(n){return this.each(function(){var o=t(this),a=o.data(e),s="object"==typeof n&&n;a||o.data(e,a=new i(this,s)),"string"==typeof n&&a[n]()})},i.NAME=e,t.fn.labelSelector.Constructor=i,t(function(){t('[data-provide="labelSelector"]').labelSelector()})}(jQuery),function(t){"use strict";var e="zui.fileInput",i=t.BYTE_UNITS={B:1,KB:1024,MB:1048576,GB:1073741824,TB:1099511627776},n=t.formatBytes=function(t,e,n){return void 0===e&&(e=2),n||(n=ts.fileMaxSize&&(h.val(""),(window.bootbox||window).alert(s.fileSizeError.format(n(s.fileMaxSize)))),r.update()}),r.update()};a.prototype.getFile=function(){var t=this.$input.prop("files");return t&&t[0]},a.prototype.update=function(){var t=this,e=t.$,i=t.getFile(),o=!i;e.toggleClass("normal",!o).toggleClass("empty",o),i?(t.oldName=i.name,e.find(".file-title").text(i.name).attr("title",i.name),e.find(".file-size").text(n(i.size)),e.find(".file-editbox").val(i.name).attr("size",i.name.length),t.options.onSelect&&t.options.onSelect(i,t)):e.find(".file-editbox").val("")},a.DEFAULTS={fileMaxSize:0,fileSizeError:"无法上传大于 {0} 的文件。"},t.fn.fileInput=function(i){return this.each(function(){var n=t(this),o=n.data(e),s="object"==typeof i&&i;o||n.data(e,o=new a(this,s)),"string"==typeof i&&o[i]()})},a.NAME=e,t.fn.fileInput.Constructor=a,t(function(){t('[data-provide="fileInput"]').fileInput()});var s="zui.fileInputList",r=function(e,i){var n=this;n.name=s;var o=n.$=t(e);i=n.options=t.extend({},r.DEFAULTS,this.$.data(),i),n.$template=o.find(".file-input").detach(),n.add()};r.prototype.add=function(){var t=this,e=t.options,i=t.$template.clone();"before"===e.appendWay?t.$.prepend(i):t.$.append(i),i.fileInput({fileMaxSize:e.eachFileMaxSize,fileSizeError:e.fileSizeError,onDelete:function(e){e.$.remove(),t.options.onDelete&&t.options.onDelete(e,t)},onSelect:function(e,i){t.add(),t.options.onSelect&&t.options.onSelect(e,i,t)}})},r.DEFAULTS={fileMaxSize:0,eachFileMaxSize:0,appendWay:"after",fileSizeError:"无法上传大于 {0} 的文件。"},t.fn.fileInputList=function(e){return this.each(function(){var i=t(this),n=i.data(s),o="object"==typeof e&&e;n||i.data(s,n=new r(this,o)),"string"==typeof e&&n[e]()})},r.NAME=s,t.fn.fileInputList.Constructor=r,t(function(){t('[data-provide="fileInputList"]').fileInputList()})}(jQuery),function(t){window.config||(window.config={}),t.createLink=window.createLink=function(e,n,o,a,s,r,l){if("object"==typeof e)return t.createLink(e.moduleName,e.methodName,e.vars,e.viewType,e.isOnlyBody,e.hash,e.tid);if(t.tabSession&&!l&&(l=t.tabSession.getTid()),a||(a=config.defaultView),s||(s=!1),o)for("string"==typeof o&&(o=o.split("&")),i=0;i'+d+"")}}t.val()||(time=e(a.format("hh:mm")),time=time-time%10+10,t.val(n(time)))};t.fn.timeSpanControl=function(i){return this.each(function(){var s=t(this),r=t.extend({},i,s.data()),l=s.find('[name="begin"],.control-time-begin'),h=s.find('[name="end"],.control-time-end'),c=function(){var t=l.val();if(s.find(".hide-empty-begin").toggleClass("hide",!t),t){var i=n(e(t)+30);h.find('option[value="'+i+'"]').length&&h.val(i),r.onChange&&r.onChange(h,i)}};if(s.data("timeSpanControlInit")){if(r.begin){var d=o(r.begin).format("hh:mm");l.find('option[value="'+d+'"]').length&&l.val(d),r.onChange&&r.onChange(l,d)}if(r.end){var u=o(r.end).format("hh:mm");h.find('option[value="'+u+'"]').length&&h.val(u),r.onChange&&r.onChange(h,u)}}else l.on("change",c),a(l,r.begin),a(h,r.end),s.data("timeSpanControlInit",!0);r.end||c()})},t.timeSpanControl={convertTimeToNum:e,convertNumToTime:n,initTimeSelect:a,createTime:o};var s=t.setSearchType=function(e,i){var n=t("#searchType");e||(e=n.val()),e=e||"bug",n.val(e);var o=t("#searchTypeMenu");o.find("li.selected").removeClass("selected");var a=o.find('a[data-value="'+e+'"]'),s=a.text();a.parent().addClass("selected"),t("#searchTypeName").text(s),i||t("#searchInput").focus()};t.gotoObject=function(e,i){if(e||(e=t("#searchType").val()),i||(i=t("#searchInput").val()),i&&e)if(i=i.replace(/[^\d]/g,"")){var n=e.split("-");e=n[0];var o=n.length>1?n[1]:"testsuite"===e?"library":"view",a=t.createLink(e,o,"id="+i);t.apps?t.apps.open(a):window.location.href=a}else{var s={zh_cn:"请输入数字ID进行搜索",zh_tw:"請輸入數值ID行搜索"};alert(lang.searchTip||s[t.zui.clientLang()]||"Please enter a numberic id to search")}t("#searchInput").val(i).focus()},t(function(){s(null,!0),t(document).on("keydown",function(e){e.ctrlKey&&71===e.keyCode&&(t("#searchInput").val("").focus(),e.stopPropagation(),e.preventDefault())})}),t.removeAnchor=window.removeAnchor=function(t){var e=t.lastIndexOf("#");return e>-1?t.substr(0,e):t},t.refreshPage=function(t){t?window.parent.location.reload():window.location.reload()},t.selectLang=window.selectLang=function(e){t.cookie("lang",e,{expires:config.cookieLife,path:config.webRoot}),t.ajaxSendScore("selectLang"),t.refreshPage(1)},t.selectTheme=window.selectTheme=function(e){t.cookie("theme",e,{expires:config.cookieLife,path:config.webRoot}),t.ajaxSendScore("selectTheme"),t.refreshPage(1)},t.zui.Picker&&t.zui.Picker.enableChosen(),t.chosenDefaultOptions={middle_highlight:!0,disable_search_threshold:1,compact_search:!0,allow_single_deselect:!0,placeholder_text_single:" ",placeholder_text_multiple:" ",search_contains:!0,max_drop_width:500,max_drop_height:245,no_wrap:!0,drop_direction:function(){var e=t(this.container).closest(".table-responsive:not(.scroll-none)");if(e.length){if(this.drop_directionFixed)return this.drop_directionFixed;e.css("position","relative");var i="down",n=this.container.find(".chosen-drop"),o=this.container.position(),a=n.outerHeight();return o.top>=a&&o.top+31+a>e.outerHeight()&&(i="up"),this.drop_directionFixed=i,i}return"auto"}},t.chosenSimpleOptions=t.extend({},t.chosenDefaultOptions,{disable_search_threshold:6}),t.fn._chosen=t.fn.chosen,t.fn.chosen=function(e){return"string"==typeof e?this._chosen(e):this.each(function(){var i=t(this).addClass("chosen-controled");return i._chosen(t.extend({},i.hasClass("chosen-simple")?t.chosenSimpleOptions:t.chosenDefaultOptions,i.data(),e))})},t.fn.chosen.Constructor=t.fn._chosen.Constructor,t(function(){t(".chosen,.chosen-simple").each(function(){var e=t(this);e.closest(".template").length||e.chosen()})}),t.extend(t.fn.pager.Constructor.DEFAULTS,{maxNavCount:8,prevIcon:"icon-angle-left",nextIcon:"icon-angle-right",firstIcon:"icon-first-page",lastIcon:"icon-last-page",navEllipsisItem:"…",menuDirection:"dropup",pageSizeOptions:[5,10,15,20,25,30,35,40,45,50,100,200,500,1e3,2e3],elements:["total_text","size_menu","first_icon","prev_icon",'
    {page}/{totalPage}
    ',"next_icon","last_icon"],onPageChange:function(e,i){e.recPerPage!==i.recPerPage&&t.cookie(this.options.pageCookie,e.recPerPage,{expires:config.cookieLife,path:config.webRoot}),e.recPerPage!==i.recPerPage&&(window.location.href=this.createLink())}}),t.extend(!0,t.zui.Messager.DEFAULTS,{cssClass:"messagger-zt",icons:{success:"check-circle",info:"chat-line",warning:"exclamation-sign",danger:"exclamation-sign"}}),t.fn.reverseOrder=function(){return this.each(function(){var e=t(this);e.prependTo(e.parent())})};var r=function(e,i){var n=t(e);if(!n.data("historiesInited")){n.data("historiesInited",1),i=t.extend({},n.data(),i);var o=n.find(".histories-list"),a=!0,s=!1;n.on("click",".btn-reverse",function(){o.children("li").reverseOrder(),a=!a,t(this).find(".icon").toggleClass("icon-arrow-up",a).toggleClass("icon-arrow-down",!a);var e="#lastComment",i=t(e);i.length&&window.KindEditor&&(window.KindEditor.remove(e),i.kindeditor())}).on("click",".btn-expand-all",function(){var e=t(this).find(".icon");s=!s,e.toggleClass("icon-plus",!s).toggleClass("icon-minus",s),o.children("li").toggleClass("show-changes",s)}).on("click",".btn-expand",function(){t(this).closest("li").toggleClass("show-changes")}).on("click",".btn-strip",function(){var e=t(this),n=e.find(".icon"),o=n.hasClass("icon-code");n.toggleClass("icon-code",!o).toggleClass("icon-text",o),e.attr("title",o?i.original:i.textdiff),e.closest("li").toggleClass("show-original",o)}),o.find(".btn-strip").attr("title",i.original);var r=n.find(".modal-comment").modal({show:!1}).on("shown.zui.modal",function(){var t=r.find("#comment");t.length&&(t.focus(),window.editor&&window.editor.comment&&window.editor.comment.focus())}).on("show.zui.modal",function(){var e=r.find("#comment");e.length&&!e.data("keditor")&&t.fn.kindeditor&&e.kindeditor()});n.on("click",".btn-comment",function(t){r.modal("toggle"),t.preventDefault()}).on("click",".btn-edit-comment,.btn-hide-form",function(){t(this).closest("li").toggleClass("show-form")});var l=n.find(".comment-edit-form");l.ajaxForm({success:function(t,e,i,n){setTimeout(function(){l.closest("li").removeClass("show-form")},2e3)}})}};t.fn.histories=function(t){return this.each(function(){r(this,t)})},t(function(){t(".histories").histories()});var l=0,h=0;t.toggleSidebar=function(e){var i=t("#sidebar");if(i.length){var n=t("main");if(void 0===e)e=n.hasClass("hide-sidebar");else if(e&&!n.hasClass("hide-sidebar"))return;n.toggleClass("hide-sidebar",!e),clearTimeout(l),t.zui.store.set(h,e);var o=i.children(".cell"),a={overflow:"visible",maxHeight:"initial"};e?(i.addClass("showing"),l=setTimeout(function(){i.removeClass("showing"),i.trigger("sidebar.toggle",e)},210)):(i.trigger("sidebar.toggle",e),t(window).width()<1900&&(a={overflow:"hidden",maxHeight:t(window).height()-45})),o.css(a)}};var c=t.initSidebar=function(){var e=t("#sidebar");if(e.length){if(e.data("init"))return!0;h="sidebar:"+(e.data("id")||config.currentModule+"/"+config.currentMethod);var i=t("main");if(i.length){i.on("click",".sidebar-toggle",function(){t.toggleSidebar(i.hasClass("hide-sidebar"))});var n=t.zui.store.get(h,e.data("hide")!==!1);n===!1&&e.addClass("no-animate"),t.toggleSidebar(n),n===!1&&setTimeout(function(){e.removeClass("no-animate")},500);var o=e.find(".sidebar-toggle");if(o.length){var a=function(){var e=o[0].getBoundingClientRect(),i=t(window).height(),n=Math.max(0,Math.floor(Math.min(i-40,e.top+e.height)-Math.max(e.top,0))/2)+(e.top<0?0-e.top:0);o.removeClass("fade").find(".icon").css("top",n+(t.zui.browser.isIE()?(i-80)/2:0))};a(),e.data("init",1).on("sidebar.toggle",a);var s=t.zui.browser.isIE()?1500:0,r=0,l=null,c=function(){var t=Date.now();return l&&(clearTimeout(l),l=null),t-rtr input[type="checkbox"]:checked');i.each(function(){var i=parseInt(t(this).val(),10);NaN!==i&&e.push(i)}),t.cookie("checkedItem",e.join(","),{expires:config.cookieLife,path:config.webRoot})},t.extend(t.fn.modal.bs.Constructor.DEFAULTS,{scrollInside:!0,backdrop:"static",headerHeight:100}),t.extend(t.zui.ModalTrigger.DEFAULTS,{scrollInside:!0,backdrop:"static",headerHeight:40}),t.fn.initIframeModal=function(){return this.each(function(){var e=t(this);if(!e.parents('[data-ride="table"],.skip-iframe-modal').length){var i={type:"iframe"};e.hasClass("export")&&t.extend(i,{width:800,shown:setCheckedCookie},e.data()),e.modalTrigger(i)}})},t(function(){t("a.iframe,.export").initIframeModal()});var d=function(){var e,i,n=t(this),o=t.extend({limitSize:40,suffix:"…"},n.data()),a=n.text();if(a.length>o.limitSize){e=a,i=a.substr(0,o.limitSize)+o.suffix,n.text(i).addClass("limit-text-on");var s=o.toggleBtn?t(o.toggleBtn):n.next(".text-limit-toggle");s.text(s.data("textExpand")),s.on("click",function(){var t=n.toggleClass("limit-text-on").hasClass("limit-text-on");n.text(t?i:e),s.text(s.data(t?"textExpand":"textCollapse"))})}else(o.toggleBtn?t(o.toggleBtn):n.next(".text-limit-toggle")).hide()};t.fn.textLimit=function(){return this.each(d)},t(function(){t(".text-limit").textLimit()}),t.fixedTableHead=window.fixedTableHead=function(e,i){var n=t(e);if(n.is("table")||(n=n.find("table")),n.length){var o=t(i||window),a=null,s=function(){ -var e=n.children("thead"),i=e[0].getBoundingClientRect(),o=n.next(".fixed-head-table");if(i.top<0){var s=e.width();if(o.length){if(a!==s){a=s;var r=o.find("th");e.find("th").each(function(e){r.eq(e).width(t(this).width())})}}else{var o=t("
    ").addClass(n.attr("class")),l=e.clone(),r=l.find("th");e.find("th").each(function(e){r.eq(e).width(t(this).width())}),o.append(l).insertAfter(n)}o.css({left:i.left,width:i.width}).show()}else o.hide()};o.on("scroll",s).on("resize",s),s()}},t(document).on("click","tr[data-url]",function(){var e=t(this),i=e.data("href")||e.data("url");i&&(window.location.href=i)}),"yes"===config.onlybody&&self===parent&&(window.location.href=window.location.href.replace("?onlybody=yes","").replace("&onlybody=yes","")),t(function(){t("body").addClass("m-{currentModule}-{currentMethod}".format(config))});var u,f,p,g,m,v=function(){u||(u=t("#subNavbar"),f=t("#pageNav"),p=t("#pageActions"),g=u.children(".nav"),m=g.outerWidth());var e=u.outerWidth(),i=f.outerWidth()||0,n=p.outerWidth()||0;if(i=i?i+15:0,n=n?n+15:0,!i&&!n)return void g.css({maxWidth:null,left:null,position:"static"});var o=Math.max(300,e-i-n),a=Math.min(o,m),s=(e-a)/2,r=i&&s.btn-toolbar");if(e.length){var i,n,o=e.children(),a=o.length,s=!1,r=null;if(a)for(o.each(function(e){i=t(this),n=i.is(".divider"),n&&!r&&i.hide(),s||n||(s=!0),r=n?null:i,!n||e!==a-1&&0!==e||i.hide()});i.length&&i.is(".divider");)i=i.hide().prev();s||e.hide()}};t(function(){t(".input-group,.btn-group").fixInputGroup(),k()}),window.holders&&t.each(window.holders,function(e){var i=t("#"+e);i.length&&i.is("input")&&i.attr("placeholder",window.holders[e])}),t(function(){var e=t(".table-responsive"),i=t.fixTableResponsive=function(){e.each(function(){this.scrollHeight-3<=this.clientHeight&&this.scrollWidth-3<=this.clientWidth?t(this).addClass("scroll-none").css("overflow","visible"):t(this).removeClass("scroll-none").css("overflow","auto")})};e.length&&(t(window).on("resize",i),setTimeout(i,100))});var T=function(){var e=this,i=t(e),n=i.closest("tr").find("textarea");if(n.length){var o=32;n.each(function(){var e=t(this).closest("td"),i=e.css("height");e.css("height",this.style.height),this.style.height="auto";var n=this.value?this.scrollHeight+2:32;o=Math.max(o,n),e.css("height",i)}),n.css("height",o)}else{e.style.height="auto";var a=e.value?e.scrollHeight+2:32;e.style.height=a+"px"}};t.autoResizeTextarea=function(e){t(e).each(T)},t(function(){t("textarea.autosize").each(T),t(document).on("input paste change","textarea.autosize",T)}),t(function(){var e=t("#dropMenu,.drop-menu");e.length&&e.on("click",".toggle-right-col",function(e){t(this).closest("#dropMenu,.drop-menu").toggleClass("show-right-col"),e.stopPropagation(),e.preventDefault()})});var S="undefined"!=typeof InstallTrigger;t.zui.browser.firefox=S,t("html").toggleClass("is-firefox",S).toggleClass("not-firefox",!S),t(function(){var e=t("#mainContent>.main-col"),i=e.children(".main-actions");if(i.length){var n=i.prev();if(i.length&&n.length){t('
    ').css("height",i.outerHeight()).insertAfter(i);var o=0,a=function(){var e=n[0].getBoundingClientRect(),s=e.top+e.height+120>t(window).height();if(t("body").toggleClass("main-actions-fixed",s),s){var r=n.width();r?i.width(r):o<10&&setTimeout(a,1e3)}o++};t.resetToolbarPosition=a,a(),t(window).on("resize scroll",a)}}}),t(document).on("show.zui.modal",function(e){t("body.body-modal").length&&window.parent&&window.parent!==window&&t(e.target).is(".modal")&&window.parent.$("body").addClass("hide-modal-close")}).on("hidden.zui.modal",function(){t("body.body-modal").length&&window.parent&&window.parent!==window&&window.parent.$("body").removeClass("hide-modal-close")}),t(function(){var e=t(".dropdown-menu.with-search");e.length&&(e.find(".menu-search").on("click",function(t){return t.stopPropagation(),!1}),e.on("keyup change paste","input",function(){var e=t(this),i=e.closest(".dropdown-menu.with-search"),n=e.val().toLowerCase(),o=i.find(".option");""==n?o.removeClass("hide"):o.each(function(){var e=t(this);e.toggleClass("hide",e.text().toString().toLowerCase().indexOf(n)<0&&e.data("key").toString().toLowerCase().indexOf(n)<0)})}),e.parents(".dropdown-submenu").one("mouseenter",function(){var e=t(this).find(".dropdown-list")[0];e&&e.getBoundingClientRect&&setTimeout(function(){var t=270,i=e.getBoundingClientRect();i.top<0&&(t=Math.min(270,i.height)+i.top),e.style.maxHeight=Math.min(270,t)+"px"},50)})),t(".dropdown-menu.with-search .menu-search").on("click",function(t){return t.stopPropagation(),!1})})}(jQuery),function(t){function e(){var e=window.parent,i=config.currentModule,n=config.currentMethod,o="index"===i&&"index"===n,a="#_single"===location.hash||o||!t("#mainHeader,#editorNav").length||"tutorial"===i||"install"===i||"upgrade"===i||"user"===i&&("login"===n||"deny"===n)||"my"===i&&"changepassword"===n||t("body").hasClass("allow-self-open"),s=location.href;if(e===window&&!a){var r=location.pathname+location.search;return void(location.href=t.createLink("index","index","")+"#app="+encodeURIComponent(r))}if(e!==window&&e.$.apps){o&&e.location.reload();var l=window.name;if(0===l.indexOf("app-")){t.apps=window.apps=e.$.apps;var h=l.substr(4);t.appCode=h,t(document).on("click",function(t){var i=e.document.getElementById(window.name);if(i){var n=e.document.getElementById(i.name)||i;n&&n.dispatchEvent(new Event(t.type,{bubbles:!0}))}}).on("click","a,.open-in-app,.show-in-app",function(e){var i=t(this);if(!i.is("[data-modal],[data-toggle],[data-ride],[data-tab],.iframe,.not-in-app,[target]")&&!i.data("zui.modaltrigger")){var n=i.hasClass("show-in-app")?"":i.attr("href")||(i.is("a")?"":i.data("url")),o=i.data("app")||i.data("group");if(n){if(0===n.indexOf("javascript:")||"#"===n[0])return;var a=t.parseLink(n);if("index"===a.moduleName&&"index"===a.methodName)return window.location.reload(),void e.preventDefault()}else if(!o)return;o||(o=t.apps.getAppCode(n)),o&&("help"===o&&(t.apps.appsMap.help.text=i.text(),t.apps.appsMap.help.url||(t.apps.appsMap.help.url=n)),t.apps.open(n,o)&&e.preventDefault())}}),t.apps.updateUrl(h,s,document.title)}}}function i(){var e=t("#navbar>.nav");if(e.length){var i=t("#heading"),n=+i.css("left").replace("px",""),o=i.outerWidth(),a=e.width(),s=t("#mainHeader>.container").width()-2*n,r=Math.floor((s-a)/2);e.css("marginLeft",r>>0;if(0===o)return!1;for(var a=0|e,s=Math.max(a>=0?a:o-Math.abs(a),0);s1&&("?"===e[0]&&(e=e.substr(1)),e.split("&").forEach(function(t){var e=t.split("=",2);if(e.length>1)try{i[e[0]]=decodeURIComponent(e[1])}catch(n){i[e[0]]=""}else i[e[0]]=""})),t?i[t]:i},t.parseLink=function(e){if(!e)return{};var i=0===e.indexOf("http:")||0===e.indexOf("https:");if(i){var n=window.location.origin;if(e.indexOf(n)<0)return{};e=e.substr((n+config.webRoot).length)}var o=e.split("#"),a=o[0].split("?"),s=a[1],r=s?t.getSearchParam("",s):{},l=a[0],h={isOnlyBody:"yes"===r.onlybody,vars:[],hash:o[1]||"",params:r,tid:r.tid||""};if("GET"===config.requestType){h.moduleName=r[config.moduleVar]||"index",h.methodName=r[config.methodVar]||"index",h.viewType=r[config.viewVar]||config.defaultView;for(var c in r)c!==config.moduleVar&&c!==config.methodVar&&c!==config.viewVar&&"onlybody"!==c&&"tid"!==c&&h.vars.push([c,r[c]])}else{var d=l.lastIndexOf("/");d===l.length-1&&(l=l.substr(0,d),d=l.lastIndexOf("/")),d>=0&&(l=l.substr(d+1));var u=l.lastIndexOf(".");u>=0?(h.viewType=l.substr(u+1),l=l.substr(0,u)):h.viewType=config.defaultView;var f=l.split(config.requestFix);if(h.moduleName=f[0]||"index",h.methodName=f[1]||"index",f.length>2)for(var p=2;p.nav>li").length>10&&(i(),t(window).on("resize",i)),setTimeout(n,1e3)})}(jQuery),function(t){"use strict";function e(e,i){"object"!=typeof i&&(i={user:i});var n=t(e);i=t.extend({},n.data(),i);var o=i.user;"string"==typeof o&&(o={account:o});var a={},s=i.size;s&&(a.width=s,a.height=s,a.lineHeight=s,Number.isNaN(+s)||n.addClass("size-"+s));var r=!!o.avatar;if(n.toggleClass("has-image",r).toggleClass("has-text",!r),n.empty(),r)n.append(t("").attr("src",o.avatar));else{var l=t.zui.strCode(o.account)*(i.hueDistance||43)%360;a.background="hsl("+l+","+(i.saturation||"40%")+","+(i.lightness||"60%")+")",Number.isNaN(+s)||(a.fontSize=Math.round(2*s/3)+"px"),n.append(t("").text(o.account[0].toUpperCase()))}return n.css(a)}t.fn.avatar=function(t){return this.each(function(){e(this,t)})}}(jQuery),$.zui.lang("de",{"zui.pager":{pageOfText:"Seite {0}",prev:"Zurück",next:"Nächste Seite",first:"Erste Seite",last:"Letzte Seite","goto":"Goto",pageOf:"Seite {page}",totalPage:"{totalPage} Seiten",totalCount:"Total: {recTotal} Artikel",pageSize:"{recPerPage} Artikel pro Seite",itemsRange:"Seiten {start} bis {end}",pageOfTotal:"Seite {page}/{totalPage}"},"zui.boards":{append2end:"Gehen Sie zum Ende"},"zui.browser":{tip:"Online. Sorgenfrei. Aktualisiere deinen Browser noch heute!"},"zui.calendar":{weekNames:["Son","Mon","Die","Mit","Don","Fri","Sam"],monthNames:["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],today:"Heute",year:"{0}Jahr",month:"{0}Monat",yearMonth:"{0}-{1}"},"zui.chosenIcons":{emptyIcon:"[Kein Icon]",commonIcons:"Gemeinsame Symbole",webIcons:"Web-Symbol",editorIcons:"Editor-Symbol",directionalIcons:"Pfeil Zusammenfluss",otherIcons:"Andere Symbole"},"zui.colorPicker":{errorTip:"Kein gültiger Farbwert"},"zui.datagrid":{errorCannotGetDataFromRemote:"Daten vom Remote-Server ({0}) können nicht abgerufen werden.",errorCannotHandleRemoteData:"Die vom Remote-Server zurückgegebenen Daten können nicht verarbeitet werden."},"zui.guideViewer":{prevStep:"Vorheriger Schritt",nextStep:"Nächster Schritt"},"zui.tabs":{reload:"Neu laden",close:"Schliessen",closeOthers:"Schließen Sie andere Registerkarten",closeRight:"Schließen Sie die rechte Registerkarte",reopenLast:"Letzten geschlossenen Tab wiederherstellen",errorCannotFetchFromRemote:"Inhalt kann nicht vom Remote-Server abgerufen werden ({0})."},"zui.uploader":{},datetimepicker:{days:["Sonntag","Montag","Diensteg","Mittwoch","Donnerstag","Freitag","Samstag"],daysShort:["Son","Mon","Die","Mit","Don","Fri","Sam"],daysMin:["Son","Mon","Die","Mit","Don","Fri","Sam"],months:["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],monthsShort:["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],today:"Heute",suffix:[],meridiem:[]},chosen:{no_results_text:"Nicht gefunden"},bootbox:{OK:"OK",CANCEL:"Stornieren",CONFIRM:"Bestätigen"}}),$.zui.lang("fr",{"zui.pager":{pageOfText:"Page {0}",prev:"Prev",next:"Suivant",first:"First",last:"Last","goto":"Goto",pageOf:"Page {page}",totalPage:"{totalPage} pages",totalCount:"Total: {recTotal} items",pageSize:"{recPerPage} per page",itemsRange:"De {start} à {end}",pageOfTotal:"Page {page} de {totalPage}"},"zui.boards":{append2end:"Aller jusqu'au bout"},"zui.browser":{tip:"Naviguez sans crainte sur Internet. Mettez votre navigateur à jour dès aujourd'hui!"},"zui.calendar":{weekNames:["Dim","Lun","Mar","Mer","Jeu","Ven","Sam"],monthNames:["Jan","Fév","Mar","Avr","Mai","Juin","Juil","Août","Sep","Oct","Nov","Déc"],today:"Aujourd'hui",year:"{0} Année",month:"{0} Mois",yearMonth:"{0}-{1}"},"zui.chosenIcons":{emptyIcon:"[Aucune icône]",commonIcons:"Icônes communes",webIcons:"Icône Web",editorIcons:"Icône de l'éditeur",directionalIcons:"Flèche confluence",otherIcons:"Autres icônes"},"zui.colorPicker":{errorTip:"Pas une valeur de couleur valide"},"zui.datagrid":{errorCannotGetDataFromRemote:"Impossible d'obtenir les données du serveur distant ({0}).",errorCannotHandleRemoteData:"Impossible de traiter les données renvoyées par le serveur distant."},"zui.guideViewer":{prevStep:"Étape précédente",nextStep:"Prochaine étape"},"zui.tabs":{reload:"Recharger",close:"Fermer",closeOthers:"Fermez les autres onglets",closeRight:"Fermer l'onglet de droite",reopenLast:"Restaurer le dernier onglet fermé",errorCannotFetchFromRemote:"Impossible d'obtenir le contenu du serveur distant ({0})."},"zui.uploader":{},datetimepicker:{days:["Dimanche","Lundi","Mardi","Mercredi","Jeudi","Vendredi","Samedi"],daysShort:["Dim","Lun","Mar","Mer","Jeu","Ven","Sam"],daysMin:["Dim","Lun","Mar","Mer","Jeu","Ven","Sam"],months:["Jan","Fév","Mar","Avr","Mai","Juin","Juil","Août","Sep","Oct","Nov","Déc"],monthsShort:["Jan","Fév","Mar","Avr","Mai","Juin","Juil","Août","Sep","Oct","Nov","Déc"],today:"Aujourd'hui",suffix:[],meridiem:[]},chosen:{no_results_text:"Pas trouvé"},bootbox:{OK:"D'accord",CANCEL:"Annuler",CONFIRM:"Confirmer"}}); \ No newline at end of file +var e=n.children("thead"),i=e[0].getBoundingClientRect(),o=n.next(".fixed-head-table");if(i.top<0){var s=e.width();if(o.length){if(a!==s){a=s;var r=o.find("th");e.find("th").each(function(e){r.eq(e).width(t(this).width())})}}else{var o=t("
    ").addClass(n.attr("class")),l=e.clone(),r=l.find("th");e.find("th").each(function(e){r.eq(e).width(t(this).width())}),o.append(l).insertAfter(n)}o.css({left:i.left,width:i.width}).show()}else o.hide()};o.on("scroll",s).on("resize",s),s()}},t(document).on("click","tr[data-url]",function(){var e=t(this),i=e.data("href")||e.data("url");i&&(window.location.href=i)}),"yes"===config.onlybody&&self===parent&&(window.location.href=window.location.href.replace("?onlybody=yes","").replace("&onlybody=yes","")),t(function(){t("body").addClass("m-{currentModule}-{currentMethod}".format(config))});var u,f,p,g,m,v=function(){u||(u=t("#subNavbar"),f=t("#pageNav"),p=t("#pageActions"),g=u.children(".nav"),m=g.outerWidth());var e=u.outerWidth(),i=f.outerWidth()||0,n=p.outerWidth()||0;if(i=i?i+15:0,n=n?n+15:0,!i&&!n)return void g.css({maxWidth:null,left:null,position:"static"});var o=Math.max(300,e-i-n),a=Math.min(o,m),s=(e-a)/2,r=i&&s.btn-toolbar");if(e.length){var i,n,o=e.children(),a=o.length,s=!1,r=null;if(a)for(o.each(function(e){i=t(this),n=i.is(".divider"),n&&!r&&i.hide(),s||n||(s=!0),r=n?null:i,!n||e!==a-1&&0!==e||i.hide()});i.length&&i.is(".divider");)i=i.hide().prev();s||e.hide()}};t(function(){t(".input-group,.btn-group").fixInputGroup(),k()}),window.holders&&t.each(window.holders,function(e){var i=t("#"+e);i.length&&i.is("input")&&i.attr("placeholder",window.holders[e])}),t(function(){var e=t(".table-responsive"),i=t.fixTableResponsive=function(){e.each(function(){this.scrollHeight-3<=this.clientHeight&&this.scrollWidth-3<=this.clientWidth?t(this).addClass("scroll-none").css("overflow","visible"):t(this).removeClass("scroll-none").css("overflow","auto")})};e.length&&(t(window).on("resize",i),setTimeout(i,100))});var T=function(){var e=this,i=t(e),n=i.closest("tr").find("textarea");if(n.length){var o=32;n.each(function(){var e=t(this).closest("td"),i=e.css("height");e.css("height",this.style.height),this.style.height="auto";var n=this.value?this.scrollHeight+2:32;o=Math.max(o,n),e.css("height",i)}),n.css("height",o)}else{e.style.height="auto";var a=e.value?e.scrollHeight+2:32;e.style.height=a+"px"}};t.autoResizeTextarea=function(e){t(e).each(T)},t(function(){t("textarea.autosize").each(T),t(document).on("input paste change","textarea.autosize",T)}),t(function(){var e=t("#dropMenu,.drop-menu");e.length&&e.on("click",".toggle-right-col",function(e){t(this).closest("#dropMenu,.drop-menu").toggleClass("show-right-col"),e.stopPropagation(),e.preventDefault()})});var S="undefined"!=typeof InstallTrigger;t.zui.browser.firefox=S,t("html").toggleClass("is-firefox",S).toggleClass("not-firefox",!S),t(function(){var e=t("#mainContent>.main-col"),i=e.children(".main-actions");if(i.length){var n=i.prev();if(i.length&&n.length){t('
    ').css("height",i.outerHeight()).insertAfter(i);var o=0,a=function(){var e=n[0].getBoundingClientRect(),s=e.top+e.height+120>t(window).height();if(t("body").toggleClass("main-actions-fixed",s),s){var r=n.width();r?i.width(r):o<10&&setTimeout(a,1e3)}o++};t.resetToolbarPosition=a,a(),t(window).on("resize scroll",a)}}}),t(document).on("show.zui.modal",function(e){t("body.body-modal").length&&window.parent&&window.parent!==window&&t(e.target).is(".modal")&&window.parent.$("body").addClass("hide-modal-close")}).on("hidden.zui.modal",function(){t("body.body-modal").length&&window.parent&&window.parent!==window&&window.parent.$("body").removeClass("hide-modal-close")}),t(function(){var e=t(".dropdown-menu.with-search");e.length&&(e.find(".menu-search").on("click",function(t){return t.stopPropagation(),!1}),e.on("keyup change paste","input",function(){var e=t(this),i=e.closest(".dropdown-menu.with-search"),n=e.val().toLowerCase(),o=i.find(".option");""==n?o.removeClass("hide"):o.each(function(){var e=t(this);e.toggleClass("hide",e.text().toString().toLowerCase().indexOf(n)<0&&e.data("key").toString().toLowerCase().indexOf(n)<0)})}),e.parents(".dropdown-submenu").one("mouseenter",function(){var e=t(this).find(".dropdown-list")[0];e&&e.getBoundingClientRect&&setTimeout(function(){var t=270,i=e.getBoundingClientRect();i.top<0&&(t=Math.min(270,i.height)+i.top),e.style.maxHeight=Math.min(270,t)+"px"},50)})),t(".dropdown-menu.with-search .menu-search").on("click",function(t){return t.stopPropagation(),!1})})}(jQuery),function(t){function e(){var e=window.parent,i=config.currentModule,n=config.currentMethod,o="index"===i&&"index"===n,a="#_single"===location.hash||o||!t("#mainHeader,#editorNav").length||"tutorial"===i||"install"===i||"upgrade"===i||"user"===i&&("login"===n||"deny"===n)||"my"===i&&"changepassword"===n||t("body").hasClass("allow-self-open"),s=location.href;if(e===window&&!a){var r=location.pathname+location.search;return void(location.href=t.createLink("index","index","")+"#app="+encodeURIComponent(r))}if(e!==window&&e.$.apps){o&&e.location.reload();var l=window.name;if(0===l.indexOf("app-")){t.apps=window.apps=e.$.apps;var h=l.substr(4);t.appCode=h,t(document).on("click",function(t){var i=e.document.getElementById(window.name);if(i){var n=e.document.getElementById(i.name)||i;n&&n.dispatchEvent(new Event(t.type,{bubbles:!0}))}}).on("click","a,.open-in-app,.show-in-app",function(e){var i=t(this);if(!i.is("[data-modal],[data-toggle],[data-ride],[data-tab],.iframe,.not-in-app,[target]")&&!i.data("zui.modaltrigger")){var n=i.hasClass("show-in-app")?"":i.attr("href")||(i.is("a")?"":i.data("url")),o=i.data("app")||i.data("group");if(n){if(0===n.indexOf("javascript:")||"#"===n[0])return;var a=t.parseLink(n);if("index"===a.moduleName&&"index"===a.methodName)return window.location.reload(),void e.preventDefault()}else if(!o)return;o||(o=t.apps.getAppCode(n)),o&&("help"===o&&(t.apps.appsMap.help.text=i.text(),t.apps.appsMap.help.url||(t.apps.appsMap.help.url=n)),t.apps.open(n,o)&&e.preventDefault())}}),t.apps.updateUrl(h,s,document.title)}}}function i(){var e=t("#navbar>.nav");if(e.length){var i=t("#heading"),n=+i.css("left").replace("px",""),o=i.outerWidth(),a=e.width(),s=t("#mainHeader>.container").width()-2*n,r=Math.floor((s-a)/2);e.css("marginLeft",r>>0;if(0===o)return!1;for(var a=0|e,s=Math.max(a>=0?a:o-Math.abs(a),0);s1&&("?"===e[0]&&(e=e.substr(1)),e.split("&").forEach(function(t){var e=t.split("=",2);if(e.length>1)try{i[e[0]]=decodeURIComponent(e[1])}catch(n){i[e[0]]=""}else i[e[0]]=""})),t?i[t]:i},t.parseLink=function(e){if(!e)return{};var i=0===e.indexOf("http:")||0===e.indexOf("https:");if(i){var n=window.location.origin;if(e.indexOf(n)<0)return{};e=e.substr((n+config.webRoot).length)}var o=e.split("#"),a=o[0].split("?"),s=a[1],r=s?t.getSearchParam("",s):{},l=a[0],h={isOnlyBody:"yes"===r.onlybody,vars:[],hash:o[1]||"",params:r,tid:r.tid||""};if("GET"===config.requestType){h.moduleName=r[config.moduleVar]||"index",h.methodName=r[config.methodVar]||"index",h.viewType=r[config.viewVar]||config.defaultView;for(var c in r)c!==config.moduleVar&&c!==config.methodVar&&c!==config.viewVar&&"onlybody"!==c&&"tid"!==c&&h.vars.push([c,r[c]])}else{var d=l.lastIndexOf("/");d===l.length-1&&(l=l.substr(0,d),d=l.lastIndexOf("/")),d>=0&&(l=l.substr(d+1));var u=l.lastIndexOf(".");u>=0?(h.viewType=l.substr(u+1),l=l.substr(0,u)):h.viewType=config.defaultView;var f=l.split(config.requestFix);if(h.moduleName=f[0]||"index",h.methodName=f[1]||"index",f.length>2)for(var p=2;p.nav>li").length>10&&(i(),t(window).on("resize",i)),setTimeout(n,1e3)})}(jQuery),function(t){"use strict";function e(e,i){"object"!=typeof i&&(i={user:i});var n=t(e);i=t.extend({},n.data(),i);var o=i.user;"string"==typeof o&&(o={account:o});var a={},s=i.size;s&&(a.width=s,a.height=s,a.lineHeight=s,Number.isNaN(+s)||n.addClass("size-"+s));var r=!!o.avatar;if(n.toggleClass("has-image",r).toggleClass("has-text",!r),n.empty(),r)n.append(t("").attr("src",o.avatar));else{var l=t.zui.strCode(o.account)*(i.hueDistance||43)%360;a.background="hsl("+l+","+(i.saturation||"40%")+","+(i.lightness||"60%")+")",Number.isNaN(+s)||(a.fontSize=Math.round(2*s/3)+"px"),n.append(t("").text(o.account[0].toUpperCase()))}return n.css(a)}t.fn.avatar=function(t){return this.each(function(){e(this,t)})}}(jQuery),$.zui.lang("de",{"zui.pager":{pageOfText:"Seite {0}",prev:"Zurück",next:"Nächste Seite",first:"Erste Seite",last:"Letzte Seite","goto":"Goto",pageOf:"Seite {page}",totalPage:"{totalPage} Seiten",totalCount:"Total: {recTotal} Artikel",pageSize:"{recPerPage} Artikel pro Seite",itemsRange:"Seiten {start} bis {end}",pageOfTotal:"Seite {page}/{totalPage}"},"zui.boards":{append2end:"Gehen Sie zum Ende"},"zui.browser":{tip:"Online. Sorgenfrei. Aktualisiere deinen Browser noch heute!"},"zui.calendar":{weekNames:["Son","Mon","Die","Mit","Don","Fri","Sam"],monthNames:["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],today:"Heute",year:"{0}Jahr",month:"{0}Monat",yearMonth:"{0}-{1}"},"zui.chosenIcons":{emptyIcon:"[Kein Icon]",commonIcons:"Gemeinsame Symbole",webIcons:"Web-Symbol",editorIcons:"Editor-Symbol",directionalIcons:"Pfeil Zusammenfluss",otherIcons:"Andere Symbole"},"zui.colorPicker":{errorTip:"Kein gültiger Farbwert"},"zui.datagrid":{errorCannotGetDataFromRemote:"Daten vom Remote-Server ({0}) können nicht abgerufen werden.",errorCannotHandleRemoteData:"Die vom Remote-Server zurückgegebenen Daten können nicht verarbeitet werden."},"zui.guideViewer":{prevStep:"Vorheriger Schritt",nextStep:"Nächster Schritt"},"zui.tabs":{reload:"Neu laden",close:"Schliessen",closeOthers:"Schließen Sie andere Registerkarten",closeRight:"Schließen Sie die rechte Registerkarte",reopenLast:"Letzten geschlossenen Tab wiederherstellen",errorCannotFetchFromRemote:"Inhalt kann nicht vom Remote-Server abgerufen werden ({0})."},"zui.uploader":{},datetimepicker:{days:["Sonntag","Montag","Diensteg","Mittwoch","Donnerstag","Freitag","Samstag"],daysShort:["Son","Mon","Die","Mit","Don","Fri","Sam"],daysMin:["Son","Mon","Die","Mit","Don","Fri","Sam"],months:["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],monthsShort:["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],today:"Heute",suffix:[],meridiem:[]},chosen:{no_results_text:"Nicht gefunden"},bootbox:{OK:"OK",CANCEL:"Stornieren",CONFIRM:"Bestätigen"}}),$.zui.lang("fr",{"zui.pager":{pageOfText:"Page {0}",prev:"Prev",next:"Suivant",first:"First",last:"Last","goto":"Goto",pageOf:"Page {page}",totalPage:"{totalPage} pages",totalCount:"Total: {recTotal} items",pageSize:"{recPerPage} per page",itemsRange:"De {start} à {end}",pageOfTotal:"Page {page} de {totalPage}"},"zui.boards":{append2end:"Aller jusqu'au bout"},"zui.browser":{tip:"Naviguez sans crainte sur Internet. Mettez votre navigateur à jour dès aujourd'hui!"},"zui.calendar":{weekNames:["Dim","Lun","Mar","Mer","Jeu","Ven","Sam"],monthNames:["Jan","Fév","Mar","Avr","Mai","Juin","Juil","Août","Sep","Oct","Nov","Déc"],today:"Aujourd'hui",year:"{0} Année",month:"{0} Mois",yearMonth:"{0}-{1}"},"zui.chosenIcons":{emptyIcon:"[Aucune icône]",commonIcons:"Icônes communes",webIcons:"Icône Web",editorIcons:"Icône de l'éditeur",directionalIcons:"Flèche confluence",otherIcons:"Autres icônes"},"zui.colorPicker":{errorTip:"Pas une valeur de couleur valide"},"zui.datagrid":{errorCannotGetDataFromRemote:"Impossible d'obtenir les données du serveur distant ({0}).",errorCannotHandleRemoteData:"Impossible de traiter les données renvoyées par le serveur distant."},"zui.guideViewer":{prevStep:"Étape précédente",nextStep:"Prochaine étape"},"zui.tabs":{reload:"Recharger",close:"Fermer",closeOthers:"Fermez les autres onglets",closeRight:"Fermer l'onglet de droite",reopenLast:"Restaurer le dernier onglet fermé",errorCannotFetchFromRemote:"Impossible d'obtenir le contenu du serveur distant ({0})."},"zui.uploader":{},datetimepicker:{days:["Dimanche","Lundi","Mardi","Mercredi","Jeudi","Vendredi","Samedi"],daysShort:["Dim","Lun","Mar","Mer","Jeu","Ven","Sam"],daysMin:["Dim","Lun","Mar","Mer","Jeu","Ven","Sam"],months:["Jan","Fév","Mar","Avr","Mai","Juin","Juil","Août","Sep","Oct","Nov","Déc"],monthsShort:["Jan","Fév","Mar","Avr","Mai","Juin","Juil","Août","Sep","Oct","Nov","Déc"],today:"Aujourd'hui",suffix:[],meridiem:[]},chosen:{no_results_text:"Pas trouvé"},bootbox:{OK:"D'accord",CANCEL:"Annuler",CONFIRM:"Confirmer"}}); diff --git a/www/theme/default/x.style.css b/www/theme/default/x.style.css index cc2bbff026..ba7f5b51ea 100644 --- a/www/theme/default/x.style.css +++ b/www/theme/default/x.style.css @@ -8,5 +8,5 @@ body{padding:0px; margin: 0px;background-color: #fff;} #mainContent .cell{box-shadow:none; -webkit-box-shadow: none;} .xuanxuan-card{padding-bottom:55px;} -.xuancard-actions{width: 100%;text-align: center; padding: 10px 0;} -.xuancard-actions.fixed{position: fixed; background: #fff; border-top: 1px solid #ddd; bottom: 0; margin-bottom: 0; z-index: 999; box-shadow: 0 -2px 10px rgba(0,0,0,0.15);} +.xuancard-actions{width: 100%;text-align: center; height: 34px;} +.xuancard-actions.fixed{position: fixed; background: #fff; border-top: 1px solid #ddd; bottom: 0; margin-bottom: 0; z-index: 999;} diff --git a/www/theme/zui/css/min.css b/www/theme/zui/css/min.css index b2f898482a..e967cb4c4f 100644 --- a/www/theme/zui/css/min.css +++ b/www/theme/zui/css/min.css @@ -1,5 +1,5 @@ /*! - * ZUI: ZUI for Zentao - v1.10.0 - 2021-11-04 + * ZUI: ZUI for Zentao - v1.10.0 - 2021-11-18 * http://openzui.com * GitHub: https://github.com/easysoft/zui.git * Copyright (c) 2021 cnezsoft.com; Licensed MIT @@ -13,4 +13,4 @@ * * Copyright (c) 2011-2016 Harvest http://getharvest.com * MIT License, https://github.com/harvesthq/chosen/blob/master/LICENSE.md -*/.chosen-container{position:relative;display:block;font-size:13px;vertical-align:middle;zoom:1;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.chosen-container .chosen-drop{position:absolute;top:100%;z-index:1010;display:none;width:100%;background:#fff;border:1px solid #b6bdcc;border:1px solid rgba(0,0,0,.15);border-top:0;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.chosen-container .chosen-drop.chosen-drop-size-limited{border-top:1px solid rgba(0,0,0,.15)}.chosen-container .chosen-drop.chosen-auto-max-width{min-width:100%;border-top:1px solid rgba(0,0,0,.15);opacity:0}.chosen-container .chosen-drop.chosen-auto-max-width>.chosen-results>li{display:inline-block;white-space:nowrap}.chosen-container .chosen-drop.chosen-auto-max-width.in{opacity:1}.chosen-container .chosen-drop.chosen-auto-max-width.in>.chosen-results>li{display:block;white-space:normal}.chosen-container .chosen-drop.chosen-no-wrap>.chosen-results>li{overflow:hidden;text-overflow:ellipsis;white-space:nowrap!important}.chosen-container.chosen-with-drop .chosen-drop{display:block}.chosen-container a{cursor:pointer}.chosen-container.chosen-up .chosen-drop{top:inherit;bottom:100%;margin-top:auto;margin-bottom:-1px;border-radius:2px 2px 0 0;-webkit-box-shadow:0 -3px 5px rgba(0,0,0,.175);box-shadow:0 -3px 5px rgba(0,0,0,.175)}.chosen-container.chosen-highlight-selected .result-selected{color:#0c64eb;background:#e9f2fb}.chosen-container-single .chosen-single{display:block;width:100%;height:32px;padding:5px 8px;overflow:hidden;line-height:1.42857143;color:#222;text-decoration:none;white-space:nowrap;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #dcdcdc;border-radius:2px;-webkit-box-shadow:none;box-shadow:none;-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s,-webkit-box-shadow ease-in-out .15s}.chosen-container-single .chosen-default{color:#838a9d}.chosen-container-single .chosen-single>span{display:block;margin-right:26px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.chosen-container-single .chosen-single-with-deselect span{margin-right:38px}.chosen-container-single .chosen-single abbr{position:absolute;top:5px;right:24px;display:block;width:20px;height:20px;font-family:sans-serif;font-size:18px;font-weight:700;line-height:18px;color:#000;text-align:center;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.chosen-container-single .chosen-single abbr:before{display:block;content:'×'}.chosen-container-single .chosen-single abbr:focus,.chosen-container-single .chosen-single abbr:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}.chosen-container-single .chosen-single div{position:absolute;top:0;right:0;display:block;height:100%;padding:5px 8px}.chosen-container-single .chosen-single div b{display:inline-block;width:0;height:0;margin-bottom:2px;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent;opacity:.5}.chosen-container-single .chosen-search{position:relative;z-index:1010;padding:3px 4px;margin:0;white-space:nowrap}.chosen-container-single .chosen-search input[type=text]{width:100%;height:27px;padding:2px 26px 2px 8px;margin:1px 0;font-size:12px;line-height:1.5;background-color:#fff;border:1px solid #dcdcdc;border-radius:2px;outline:0}.chosen-container-single .chosen-search input[type=text]:focus{border-color:#0c64eb}.chosen-container-single .chosen-search:before{position:absolute;top:10px;right:10px;display:block;font-family:ZentaoIcon;font-size:14px;font-style:normal;font-weight:400;font-variant:normal;line-height:1;color:#838a9d;text-transform:none;content:"\e928";speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.chosen-container-single .chosen-drop{margin-top:-1px;-webkit-background-clip:padding-box;background-clip:padding-box;border-radius:0 0 4px 4px}.chosen-container-single.chosen-container-single-nosearch .chosen-search{position:absolute;left:-9999px}.chosen-container .chosen-results{position:relative;max-height:240px;padding:0;margin:0;overflow-x:hidden;overflow-y:auto;-webkit-overflow-scrolling:touch}.chosen-container .chosen-results li{display:none;padding:5px 10px;margin:0;line-height:15px;list-style:none;-webkit-transition:background-color .2s cubic-bezier(.175,.885,.32,1);-o-transition:background-color .2s cubic-bezier(.175,.885,.32,1);transition:background-color .2s cubic-bezier(.175,.885,.32,1);-webkit-touch-callout:none}.chosen-container .chosen-results li.active-result{display:list-item;cursor:pointer}.chosen-container .chosen-results li.disabled-result{display:list-item;color:#ccc;cursor:default}.chosen-container .chosen-results li.highlighted{color:#fff;background-color:#0c64eb}.chosen-container .chosen-results li.no-results{display:list-item;background:#f4f4f4}.chosen-container .chosen-results li.group-result{display:list-item;font-weight:700;cursor:default}.chosen-container .chosen-results li.group-option{padding-left:15px}.chosen-container .chosen-results li em{font-style:normal;text-decoration:underline}.chosen-container-multi .chosen-choices{position:relative;width:100%;min-height:32px;min-height:30px\9;padding:0;margin:0;overflow:hidden;cursor:text;background-color:#fff;border:1px solid #dcdcdc;border-radius:2px;-webkit-box-shadow:none;box-shadow:none;-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s,-webkit-box-shadow ease-in-out .15s}.chosen-container-multi .chosen-choices:after,.chosen-container-multi .chosen-choices:before{display:table;content:" "}.chosen-container-multi .chosen-choices:after{clear:both}.chosen-container-multi .chosen-choices li{display:block;float:left;padding:0 6px;margin:5px 4px;list-style:none}.chosen-container-multi .chosen-choices li.search-field{padding:0;line-height:12px;white-space:nowrap}.chosen-container-multi .chosen-choices li.search-field input[type=text]{height:20px;font-size:100%;color:#838a9d;background:0 0!important;border:0!important;border-radius:0;outline:0;-webkit-box-shadow:none;box-shadow:none}.chosen-container-multi .chosen-choices li.search-field .default{color:#999}.chosen-container-multi .chosen-choices li.search-field:before{position:absolute;right:8px;bottom:8px;display:block;font-family:ZentaoIcon;font-size:14px;font-style:normal;font-weight:400;font-variant:normal;line-height:1;color:#838a9d;text-transform:none;content:"\e928";opacity:0;-webkit-transition:opacity .2s cubic-bezier(.175,.885,.32,1);-o-transition:opacity .2s cubic-bezier(.175,.885,.32,1);transition:opacity .2s cubic-bezier(.175,.885,.32,1);speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.chosen-container-multi .chosen-choices li.search-choice{position:relative;padding:3px 20px 3px 5px;line-height:12px;cursor:default;background-color:#f1f1f1;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #cbd0db;border-radius:3px;-webkit-box-shadow:0 0 2px #fff inset,0 1px 0 rgba(0,0,0,.05);box-shadow:0 0 2px #fff inset,0 1px 0 rgba(0,0,0,.05);-webkit-transition:all .4s cubic-bezier(.175,.885,.32,1);-o-transition:all .4s cubic-bezier(.175,.885,.32,1);transition:all .4s cubic-bezier(.175,.885,.32,1)}.chosen-container-multi .chosen-choices li.search-choice:hover{background-color:#fff;border-color:#adb5c6;-webkit-box-shadow:0 1px 0 rgba(0,0,0,.1);box-shadow:0 1px 0 rgba(0,0,0,.1)}.chosen-container-multi .chosen-choices li.search-choice .search-choice-close{position:absolute;top:1px;right:0;display:block;width:20px;height:18px;line-height:18px;color:#000;text-align:center;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.chosen-container-multi .chosen-choices li.search-choice .search-choice-close:before{font-family:ZentaoIcon;font-size:14px;font-style:normal;font-weight:400;font-variant:normal;line-height:1;text-shadow:0 1px 0 #fff;text-transform:none;content:'\d7';speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.chosen-container-multi .chosen-choices li.search-choice .search-choice-close:focus,.chosen-container-multi .chosen-choices li.search-choice .search-choice-close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}.chosen-container-multi .chosen-choices li.search-choice-disabled{padding-right:5px;color:#666;background-color:#e4e4e4;border:1px solid #ccc}.chosen-container-multi .chosen-choices li.search-choice-focus{background:#d4d4d4}.chosen-container-multi .chosen-choices li.search-choice-focus .search-choice-close{background-position:-42px -10px}.chosen-container-multi .chosen-results{padding:5px 0;margin:0}.chosen-container-multi .chosen-drop .result-selected{display:list-item;color:#ccc;cursor:default}.chosen-container-active .chosen-single{border-color:#0c64eb;-webkit-box-shadow:none,0 0 8px rgba(12,100,235,.6);box-shadow:none,0 0 8px rgba(12,100,235,.6)}.chosen-container-active.chosen-with-drop .chosen-single{border:1px solid #b6bdcc;border:1px solid rgba(0,0,0,.15);border-bottom-right-radius:0!important;border-bottom-left-radius:0!important;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.chosen-container-active.chosen-with-drop .chosen-single div{background:0 0;border-left:none}.chosen-container-active.chosen-with-drop .chosen-single div b{content:"";border-top:0 dotted;border-bottom:4px solid}.chosen-container-active.chosen-with-drop.chosen-up .chosen-single{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:2px;border-bottom-left-radius:2px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.chosen-container-active .chosen-choices{border-color:#0c64eb;-webkit-box-shadow:none,0 0 8px rgba(12,100,235,.6);box-shadow:none,0 0 8px rgba(12,100,235,.6)}.chosen-container-active .chosen-choices li.search-field input[type=text]{color:#111!important}.chosen-container-active .chosen-choices li.search-field:before{opacity:1}.chosen-disabled{cursor:default;opacity:.5!important}.chosen-disabled .chosen-single{cursor:default}.chosen-disabled .chosen-choices .search-choice .search-choice-close{cursor:default}.chosen-compact.chosen-container-single .chosen-single>.chosen-search{left:0;display:none;padding:3px 4px;opacity:0}.chosen-compact.chosen-container-single .chosen-single>.chosen-search>input{height:25px;padding:2px 26px 2px 4px;font-size:inherit}.chosen-compact.chosen-container-single .chosen-single>.chosen-search:before{top:9px}.chosen-compact.chosen-with-search.chosen-with-drop .chosen-single>.chosen-search{display:block;opacity:1}select.chosen[multiple]{height:32px;overflow:hidden}select.chosen[multiple] option{visibility:hidden}.container,.container-fixed,.container-fluid{position:relative}.container{max-width:1800px!important}body{background-color:#efefef}body.article-content,body.body-modal{background:0 0}body.body-modal{padding:0}@media screen and (min-width:1920px){body{font-size:14px}}a:active,a:focus,button:active,button:focus{outline:0!important}.strong{font-weight:700}.font-normal{font-weight:400!important}.text-middle{vertical-align:middle!important}.text-bottom{vertical-align:bottom!important}.text-top{vertical-align:top!important}.inline-block{display:inline-block!important}.layer{border-radius:4px;-webkit-box-shadow:0 0 20px 0 #bdc9d8;box-shadow:0 0 20px 0 #bdc9d8}.space{margin-bottom:20px}.space-lg{margin-bottom:30px}.space-sm{margin-bottom:10px}.muted{opacity:.5}.text-muted em{color:#3c4353}.no-animate{-webkit-transition:none!important;-o-transition:none!important;transition:none!important}.template{display:none!important}.text-left{text-align:left!important}.text-yellow.icon-folder{color:#ffe066}.table-row{display:table;width:100%;table-layout:fixed}.table-col,.table-row>.col,.table-row>[class*=col-],.table-row>[class*="-col"]{display:table-cell;float:none;vertical-align:top}.side-col{width:200px;padding-right:20px}.side-col.col-4{width:33.3333333%}.col-lg{width:260px}.col-xl{width:320px}.col-sm{width:150px}.col-xs{width:100px}.main-col+.side-col{padding-right:0;padding-left:20px}.row-grid>[class*=col-],.row-grid>[class*="-col"]{padding-top:6px;padding-bottom:6px}hr.space{margin:10px 0;border:none}hr.space-sm{margin:5px 0;border:none}.text-secondary{color:#16a8f8}a.text-primary{color:#0c64eb}.nav-primary>li>a{min-width:100px;padding:5px 8px;color:#838a9d;border-color:#e7f1fc}.nav-primary>li.active>a{color:#0c64eb;background-color:#e7f1fc;border-color:#e7f1fc}.nav-primary>li.active>a:hover{color:#0c64eb;background-color:#c3dcf7;border-color:#c3dcf7}.end-marker{margin-bottom:20px;color:#cbd0db;text-align:center}@-webkit-keyframes highlight{0%{background:#fff;outline:1px solid transparent}100%{background:#fff0d5;outline:2px solid #ffdcbc}}@-o-keyframes highlight{0%{background:#fff;outline:1px solid transparent}100%{background:#fff0d5;outline:2px solid #ffdcbc}}@keyframes highlight{0%{background:#fff;outline:1px solid transparent}100%{background:#fff0d5;outline:2px solid #ffdcbc}}.highlight{-webkit-animation:highlight .5s linear 0s 2 alternate;-o-animation:highlight .5s linear 0s 2 alternate;animation:highlight .5s linear 0s 2 alternate}.progress.inline-block{width:100px;margin:0}.w-p5{width:5%!important}.w-p10{width:10%!important}.w-p15{width:15%!important}.w-p20{width:20%!important}.w-p25{width:25%!important}.w-p30{width:30%!important}.w-p35{width:35%!important}.w-p40{width:40%!important}.w-p45{width:45%!important}.w-p50{width:50%!important}.w-p55{width:55%!important}.w-p60{width:60%!important}.w-p65{width:65%!important}.w-p70{width:70%!important}.w-p75{width:75%!important}.w-p80{width:80%!important}.w-p85{width:85%!important}.w-p90{width:90%!important}.w-p94{width:94%!important}.w-p95{width:95%!important}.w-p98{width:98%!important}.w-p99{width:99%!important}.w-p100{width:100%!important}.w-auto{width:auto!important}.w-10px{width:10px!important}.w-20px{width:20px!important}.w-30px{width:30px!important}.w-35px{width:35px!important}.w-40px{width:40px!important}.w-45px{width:45px!important}.w-50px{width:50px!important}.w-60px{width:60px!important}.w-70px{width:70px!important}.w-80px{width:80px!important}.w-90px{width:90px!important}.w-100px{width:100px!important}.w-110px{width:110px!important}.w-120px{width:120px!important}.w-130px{width:130px!important}.w-140px{width:140px!important}.w-150px{width:150px!important}.w-160px{width:160px!important}.w-180px{width:180px!important}.w-200px{width:200px!important}.w-230px{width:230px!important}.w-250px{width:250px!important}.w-300px{width:300px!important}.w-400px{width:400px!important}.w-500px{width:500px!important}.w-600px{width:600px!important}.w-700px{width:700px!important}.w-800px{width:800px!important}.w-900px{width:900px!important}.mw-200px{max-width:200px!important}.mw-300px{max-width:300px!important}.mw-400px{max-width:400px!important}.mw-500px{max-width:500px!important}.mw-600px{max-width:600px!important}.mw-700px{max-width:700px!important}.mw-800px{max-width:800px!important}.mw-900px{max-width:900px!important}.mw-1400px{max-width:1400px!important}.w-id{width:70px!important}.w-pri{width:40px!important}.w-severity{width:50px!important}.w-hour{width:57px!important}.w-date{width:90px!important}.w-status{width:60px!important}.w-resolution,.w-type,.w-user{width:80px!important}.w-p15-f{width:15%!important;min-width:120px!important}.w-p25-f{width:25%!important;min-width:200px!important}.w-p35-f{width:35%!important;min-width:300px!important}.w-p45-f{width:45%!important;min-width:400px!important}.h-5px{height:5px!important}.h-10px{height:10px!important}.h-20px{height:20px!important}.h-30px{height:30px!important}.h-35px{height:35px!important}.h-40px{height:40px!important}.h-45px{height:45px!important}.h-50px{height:50px!important}.h-60px{height:60px!important}.h-70px{height:70px!important}.h-80px{height:80px!important}.h-100px{height:100px!important}.h-120px{height:120px!important}.h-130px{height:130px!important}.h-140px{height:140px!important}.h-150px{height:150px!important}.h-200px{height:200px!important}.pd-0{padding:0!important}.mg-0{margin:0!important}.mgb-20{margin-bottom:20px!important}.mgb-10{margin-bottom:10px!important}.pdb-20{padding-bottom:20px!important}.pdt-20{padding-top:20px!important}.br-0{border-radius:0!important}.bd-0,.bd-none,.borderless{border:none!important}.bg-none{background:0 0!important}.red{color:#ff5d5d!important}.icon-pro-version{font-size:14px!important}.icon-pro-version:before{position:relative;top:-1px;font-size:14px;color:#ff5d5d;content:"\e92b"}.bg-primary{color:#fff;background:#1183fb -webkit-gradient(linear,right top,left top,from(#0a48d1),to(#1183fb));background:#1183fb -webkit-linear-gradient(right,#0a48d1 0,#1183fb 100%);background:#1183fb -o-linear-gradient(right,#0a48d1 0,#1183fb 100%);background:#1183fb linear-gradient(-90deg,#0a48d1 0,#1183fb 100%);background-color:#00b1fd}.bg-secondary{color:#fff;background:#16a8f8}.hl-tutorial{position:relative!important;z-index:1010!important;-webkit-box-shadow:0 0 0 0 #000!important;box-shadow:0 0 0 0 #000!important;-webkit-transition:-webkit-box-shadow 1s!important;-o-transition:box-shadow 1s!important;transition:-webkit-box-shadow 1s!important;transition:box-shadow 1s!important;transition:box-shadow 1s,-webkit-box-shadow 1s!important}.hl-tutorial.hl-in{-webkit-box-shadow:0 0 20px 0 #ffff8d,0 0 0 2px #ffd180,0 0 0 3000px rgba(0,0,0,.2)!important;box-shadow:0 0 20px 0 #ffff8d,0 0 0 2px #ffd180,0 0 0 3000px rgba(0,0,0,.2)!important}.btn.tooltip-tutorial,.hl-tutorial.hl-in:hover{position:relative!important;z-index:1010!important;-webkit-box-shadow:0 0 30px 0 #ffff8d,0 0 0 5px #ffd180,0 0 0 3000px rgba(0,0,0,.3)!important;box-shadow:0 0 30px 0 #ffff8d,0 0 0 5px #ffd180,0 0 0 3000px rgba(0,0,0,.3)!important}.tooltip-max .tooltip-inner{max-width:1000px;padding:8px 10px}.transition-all *{-webkit-transition:all .2s!important;-o-transition:all .2s!important;transition:all .2s!important}.scroll-x{overflow-x:auto!important}.scroll-y{overflow-y:auto!important}.divider+.divider{display:none}.ie *{-webkit-transition:none!important;-o-transition:none!important;transition:none!important}@font-face{font-family:Oswald;font-weight:400;src:url(../fonts/Oswald-Regular.ttf)}@font-face{font-family:Oswald;font-weight:500;src:url(../fonts/Oswald-Medium.ttf)}@font-face{font-family:Oswald;font-weight:300;src:url(../fonts/Oswald-Light.ttf)}.num{font-family:Oswald;font-weight:400}@font-face{font-family:ZentaoIcon;font-style:normal;font-weight:400;src:url(../fonts/ZentaoIcon.eot?v=1.18);src:url(../fonts/ZentaoIcon.eot?#iefix&v=1.18) format('embedded-opentype'),url(../fonts/ZentaoIcon.woff?v=1.18) format('woff'),url(../fonts/ZentaoIcon.ttf?v=1.18) format('truetype'),url(../fonts/ZentaoIcon.svg#regular?v=1.18) format('svg')}.icon,[class*=" icon-"],[class^=icon-]{font-family:ZentaoIcon;font-size:14px;font-style:normal;font-weight:400;font-variant:normal;line-height:1;text-transform:none;speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon:before{display:inline-block;min-width:14px;text-align:center}a .icon,a [class*=" icon-"],a [class^=icon-]{display:inline}.icon-lg:before{font-size:1.33333333em;vertical-align:-10%}.icon-2x{font-size:28px}.icon-3x{font-size:42px}.icon-4x{font-size:56px}.icon-5x{font-size:70px}.icon-spin{display:inline-block;-webkit-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;animation:spin 2s infinite linear}a .icon-spin{display:inline-block;text-decoration:none}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0);transform:rotate(0)}100%{-o-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes spin{0%{-webkit-transform:rotate(0);-o-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);-o-transform:rotate(359deg);transform:rotate(359deg)}}.icon-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.icon-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.icon-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.icon-flip-horizontal{-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);-o-transform:scale(-1,1);transform:scale(-1,1)}.icon-flip-vertical{-webkit-transform:scale(1,-1);-ms-transform:scale(1,-1);-o-transform:scale(1,-1);transform:scale(1,-1)}.icon.icon-flip-horizontal,.icon.icon-flip-vertical,.icon.icon-rotate-180,.icon.icon-rotate-270,.icon.icon-rotate-90{display:inline-block}.icon-zentao:before{content:"\e901"}.icon-zentao-alt:before{content:"\e900"}.icon-help:before{content:"\e968"}.icon-import:before{content:"\e904"}.icon-download:before{content:"\e904"}.icon-export:before{content:"\e905"}.icon-lightbulb:before{content:"\e91c"}.icon-close:before{content:"\e936"}.icon-check:before{content:"\e5ca"}.icon-plus:before{content:"\e925"}.icon-minus:before{content:"\e926"}.icon-expand-alt:before{content:"\e6f1"}.icon-collapse-alt:before{content:"\e6f2"}.icon-fullscreen:before{content:"\e96b"}.icon-star-empty:before{content:"\e94a"}.icon-star:before{content:"\e94b"}.icon-exclamation-sign:before{content:"\e930"}.icon-flag:before{content:"\e937"}.icon-check-circle:before{content:"\e92f"}.icon-check-sign:before{content:"\e938"}.icon-chart-pie:before{content:"\e95b"}.icon-history:before{content:"\e95f"}.icon-pencil:before{content:"\e254"}.icon-search:before{content:"\e928"}.icon-restart:before{content:"\e95e"}.icon-cog:before{content:"\e93b"}.icon-chart-line:before{content:"\e95c"}.icon-chart-bar:before{content:"\e95d"}.icon-bar-chart:before{content:"\e95d"}.icon-exchange:before{content:"\e927"}.icon-severity:before{content:"\e973"}.icon-book:before{content:"\f02d"}.icon-treemap-alt:before{content:"\e971"}.icon-severity-solid:before{content:"\e902"}.icon-chat-line:before{content:"\e998"}.icon-stack:before{content:"\e943"}.icon-cube:before{content:"\e967"}.icon-minus-sign:before{content:"\e939"}.icon-bars-sign:before{content:"\e93a"}.icon-chat:before{content:"\e940"}.icon-message:before{content:"\e940"}.icon-more:before{content:"\e744"}.icon-certificate:before{content:"\f0a3"}.icon-bell:before{content:"\e7f5"}.icon-columns:before{content:"\f0db"}.icon-envelope-o:before{content:"\e92a"}.icon-unfold-all:before{content:"\e931"}.icon-fold-all:before{content:"\e932"}.icon-bars:before{content:"\e948"}.icon-cards-view:before{content:"\e949"}.icon-ellipsis-v:before{content:"\e5d4"}.icon-spinner-indicator:before{content:"\e982"}.icon-up-circle:before{content:"\e92b"}.icon-right-circle:before{content:"\e92c"}.icon-down-circle:before{content:"\e92d"}.icon-left-circle:before{content:"\e92e"}.icon-angle-double-right:before{content:"\f101"}.icon-angle-down:before{content:"\e313"}.icon-angle-left:before{content:"\e314"}.icon-angle-right:before{content:"\e315"}.icon-angle-top:before{content:"\e316"}.icon-first-page:before{content:"\e5dc"}.icon-last-page:before{content:"\e5dd"}.icon-caret-down:before{content:"\f0d7"}.icon-caret-up:before{content:"\f0d8"}.icon-caret-left:before{content:"\f0d9"}.icon-caret-right:before{content:"\f0da"}.icon-sort:before{content:"\f0dc"}.icon-sort-down:before{content:"\f0dd"}.icon-sort-up:before{content:"\f0de"}.icon-arrow-up:before{content:"\e923"}.icon-arrow-down:before{content:"\e924"}.icon-arrow-left:before{content:"\e952"}.icon-arrow-right:before{content:"\e93e"}.icon-chevron-left:before{content:"\e934"}.icon-chevron-right:before{content:"\e935"}.icon-chevron-double-up:before{content:"\e959"}.icon-chevron-double-down:before{content:"\e95a"}.icon-folder-account:before{content:"\e942"}.icon-folder-move:before{content:"\e960"}.icon-folder-plus:before{content:"\e961"}.icon-folder-upload:before{content:"\e962"}.icon-folder-star:before{content:"\e963"}.icon-folder-edit:before{content:"\e964"}.icon-folder-download:before{content:"\e965"}.icon-folder-outline:before{content:"\e966"}.icon-folder:before{content:"\e944"}.icon-folder-o:before{content:"\e945"}.icon-folder-open-o:before{content:"\e946"}.icon-folder-open:before{content:"\e947"}.icon-color:before{content:"\e93c"}.icon-paper-clip:before{content:"\e93d"}.icon-text:before{content:"\e929"}.icon-share:before{content:"\f064"}.icon-list:before{content:"\e9a8"}.icon-format-list-bulleted:before{content:"\e9a8"}.icon-format-bold:before{content:"\e953"}.icon-format-header-pound:before{content:"\e954"}.icon-format-italic:before{content:"\e955"}.icon-format-list-numbers:before{content:"\e969"}.icon-format-quote-close:before{content:"\e96a"}.icon-image:before{content:"\e96c"}.icon-table-large:before{content:"\e96d"}.icon-aiux:before{content:"\e99e"}.icon-qc:before{content:"\e986"}.icon-qc-q:before{content:"\e985"}.icon-qc-c:before{content:"\e987"}.icon-menu-my:before{content:"\e97a"}.icon-home:before{content:"\e97a"}.icon-program:before{content:"\e9aa"}.icon-lightbulb-alt:before{content:"\e98f"}.icon-product:before{content:"\e98f"}.icon-rocket:before{content:"\e99c"}.icon-project:before{content:"\e99c"}.icon-run:before{content:"\e9a9"}.icon-test:before{content:"\e956"}.icon-infinite:before{content:"\e9a3"}.icon-devops:before{content:"\e9a3"}.icon-ops:before{content:"\e903"}.icon-doc:before{content:"\e99b"}.icon-menu-doc:before{content:"\e99b"}.icon-statistic:before{content:"\e999"}.icon-menu-backend:before{content:"\e993"}.icon-assets:before{content:"\e9ae"}.icon-diamond:before{content:"\e9ae"}.icon-feedback:before{content:"\e991"}.icon-flow:before{content:"\e994"}.icon-oa:before{content:"\e9a1"}.icon-more-circle:before{content:"\e988"}.icon-controls:before{content:"\e995"}.icon-account:before{content:"\e992"}.icon-about:before{content:"\e996"}.icon-cog-outline:before{content:"\e997"}.icon-backend:before{content:"\e997"}.icon-exit:before{content:"\e99a"}.icon-theme:before{content:"\e9a0"}.icon-globe:before{content:"\f0ac"}.icon-lang:before{content:"\f0ac"}.icon-usecase:before{content:"\e99d"}.icon-code:before{content:"\e990"}.icon-summary:before{content:"\e9ad"}.icon-more-alt:before{content:"\e9a7"}.icon-waterfall:before{content:"\e9a4"}.icon-manual:before{content:"\e98d"}.icon-kanban:before{content:"\e983"}.icon-lane:before{content:"\e9b1"}.icon-thumbs-up:before{content:"\f087"}.icon-thumbs-down:before{content:"\f088"}.icon-hash:before{content:"\e9ab"}.icon-version:before{content:"\e9ab"}.icon-p-square:before{content:"\e97b"}.icon-video-play:before{content:"\e97f"}.icon-plus-solid-circle:before{content:"\e974"}.icon-s:before{content:"\e975"}.icon-c:before{content:"\e976"}.icon-t:before{content:"\e977"}.icon-guide:before{content:"\e978"}.icon-todo:before{content:"\e979"}.icon-side-left:before{content:"\e9b3"}.icon-side-right:before{content:"\e9b2"}.icon-fullscreen-exit:before{content:"\e972"}.icon-alert:before{content:"\e99f"}.icon-back:before{content:"\e93f"}.icon-swap:before{content:"\e9b0"}.icon-clock:before{content:"\e97c"}.icon-cost:before{content:"\e97d"}.icon-pencil-alt:before{content:"\e984"}.icon-rich-text:before{content:"\e913"}.icon-markdown:before{content:"\e916"}.icon-excel:before{content:"\e933"}.icon-text-link:before{content:"\e94d"}.icon-ppt:before{content:"\e957"}.icon-word:before{content:"\e958"}.icon-doc-lib:before{content:"\e96f"}.icon-file:before{content:"\f016"}.icon-file-empty:before{content:"\f016"}.icon-file-text:before{content:"\f0f6"}.icon-file-alt:before{content:"\f15b"}.icon-file-text-alt:before{content:"\f15c"}.icon-file-pdf:before{content:"\f1c1"}.icon-file-word:before{content:"\f1c2"}.icon-file-excel:before{content:"\f1c3"}.icon-file-powerpoint:before{content:"\f1c4"}.icon-file-image:before{content:"\f1c5"}.icon-file-archive:before{content:"\f1c6"}.icon-file-audio:before{content:"\f1c7"}.icon-file-video:before{content:"\f1c8"}.icon-file-code:before{content:"\f1c9"}.icon-menu-collapse:before{content:"\e980"}.icon-menu-expand:before{content:"\e981"}.icon-group:before{content:"\e97e"}.icon-menu-users:before{content:"\e97e"}.icon-persons:before{content:"\e97e"}.icon-team:before{content:"\e97e"}.icon-estimate:before{content:"\e9ac"}.icon-sprint:before{content:"\e9a2"}.icon-shield-check:before{content:"\e9a5"}.icon-ok:before{content:"\e9a6"}.icon-printer:before{content:"\e906"}.icon-bullhorn:before{content:"\e910"}.icon-person:before{content:"\e941"}.icon-fields:before{content:"\e989"}.icon-trigger:before{content:"\e98a"}.icon-layout:before{content:"\e98b"}.icon-audit:before{content:"\e98c"}.icon-cancel:before{content:"\e951"}.icon-ban-circle:before{content:"\e951"}.icon-eye:before{content:"\e94e"}.icon-eye-off:before{content:"\e96e"}.icon-unlock:before{content:"\e94f"}.icon-lock:before{content:"\e950"}.icon-private:before{content:"\e950"}.icon-move:before{content:"\e94c"}.icon-hand-right:before{content:"\e907"}.icon-checked:before{content:"\e908"}.icon-off:before{content:"\e909"}.icon-start:before{content:"\e90a"}.icon-play:before{content:"\e90a"}.icon-time:before{content:"\e90b"}.icon-edit:before{content:"\e90c"}.icon-trash:before{content:"\e90d"}.icon-link:before{content:"\e90e"}.icon-unlink:before{content:"\e90f"}.icon-bug:before{content:"\e911"}.icon-list-alt:before{content:"\e912"}.icon-change:before{content:"\e970"}.icon-alter:before{content:"\e970"}.icon-glasses:before{content:"\e914"}.icon-review:before{content:"\e914"}.icon-sitemap:before{content:"\e915"}.icon-testcase:before{content:"\e915"}.icon-pluses:before{content:"\e917"}.icon-report-list:before{content:"\e918"}.icon-magic:before{content:"\e919"}.icon-active:before{content:"\e919"}.icon-treemap:before{content:"\e91a"}.icon-confirm:before{content:"\e91b"}.icon-split:before{content:"\e98e"}.icon-delay:before{content:"\e91d"}.icon-calendar:before{content:"\e91d"}.icon-pause:before{content:"\e91e"}.icon-ban:before{content:"\e91f"}.icon-plus-bold:before{content:"\e920"}.icon-copy:before{content:"\e921"}.icon-refresh:before{content:"\e922"}.icon-sm:before{font-size:14px;vertical-align:10%}.icon-qc{position:relative}.icon-qc:before{width:1em;color:#7cb938;content:"\e985"}.icon-qc:after{position:absolute;top:0;left:0;width:1em;height:1em;font-family:ZentaoIcon;font-size:14px;font-size:inherit;font-style:normal;font-weight:400;font-variant:normal;line-height:1;color:#36a742;text-transform:none;content:"\e987";speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-message.has-dot{position:relative}.icon-message.has-dot::after{position:absolute;top:-3px;right:-5px;display:block;width:6px;height:6px;content:' ';background-color:#ff5d5d;border-radius:50%}.icon-project{-webkit-transform:scale(1.2);-ms-transform:scale(1.2);-o-transform:scale(1.2);transform:scale(1.2)}.icon-product{-webkit-transform:scale(1.15);-ms-transform:scale(1.15);-o-transform:scale(1.15);transform:scale(1.15)}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:13px;font-weight:400;line-height:18px;text-align:center;white-space:nowrap;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border:1px solid transparent;border-radius:4px;-webkit-transition:.4s cubic-bezier(.175,.885,.32,1);-o-transition:.4s cubic-bezier(.175,.885,.32,1);transition:.4s cubic-bezier(.175,.885,.32,1);-webkit-transition-property:background,border,outline,opacity,-webkit-box-shadow;-o-transition-property:background,border,box-shadow,outline,opacity;transition-property:background,border,outline,opacity,-webkit-box-shadow;transition-property:background,border,box-shadow,outline,opacity;transition-property:background,border,box-shadow,outline,opacity,-webkit-box-shadow}.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:focus,.btn:hover{color:#3c4353;text-decoration:none}.btn:active{text-decoration:none;background-image:none;outline:0;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.1)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{pointer-events:none;cursor:not-allowed;filter:grayscale(1);-webkit-box-shadow:none;box-shadow:none;opacity:.5;-webkit-filter:grayscale(1)}.btn{color:#3c4353;background-color:#fff;border-color:#d6dae3}.btn.active,.btn.hover,.btn:active,.btn:focus,.btn:hover,.open .dropdown-toggle.btn{color:#3c4353;background-color:rgba(255,255,255,.8);border-color:#b8bfce}.btn.active,.btn:active,.open .dropdown-toggle.btn{background-color:#f2f2f2;background-image:none;border-color:#b8bfce}.btn.disabled,.btn.disabled.active,.btn.disabled:active,.btn.disabled:focus,.btn.disabled:hover,.btn[disabled],.btn[disabled].active,.btn[disabled]:active,.btn[disabled]:focus,.btn[disabled]:hover,fieldset[disabled] .btn,fieldset[disabled] .btn.active,fieldset[disabled] .btn:active,fieldset[disabled] .btn:focus,fieldset[disabled] .btn:hover{color:rgba(60,67,83,.3);background-color:#fff;border-color:#d6dae3}.btn-gray{color:#82899f;background-color:#f1f1f1;border-color:#f1f1f1}.btn-gray.active,.btn-gray.hover,.btn-gray:active,.btn-gray:focus,.btn-gray:hover,.open .dropdown-toggle.btn-gray{color:#82899f;background-color:rgba(241,241,241,.8);border-color:#d8d8d8}.btn-gray.active,.btn-gray:active,.open .dropdown-toggle.btn-gray{background-color:#e4e4e4;background-image:none;border-color:#d8d8d8}.btn-gray.disabled,.btn-gray.disabled.active,.btn-gray.disabled:active,.btn-gray.disabled:focus,.btn-gray.disabled:hover,.btn-gray[disabled],.btn-gray[disabled].active,.btn-gray[disabled]:active,.btn-gray[disabled]:focus,.btn-gray[disabled]:hover,fieldset[disabled] .btn-gray,fieldset[disabled] .btn-gray.active,fieldset[disabled] .btn-gray:active,fieldset[disabled] .btn-gray:focus,fieldset[disabled] .btn-gray:hover{color:rgba(130,137,159,.3);background-color:#f1f1f1;border-color:#f1f1f1}.btn-primary{color:#fff;background-color:#0c64eb;border-color:transparent}.btn-primary.active,.btn-primary.hover,.btn-primary:active,.btn-primary:focus,.btn-primary:hover,.open .dropdown-toggle.btn-primary{color:#fff;background-color:rgba(12,100,235,.8);border-color:rgba(0,0,0,0)}.btn-primary.active,.btn-primary:active,.open .dropdown-toggle.btn-primary{background-color:#0b5ad3;background-image:none;border-color:rgba(0,0,0,0)}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{color:rgba(255,255,255,.3);background-color:#0c64eb;border-color:transparent}.btn-secondary{color:#fff;background-color:#16a8f8;border-color:transparent}.btn-secondary.active,.btn-secondary.hover,.btn-secondary:active,.btn-secondary:focus,.btn-secondary:hover,.open .dropdown-toggle.btn-secondary{color:#fff;background-color:rgba(22,168,248,.8);border-color:rgba(0,0,0,0)}.btn-secondary.active,.btn-secondary:active,.open .dropdown-toggle.btn-secondary{background-color:#079ced;background-image:none;border-color:rgba(0,0,0,0)}.btn-secondary.disabled,.btn-secondary.disabled.active,.btn-secondary.disabled:active,.btn-secondary.disabled:focus,.btn-secondary.disabled:hover,.btn-secondary[disabled],.btn-secondary[disabled].active,.btn-secondary[disabled]:active,.btn-secondary[disabled]:focus,.btn-secondary[disabled]:hover,fieldset[disabled] .btn-secondary,fieldset[disabled] .btn-secondary.active,fieldset[disabled] .btn-secondary:active,fieldset[disabled] .btn-secondary:focus,fieldset[disabled] .btn-secondary:hover{color:rgba(255,255,255,.3);background-color:#16a8f8;border-color:transparent}.btn-warning{color:#fff;background-color:#ff9800;border-color:transparent}.btn-warning.active,.btn-warning.hover,.btn-warning:active,.btn-warning:focus,.btn-warning:hover,.open .dropdown-toggle.btn-warning{color:#fff;background-color:rgba(255,152,0,.8);border-color:rgba(0,0,0,0)}.btn-warning.active,.btn-warning:active,.open .dropdown-toggle.btn-warning{background-color:#e68900;background-image:none;border-color:rgba(0,0,0,0)}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{color:rgba(255,255,255,.3);background-color:#ff9800;border-color:transparent}.btn-danger{color:#fff;background-color:#ff5d5d;border-color:transparent}.btn-danger.active,.btn-danger.hover,.btn-danger:active,.btn-danger:focus,.btn-danger:hover,.open .dropdown-toggle.btn-danger{color:#fff;background-color:rgba(255,93,93,.8);border-color:rgba(0,0,0,0)}.btn-danger.active,.btn-danger:active,.open .dropdown-toggle.btn-danger{background-color:#ff4343;background-image:none;border-color:rgba(0,0,0,0)}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{color:rgba(255,255,255,.3);background-color:#ff5d5d;border-color:transparent}.btn-success{color:#fff;background-color:#00da88;border-color:transparent}.btn-success.active,.btn-success.hover,.btn-success:active,.btn-success:focus,.btn-success:hover,.open .dropdown-toggle.btn-success{color:#fff;background-color:rgba(0,218,136,.8);border-color:rgba(0,0,0,0)}.btn-success.active,.btn-success:active,.open .dropdown-toggle.btn-success{background-color:#00c178;background-image:none;border-color:rgba(0,0,0,0)}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{color:rgba(255,255,255,.3);background-color:#00da88;border-color:transparent}.btn-info{color:#0c64eb;background-color:#e9f2fb;border-color:transparent}.btn-info.active,.btn-info.hover,.btn-info:active,.btn-info:focus,.btn-info:hover,.open .dropdown-toggle.btn-info{color:#0c64eb;background-color:rgba(233,242,251,.8);border-color:rgba(0,0,0,0)}.btn-info.active,.btn-info:active,.open .dropdown-toggle.btn-info{background-color:#d3e5f7;background-image:none;border-color:rgba(0,0,0,0)}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{color:rgba(12,100,235,.3);background-color:#e9f2fb;border-color:transparent}.btn-link{padding-right:6px;padding-left:6px;font-weight:400;color:#3c495c;text-shadow:none;cursor:pointer;background:0 0;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover,.btn-link[disabled],fieldset[disabled] .btn-link{border-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link:focus,.btn-link:hover{color:#222;background:#f1f1f1;background:rgba(0,0,0,.075)}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#cbd0db;text-decoration:none}.btn-lg{padding:11px 16px;font-size:14px;line-height:18px;border-radius:4px}.btn-mini,.btn-sm{padding:3px 8px;font-size:12px;line-height:18px;border-radius:4px}.btn-mini,.btn-xs{padding:0 5px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.btn-wide{min-width:120px}.btn-limit{max-width:180px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.btn-limit>.caret{position:absolute;top:14px;right:8px}.btn-circle{border-radius:17px}.btn>.label-icon{top:3px;padding:3px;margin:-2px;background-color:rgba(0,0,0,.2);border-radius:12px}.btn>.label-icon>.icon{font-size:16px;line-height:18px}.btn>.icon+.text{margin-left:5px}.btn.btn-sm.btn-circle{border-radius:12px}.btn.btn-sm>.label-icon{top:2px;width:20px;height:20px;padding:1px;line-height:20px}.btn.btn-sm>.label-icon>.icon{position:relative;top:-1px;display:inline-block;font-size:14px;line-height:18px}.btn-icon-left{position:relative;padding-left:35px;overflow:hidden;text-align:right}.btn-icon-left>.label-icon{position:absolute;left:5px;margin:0}.btn-icon-left>.icon{position:absolute;top:0;bottom:0;left:0;display:block;width:30px;line-height:30px;color:#16a8f8;text-align:center;background:#e9f2fb}.btn-icon-left.btn-sm{padding-left:28px}.btn-icon-left.btn-sm>.label-icon{left:2px}.btn-icon-left.btn-sm>.icon{width:24px;line-height:24px}.btn-icon-right{position:relative;padding-right:35px;text-align:left}.btn-icon-right>.label-icon{position:absolute;right:5px;margin:0}.btn-icon-right.btn-sm{padding-right:28px}.btn-icon-right.btn-sm>.label-icon{right:2px}.btn-icon{min-width:32px;padding-right:0;padding-left:0}.btn-icon.btn-sm{width:24px;min-width:24px;height:24px}.btn-group{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group>.btn-group{float:left}.btn-group>.btn{border-radius:0}.btn-group>.btn:first-child{border-top-left-radius:2px;border-bottom-left-radius:2px}.btn-group>.btn:last-child{border-top-right-radius:2px;border-bottom-right-radius:2px}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.dropdown{float:left;margin-right:10px}.btn-toolbar>.btn-group:last-child,.btn-toolbar>.btn:last-child,.btn-toolbar>.dropdown:last-child{margin-right:0}.btn-toolbar>.divider{float:left;height:20px;margin:7px 5px 4px 10px;border-left:1px solid rgba(0,0,0,.1)}.btn-toolbar .space{float:left;min-height:1px;margin:0 10px 0 10px}.btn-toolbar .input-control{float:left;width:120px}.btn-toolbar .page-title{float:left;line-height:34px}.btn-toolbar .page-title .text{font-size:14px;font-weight:700}.btn-toolbar .page-title .label{top:-2px;margin-right:10px}.btn-toolbar .divider+.page-title{margin-left:15px}.btn-active-line{position:relative;font-weight:700;color:#0c64eb}.btn-active-line:after{position:absolute;right:5px;bottom:1px;left:5px;display:block;content:' ';border-bottom:2px solid #0c64eb}.btn-active-line:hover{color:#0c64eb}.btn-active-text .text{position:relative;top:-1px;display:inline-block;font-weight:700;color:#0c64eb}.btn-active-text .text:after{position:absolute;bottom:-5px;display:block;width:100%;content:' ';border-bottom:2px solid #0c64eb}.angle-btn{position:relative;padding:1px;background:#fff;border:1px solid #cbd0db;border-right:none}.angle-btn:first-child{border-radius:2px 0 0 2px}.btn-toolbar>.angle-btn{margin-right:8px}.angle-btn:after,.angle-btn:before{position:absolute;top:-1px;right:-8px;display:block;width:0;height:0;content:' ';border-color:transparent transparent transparent #cbd0db;border-style:solid;border-width:17px 0 17px 8px}.angle-btn:after{right:-7px;border-color:transparent transparent transparent #fff;border-radius:2px}.angle-btn .btn{padding:6px;font-weight:700;background:#fff;border:none;border-radius:4px!important}.angle-btn .btn.btn-limit{padding-right:16px}.angle-btn .btn.btn-limit>.caret{right:4px}.angle-btn+.angle-btn{border-left:none}.angle-btn+.angle-btn>.btn-group:first-child{padding-left:8px}.angle-btn+.angle-btn>.btn-group:first-child:after,.angle-btn+.angle-btn>.btn-group:first-child:before{position:absolute;top:-2px;left:0;display:block;width:0;height:0;content:' ';border-color:transparent transparent transparent #cbd0db;border-style:solid;border-width:17px 0 17px 8px}.angle-btn+.angle-btn>.btn-group:first-child:after{left:-1px;border-color:transparent transparent transparent #fff;border-width:17px 0 17px 8px}.btn-toolbar>.angle-btn.active,.btn-toolbar>.angle-btn:last-child{border-color:#0c64eb}.btn-toolbar>.angle-btn.active .btn,.btn-toolbar>.angle-btn:last-child .btn{color:#0c64eb}.btn-toolbar>.angle-btn.active:after,.btn-toolbar>.angle-btn.active:before,.btn-toolbar>.angle-btn:last-child:after,.btn-toolbar>.angle-btn:last-child:before{border-color:transparent transparent transparent #0c64eb}.btn-toolbar>.angle-btn.active:after,.btn-toolbar>.angle-btn:last-child:after{border-color:transparent transparent transparent #fff}.btn-toolbar>.angle-btn+.angle-btn:last-child>.btn-group:first-child:after,.btn-toolbar>.angle-btn+.angle-btn:last-child>.btn-group:first-child:before,.btn-toolbar>.angle-btn.active+.angle-btn>.btn-group:first-child:after,.btn-toolbar>.angle-btn.active+.angle-btn>.btn-group:first-child:before{border-color:transparent transparent transparent #0c64eb}.btn-toolbar>.angle-btn+.angle-btn:last-child>.btn-group:first-child:after,.btn-toolbar>.angle-btn.active+.angle-btn>.btn-group:first-child:after{border-color:transparent transparent transparent #fff}.btn-toolbar>.angle-btn.active+.angle-btn,.btn-toolbar>.angle-btn.normal{border-color:#cbd0db}.btn-toolbar>.angle-btn.active+.angle-btn .btn,.btn-toolbar>.angle-btn.normal .btn{color:#3c4353}.btn-toolbar>.angle-btn.active+.angle-btn:after,.btn-toolbar>.angle-btn.active+.angle-btn:before,.btn-toolbar>.angle-btn.normal:after,.btn-toolbar>.angle-btn.normal:before{border-color:transparent transparent transparent #cbd0db}.btn-toolbar>.angle-btn.active+.angle-btn:after,.btn-toolbar>.angle-btn.normal:after{border-color:transparent transparent transparent #fff}.btn-toolbar>.angle-btn.active+.angle-btn>.btn-group:first-child:before,.btn-toolbar>.angle-btn.normal>.btn-group:first-child:before{border-color:transparent transparent transparent #cbd0db!important}.nav>li>.btn.btn-primary{color:#fff}.nav>li>.btn.btn-primary:focus,.nav>li>.btn.btn-primary:hover{background:rgba(12,100,235,.8)}.btn.btn-action,.c-actions .btn{display:inline-block;width:26px;padding:2px;overflow:hidden;line-height:20px;color:#16a8f8;background:0 0;border-color:transparent}.btn.btn-action>i,.c-actions .btn>i{position:relative;top:1px;font-size:18px}.btn.btn-action:hover,.c-actions .btn:hover{color:#0c64eb;background-color:#d3e5f7}.c-actions .btn+.btn{margin-left:-4px}.label{position:relative;display:inline-block;padding:3px 5px;font-size:12px;font-weight:400;vertical-align:middle;border-radius:2px}.label+.label{margin-left:4px}.label-pale{background:#bed8f3!important}.label-badge{border-radius:9px}.label-light{color:#3c4353;background-color:#ddd}.label-primary{background:#0c64eb!important}.label-gray{color:#878da0;background:#e8ebef}.label-outline.label-danger{color:#ff5d5d;background:#ffebee;border-color:rgba(255,93,93,.25)}.label-outline.label-light{color:#838a9d;background:#f2f5fb;border-color:#e1e5ee}.label-primary.label-outline{background:#e9f2fb!important;border-color:rgba(12,100,235,.25)}.label-outline.label-success{background:#e8f5e9;border-color:rgba(0,218,136,.25)}.label-outline.label-info{border-color:rgba(33,150,243,.25)}.label-outline.label-warning{border-color:rgba(255,152,0,.25)}.label-dot{position:relative;top:-1px;padding:0;border-radius:50%}.label-dot+.status-text{display:inline-block;margin-left:5px}.label-icon{min-width:18px;padding:0;line-height:18px;border-radius:10px}.label-id{display:inline-block;min-width:30px;padding:0 5px;font-size:12px;line-height:16px;color:#838a9d;text-align:center;vertical-align:middle;background-color:transparent;border:1px solid #838a9d;border-radius:2px}.pri-1,.todo-pri-1{color:#ff5d5d}[class*=" status-"],[class^=status-]{color:#3c4353}.status-changed,.status-delayed,.status-doing,.status-fail,.status-investigate{color:#ff5d5d}.status-changed>.label-dot,.status-delayed>.label-dot,.status-doing>.label-dot,.status-fail>.label-dot,.status-investigate>.label-dot{background-color:#ff5d5d}.status-wait{color:#838a9d}.status-wait>.label-dot{background-color:#7ec5ff}.status-unclosed{color:#838a9d}.status-unclosed>.label-dot{background-color:#0c64eb}.status-done,.status-normal,.status-pass,.status-resolved{color:#43a047}.status-done>.label-dot,.status-normal>.label-dot,.status-pass>.label-dot,.status-resolved>.label-dot{background-color:#00da88}.status-postpone{color:#838a9d}.status-postpone>.label-dot{background-color:#ff5d5d}.status-blocked{position:relative;left:-5px;display:inline-block;padding:0 5px;line-height:20px;color:#3c4353;background:#fff3e0;border-radius:10px}.status-blocked>.label-dot{background-color:#ff9800}.status-pause,.status-suspended{color:#ff9800}.status-pause>.label-dot,.status-suspended>.label-dot{background-color:#ff9800}.status-active.status-bug,.status-draft{color:#8666b8}.status-active.status-bug>.label-dot,.status-draft>.label-dot{background-color:#8666b8}.status-closed,.status-terminate{color:#838a9d}.status-closed>.label-dot,.status-terminate>.label-dot{background-color:#838a9d}.status-cancel{color:#838a9d}.status-cancel>.label-dot{background-color:#cbd0db}.label-pri{display:inline-block;min-width:18px;max-width:67px;height:18px;padding:0 4px;overflow:hidden;font-size:12px;line-height:16px;color:#838a9d;text-align:center;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle;border:1px solid #838a9d;border-radius:10px}.label-selector>.label-pri,[class*=label-pri-]{color:#158af1;border-color:#2098ee}.label-pri-1,.label-selector>.label-pri[data-value="1"]{color:#d50000;border-color:#d50000}.label-pri-2,.label-selector>.label-pri[data-value="2"]{color:#ff9800;border-color:#ff9800}.label-pri-3,.label-selector>.label-pri[data-value="3"]{color:#2098ee;border-color:#2098ee}.label-pri-4,.label-selector>.label-pri[data-value="4"]{color:#009688;border-color:#009688}.label-pri-5,.label-selector>.label-pri[data-value="5"]{color:#838a9d;border-color:#838a9d}.label-pri-0,.label-selector>.label-pri.active[data-value="0"]{color:#d5d9df;border-color:#d5d9df}.label-severity{position:relative;display:inline-block;width:24px;height:20px;font-weight:bolder;text-align:center;vertical-align:middle}.label-severity:before{position:absolute;top:-3px;left:0;z-index:0;display:block;font-family:ZentaoIcon;font-size:14px;font-size:24px;font-style:normal;font-weight:400;font-variant:normal;line-height:1;color:inherit;text-transform:none;content:"\e973";speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.label-severity:after{position:absolute;top:7px;left:0;z-index:1;display:block;width:24px;font-size:12px;line-height:12px;text-align:center;content:attr(data-severity)}.label-severity[data-severity],.label-severity[data-value]{color:#ccc}.label-severity[data-severity="0"],.label-severity[data-value="0"]{color:#ccc}.label-severity[data-severity="0"]:after,.label-severity[data-value="0"]:after{display:none}.label-severity[data-severity="1"],.label-severity[data-value="1"]{color:#c62828}.label-severity[data-severity="2"],.label-severity[data-value="2"]{color:#ff8f00}.label-severity[data-severity="3"],.label-severity[data-value="3"]{color:#fdd835}.label-severity[data-severity="4"],.label-severity[data-value="4"]{color:#cddc39}.label-severity[data-severity="5"],.label-severity[data-value="5"]{color:#8bc34a}.label-severity-custom[data-severity]{color:#d5d9df}.label-severity-custom[data-severity="1"]{color:#c62828}.label-severity-custom[data-severity="2"]{color:#ff8f00}.label-severity-custom[data-severity="3"]{color:#fdd835}.label-severity-custom[data-severity="4"]{color:#cddc39}.label-severity-custom[data-severity="5"]{color:#8bc34a}.label-selector{padding:0 10px}.label-selector>.label{display:inline-block;min-width:24px;height:24px;padding:0 5px;font-size:14px;line-height:20px;text-align:center;cursor:pointer;background:0 0;border:2px solid #d5d9df;border-radius:15px}.label-selector>.label+.label{margin-left:10px}.label-selector>.label.empty{border-color:transparent}.label-selector>.label.label-severity{font-size:12px;line-height:28px;border-color:transparent}.label-selector>.label.label-severity:before{top:-2px;left:-2px}.label-selector>.label.label-severity:after{display:none}.label-selector>.label.label-severity.active{background:0 0;filter:none;-webkit-filter:none}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{margin-top:2px}.ie .checkbox input[type=checkbox],.ie .checkbox-inline input[type=checkbox],.ie .radio input[type=radio],.ie .radio-inline input[type=radio]{margin-top:4px}.is-firefox .checkbox input[type=checkbox],.is-firefox .checkbox-inline input[type=checkbox],.is-firefox .radio input[type=radio],.is-firefox .radio-inline input[type=radio]{margin-top:3px}.checkbox-primary,.radio-primary{position:relative;display:block;vertical-align:middle}.checkbox-primary.inline-block,.radio-primary.inline-block{display:inline-block}.checkbox-primary.inline-block+.inline-block,.radio-primary.inline-block+.inline-block{margin-left:15px}.checkbox-primary>input,.radio-primary>input{position:absolute;top:0;left:0;z-index:3;width:100%;height:100%;margin:0;opacity:0}.checkbox-primary>label,.radio-primary>label{display:block;height:20px;padding-left:30px;margin:0;font-weight:400;line-height:20px;cursor:pointer}.checkbox-primary>label:after,.checkbox-primary>label:before,.radio-primary>label:after,.radio-primary>label:before{position:absolute;top:1px;right:0;left:0;display:block;width:18px;height:18px;line-height:18px;text-align:center;content:' ';border-radius:3px}.checkbox-primary>label:after,.radio-primary>label:after{z-index:1;border:2px solid #eee;border-color:rgba(0,0,0,.15);-webkit-transition:.4s cubic-bezier(.175,.885,.32,1);-o-transition:.4s cubic-bezier(.175,.885,.32,1);transition:.4s cubic-bezier(.175,.885,.32,1);-webkit-transition-property:border,background-color;-o-transition-property:border,background-color;transition-property:border,background-color}.checkbox-primary>label:before,.radio-primary>label:before{top:3px;z-index:2;font-family:ZentaoIcon;font-size:14px;font-style:normal;font-weight:400;font-weight:900;font-variant:normal;line-height:1;text-transform:none;content:"\e5ca";opacity:0;-webkit-transition:.2s cubic-bezier(.175,.885,.32,1);-o-transition:.2s cubic-bezier(.175,.885,.32,1);transition:.2s cubic-bezier(.175,.885,.32,1);-webkit-transition-property:opacity,-webkit-transform;-o-transition-property:opacity,-o-transform;transition-property:opacity,-webkit-transform;transition-property:opacity,transform;transition-property:opacity,transform,-webkit-transform,-o-transform;-webkit-transform:scale(0);-ms-transform:scale(0);-o-transform:scale(0);transform:scale(0);speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.checkbox-primary.checked>label:after,.checkbox-primary>input:checked+label:after,.radio-primary.checked>label:after,.radio-primary>input:checked+label:after{background-color:#00da88;border-color:#00da88;border-width:4px}.checkbox-primary.checked>label:before,.checkbox-primary>input:checked+label:before,.radio-primary.checked>label:before,.radio-primary>input:checked+label:before{color:#fff;opacity:1;-webkit-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}.checkbox-primary.focus>label:after,.checkbox-primary>input:focus+label:after,.radio-primary.focus>label:after,.radio-primary>input:focus+label:after{border-color:#00da88;-webkit-box-shadow:0 0 0 3px rgba(0,218,136,.2);box-shadow:0 0 0 3px rgba(0,218,136,.2)}.checkbox-primary:hover>label:after,.radio-primary:hover>label:after{border-color:#00da88}.checkbox-primary.checkbox-right>label,.radio-primary.checkbox-right>label{padding:0 30px 0 0}.checkbox-primary.checkbox-right>label:after,.checkbox-primary.checkbox-right>label:before,.radio-primary.checkbox-right>label:after,.radio-primary.checkbox-right>label:before{right:0;left:auto}.checkbox-primary input:disabled+label:after,.checkbox-primary.disabled>label:after,.radio-primary input:disabled+label:after,.radio-primary.disabled>label:after{background-color:#e5e5e5!important;border-color:#bbb!important}.checkbox-primary input:disabled:checked+label:after,.checkbox-primary.checked.disabled>label:after,.radio-primary input:disabled:checked+label:after,.radio-primary.checked.disabled>label:after{background-color:#bbb!important}.radio-primary>label:after{border-radius:50%}.radio-primary>label:before{top:7px;left:6px;width:6px;height:6px;content:' ';border:none;border-radius:50%}.radio-primary.checked>label:after,.radio-primary>input:checked+label:after{background-color:transparent;border-color:#00da88;border-width:2px}.radio-primary.checked>label:before,.radio-primary>input:checked+label:before{background-color:#00da88}.radio-primary input:disabled:checked+label:after,.radio-primary.checked.disabled>label:after{background-color:transparent;border-color:#bbb}.radio-primary input:disabled:checked+label:before,.radio-primary.checked.disabled>label:before{background-color:#bbb}.panel{position:relative;margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05),0 2px 6px 0 rgba(0,0,0,.045);box-shadow:0 1px 1px rgba(0,0,0,.05),0 2px 6px 0 rgba(0,0,0,.045)}.panel-body{padding:20px}.panel-body.has-table{padding:10px}.panel-body.has-table .table{margin-bottom:0;table-layout:fixed}.panel-heading{padding:12px 48px 12px 20px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading+.panel-body{padding-top:0}.panel-title{font-size:14px;font-weight:700;line-height:20px}.panel-title .label{top:-1px}.panel-actions{position:absolute;top:0;right:0;padding:7px 8px}.panel:hover .panel-actions{z-index:10}.panel-actions>li>a{display:inline-block;min-width:30px;padding:0 5px;line-height:30px;color:#a6aab8;text-align:center;border-radius:4px}.panel-actions>li>a:hover{color:#3c495c;text-decoration:initial;background-color:#f1f1f1}.panel-actions .btn-icon{color:#a6aab8}.panel-actions .btn.text-primary{color:#0c64eb}.panel .empty-tip{padding:30px 10px 50px;font-size:14px;color:#838a9d;text-align:center}.progress-text-left{position:relative;margin:7px 0;margin-left:35px;overflow:visible}.progress-text-left .progress-text{position:absolute;top:-7px;left:-35px;display:block;width:35px;height:20px;padding-right:5px;line-height:20px;color:#838a9d;text-align:right}.chart-color{width:20px}.chart-color-dot{display:inline-block;width:10px;height:10px;border-radius:50%}.chart-row{margin-top:10px}.chart-row+.chart-row{padding-top:10px;border-top:1px solid #eee}.chart-wrapper{padding:10px 5px;background:#eee}.chart-wrapper>h4{margin:5px 0 10px}.table-wrapper{max-height:250px;overflow:auto}.table-wrapper .table{margin:0}.progress-pie{position:relative}.progress-pie canvas{display:block}.progress-pie .progress-info{position:absolute;top:0;left:0;width:100%;height:100%;padding-top:25px;text-align:center}.progress-pie .progress-info>small{display:block;line-height:14px;color:#a6aab8}.progress-pie .progress-info>strong{display:block;font-size:36px;line-height:40px}.progress-pie .progress-info>strong>small{font-size:20px}.progress-pie-120 .progress-info{padding-top:30px}.progress-pie-120 .progress-info>small{line-height:18px}.progress-pie-50 .progress-info{padding-top:4px}.progress-pie-50 .progress-info>strong{font-size:20px;font-weight:400}.progress-pie-50 .progress-info>strong>small{font-size:14px}.progress-pie[data-value="100"] .progress-info>strong{-webkit-transform:scale(.7);-ms-transform:scale(.7);-o-transform:scale(.7);transform:scale(.7)}.progress-pie-24 .progress-info{right:-10px;left:-10px;width:auto;padding-top:0;font-size:12px;line-height:24px;-webkit-transform:scale(.9,1);-ms-transform:scale(.9,1);-o-transform:scale(.9,1);transform:scale(.9,1)}.progress-pie-24[data-value="100"] .progress-info{-webkit-transform:scale(.8,1);-ms-transform:scale(.8,1);-o-transform:scale(.8,1);transform:scale(.8,1)}.progress-pie-26 .progress-info{right:-10px;left:-10px;width:auto;padding-top:0;font-size:12px;line-height:26px;-webkit-transform:scale(.9,1);-ms-transform:scale(.9,1);-o-transform:scale(.9,1);transform:scale(.9,1)}.progress-pie-26[data-value="100"] .progress-info{-webkit-transform:scale(.8,1);-ms-transform:scale(.8,1);-o-transform:scale(.8,1);transform:scale(.8,1)}.status-bars{display:table;width:100%;height:140px;padding:5px;padding-top:50px;margin:0;overflow:hidden}.status-bars>li{position:relative;display:table-cell;text-align:center;vertical-align:bottom}.status-bars .bar{position:absolute;bottom:20px;left:50%;display:block;width:10px;margin-left:-5px;background:#0c64eb;border-radius:5px 5px 0 0}.status-bars .bar:after{position:absolute;right:-50px;bottom:0;left:-50px;display:block;height:1px;content:' ';background:#eee}.status-bars .title{font-size:12px;font-weight:400;color:#a6a8b6}.status-bars .value{position:relative;top:-20px;left:-20px;display:inline-block;width:50px;font-size:16px;font-weight:700;text-align:center}.status-bars-h{display:block;padding-right:50px;padding-left:60px;list-style:none}.status-bars-h>li{position:relative;height:40px;border-left:1px solid #eee}.status-bars-h .bar{position:relative;top:15px;display:block;height:10px;line-height:20px;background:#0c64eb;border-radius:0 5px 5px 0}.status-bars-h .title{position:absolute;top:-5px;left:-60px;width:60px;padding-right:10px;font-size:12px;color:#a6a8b6;text-align:right}.status-bars-h .value{position:absolute;top:-5px;right:-50px;display:block;width:40px;font-size:14px;font-weight:700;text-align:left;white-space:nowrap}.messager{border-radius:4px;-webkit-box-shadow:0 4px 16px rgba(0,0,0,.2),0 2px 8px rgba(0,0,0,.1);box-shadow:0 4px 16px rgba(0,0,0,.2),0 2px 8px rgba(0,0,0,.1)}.messager-icon{vertical-align:middle}.messager-icon>.icon{font-size:24px}.messager-content{padding:18px 20px;font-size:18px;line-height:30px}.messager-content>.icon{font-size:28px;line-height:30px}.messager-actions{vertical-align:middle}.messagger-zt{color:#3c4353;background-color:#fff!important}.messagger-zt .messager-icon>.icon{color:#0c64eb}.messagger-zt .messager-actions>.action{color:#838a9d}.messagger-zt.messager-success .messager-icon>.icon{color:#00da88}.messagger-zt.messager-danger .messager-icon>.icon{color:#ff5d5d}.messagger-zt.messager-warning .messager-icon>.icon{color:#ff9800}.messagger-zt.messager-info .messager-icon>.icon{color:#2196f3}.tree{padding-left:0;overflow:hidden}.tree ul{position:relative;display:none;padding-left:0}.tree li{position:relative;padding:2px 0 2px 15px;list-style:none}.tree li.heading{padding-left:5px;color:#3c495c}.tree li>a{display:block;max-width:90%;padding:2px 6px;color:#3c495c;word-break:break-all}.tree li>a:hover{color:#3c4353}.tree li>a.tree-toggle:hover{background:0 0}.tree li.active>a{position:relative;font-weight:700;color:#0c64eb}.tree li>.list-toggle{position:absolute;top:1px;left:1px;z-index:10;width:20px;font-size:14px;line-height:22px;color:#cbd0db;text-align:center;cursor:pointer;-webkit-transition:all .2s;-o-transition:all .2s;transition:all .2s}.tree li>.list-toggle:before{content:"\f0da"}.tree li>.list-toggle:active,.tree li>.list-toggle:hover{color:#0c64eb}.tree li.has-active-item>.list-toggle{color:#3c4353}.tree li.has-list.open>ul{display:block}.tree li.has-list.open>.list-toggle{-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.tree li.has-list.open:before{position:absolute;top:16px;bottom:-5px;left:10px;display:block;content:' ';border-left:1px solid #d8d8d8}.tree-actions{display:inline-block;margin-left:5px;vertical-align:middle}.tree-actions a{display:inline-block;margin-left:5px;font-size:13px;opacity:.6}.tree-actions a:hover{opacity:1}.tree li>.module-name{color:#3c495c;vertical-align:middle}.tree li>.module-name:hover{background-color:#f0f2f5}.tree li>.module-name:hover>a{color:#3c4353}.treemap-node-fold-icon:before{position:relative;left:-4px;min-width:18px}.dropdown-menu{padding:5px 0;border-color:rgba(0,0,0,.1)}.dropdown-menu>li{padding:0 10px}.dropdown-menu>li>a{padding:2px 10px;margin:5px 0;border-radius:3px}.dropdown-menu>li>a>.icon{position:relative;left:-5px;opacity:.5}.dropdown-menu>li>a:hover>.icon{opacity:.8}.dropdown-menu>li.active>a,.dropdown-menu>li.selected>a{position:relative;color:#fff;background-color:#16a8f8}.dropdown-menu>li.selected>a:after{position:absolute;top:2px;right:4px;display:block;font-family:ZentaoIcon;font-size:14px;font-style:normal;font-weight:400;font-variant:normal;line-height:1;line-height:20px;text-transform:none;content:"\e5ca";speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.dropdown-menu>li.divider{margin:10px}.dropdown-submenu>a:after{margin-right:-5px}.dropdown-submenu>.dropdown-menu.pull-left{margin-left:-1px}.dropdown-submenu:focus>a,.dropdown-submenu:hover>a{color:#3c4353;background-color:#e9f2fb}.dropdown-submenu:hover>a:after{border-left-color:#0c64eb}.dropdown-submenu>a:hover:after{border-left-color:#fff}.pager .btn{padding:3px 10px}.pager .btn .caret{opacity:.7}.pager>li>.pager-label{padding:2px;line-height:20px}.pager>li>.pager-item{min-width:20px;padding:1px;margin:2px 0;font-size:16px;line-height:20px;text-align:center;background:0 0;border-color:transparent}.pager>li>.pager-item:hover{background-color:rgba(0,0,0,.1)}.pager>li>.pager-item>.icon{position:relative;top:-1px}.pager>li>.btn:hover,.pager>li>a:hover{background:rgba(0,0,0,.1)}.pager>li.disabled>a.pager-item{background:0 0;border-color:transparent;opacity:.5}.pager>li.active>a{background-color:#16a8f8}.pager>li .btn-group .btn{padding:1px;margin:1px 0;border-radius:4px}.pager .dropdown-menu{width:200px}.pager .dropdown-menu>li{float:left;width:33.333333%}.modal-dialog{width:900px;max-width:1360px;border:none;border-radius:0;-webkit-box-shadow:0 0 20px 0 rgba(0,0,0,.25);box-shadow:0 0 20px 0 rgba(0,0,0,.25)}.modal-dialog.modal-md{width:700px}.modal-dialog.modal-xs{width:400px}.modal-dialog.modal-sm{width:500px}.modal-dialog.modal-lg{width:1200px}.modal-dialog.modal-fullscreen{position:fixed;max-width:initial}.modal-header{padding:20px 0;margin:0 20px}.modal-header>.close{color:#838a9d;text-shadow:0 1px 0 rgba(255,255,255,.85);opacity:1}.modal-header>.close:hover{color:#222}.modal-footer{padding:20px 0;margin:0 20px}.modal-title{font-size:14px;font-weight:400;line-height:20px}.modal-actions{position:absolute;top:16px;right:16px}.modal-actions .divider{position:relative;top:5px;display:inline-block;width:0;height:20px;margin:0 10px;border-left:#eee 1px solid}.modal-actions>.dropdown{display:inline-block}.modal-body{padding:20px}.modal-iframe .modal-body>iframe{border-radius:0}.modal.fade .modal-dialog{-webkit-transform:translate(0,-3%);-ms-transform:translate(0,-3%);-o-transform:translate(0,-3%);transform:translate(0,-3%)}.modal.fade.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-simple .modal-footer{padding-top:0;border-top:none}.modal-iframe .modal-header{position:relative;z-index:10;min-height:0;padding:0;border:none}.modal-iframe .modal-title{display:none}.modal-iframe .modal-header .close{position:absolute;top:12px;right:10px;font-size:32px;font-weight:200}.modal-iframe .modal-dialog{overflow:hidden}.modal-inverse .modal-header>.close{color:rgba(255,255,255,.7);text-shadow:none}.modal-inverse .modal-header>.close:hover{color:#fff}.modal-scroll-inside>.modal-dialog{max-height:100%}.hide-modal-close .modal-iframe .modal-header .close{display:none}.tile{text-align:center}.tile-title{line-height:20px;color:#3c495c}.tile-amount{font-size:32px;font-weight:700;line-height:56px}.timeline>li{position:relative;list-style:none}.timeline>li:before,.timeline>li>a:after,.timeline>li>div:after{position:absolute;left:-20px;display:block;width:15px;height:15px;content:' ';border-radius:50%}.timeline>li:before{top:12px;left:-16px;z-index:3;width:7px;height:7px;background-color:#cbd0db;border:none;border:1px solid #cbd0db}.timeline>li>a:after,.timeline>li>div:after{top:11px;left:-17px;z-index:3;width:9px;height:9px;background-color:#0c64eb;border-radius:50%;opacity:0}.timeline>li+li:after{position:absolute;top:-12px;bottom:20px;left:-13px;z-index:1;display:block;content:' ';border-left:1px solid #eee}.timeline>li.active>a:after,.timeline>li.active>div:after{opacity:1}.timeline>li.active:before{top:8px;left:-20px;width:15px;height:15px;background-color:rgba(12,100,235,.2);border:none}.timeline>li>a,.timeline>li>div{display:block;padding:5px;line-height:20px}.timeline>li.active>a{color:#3c4353}.timeline-tag{position:absolute;top:5px;left:-115px;font-size:12px}.timeline-tag-left{padding-left:115px}.timeline-sm{font-size:12px}.timeline-sm>li:before,.timeline-sm>li>a:after,.timeline-sm>li>div:after{top:10px;left:-20px;width:11px;height:11px}.timeline-sm>li.active:before,.timeline-sm>li:before{top:10px;left:-18px;width:11px;height:11px;background:0 0;border:1px solid #eee}.timeline-sm>li>a,.timeline-sm>li>div{line-height:20px}.timeline-sm>li>a:after,.timeline-sm>li>div:after{top:13px;left:-15px;width:5px;height:5px}.form-control{-webkit-box-shadow:none;box-shadow:none}.form-horizontal .form-group>label{padding-right:0}.form-actions{margin-top:20px;margin-bottom:0}.form-actions .btn{margin-right:10px}form label{font-weight:400;color:#3c495c}.form-group .btn+.btn{margin-left:5px}.table-form{margin-bottom:0;table-layout:fixed}.table-form>thead>tr>th.required:after{position:relative;top:3px;right:auto;left:4px;display:inline-block;vertical-align:middle}.table-form>tbody>tr>td,.table-form>tbody>tr>th,.table-form>tfoot>tr>td,.table-form>thead>tr>th{padding:7px;vertical-align:middle;border-bottom:none}.table-form>tfoot>tr>td{padding:20px 7px 10px}.table-form>tbody>tr>th{width:100px;font-weight:700;text-align:right}.table-form .input-group{width:100%}.chosen-container-single .chosen-single{position:relative}.chosen-container-single .chosen-single>span{height:20px;line-height:20px;white-space:normal}.chosen-container-single .chosen-single div b{position:relative;top:1px;color:#cbd0db}.chosen-container-single .chosen-search:before{top:8px;right:15px}.chosen-container-multi .chosen-choices li.search-choice{font-size:13px;background:#eee;border-color:#cbd0db;-webkit-box-shadow:none;box-shadow:none}.chosen-container-single .chosen-search input[type=text]{height:30px;padding:3px 25px 3px 5px}.chosen-container-single .chosen-search{padding:3px 10px 0}.chosen-container-single .chosen-single{overflow:visible}.chosen-container .chosen-results{max-height:245px;padding:10px}.chosen-container .chosen-results>li{border-radius:4px}.chosen-container .chosen-results li.highlighted em{color:#fff}.table-responsive .chosen-container .chosen-results{max-height:200px}.chosen-compact.chosen-container-single .chosen-single>.chosen-search{top:-2px;right:-1px;bottom:-1px;left:-1px;display:none;height:auto;padding:0;opacity:0}.chosen-compact.chosen-container-single .chosen-single>.chosen-search>input{height:31px;padding:5px 26px 5px 8px;font-size:inherit;line-height:20px}.chosen-compact.chosen-container-single .chosen-single>.chosen-search:before{top:7px;right:8px}.datetimepicker{padding:10px}.datetimepicker td.day.today{background-color:#f77}.datetimepicker td.day.active{background-color:#16a8f8}.datetimepicker tfoot th,.datetimepicker thead th{color:#838a9d}.input-control .colorpicker{top:0;z-index:auto;opacity:1}.input-control .colorpicker .btn{padding:5px}.input-control .input-control-icon-right.btn{top:0}.colorpicker .dropdown-menu{min-width:232px;padding:5px 10px 10px 10px}.colorpicker .dropdown-menu>li{display:block;float:left;padding:5px}.colorpicker .dropdown-menu>li.heading{width:100%;margin-bottom:5px;font-size:16px;font-weight:700;text-align:left}.colorpicker .dropdown-menu>li.heading>.icon-close{position:relative;top:4px;float:right;cursor:pointer;opacity:.6}.colorpicker .dropdown-menu>li>a{position:relative;display:block;width:100%;height:100%;padding:0;margin:0;font-family:ZentaoIcon;font-size:14px;font-style:normal;font-weight:400;font-variant:normal;line-height:1;text-align:center;text-transform:none;border:1px solid transparent;border-radius:50%;speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.colorpicker .dropdown-menu>li>a:before{position:absolute;top:0;display:block;width:100%;height:20px;line-height:18px}.colorpicker .dropdown-menu>li>a:hover{-webkit-box-shadow:0 1px 4px rgba(0,0,0,.25);box-shadow:0 1px 4px rgba(0,0,0,.25)}.colorpicker .dropdown-menu>li>a.active:before{font-size:14px;content:"\e5ca"}.colorpicker .dropdown-menu>li>a.empty{color:#666;background:#fff}.colorpicker .dropdown-menu>li>a.empty:before{content:"\e90d"}.colorpicker .btn{position:relative}.colorpicker .btn .color-bar{position:absolute;right:5px;bottom:3px;left:5px;height:3px}.colorpicker .btn .color-bar[style*='background: ']+.ic{position:relative;top:-2px}.colorpicker .btn .ic{color:#cbd0db}.colorpicker .btn:hover .ic{color:#838a9d}.input-group .colorpicker{z-index:3}.input-group .chosen-container{display:table-cell}.input-group-addon{border-right-width:0;border-left-width:0}.input-group-addon:first-child{border-left-width:1px}.input-group-addon:last-child{border-right-width:1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin:0}.input-group-cell{display:table-cell;width:1%;padding:0 12px;white-space:nowrap;vertical-align:middle}.ke-container{border-color:#dcdcdc!important;border-radius:2px!important}.ke-container.focus{border-color:#0c64eb!important}.ke-toolbar{border-color:#dcdcdc!important}.required:after{top:6px;right:-10px;font-size:20px}td.required:after{top:12px;right:-5px}.input-group>.chosen-container.required:after,.input-group>.input-control.required:after{top:1px;right:1px;z-index:2}.input-group.required .required:after{display:none}.file-input{position:relative;width:-webkit-fit-content;width:-moz-fit-content;width:fit-content}.file-input .input-group{width:auto}.file-input .input-group>.input-group-cell:first-child{padding-right:0;padding-left:7px}.file-input input[type=file]{position:absolute;width:0;height:0;opacity:0}.file-input .file-title{display:inline-block;max-width:400px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle}.file-input .file-editbox{min-width:200px;max-width:100%}.file-input .file-size{display:inline-block;vertical-align:middle}.edit .file-input-empty,.file-input-edit,.file-input-normal,.normal .file-input-empty{display:none}.edit .file-input-edit,.normal .file-input-normal{display:block}.edit .file-input-edit.input-group,.normal .file-input-normal.input-group{display:table}.edit .file-input-normal{display:none!important}.file-input-normal>.input-group-btn{width:auto}.input-group .chosen-container-active .chosen-choices{border-color:#0c64eb!important}.input-group .chosen-container{min-width:100px}.input-group .input-group-btn .btn>.icon{line-height:17px}.os-mac select.form-control{-webkit-appearance:none;background-image:url(data:image/gif;base64,R0lGODlhCQAFAIAAAMvQ2////yH5BAEAAAEALAAAAAAJAAUAAAIKhH+BGYoNGWxgFgA7);background-image:url(data:image/gif;base64,R0lGODlhBwAEAIAAAMvQ2////yH5BAEAAAEALAAAAAAHAAQAAAIIhA+BGWoNWSgAOw==);background-repeat:no-repeat;background-position:right 5px top 12px;-moz-appearance:none}input::-webkit-contacts-auto-fill-button{position:absolute;right:0;display:none!important;pointer-events:none;visibility:hidden}.chosen-choices.has-error,.chosen-single.has-error,.form-control.has-error{border-color:#ff5d5d!important;-webkit-box-shadow:0 0 6px #ffc3c3!important;box-shadow:0 0 6px #ffc3c3!important}.popover-success.popover-form-result{font-weight:700;color:#fff;background:#00da88}.popover-success.popover-form-result.popover.right .arrow:after{border-right-color:#00da88}.form-unsaved{outline:2px solid #ff9800;-webkit-box-shadow:0 1px 12px #ff9800;box-shadow:0 1px 12px #ff9800;-webkit-transition:all .5s;-o-transition:all .5s;transition:all .5s}#mainHeader{height:50px;color:#fff;background:#1183fb -webkit-gradient(linear,right top,left top,from(#0a48d1),to(#1183fb));background:#1183fb -webkit-linear-gradient(right,#0a48d1 0,#1183fb 100%);background:#1183fb -o-linear-gradient(right,#0a48d1 0,#1183fb 100%);background:#1183fb linear-gradient(-90deg,#0a48d1 0,#1183fb 100%);background-color:#1183fb;border-top-color:#0c64eb;border-bottom-color:#e9f2fb}#mainHeader>.container{min-width:1200px;padding:0}#heading{position:absolute;top:10px;left:20px}@media (min-width:1400px){#heading{left:40px}}#heading h1{float:left;max-width:250px;margin:0;overflow:hidden;font-size:20px;font-weight:400;line-height:30px;text-overflow:ellipsis;white-space:nowrap}#heading h1 a{color:inherit;text-decoration:inherit}#heading h1.long-name{position:relative;top:-5px;display:table-cell;font-size:16px;line-height:20px;word-break:break-all;white-space:normal}#heading>.btn{display:block;float:left;height:20px;padding:1px 5px;margin:0;margin:5px 0 0 10px;font-size:12px;font-weight:lighter;line-height:18px;background-color:rgba(255,255,255,.2);border:none}#heading>.btn:hover{background-color:rgba(0,0,0,.1)}#navbar{margin:0 auto;text-align:center}#navbar .nav{display:inline-block}#navbar .nav>li>a{padding:10px;line-height:30px;color:#fff;border-radius:0;opacity:.9}@media (max-width:1400px){#navbar .nav>li>a{padding:10px 8px}}#navbar .nav>li>a:focus,#navbar .nav>li>a:hover{background:rgba(0,0,0,.15);opacity:1}#navbar .nav>li.active>a{font-weight:700;background:rgba(0,0,0,.1);opacity:1}#navbar .nav>li.divider{display:block;width:2px;height:20px;margin:15px 8px;background:rgba(255,255,255,.4)}@media (max-width:1400px){#navbar .nav>li.divider{margin:15px 5px}}@media (max-width:1300px){#navbar .nav>li.divider{margin:15px 3px}}#navbar .nav>li.divider:last-child{display:none}#navbar .nav .dropdown-menu li>a{text-align:left}#toolbar{position:absolute;top:12px;right:20px;font-size:12px;color:#fff}@media (min-width:1400px){#toolbar{right:40px}}#extraNav{text-align:right}#extraNav>li{display:inline-block;float:none;text-align:left}#extraNav>li>a{display:block;padding:0;color:#fff;opacity:.75}#extraNav>li>a:hover{text-decoration:unset;background-color:rgba(0,0,0,.1);opacity:1}#extraNav>li.open>a{background-color:rgba(0,0,0,.1)}#extraNav>li+li{margin-left:10px}#showSearchGo{color:#fff;background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.5)}#searchbox{position:relative;float:left;width:150px}#searchbox .input-group-btn .btn{position:relative;padding:1px 4px;font-size:12px;line-height:20px;color:#fff;background-color:rgba(255,255,255,.15);border-right:none;border-radius:2px}#searchbox .input-group-btn .btn:after{position:absolute;top:3px;right:0;bottom:3px;display:block;width:1px;content:' ';background-color:rgba(255,255,255,.15)}#searchbox .input-group-btn .btn:hover{background-color:rgba(255,255,255,.25)}#searchGo{position:absolute;top:0;right:-1px;z-index:9;min-width:24px;height:24px;padding:2px 3px;font-size:12px;line-height:20px;color:#fff;background-color:#16a8f8;border-radius:2px}#searchGo:hover{color:#fff!important;background-color:#0c64eb}#searchInput{height:24px;padding:2px 30px 2px 5px;color:#fff;text-align:left;background:rgba(255,255,255,.15);border-color:transparent;border-radius:0 12px 12px 0;-webkit-transition:background .2s,border .2s;-o-transition:background .2s,border .2s;transition:background .2s,border .2s}#searchInput:hover{background:rgba(255,255,255,.25)}#searchInput:focus{color:#333;background:#fff}#searchInput::-webkit-input-placeholder{font-size:12px;color:#fff;color:rgba(255,255,255,.5)}#searchInput::-moz-placeholder{font-size:12px;color:#fff;color:rgba(255,255,255,.5)}#searchInput:-ms-input-placeholder{font-size:12px;color:#fff;color:rgba(255,255,255,.5)}#searchInput::placeholder{font-size:12px;color:#fff;color:rgba(255,255,255,.5)}#searchInput:focus::-webkit-input-placeholder{color:#838a9d}#searchInput:focus::-moz-placeholder{color:#838a9d}#searchInput:focus:-ms-input-placeholder{color:#838a9d}#searchInput:focus::placeholder{color:#838a9d}#searchTypeMenu{min-width:220px}#searchTypeMenu>li{float:left;width:50%}#searchTypeMenu>li>a{margin:4px 0}#userNav .avatar{display:inline-block;vertical-align:middle}#userNav>li>a{padding:2px 6px;line-height:20px;color:#fff;opacity:.9}#userNav>li>a .user-name{max-width:100px;overflow:hidden;font-size:15px;text-overflow:ellipsis;white-space:nowrap}#userNav>li>a:hover{background-color:rgba(0,0,0,.1);opacity:1}#userNav>li>a:hover>i{opacity:1}#userNav>li>a span{display:inline-block;vertical-align:middle}#userNav>li.open>a{background-color:rgba(0,0,0,.1)}#userNav>li.has-new-items>a{position:relative}#userNav>li.has-new-items>a:before{position:absolute;top:3px;right:-1px;display:block;width:4px;height:4px;content:' ';background-color:#ff5d5d;border-radius:50%}#userNav .dropdown-menu{min-width:150px}#userNav .dropdown-menu>li>a>.icon{position:absolute;top:10px;right:5px;display:block;width:20px;height:20px;line-height:20px;text-align:center}#userNav .user-profile-item>a{position:relative;padding-left:45px}#userNav .user-profile-item .avatar{position:absolute;top:6px;left:5px}#userNav .user-profile-name{font-size:16px}#userNav .user-profile-role{font-size:12px;color:#a9abb8}#userNav .no-role .user-profile-role{display:none}#userNav .no-role .user-profile-name{line-height:40px}#subHeader{min-height:50px;background:#fff}#subHeader>.container{padding:0 20px}@media (min-width:1400px){#subHeader>.container{padding:0 40px}}#pageNav{position:absolute;top:8px;left:0;left:20px}@media (min-width:1400px){#pageNav{left:40px}}#subNavbar{margin-top:5px;font-size:14px;text-align:center}#subNavbar .nav{display:inline-block}#subNavbar .nav>li>a{padding:8px 12px;line-height:24px;color:#3c495c}#subNavbar .nav>li>a:hover{color:#3c495c;background-color:rgba(0,0,0,.075)}#subNavbar .nav>li.active>a{font-weight:700;color:#0c64eb}#subNavbar .nav>li.divider{display:block;width:2px;height:20px;margin:9px 5px;background-color:rgba(0,0,0,.05)}#subNavbar .dropdown-menu{text-align:left}[lang=en] #subNavbar>.nav>li>a{padding-right:8px;padding-left:8px}#pageActions{position:absolute;top:9px;right:20px}@media (min-width:1400px){#pageActions{right:40px}}.cell{padding:10px;background-color:#fff;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05),0 2px 6px 0 rgba(0,0,0,.045);box-shadow:0 1px 1px rgba(0,0,0,.05),0 2px 6px 0 rgba(0,0,0,.045)}.cell+.cell{margin-top:10px}.cell>.panel{margin:0;-webkit-box-shadow:none;box-shadow:none}.cell>.panel>.panel-heading{padding:5px 5px 10px}.cell>.panel>.panel-heading .panel-actions{padding:0}.cell>.panel>.panel-body{padding:5px}.cell>.table{margin:0}#main{min-width:1200px;padding:20px 0}#main>.container{padding:0 20px}@media (min-width:1400px){#main>.container{padding:0 40px}}#header,#header+#main{min-width:1200px}#mainMenu{margin:-10px 0 8px}.main-content{padding:20px;background-color:#fff;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05),0 2px 6px 0 rgba(0,0,0,.045);box-shadow:0 1px 1px rgba(0,0,0,.05),0 2px 6px 0 rgba(0,0,0,.045)}@media (min-width:1400px){.main-content>.center-block{max-width:1350px;padding:20px;border:1px solid #eee}.main-content>.center-block .main-header{background-color:#f1f1f1}}.main-content>h2{margin:0 0 20px}.main-content .cell{-webkit-box-shadow:none;box-shadow:none}.main-header{padding:5px 20px;border-bottom:1px solid #eee}.main-header:after,.main-header:before{display:table;content:" "}.main-header:after{clear:both}.main-header>h2{display:block;float:left;margin:0 10px 0 0;font-size:14px;line-height:34px}.main-header>h2 .label-id{margin-right:5px}.main-header>h2 small{font-size:14px;font-weight:400}.main-content .main-header{margin:-20px -20px 10px}.main-header .label{top:-1px}.main-row{display:table;width:100%;table-layout:fixed}.main-row>*{display:table-cell;vertical-align:top}@media (max-width:720px){.main-row{display:block}.main-row>*{display:block;width:100%}.main-row .side-col{width:100%;padding:0}.main-row .main-col+.side-col,.main-row .side-col+.main-col{margin-top:10px}}.main-row.hide-side .side-col{display:none}.main-form{margin:0}@media (min-width:720px){.main-content>.center-block .main-form{padding-right:20px}}#main .side-col .tabs{padding:5px}#main .side-col .nav-tabs{margin:0 5px 5px 5px;border-bottom:1px solid #ddd}#main .side-col .nav-tabs>li{margin:0}#main .side-col .nav-tabs>li+li{margin-left:10px}#main .side-col .nav-tabs>li>a{position:relative;padding:8px 5px;border:none;border-radius:2px!important}#main .side-col .nav-tabs>li.active>a{font-weight:700;color:#3c4353}#main .side-col .nav-tabs>li.active>a:before{position:absolute;right:0;bottom:-1px;left:0;display:block;height:2px;content:' ';background:#0c64eb}#main .side-col .tab-content .tab-pane table{border:none}.main-actions .btn-toolbar{display:inline-block;padding:4px 15px;color:#fff;pointer-events:auto;background:#717171;background-color:rgba(90,90,90,.85);border-radius:4px}.main-actions .btn-toolbar .divider{margin-right:15px;margin-left:15px;border-color:rgba(255,255,255,.1)}.main-actions .btn-toolbar .btn{height:30px;padding-right:10px;padding-left:10px;margin-right:0;color:#fff;background-color:transparent;border:none}.main-actions .btn-toolbar .btn+.btn{margin-left:10px}.main-actions .btn-toolbar .btn:focus,.main-actions .btn-toolbar .btn:hover{background-color:rgba(255,255,255,.2)}.main-actions .btn-toolbar .btn.btn-icon{min-width:32px;padding-right:0;padding-left:0}.main-actions .btn-toolbar .btn+.btn-group{margin-right:0;margin-left:10px}#mainContent .main-col>.main-actions{padding:30px 0 0 0;text-align:center}#mainContent .main-col>.main-actions>.btn-toolbar{visibility:visible;opacity:1;-webkit-transition:opacity .2s;-o-transition:opacity .2s;transition:opacity .2s}#mainActions{position:fixed;top:0;right:0;bottom:0;left:0;text-align:center;pointer-events:none}#mainActions .btn-toolbar{position:relative;top:-90px}#mainActions .dropdown-menu{text-align:left}#mainActions>.container{height:100%}.main-actions-holder{display:none}.main-actions-fixed .main-actions-holder{display:block}.main-actions-fixed #mainContent .main-col>.main-actions{position:fixed;bottom:10px}.main-actions-fixed.body-modal #mainContent .main-col>.main-actions{bottom:20px}#nextPage,#prevPage{position:absolute;top:50%;left:-10px;width:40px;height:60px;padding:10px 0;margin-top:-30px;line-height:40px;color:#fff;text-align:center;pointer-events:auto;background:#717171;background-color:rgba(90,90,90,.85);-webkit-box-shadow:0 2px 15px 2px rgba(0,0,0,.05);box-shadow:0 2px 15px 2px rgba(0,0,0,.05)}#nextPage:hover,#prevPage:hover{-webkit-box-shadow:0 2px 15px 2px rgba(0,0,0,.15);box-shadow:0 2px 15px 2px rgba(0,0,0,.15)}#nextPage>i,#prevPage>i{display:block;font-size:18px;line-height:36px}#nextPage{right:-10px;left:auto}@media (max-width:1800px){#prevPage{left:-3px}#nextPage{right:-3px}}#sidebarHeader{position:relative;float:left;width:180px;height:34px;padding-right:20px;margin-right:20px;background:#fff;border-left:4px solid #0c64eb;border-radius:4px 2px 2px 4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05),0 2px 6px 0 rgba(0,0,0,.045);box-shadow:0 1px 1px rgba(0,0,0,.05),0 2px 6px 0 rgba(0,0,0,.045)}#sidebarHeader:after{position:absolute;top:-1px;right:-8px;display:block;width:0;height:0;content:' ';border-color:transparent transparent transparent #fff;border-style:solid;border-width:18px 0 18px 8px}#sidebarHeader .title{padding:0 5px;overflow:hidden;font-size:14px;font-weight:700;line-height:32px;color:#0c64eb;text-align:center;text-overflow:ellipsis;white-space:nowrap}#sidebarHeader .title>a{position:absolute;top:0;right:0;width:20px;opacity:.5}#sidebarHeader .title>a:hover{opacity:1}#sidebar{position:relative;-webkit-transition:width .2s,padding .2s;-o-transition:width .2s,padding .2s;transition:width .2s,padding .2s}#sidebar>.sidebar-toggle{position:absolute;top:0;right:5px;bottom:0;width:10px;cursor:pointer;background:0 0;border-radius:5px;-webkit-transition:background-color .2s,opacity .5s;-o-transition:background-color .2s,opacity .5s;transition:background-color .2s,opacity .5s}#sidebar>.sidebar-toggle>.icon{position:absolute;top:50%;left:-1px;width:12px;height:30px;margin-top:-10px;line-height:30px;color:#fff;text-align:center;background:#79cdfb;border-radius:6px}#sidebar>.sidebar-toggle>.icon:before{position:relative;left:-1px}#sidebar>.sidebar-toggle:before{position:absolute;top:0;right:-5px;bottom:0;left:-5px;display:block;content:' '}#sidebar>.sidebar-toggle:hover{background:rgba(0,0,0,.075)}#sidebar>.cell{position:relative;left:0;width:180px;-webkit-transition:left .2s,opacity .2s;-o-transition:left .2s,opacity .2s;transition:left .2s,opacity .2s}#sidebar.no-animate>.cell{display:none;-webkit-transition:none;-o-transition:none;transition:none}.hide-sidebar #sidebar>.cell{position:absolute;left:-200px;visibility:hidden;opacity:0}.hide-sidebar #sidebar{position:relative;width:0;padding:0}.hide-sidebar #sidebar>.sidebar-toggle>.icon:before{content:"\e315"}@media (max-width:720px){#sidebar>.cell{width:100%}}#queryBox{max-height:0;padding:0;overflow:hidden;-webkit-transition:cubic-bezier(.175,.885,.32,1) .2s;-o-transition:cubic-bezier(.175,.885,.32,1) .2s;transition:cubic-bezier(.175,.885,.32,1) .2s;-webkit-transition-property:padding,max-height,margin;-o-transition-property:padding,max-height,margin;transition-property:padding,max-height,margin}#queryBox>form{visibility:hidden;-webkit-transition:visibility .2s .2s;-o-transition:visibility .2s .2s;transition:visibility .2s .2s}#queryBox.loading{height:50px}#queryBox.show{min-height:110px;max-height:300px;margin-bottom:10px;overflow:visible}#queryBox.show>form{visibility:visible}#queryBox.divider{border-bottom:1px solid #eee}#main .querybox-toggle.querybox-opened{position:relative;color:#0c64eb;background:0 0;border:none}#main .querybox-toggle.querybox-opened:before{position:absolute;bottom:-14px;left:50%;width:0;height:0;content:' ';border-color:transparent transparent #fff transparent;border-style:solid;border-width:0 10px 10px 10px}#contentNav{padding:5px;background:#fff;border-bottom:1px solid #eee}#contentNav .nav>li>a{position:relative;padding:6px 10px;color:#838a9d}#contentNav .nav>li.active>a{font-weight:700;color:#0c64eb}#contentNav .nav>li.active>a:before{position:absolute;right:10px;bottom:3px;left:10px;display:block;height:2px;content:' ';background:#0c64eb}.body-modal{padding-bottom:0}.body-modal #main,.body-modal .container{min-width:0!important}.body-modal #main{padding:0}.body-modal .main-header{position:fixed;top:0;right:20px;left:20px;z-index:100;padding:13px 48px 13px 0;margin:0;background:#fff}.body-modal #mainContent{padding-top:70px}.body-modal .main-header>h2{max-width:100%;overflow:hidden;font-size:14px;text-overflow:ellipsis;white-space:nowrap}.body-modal .cell,.body-modal .main-content{-webkit-box-shadow:none;box-shadow:none}.body-modal #mainMenu{position:fixed;top:0;right:0;left:0;z-index:100;padding:12px 60px 12px 10px;margin:0;background:#fff}.body-modal #mainMenu>.btn-toolbar.pull-left.divider{display:none}.body-modal #mainMenu>.btn-toolbar{width:100%;margin-left:20px}.body-modal #mainMenu>.btn-toolbar>.divider:first-child{display:none}.body-modal #mainMenu>.btn-toolbar .page-title{width:100%;margin-left:0}.body-modal #mainMenu>.btn-toolbar .page-title>.text{position:relative;top:-2px;display:inline-block;max-width:85%;max-width:-webkit-calc(100% - 100px);max-width:calc(100% - 100px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle}.body-modal #mainMenu+#mainContent.main-row{padding:60px 10px 0}.body-modal #mainMenu+#mainContent.main-row .cell{border:1px solid #efefef;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05),0 2px 6px 0 rgba(0,0,0,.045);box-shadow:0 1px 1px rgba(0,0,0,.05),0 2px 6px 0 rgba(0,0,0,.045)}.body-modal #mainActions{top:auto}.body-modal #mainActions .btn-toolbar{top:auto;bottom:10px}.body-modal.m-bug-view,.body-modal.m-story-view,.body-modal.m-task-view,.body-modal.m-testcase-view,.body-modal.m-testtask-view,.body-modal.m-todo-view{padding-bottom:20px;border-radius:3px}#tabsNav{position:relative}#tabsNav .tab-pane>.actions{position:absolute;top:-8px;right:0}#tabsNav .tab-pane>.cell,#tabsNav .tab-pane>.main-table{padding:0;border:1px solid #cbd0db;border-top:none;border-radius:0 0 4px 4px}#tabsNav .tab-pane>.cell .detail-title{padding-left:5px}#helpContent{position:fixed;top:50px;right:0;bottom:40px;left:0;display:none;background-color:#fff}#helpContent .load-error{display:none;padding:20px}#helpContent .show-error .load-error{display:block}.text-middle td,.text-middle th{vertical-align:middle}.text-center td,.text-center th{text-align:center}.c-sm{width:40px}.c-id{width:90px}.c-id-sm{width:70px}.c-id-xs{width:55px}.c-date{width:100px}.c-num,.c-pri,.c-type{width:80px;overflow:hidden}.c-begin,.c-end,.c-time{width:65px}.c-hours{width:60px}.c-actions-1{width:50px}.c-actions-2{width:75px}.c-actions-3{width:102px}.c-actions,.c-actions-4{width:128px}.c-actions-5{width:155px}.c-actions-6{width:180px}.c-product,.c-project{width:180px}.c-plan{width:130px}.c-datetime{width:120px}.c-stage,.c-status,.c-user{width:80px}.c-side{width:200px;border-right:10px solid #efefef}.c-assign,.c-assignedTo,.c-openedBy{width:130px}.c-progress{width:155px}.c-assign,.c-assignedTo,.c-openedBy,.c-product,.c-project,.c-status,.c-url,.c-user{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}td.c-name,td.c-title{overflow:hidden;text-align:left!important;text-overflow:ellipsis;white-space:nowrap}td.c-actions{position:relative;padding-top:0;padding-bottom:0;overflow:hidden;white-space:nowrap;vertical-align:middle}td.c-actions .btn-link{color:#3c495c;background:0 0}td.c-actions .btn-link:hover{color:#0c64eb;background:#e9f2fb}td.c-actions .more{position:absolute;top:50%;right:100%;display:none;padding-right:4px;padding-left:20px;margin-top:-15px;margin-right:-6px;white-space:nowrap;background-color:#fafafa;-webkit-transition:opacity .3s,margin .3s;-o-transition:opacity .3s,margin .3s;transition:opacity .3s,margin .3s}tr:hover td.c-actions .more{display:block}td:hover+td.c-actions>.more{margin-right:-15px;pointer-events:none;opacity:.15}tr[data-url]{cursor:pointer}.table tbody>tr>td,.table thead>tr>th{vertical-align:middle}.table tbody>tr>td.has-btn,.table thead>tr>th.has-btn{padding-top:1px;padding-bottom:1px;overflow:visible}.table tbody>tr>td .progress,.table thead>tr>th .progress{height:6px}.table .em,.table em{color:#3c4353}.table .divider{border-bottom:10px solid #efefef}.table .divider-top{border-top:10px solid #efefef}.table .btn-icon-left{max-width:100%;padding-left:20px;overflow:hidden;line-height:18px;text-align:left;text-overflow:ellipsis;background:0 0;border-color:#eaf3fc}.table .btn-icon-left>.icon{width:20px;font-size:14px;background:0 0!important;opacity:0}.table .btn-icon-left.btn-sm{height:26px;font-size:13px}.table .btn-icon-left:active,.table .btn-icon-left:focus,.table .btn-icon-left:hover{border-color:rgba(0,0,0,.2)}.table .btn-icon-left:active>.icon,.table .btn-icon-left:focus>.icon,.table .btn-icon-left:hover>.icon{opacity:1}.table .btn-icon-left>.text{padding-left:25px}.table thead>tr>th.c-assign,.table thead>tr>th.c-assignedTo{padding-left:29px}.table a{vertical-align:middle}.table tbody>tr:last-child{border-bottom:none}.table caption{margin-bottom:5px;background:#f1f1f1;border:none}.is-firefox .table .btn-icon-left>.icon{line-height:22px}.main-table{border-radius:4px}.main-table>.table,.main-table>.table-footer,.main-table>.table-header,.main-table>.table-responsive{-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05),0 2px 6px 0 rgba(0,0,0,.045);box-shadow:0 1px 1px rgba(0,0,0,.05),0 2px 6px 0 rgba(0,0,0,.045)}.main-table .table{font-size:13px;table-layout:fixed;background-color:#fff;border-radius:4px 4px 0 0}.main-table .table.table-lg{font-size:14px}.main-table .table .btn-icon-left{border-color:transparent}.main-table .table .btn-icon-left>.icon{background:0 0;border-radius:4px}.main-table .table .btn-icon-left.btn-sm{height:26px}.main-table .table .btn-icon-left:hover{border-color:rgba(0,0,0,.2)}.main-table .table .btn-icon-left:hover>.icon{background:#e9f2fb;border-radius:4px 0 0 4px}.main-table tbody>tr>td,.main-table thead>tr>th{min-height:36px;padding:2px 8px;line-height:30px}.main-table tbody>tr>td:first-child,.main-table thead>tr>th:first-child{padding-right:4px;padding-left:15px}.main-table thead>tr>th{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border-bottom:1px solid #ddd}.main-table tbody>tr:nth-child(odd){background-color:#f5f5f5}.main-table tbody>tr:last-child>td{border-bottom:1px solid #ddd}.main-table tbody>tr>td{position:relative;border-bottom:none;border-bottom:1px solid #eee}.main-table tbody>tr>td .label{max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.main-table tbody>tr>td>a{line-height:28px;color:#0c60e1}.main-table tbody>tr>td>a:not(.btn):visited{color:#082999;opacity:.9}.main-table tbody>tr>td>a:hover,.main-table tbody>tr>td>a:visited:hover{color:#0c64eb}.main-table tbody>tr>td.c-actions{padding-right:10px}.main-table tbody>tr>td.c-side+td:before,.main-table tbody>tr>td:first-child:before{position:absolute;top:0;bottom:0;left:0;display:block;width:0;content:'';background:#0c64eb;opacity:0;-webkit-transition:.2s linear;-o-transition:.2s linear;transition:.2s linear;-webkit-transition-property:width,opacity,border-radius;-o-transition-property:width,opacity,border-radius;transition-property:width,opacity,border-radius}@-moz-document url-prefix(){.main-table tbody>tr>td.c-side+td:before,.main-table tbody>tr>td:first-child:before{bottom:-1px}}.main-table tbody>tr>td.c-side:before{display:none}.main-table tbody>tr{-webkit-transition:.2s cubic-bezier(.175,.885,.32,1);-o-transition:.2s cubic-bezier(.175,.885,.32,1);transition:.2s cubic-bezier(.175,.885,.32,1);-webkit-transition-property:background-color,-webkit-box-shadow;-o-transition-property:box-shadow,background-color;transition-property:background-color,-webkit-box-shadow;transition-property:box-shadow,background-color;transition-property:box-shadow,background-color,-webkit-box-shadow}.main-table tbody>tr:hover{background:#e9f2fb}.main-table .table-grouped tbody>tr:hover{background:#f2f7fd;-webkit-box-shadow:none;box-shadow:none}.main-table .table-grouped tbody>tr:hover td.c-actions .more{background:#f2f7fd}.main-table tbody>tr.checked{background:#fff3e0}.main-table tbody>tr.checked:hover{background:#ffebbc}.main-table tbody>tr.checked>td.c-side+td:before,.main-table tbody>tr.checked>td:first-child:before{width:4px;opacity:1}.main-table tbody>tr.checked.row-check-begin{border-top-left-radius:4px;border-top-right-radius:2px}.main-table tbody>tr.checked.row-check-begin>td:first-child:before{border-top-left-radius:4px}.main-table tbody>tr.checked.row-check-end{border-bottom-right-radius:2px;border-bottom-left-radius:4px}.main-table tbody>tr.checked.row-check-end>td:first-child:before{border-bottom-left-radius:4px}.main-table .checkbox-primary{display:inline-block;line-height:20px}.main-table .checkbox-primary label{margin:0}.main-table .table{margin:0}.table-header{padding:4px 0 12px}.table-header .table-statistic{color:#838a9d}.table-header .table-statistic strong{font-size:15px;color:#3c4353}.table-header .btn-toolbar{margin-top:-28px}.table-header.fixed-right{position:relative;z-index:5;padding:0}.table-header.fixed-right>.btn-toolbar{position:absolute;top:1px;right:1px;z-index:1;padding:1px;margin:0;background:#fff;border-radius:4px}.table-header.fixed-right>.btn-toolbar .btn{opacity:.65}.table-header.fixed-right>.btn-toolbar .btn:hover{opacity:1}.table-header-fixed .table-header{position:fixed;top:0}.table-header-fixed .table-header>.btn-toolbar{background-color:transparent}.table-header-fixed .table-header>.btn-toolbar .btn{color:#fff}.table-footer{position:relative;min-height:40px;padding:6px 15px;background:#fff;border-radius:0 0 4px 4px}.body-modal .table-footer{margin-bottom:20px}.talbe-lg+.table-footer{padding:11px 15px}.table-footer .btn-toolbar,.table-footer .checkbox-primary{float:left}.table-footer .btn-toolbar+.btn-toolbar{margin-left:8px}.table-footer .checkbox-primary{margin:5px 20px 0 0}.table-footer .checkbox-primary.checked label:after{border-color:#00da88!important}.table-footer .btn{padding:3px 10px;line-height:20px}.table-footer .pager{position:absolute;top:0;right:0;z-index:4;height:40px;padding:6px 5px 6px 10px;margin:0;background:#fff;opacity:1;-webkit-transition:opacity .4s;-o-transition:opacity .4s;transition:opacity .4s}.table-footer .pager:before{position:absolute;top:0;bottom:0;left:-50px;display:block;width:50px;content:' ';background:-webkit-gradient(linear,left top,right top,from(rgba(255,255,255,0)),to(#fff));background:-webkit-linear-gradient(left,rgba(255,255,255,0) 0,#fff 100%);background:-o-linear-gradient(left,rgba(255,255,255,0) 0,#fff 100%);background:linear-gradient(to right,rgba(255,255,255,0) 0,#fff 100%)}.table-footer .pager .btn,.table-footer .pager>li>.pager-item,.table-footer .pager>li>.pager-label{color:#838a9d;background:0 0;border-color:transparent}.table-footer .pager .btn,.table-footer .pager>li>a{border-radius:3px}.table-footer .pager .btn:hover,.table-footer .pager>li>a:hover{background:rgba(0,0,0,.1)}.table-footer .pager>li.disabled>a.pager-item{opacity:1}.table-footer .form-control{height:28px;padding:3px 8px}.table-footer .table-statistic{position:relative;z-index:2;float:left;padding-right:30px;line-height:28px;color:#838a9d;background:#fff}.table-footer .table-statistic:hover{z-index:4}.table-footer .table-statistic:hover+.pager{z-index:2;opacity:.3}.table-footer .btn-toolbar+.table-statistic,.table-footer .btn-toolbar+.text{margin-left:10px}.table-footer .text{float:left;line-height:28px}.table-footer.fixed-footer{position:fixed;z-index:10;margin:0;background:rgba(75,75,75,.85);border-top-color:transparent}.table-footer.fixed-footer .checkbox-primary label{color:#fff}.table-footer.fixed-footer .checkbox-primary label:after{border-color:rgba(255,255,255,.8)}.table-footer.fixed-footer .table-statistic{color:#fff;background:0 0}.table-footer.fixed-footer .pager{background:#666}.table-footer.fixed-footer .pager:before{background:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,0)),to(#666));background:-webkit-linear-gradient(left,rgba(0,0,0,0) 0,#666 100%);background:-o-linear-gradient(left,rgba(0,0,0,0) 0,#666 100%);background:linear-gradient(to right,rgba(0,0,0,0) 0,#666 100%)}.table-footer.fixed-footer .pager .btn,.table-footer.fixed-footer .pager>li>.pager-item,.table-footer.fixed-footer .pager>li>.pager-label{color:#fff}.table-footer.fixed-footer .pager .btn:hover,.table-footer.fixed-footer .pager>li>a:hover{background:rgba(255,255,255,.3)}.table-footer.fixed-footer .pager>li.disabled>a.pager-item{opacity:.5}.table-actions{width:0;height:28px;visibility:hidden;opacity:0;-webkit-transition:opacity cubic-bezier(.175,.885,.32,1) .8s;-o-transition:opacity cubic-bezier(.175,.885,.32,1) .8s;transition:opacity cubic-bezier(.175,.885,.32,1) .8s}.table-actions.show-always{width:auto;pointer-events:none;cursor:not-allowed;visibility:visible;opacity:.75}.has-row-checked .table-actions{width:auto;pointer-events:auto!important;cursor:default;visibility:visible;opacity:1}.table-lg tbody>tr>td{padding:9px 10px}.table-lg tbody>tr>td .btn+.btn{margin-left:5px}.table.has-sort-head thead>tr>th{padding-right:0}.table.has-sort-head thead>tr>th>a{position:relative;display:inline-block;padding-right:16px;color:#3c4353}.table.has-sort-head thead>tr>th>a:after,.table.has-sort-head thead>tr>th>a:before{position:absolute;top:0;right:0;font-family:ZentaoIcon;font-size:14px;font-style:normal;font-weight:400;font-variant:normal;line-height:1;line-height:30px;color:#3c495c;text-transform:none;content:"\f0de";opacity:.5;speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.table.has-sort-head thead>tr>th>a:after{content:"\f0dd"}.table.has-sort-head thead>tr>th>a.sort-down,.table.has-sort-head thead>tr>th>a.sort-up{color:#000;text-decoration:none}.table.has-sort-head thead>tr>th>a:hover,.table.has-sort-head thead>tr>th>a:hover:after,.table.has-sort-head thead>tr>th>a:hover:before{color:#0c64eb;opacity:1}.table.has-sort-head thead>tr>th>a.sort-down:after,.table.has-sort-head thead>tr>th>a.sort-up:before{color:#000;opacity:1}.head-fixed .datatable-head-span .table,.table.fixed-header-copy{z-index:10;color:#fff;background:rgba(75,75,75,.85)}.head-fixed .datatable-head-span .table thead>tr>th,.table.fixed-header-copy thead>tr>th{color:#eee}.head-fixed .datatable-head-span .table thead>tr>th>a,.table.fixed-header-copy thead>tr>th>a{color:#eee}.head-fixed .datatable-head-span .table thead>tr>th>a:hover,.table.fixed-header-copy thead>tr>th>a:hover{color:#fff}.head-fixed .datatable-head-span .table thead>tr>th>a:after,.head-fixed .datatable-head-span .table thead>tr>th>a:before,.table.fixed-header-copy thead>tr>th>a:after,.table.fixed-header-copy thead>tr>th>a:before{color:#eee}.head-fixed .datatable-head-span .table thead>tr>th>a.sort-down,.head-fixed .datatable-head-span .table thead>tr>th>a.sort-down:after,.head-fixed .datatable-head-span .table thead>tr>th>a.sort-up,.head-fixed .datatable-head-span .table thead>tr>th>a.sort-up:before,.head-fixed .datatable-head-span .table thead>tr>th>a:hover,.head-fixed .datatable-head-span .table thead>tr>th>a:hover:after,.head-fixed .datatable-head-span .table thead>tr>th>a:hover:before,.table.fixed-header-copy thead>tr>th>a.sort-down,.table.fixed-header-copy thead>tr>th>a.sort-down:after,.table.fixed-header-copy thead>tr>th>a.sort-up,.table.fixed-header-copy thead>tr>th>a.sort-up:before,.table.fixed-header-copy thead>tr>th>a:hover,.table.fixed-header-copy thead>tr>th>a:hover:after,.table.fixed-header-copy thead>tr>th>a:hover:before{color:#fff}.head-fixed .datatable-head-span .table thead>tr>th>.dropdown>a,.table.fixed-header-copy thead>tr>th>.dropdown>a{color:#eee}.head-fixed .datatable-head-span .table thead>tr>th>.dropdown>a:hover,.table.fixed-header-copy thead>tr>th>.dropdown>a:hover{color:#fff}.head-fixed .datatable-head-span .table .checkbox-primary,.table.fixed-header-copy .checkbox-primary{z-index:1}.head-fixed .datatable-head-span .table .checkbox-primary label,.table.fixed-header-copy .checkbox-primary label{color:#fff}.head-fixed .datatable-head-span .table .checkbox-primary label:after,.table.fixed-header-copy .checkbox-primary label:after{border-color:rgba(255,255,255,.8)}.head-fixed .datatable-head-span .table .checkbox-primary.checked label:after,.table.fixed-header-copy .checkbox-primary.checked label:after{border-color:#00da88!important}.table-data{margin:0;table-layout:fixed}.table-data tbody>tr>td,.table-data tbody>tr>th{padding:6px 8px;word-break:break-all;border:none}.table-data tbody>tr>th{width:70px;padding-left:0;font-weight:400;color:#838a9d;text-align:right;vertical-align:middle}.table-data tbody>tr>td{padding-right:0}.table-data tbody>tr>td>a{color:#0c60e1}.table-data tbody>tr>td>a:not(.btn):visited{color:#082999}.table-data tbody>tr>td>a:hover,.table-data tbody>tr>td>a:visited:hover{color:#0c64eb}.table-data ol,.table-data ul{margin:0}.fixed-head-table{background:rgba(0,0,0,.7);border-bottom:1px solid #ddd}.fixed-head-table thead>tr>th{color:#fff}.table-empty-tip{padding:80px 10px;text-align:center;background:#fff}.not-firefox .table-grouped>tbody>tr>td.c-side{background:#fff!important}.table-grouped .group-toggle{cursor:pointer}.table-grouped .group-toggle.group-summary{border-top:10px solid #efefef}.table-grouped tbody>tr>td:first-child,.table-grouped thead>tr>th:first-child{padding-left:8px}.group-expand-all,.table-group-collapsed .group-collapse-all{display:none}.table-group-collapsed .group-expand-all{display:inline-block}.table-auto{table-layout:auto}.datatable .table>tbody>tr.checked.hover>td,.datatable .table>tbody>tr.checked>td.col-hover{background:#ffebbc}body.has-fixed-footer{padding-bottom:60px}.table.with-footer-fixed{margin-bottom:20px}.table-nest-hide{display:none!important}th.table-nest-title{position:relative;padding-left:30px!important}.table-nest-icon{position:relative;display:inline-block;width:22px;height:22px;font-size:16px;color:#a6aab8;text-align:center;border-radius:4px}.table-nest-toggle:before{line-height:22px;content:"\e6f2"}.table-nest-toggle:hover{color:#0c64eb;background-color:rgba(0,0,0,.1)}.table-nest-child-hide .table-nest-toggle:before{font-size:16px;content:"\e6f1"}th.table-nest-title .table-nest-toggle{position:absolute!important;top:7px;left:8px}.table-nest-toggle.table-nest-toggle-global{width:22px;height:22px;padding:0!important;line-height:22px;text-align:center;border-radius:4px}.table-nest-toggle.table-nest-toggle-global:before{position:static!important;font-size:16px!important;line-height:22px!important;content:"\e6f2"!important;opacity:1!important}.table-nest-toggle.table-nest-toggle-global:after{display:none!important}.table-nest-collapsed .table-nest-toggle.table-nest-toggle-global:before{font-size:16px!important;content:"\e6f1"!important}.disable-empty-nest-row .is-nest-child .table-nest-icon:before,.disable-empty-nest-row .no-nest .table-nest-icon:before{position:relative;top:-1px;width:6px;min-width:6px;height:6px;content:' ';background-color:#cbd0db;border-radius:1px}.table-nest-child-hover>td:first-child,.table-nest-hover>td:first-child{-webkit-box-shadow:inset 3px 0 0 #cbd0db;box-shadow:inset 3px 0 0 #cbd0db}.article-content{overflow:auto}.article-content img{margin-top:0}.article-content table{margin:10px 0}.article-content table td,.article-content table th{border:1px solid #cbd0db}.article-content table th{background:#eee}.article-content a{color:#0c64eb}.article-content a:focus,.article-content a:hover{color:#16a8f8}.article-content,.article>.content{word-wrap:break-word}.detail{padding:10px 0;margin:0 10px}.detail+.detail{padding-top:25px;border-top:1px solid #eee}.detail-title{font-size:14px;font-weight:700;line-height:20px}.detail-title>.pull-right{position:relative;top:-8px}h2.detail-title{margin:0;font-size:15px;font-weight:700}h2.detail-title .label,h2.detail-title .label-id{position:relative;top:-1px}.detail-content{padding:0;margin-top:10px}.detail-content em{color:#3c4353}.detail-content .list-unstyled>li+li{margin-top:5px}.side-col .detail-content{padding-left:0}details.detail{padding:10px 0}details.detail summary{position:relative;cursor:pointer;outline:0}details.detail summary::-webkit-details-marker{display:none}details.detail summary:after{position:absolute;top:0;right:0;font-family:ZentaoIcon;font-size:14px;font-style:normal;font-weight:400;font-variant:normal;line-height:1;text-transform:none;content:"\e316";opacity:.4;speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}details.detail[open] summary:after{content:"\e313"}.files-list{padding-left:0;list-style:none}.files-list>li>a{display:block;line-height:24px}.files-list>li>a>.icon{display:inline-block;margin-right:5px;opacity:.7}.files-list>li>a:hover{color:#0c64eb}.files-list>li>.right-icon{opacity:0;-webkit-transition:opacity .2s;-o-transition:opacity .2s;transition:opacity .2s}.files-list>li:hover>.right-icon{opacity:1}.histories-list{padding-left:15px;margin-bottom:0}.histories-list>li{position:relative}.histories-list>li+li{margin-top:5px}.histories-list>li strong{color:#3c4353}.histories-list .comment,.histories-list .show-form .comment-edit-form{padding:5px 5px 5px 10px;margin:5px 0 0;background-color:rgba(0,0,0,.025);border:1px solid #eee}.histories-list .btn-edit-comment{position:absolute;top:28px;right:2px}.histories-list .comment-edit-form,.histories-list .show-form .btn-edit-comment,.histories-list .show-form .comment{display:none}.histories-list .show-form .comment-edit-form{display:block;padding:10px;border:1px solid #eee}.histories .btn-mini{width:16px;min-width:16px;height:16px;overflow:hidden;line-height:16px;color:#cbd0db;vertical-align:-8%;border-radius:1px}.histories .btn-mini:focus,.histories .btn-mini:hover{color:#0c64eb;border-color:#0c64eb}.histories .show-changes .btn-expand>.icon:before{content:"\e926"}.histories .btn-strip{display:none}.histories .show-changes .btn-strip{display:inline-block}.history-changes{display:none;padding:5px;margin-bottom:-5px;margin-left:5px;font-size:12px;line-height:20px}.history-changes blockquote{padding:5px 5px 5px 10px;margin:5px 0 0;font-size:12px;background-color:rgba(0,0,0,.05);border-left:3px solid #eee}.history-changes blockquote.original{display:none}.show-changes .history-changes,.show-original .history-changes blockquote.original{display:block}.show-original .history-changes blockquote.textdiff{display:none}.syntaxhighlighter{overflow:auto}.list-group{overflow-y:auto}.list-group>a{display:block;padding:2px 10px 2px 5px;overflow:hidden;line-height:20px;text-overflow:ellipsis;white-space:nowrap;border-radius:4px}.list-group>a+a{margin-top:5px}.list-group>a>.icon{display:inline-block;margin-right:3px;opacity:.5}.list-group>a.selected{color:#e9f2fb;background-color:#0c64eb}.list-group>a.active{color:#0c64eb;background-color:#e9f2fb}.list-group>a.active:hover,.list-group>a:hover{color:#fff;background-color:#0c64eb}.list-group>.heading{padding:2px 5px;line-height:20px;color:#838a9d}.list-group>a+.heading{margin-top:4px}.dropup .search-box-sink{padding-top:5px;padding-bottom:45px}.dropup .search-box-sink .search-box{position:absolute;right:10px;bottom:10px;left:10px;margin:0}.dropup .search-box-sink .search-box+.list-group{height:auto;max-height:171px}.search-list{min-width:200px;max-width:300px;padding:0}.search-list .search-box{float:none;width:auto;margin:10px}.search-list .search-box .icon-search{opacity:.5}.search-list .list-group{max-height:248px;padding:5px 10px;margin:5px 0}.dropup .search-list .search-box+.list-group{height:171px;padding-top:0}.search-list .search-input{height:30px}.search-list .input-control-icon-right{height:28px;line-height:28px}.search-list .list-group>a.active{color:inherit;background-color:inherit}.search-list.searchbox-focus .list-group>a.active{color:#0c64eb;background-color:#e9f2fb}.search-list .list-group>a.active:hover,.search-list.searchbox-focus .list-group>a.active:hover{color:#fff;background-color:#0c64eb}#dropMenu{width:initial;max-width:initial}#dropMenu>.search-box{width:100%;padding:10px 10px 0;margin:0}#dropMenu>.search-box .icon-search{color:#333}#dropMenu>.search-box.has-icon-right>.form-control{padding-left:26px}#dropMenu .input-control-icon-left{top:10px;left:10px}#dropMenu .input-control-icon-right{top:11px;right:11px}#dropMenu .input-control-icon-right .icon{position:relative;top:2px}#dropMenu .list-group{max-height:initial;margin:0}#dropMenu .table-row{margin:0 -10px;table-layout:auto}#dropMenu .table-col{position:relative;width:100%;min-width:250px;max-width:450px}#dropMenu .table-col .list-group{max-height:300px;padding:0 10px 5px}#dropMenu .col-left{padding-bottom:30px}#dropMenu .col-right{display:none}#dropMenu .col-footer{position:absolute;right:0;bottom:0;left:0;padding:5px 10px;border-top:1px solid #eee}#dropMenu .col-footer>a{opacity:.8}#dropMenu .col-footer>a:hover{opacity:1}#dropMenu.show-right-col .table-col{width:50%}#dropMenu.show-right-col .col-right{display:table-cell;border-left:1px solid #eee}#dropMenu.show-right-col .col-right>.list-group{max-height:335px;margin:0}#dropMenu.show-right-col .col-right>.list-group>a{opacity:.7}#dropMenu.show-right-col .col-right>.list-group>a:hover{opacity:1}#dropMenu.show-right-col .toggle-right-col>.icon-angle-right:before{content:"\e314"}#dropMenu.has-search-text .list-group{overflow-x:hidden}#dropMenu.has-search-text>.search-box{width:100%!important}#dropMenu.has-search-text>.list-group>.table-row{display:block}#dropMenu.has-search-text>.list-group>.table-row>.table-col{display:block;width:100%}#dropMenu.has-search-text .col-left{padding-bottom:0}#dropMenu.has-search-text .pull-right.toggle-right-col{display:none}#dropMenu.has-search-text .col-left .list-group{margin-bottom:0}#dropMenu.has-search-text .col-right .list-group{margin-bottom:0}#dropMenu.has-search-text .col-right .list-group>a{opacity:.7}#dropMenu.has-search-text .col-footer,#dropMenu.has-search-text .hide-in-search{display:none}#swapper{position:relative}#swapper #dropMenu .list-group>a.active:hover,#swapper #dropMenu .search-box .list-group>a.active,#swapper #dropMenu.searchbox-focus .list-group>a.active:hover{color:#fff!important;background:#0c64eb!important}#swapper #dropMenu .tree{margin:0}.release-path{overflow:hidden}.release-line{display:table;width:100%;padding:0;table-layout:fixed}.release-line>li{display:table-cell;list-style:none}.release-line>li>a{position:relative;display:block}.release-line>li>a:before{position:absolute;left:0;display:block;width:13px;height:13px;content:' ';background:#fff;border:2px solid #838a9d;border-radius:50%}.release-line>li>a:after{position:absolute;left:5px;display:block;width:2px;height:30px;content:' ';background:#cbe0f6}.release-line>li>a>.icon{position:absolute;left:4px;font-size:24px}.release-line>li>a .title{display:block;font-size:14px;white-space:nowrap}.release-line>li>a .date,.release-line>li>a .info{display:block;max-height:18px;overflow:hidden;font-size:12px;color:#838a9d;text-overflow:ellipsis;white-space:nowrap}.release-line>li>a:hover:before{background-color:#e9f2fb}.release-line>li>a:hover:after{background-color:#838a9d}.release-line>li>a:hover .title{color:#0c64eb}.release-line>li>a:hover .date,.release-line>li>a:hover .info{color:#838a9d}.release-line>li:nth-child(odd){padding-top:80px;vertical-align:top}.release-line>li:nth-child(odd)>a{height:85px;padding-top:36px;border-top:5px solid #cbe0f6}.release-line>li:nth-child(odd)>a:before{top:-9px}.release-line>li:nth-child(odd)>a:after{top:6px}.release-line>li:nth-child(odd)>a>.icon{top:-26px}.release-line>li:nth-child(even){padding-bottom:80px;vertical-align:bottom}.release-line>li:nth-child(even)>a{height:85px;padding-bottom:36px;border-bottom:5px solid #cbe0f6}.release-line>li:nth-child(even)>a:before{bottom:-9px}.release-line>li:nth-child(even)>a:after{bottom:6px}.release-line>li:nth-child(even)>a>.icon{bottom:-2px}.release-line>li:last-child>a{border-color:transparent}.release-line>li.active>a:before{border-color:#0c64eb}.release-line>li+li>a>.date,.release-line>li+li>a>.info,.release-line>li+li>a>.title{position:relative;left:-36%}#footer{position:fixed;right:0;bottom:0;left:0;z-index:1010;height:40px;background:#fff;border-top:1px solid #eff1f7}#footer .breadcrumb{padding:10px 0;margin:0}#footer .breadcrumb>li{max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#footer .breadcrumb>.active,#footer .breadcrumb>li>a{color:#838a9e}#footer .breadcrumb>.active>.icon,#footer .breadcrumb>li>a>.icon{display:none}#footer .breadcrumb>.active:hover,#footer .breadcrumb>li>a:hover{color:#16a8f8}#footer .breadcrumb>li+li:before{content:'>'}#footer>.container{padding:0 20px}@media (min-width:1400px){#footer>.container{padding:0 40px}}#poweredBy{position:absolute;top:4px;right:0;padding:5px 10px}#poweredBy .icon-zentao{color:#0097fd}#poweredBy a{color:#3c4353}#poweredBy a:hover{color:#0c64eb}#poweredBy a:hover .icon-zentao{color:#0c64eb}#poweredBy a.text-important{color:#bd7b46}#poweredBy a.text-important:hover{color:#ff5d5d}#poweredBy a.text-primary{color:#0c64eb}#poweredBy a.text-primary:hover{color:#16a8f8}#poweredBy #aiux{color:#cbd0dc}#noticeBox .alert{-webkit-box-shadow:rgba(0,0,0,.15) 0 3px 10px,rgba(0,0,0,.25) 0 3px 10px;box-shadow:rgba(0,0,0,.15) 0 3px 10px,rgba(0,0,0,.25) 0 3px 10px}#heading{top:0}.header-btn{position:relative;padding:8px 0}.header-btn .btn{position:relative;height:34px;padding:1px 6px;margin:0;overflow:visible;font-size:13px;font-weight:400;line-height:28px;color:#fff;background-color:transparent;border-color:transparent!important;border-right:none;-webkit-transition:none;-o-transition:none;transition:none}.header-btn .btn>.caret{margin-left:0;border-width:4px}.header-btn .btn>.text{display:inline-block;max-width:150px;overflow:hidden;text-overflow:ellipsis;vertical-align:middle}.header-btn .btn:hover{-webkit-box-shadow:none;box-shadow:none}.header-btn .btn:hover,.header-btn.active .btn{color:#fff;background:rgba(0,0,0,.15)}.header-btn .btn:hover>.caret,.header-btn.active .btn>.caret{opacity:1}.header-btn+.header-btn{margin-left:10px}.header-btn+.header-btn:before{position:absolute;top:16px;left:-13px;display:block;font-family:ZentaoIcon;font-size:14px;font-size:16px;font-style:normal;font-weight:400;font-variant:normal;line-height:1;text-transform:none;content:"\e315";opacity:.6;speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.header-btn .dropdown-menu{margin-top:-10px}#toolbar{top:0;height:50px}#userNav>li{margin-right:0}#userNav>li>a{padding:10px 5px}#userNav>li>a>.icon{font-size:30px;filter:brightness(1.2) hue-rotate(30deg);opacity:.9;-webkit-filter:brightness(1.2) hue-rotate(30deg)}#userNav>li:hover>a{background-color:rgba(0,0,0,.1)}#userNav .dropdown-menu>li>a{position:relative;padding-left:24px}#userNav .dropdown-menu>li>a>.icon{top:1px;left:0}#userNav .dropdown-menu>li.user-profile-item>a{padding-left:45px} \ No newline at end of file +*/.chosen-container{position:relative;display:block;font-size:13px;vertical-align:middle;zoom:1;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.chosen-container .chosen-drop{position:absolute;top:100%;z-index:1010;display:none;width:100%;background:#fff;border:1px solid #b6bdcc;border:1px solid rgba(0,0,0,.15);border-top:0;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.chosen-container .chosen-drop.chosen-drop-size-limited{border-top:1px solid rgba(0,0,0,.15)}.chosen-container .chosen-drop.chosen-auto-max-width{min-width:100%;border-top:1px solid rgba(0,0,0,.15);opacity:0}.chosen-container .chosen-drop.chosen-auto-max-width>.chosen-results>li{display:inline-block;white-space:nowrap}.chosen-container .chosen-drop.chosen-auto-max-width.in{opacity:1}.chosen-container .chosen-drop.chosen-auto-max-width.in>.chosen-results>li{display:block;white-space:normal}.chosen-container .chosen-drop.chosen-no-wrap>.chosen-results>li{overflow:hidden;text-overflow:ellipsis;white-space:nowrap!important}.chosen-container.chosen-with-drop .chosen-drop{display:block}.chosen-container a{cursor:pointer}.chosen-container.chosen-up .chosen-drop{top:inherit;bottom:100%;margin-top:auto;margin-bottom:-1px;border-radius:2px 2px 0 0;-webkit-box-shadow:0 -3px 5px rgba(0,0,0,.175);box-shadow:0 -3px 5px rgba(0,0,0,.175)}.chosen-container.chosen-highlight-selected .result-selected{color:#0c64eb;background:#e9f2fb}.chosen-container-single .chosen-single{display:block;width:100%;height:32px;padding:5px 8px;overflow:hidden;line-height:1.42857143;color:#222;text-decoration:none;white-space:nowrap;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #dcdcdc;border-radius:2px;-webkit-box-shadow:none;box-shadow:none;-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s,-webkit-box-shadow ease-in-out .15s}.chosen-container-single .chosen-default{color:#838a9d}.chosen-container-single .chosen-single>span{display:block;margin-right:26px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.chosen-container-single .chosen-single-with-deselect span{margin-right:38px}.chosen-container-single .chosen-single abbr{position:absolute;top:5px;right:24px;display:block;width:20px;height:20px;font-family:sans-serif;font-size:18px;font-weight:700;line-height:18px;color:#000;text-align:center;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.chosen-container-single .chosen-single abbr:before{display:block;content:'×'}.chosen-container-single .chosen-single abbr:focus,.chosen-container-single .chosen-single abbr:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}.chosen-container-single .chosen-single div{position:absolute;top:0;right:0;display:block;height:100%;padding:5px 8px}.chosen-container-single .chosen-single div b{display:inline-block;width:0;height:0;margin-bottom:2px;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent;opacity:.5}.chosen-container-single .chosen-search{position:relative;z-index:1010;padding:3px 4px;margin:0;white-space:nowrap}.chosen-container-single .chosen-search input[type=text]{width:100%;height:27px;padding:2px 26px 2px 8px;margin:1px 0;font-size:12px;line-height:1.5;background-color:#fff;border:1px solid #dcdcdc;border-radius:2px;outline:0}.chosen-container-single .chosen-search input[type=text]:focus{border-color:#0c64eb}.chosen-container-single .chosen-search:before{position:absolute;top:10px;right:10px;display:block;font-family:ZentaoIcon;font-size:14px;font-style:normal;font-weight:400;font-variant:normal;line-height:1;color:#838a9d;text-transform:none;content:"\e928";speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.chosen-container-single .chosen-drop{margin-top:-1px;-webkit-background-clip:padding-box;background-clip:padding-box;border-radius:0 0 4px 4px}.chosen-container-single.chosen-container-single-nosearch .chosen-search{position:absolute;left:-9999px}.chosen-container .chosen-results{position:relative;max-height:240px;padding:0;margin:0;overflow-x:hidden;overflow-y:auto;-webkit-overflow-scrolling:touch}.chosen-container .chosen-results li{display:none;padding:5px 10px;margin:0;line-height:15px;list-style:none;-webkit-transition:background-color .2s cubic-bezier(.175,.885,.32,1);-o-transition:background-color .2s cubic-bezier(.175,.885,.32,1);transition:background-color .2s cubic-bezier(.175,.885,.32,1);-webkit-touch-callout:none}.chosen-container .chosen-results li.active-result{display:list-item;cursor:pointer}.chosen-container .chosen-results li.disabled-result{display:list-item;color:#ccc;cursor:default}.chosen-container .chosen-results li.highlighted{color:#fff;background-color:#0c64eb}.chosen-container .chosen-results li.no-results{display:list-item;background:#f4f4f4}.chosen-container .chosen-results li.group-result{display:list-item;font-weight:700;cursor:default}.chosen-container .chosen-results li.group-option{padding-left:15px}.chosen-container .chosen-results li em{font-style:normal;text-decoration:underline}.chosen-container-multi .chosen-choices{position:relative;width:100%;min-height:32px;min-height:30px\9;padding:0;margin:0;overflow:hidden;cursor:text;background-color:#fff;border:1px solid #dcdcdc;border-radius:2px;-webkit-box-shadow:none;box-shadow:none;-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s,-webkit-box-shadow ease-in-out .15s}.chosen-container-multi .chosen-choices:after,.chosen-container-multi .chosen-choices:before{display:table;content:" "}.chosen-container-multi .chosen-choices:after{clear:both}.chosen-container-multi .chosen-choices li{display:block;float:left;padding:0 6px;margin:5px 4px;list-style:none}.chosen-container-multi .chosen-choices li.search-field{padding:0;line-height:12px;white-space:nowrap}.chosen-container-multi .chosen-choices li.search-field input[type=text]{height:20px;font-size:100%;color:#838a9d;background:0 0!important;border:0!important;border-radius:0;outline:0;-webkit-box-shadow:none;box-shadow:none}.chosen-container-multi .chosen-choices li.search-field .default{color:#999}.chosen-container-multi .chosen-choices li.search-field:before{position:absolute;right:8px;bottom:8px;display:block;font-family:ZentaoIcon;font-size:14px;font-style:normal;font-weight:400;font-variant:normal;line-height:1;color:#838a9d;text-transform:none;content:"\e928";opacity:0;-webkit-transition:opacity .2s cubic-bezier(.175,.885,.32,1);-o-transition:opacity .2s cubic-bezier(.175,.885,.32,1);transition:opacity .2s cubic-bezier(.175,.885,.32,1);speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.chosen-container-multi .chosen-choices li.search-choice{position:relative;padding:3px 20px 3px 5px;line-height:12px;cursor:default;background-color:#f1f1f1;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #cbd0db;border-radius:3px;-webkit-box-shadow:0 0 2px #fff inset,0 1px 0 rgba(0,0,0,.05);box-shadow:0 0 2px #fff inset,0 1px 0 rgba(0,0,0,.05);-webkit-transition:all .4s cubic-bezier(.175,.885,.32,1);-o-transition:all .4s cubic-bezier(.175,.885,.32,1);transition:all .4s cubic-bezier(.175,.885,.32,1)}.chosen-container-multi .chosen-choices li.search-choice:hover{background-color:#fff;border-color:#adb5c6;-webkit-box-shadow:0 1px 0 rgba(0,0,0,.1);box-shadow:0 1px 0 rgba(0,0,0,.1)}.chosen-container-multi .chosen-choices li.search-choice .search-choice-close{position:absolute;top:1px;right:0;display:block;width:20px;height:18px;line-height:18px;color:#000;text-align:center;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.chosen-container-multi .chosen-choices li.search-choice .search-choice-close:before{font-family:ZentaoIcon;font-size:14px;font-style:normal;font-weight:400;font-variant:normal;line-height:1;text-shadow:0 1px 0 #fff;text-transform:none;content:'\d7';speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.chosen-container-multi .chosen-choices li.search-choice .search-choice-close:focus,.chosen-container-multi .chosen-choices li.search-choice .search-choice-close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}.chosen-container-multi .chosen-choices li.search-choice-disabled{padding-right:5px;color:#666;background-color:#e4e4e4;border:1px solid #ccc}.chosen-container-multi .chosen-choices li.search-choice-focus{background:#d4d4d4}.chosen-container-multi .chosen-choices li.search-choice-focus .search-choice-close{background-position:-42px -10px}.chosen-container-multi .chosen-results{padding:5px 0;margin:0}.chosen-container-multi .chosen-drop .result-selected{display:list-item;color:#ccc;cursor:default}.chosen-container-active .chosen-single{border-color:#0c64eb;-webkit-box-shadow:none,0 0 8px rgba(12,100,235,.6);box-shadow:none,0 0 8px rgba(12,100,235,.6)}.chosen-container-active.chosen-with-drop .chosen-single{border:1px solid #b6bdcc;border:1px solid rgba(0,0,0,.15);border-bottom-right-radius:0!important;border-bottom-left-radius:0!important;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.chosen-container-active.chosen-with-drop .chosen-single div{background:0 0;border-left:none}.chosen-container-active.chosen-with-drop .chosen-single div b{content:"";border-top:0 dotted;border-bottom:4px solid}.chosen-container-active.chosen-with-drop.chosen-up .chosen-single{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:2px;border-bottom-left-radius:2px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.chosen-container-active .chosen-choices{border-color:#0c64eb;-webkit-box-shadow:none,0 0 8px rgba(12,100,235,.6);box-shadow:none,0 0 8px rgba(12,100,235,.6)}.chosen-container-active .chosen-choices li.search-field input[type=text]{color:#111!important}.chosen-container-active .chosen-choices li.search-field:before{opacity:1}.chosen-disabled{cursor:default;opacity:.5!important}.chosen-disabled .chosen-single{cursor:default}.chosen-disabled .chosen-choices .search-choice .search-choice-close{cursor:default}.chosen-compact.chosen-container-single .chosen-single>.chosen-search{left:0;display:none;padding:3px 4px;opacity:0}.chosen-compact.chosen-container-single .chosen-single>.chosen-search>input{height:25px;padding:2px 26px 2px 4px;font-size:inherit}.chosen-compact.chosen-container-single .chosen-single>.chosen-search:before{top:9px}.chosen-compact.chosen-with-search.chosen-with-drop .chosen-single>.chosen-search{display:block;opacity:1}select.chosen[multiple]{height:32px;overflow:hidden}select.chosen[multiple] option{visibility:hidden}.container,.container-fixed,.container-fluid{position:relative}.container{max-width:1800px!important}body{background-color:#efefef}body.article-content,body.body-modal{background:0 0}body.body-modal{padding:0}@media screen and (min-width:1920px){body{font-size:14px}}a:active,a:focus,button:active,button:focus{outline:0!important}.strong{font-weight:700}.font-normal{font-weight:400!important}.text-middle{vertical-align:middle!important}.text-bottom{vertical-align:bottom!important}.text-top{vertical-align:top!important}.inline-block{display:inline-block!important}.layer{border-radius:4px;-webkit-box-shadow:0 0 20px 0 #bdc9d8;box-shadow:0 0 20px 0 #bdc9d8}.space{margin-bottom:20px}.space-lg{margin-bottom:30px}.space-sm{margin-bottom:10px}.muted{opacity:.5}.text-muted em{color:#3c4353}.no-animate{-webkit-transition:none!important;-o-transition:none!important;transition:none!important}.template{display:none!important}.text-left{text-align:left!important}.text-yellow.icon-folder{color:#ffe066}.table-row{display:table;width:100%;table-layout:fixed}.table-col,.table-row>.col,.table-row>[class*=col-],.table-row>[class*="-col"]{display:table-cell;float:none;vertical-align:top}.side-col{width:200px;padding-right:20px}.side-col.col-4{width:33.3333333%}.col-lg{width:260px}.col-xl{width:320px}.col-sm{width:150px}.col-xs{width:100px}.main-col+.side-col{padding-right:0;padding-left:20px}.row-grid>[class*=col-],.row-grid>[class*="-col"]{padding-top:6px;padding-bottom:6px}hr.space{margin:10px 0;border:none}hr.space-sm{margin:5px 0;border:none}.text-secondary{color:#16a8f8}a.text-primary{color:#0c64eb}.nav-primary>li>a{min-width:100px;padding:5px 8px;color:#838a9d;border-color:#e7f1fc}.nav-primary>li.active>a{color:#0c64eb;background-color:#e7f1fc;border-color:#e7f1fc}.nav-primary>li.active>a:hover{color:#0c64eb;background-color:#c3dcf7;border-color:#c3dcf7}.end-marker{margin-bottom:20px;color:#cbd0db;text-align:center}@-webkit-keyframes highlight{0%{background:#fff;outline:1px solid transparent}100%{background:#fff0d5;outline:2px solid #ffdcbc}}@-o-keyframes highlight{0%{background:#fff;outline:1px solid transparent}100%{background:#fff0d5;outline:2px solid #ffdcbc}}@keyframes highlight{0%{background:#fff;outline:1px solid transparent}100%{background:#fff0d5;outline:2px solid #ffdcbc}}.highlight{-webkit-animation:highlight .5s linear 0s 2 alternate;-o-animation:highlight .5s linear 0s 2 alternate;animation:highlight .5s linear 0s 2 alternate}.progress.inline-block{width:100px;margin:0}.w-p5{width:5%!important}.w-p10{width:10%!important}.w-p15{width:15%!important}.w-p20{width:20%!important}.w-p25{width:25%!important}.w-p30{width:30%!important}.w-p35{width:35%!important}.w-p40{width:40%!important}.w-p45{width:45%!important}.w-p50{width:50%!important}.w-p55{width:55%!important}.w-p60{width:60%!important}.w-p65{width:65%!important}.w-p70{width:70%!important}.w-p75{width:75%!important}.w-p80{width:80%!important}.w-p85{width:85%!important}.w-p90{width:90%!important}.w-p94{width:94%!important}.w-p95{width:95%!important}.w-p98{width:98%!important}.w-p99{width:99%!important}.w-p100{width:100%!important}.w-auto{width:auto!important}.w-10px{width:10px!important}.w-20px{width:20px!important}.w-30px{width:30px!important}.w-35px{width:35px!important}.w-40px{width:40px!important}.w-45px{width:45px!important}.w-50px{width:50px!important}.w-60px{width:60px!important}.w-70px{width:70px!important}.w-80px{width:80px!important}.w-90px{width:90px!important}.w-100px{width:100px!important}.w-110px{width:110px!important}.w-120px{width:120px!important}.w-130px{width:130px!important}.w-140px{width:140px!important}.w-150px{width:150px!important}.w-160px{width:160px!important}.w-180px{width:180px!important}.w-200px{width:200px!important}.w-230px{width:230px!important}.w-250px{width:250px!important}.w-300px{width:300px!important}.w-400px{width:400px!important}.w-500px{width:500px!important}.w-600px{width:600px!important}.w-700px{width:700px!important}.w-800px{width:800px!important}.w-900px{width:900px!important}.mw-200px{max-width:200px!important}.mw-300px{max-width:300px!important}.mw-400px{max-width:400px!important}.mw-500px{max-width:500px!important}.mw-600px{max-width:600px!important}.mw-700px{max-width:700px!important}.mw-800px{max-width:800px!important}.mw-900px{max-width:900px!important}.mw-1400px{max-width:1400px!important}.w-id{width:70px!important}.w-pri{width:40px!important}.w-severity{width:50px!important}.w-hour{width:57px!important}.w-date{width:90px!important}.w-status{width:60px!important}.w-resolution,.w-type,.w-user{width:80px!important}.w-p15-f{width:15%!important;min-width:120px!important}.w-p25-f{width:25%!important;min-width:200px!important}.w-p35-f{width:35%!important;min-width:300px!important}.w-p45-f{width:45%!important;min-width:400px!important}.h-5px{height:5px!important}.h-10px{height:10px!important}.h-20px{height:20px!important}.h-30px{height:30px!important}.h-35px{height:35px!important}.h-40px{height:40px!important}.h-45px{height:45px!important}.h-50px{height:50px!important}.h-60px{height:60px!important}.h-70px{height:70px!important}.h-80px{height:80px!important}.h-100px{height:100px!important}.h-120px{height:120px!important}.h-130px{height:130px!important}.h-140px{height:140px!important}.h-150px{height:150px!important}.h-200px{height:200px!important}.pd-0{padding:0!important}.mg-0{margin:0!important}.mgb-20{margin-bottom:20px!important}.mgb-10{margin-bottom:10px!important}.pdb-20{padding-bottom:20px!important}.pdt-20{padding-top:20px!important}.br-0{border-radius:0!important}.bd-0,.bd-none,.borderless{border:none!important}.bg-none{background:0 0!important}.red{color:#ff5d5d!important}.icon-pro-version{font-size:14px!important}.icon-pro-version:before{position:relative;top:-1px;font-size:14px;color:#ff5d5d;content:"\e92b"}.bg-primary{color:#fff;background:#1183fb -webkit-gradient(linear,right top,left top,from(#0a48d1),to(#1183fb));background:#1183fb -webkit-linear-gradient(right,#0a48d1 0,#1183fb 100%);background:#1183fb -o-linear-gradient(right,#0a48d1 0,#1183fb 100%);background:#1183fb linear-gradient(-90deg,#0a48d1 0,#1183fb 100%);background-color:#00b1fd}.bg-secondary{color:#fff;background:#16a8f8}.hl-tutorial{position:relative!important;z-index:1010!important;-webkit-box-shadow:0 0 0 0 #000!important;box-shadow:0 0 0 0 #000!important;-webkit-transition:-webkit-box-shadow 1s!important;-o-transition:box-shadow 1s!important;transition:-webkit-box-shadow 1s!important;transition:box-shadow 1s!important;transition:box-shadow 1s,-webkit-box-shadow 1s!important}.hl-tutorial.hl-in{-webkit-box-shadow:0 0 20px 0 #ffff8d,0 0 0 2px #ffd180,0 0 0 3000px rgba(0,0,0,.2)!important;box-shadow:0 0 20px 0 #ffff8d,0 0 0 2px #ffd180,0 0 0 3000px rgba(0,0,0,.2)!important}.btn.tooltip-tutorial,.hl-tutorial.hl-in:hover{position:relative!important;z-index:1010!important;-webkit-box-shadow:0 0 30px 0 #ffff8d,0 0 0 5px #ffd180,0 0 0 3000px rgba(0,0,0,.3)!important;box-shadow:0 0 30px 0 #ffff8d,0 0 0 5px #ffd180,0 0 0 3000px rgba(0,0,0,.3)!important}.tooltip-max .tooltip-inner{max-width:1000px;padding:8px 10px}.transition-all *{-webkit-transition:all .2s!important;-o-transition:all .2s!important;transition:all .2s!important}.scroll-x{overflow-x:auto!important}.scroll-y{overflow-y:auto!important}.divider+.divider{display:none}.ie *{-webkit-transition:none!important;-o-transition:none!important;transition:none!important}@font-face{font-family:Oswald;font-weight:400;src:url(../fonts/Oswald-Regular.ttf)}@font-face{font-family:Oswald;font-weight:500;src:url(../fonts/Oswald-Medium.ttf)}@font-face{font-family:Oswald;font-weight:300;src:url(../fonts/Oswald-Light.ttf)}.num{font-family:Oswald;font-weight:400}@font-face{font-family:ZentaoIcon;font-style:normal;font-weight:400;src:url(../fonts/ZentaoIcon.eot?v=1.18);src:url(../fonts/ZentaoIcon.eot?#iefix&v=1.18) format('embedded-opentype'),url(../fonts/ZentaoIcon.woff?v=1.18) format('woff'),url(../fonts/ZentaoIcon.ttf?v=1.18) format('truetype'),url(../fonts/ZentaoIcon.svg#regular?v=1.18) format('svg')}.icon,[class*=" icon-"],[class^=icon-]{font-family:ZentaoIcon;font-size:14px;font-style:normal;font-weight:400;font-variant:normal;line-height:1;text-transform:none;speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon:before{display:inline-block;min-width:14px;text-align:center}a .icon,a [class*=" icon-"],a [class^=icon-]{display:inline}.icon-lg:before{font-size:1.33333333em;vertical-align:-10%}.icon-2x{font-size:28px}.icon-3x{font-size:42px}.icon-4x{font-size:56px}.icon-5x{font-size:70px}.icon-spin{display:inline-block;-webkit-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;animation:spin 2s infinite linear}a .icon-spin{display:inline-block;text-decoration:none}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0);transform:rotate(0)}100%{-o-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes spin{0%{-webkit-transform:rotate(0);-o-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);-o-transform:rotate(359deg);transform:rotate(359deg)}}.icon-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.icon-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.icon-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.icon-flip-horizontal{-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);-o-transform:scale(-1,1);transform:scale(-1,1)}.icon-flip-vertical{-webkit-transform:scale(1,-1);-ms-transform:scale(1,-1);-o-transform:scale(1,-1);transform:scale(1,-1)}.icon.icon-flip-horizontal,.icon.icon-flip-vertical,.icon.icon-rotate-180,.icon.icon-rotate-270,.icon.icon-rotate-90{display:inline-block}.icon-zentao:before{content:"\e901"}.icon-zentao-alt:before{content:"\e900"}.icon-help:before{content:"\e968"}.icon-import:before{content:"\e904"}.icon-download:before{content:"\e904"}.icon-export:before{content:"\e905"}.icon-lightbulb:before{content:"\e91c"}.icon-close:before{content:"\e936"}.icon-check:before{content:"\e5ca"}.icon-plus:before{content:"\e925"}.icon-minus:before{content:"\e926"}.icon-expand-alt:before{content:"\e6f1"}.icon-collapse-alt:before{content:"\e6f2"}.icon-fullscreen:before{content:"\e96b"}.icon-star-empty:before{content:"\e94a"}.icon-star:before{content:"\e94b"}.icon-exclamation-sign:before{content:"\e930"}.icon-flag:before{content:"\e937"}.icon-check-circle:before{content:"\e92f"}.icon-check-sign:before{content:"\e938"}.icon-chart-pie:before{content:"\e95b"}.icon-history:before{content:"\e95f"}.icon-pencil:before{content:"\e254"}.icon-search:before{content:"\e928"}.icon-restart:before{content:"\e95e"}.icon-cog:before{content:"\e93b"}.icon-chart-line:before{content:"\e95c"}.icon-chart-bar:before{content:"\e95d"}.icon-bar-chart:before{content:"\e95d"}.icon-exchange:before{content:"\e927"}.icon-severity:before{content:"\e973"}.icon-book:before{content:"\f02d"}.icon-treemap-alt:before{content:"\e971"}.icon-severity-solid:before{content:"\e902"}.icon-chat-line:before{content:"\e998"}.icon-stack:before{content:"\e943"}.icon-cube:before{content:"\e967"}.icon-minus-sign:before{content:"\e939"}.icon-bars-sign:before{content:"\e93a"}.icon-chat:before{content:"\e940"}.icon-message:before{content:"\e940"}.icon-more:before{content:"\e744"}.icon-certificate:before{content:"\f0a3"}.icon-bell:before{content:"\e7f5"}.icon-columns:before{content:"\f0db"}.icon-envelope-o:before{content:"\e92a"}.icon-unfold-all:before{content:"\e931"}.icon-fold-all:before{content:"\e932"}.icon-bars:before{content:"\e948"}.icon-cards-view:before{content:"\e949"}.icon-ellipsis-v:before{content:"\e5d4"}.icon-spinner-indicator:before{content:"\e982"}.icon-up-circle:before{content:"\e92b"}.icon-right-circle:before{content:"\e92c"}.icon-down-circle:before{content:"\e92d"}.icon-left-circle:before{content:"\e92e"}.icon-angle-double-right:before{content:"\f101"}.icon-angle-down:before{content:"\e313"}.icon-angle-left:before{content:"\e314"}.icon-angle-right:before{content:"\e315"}.icon-angle-top:before{content:"\e316"}.icon-first-page:before{content:"\e5dc"}.icon-last-page:before{content:"\e5dd"}.icon-caret-down:before{content:"\f0d7"}.icon-caret-up:before{content:"\f0d8"}.icon-caret-left:before{content:"\f0d9"}.icon-caret-right:before{content:"\f0da"}.icon-sort:before{content:"\f0dc"}.icon-sort-down:before{content:"\f0dd"}.icon-sort-up:before{content:"\f0de"}.icon-arrow-up:before{content:"\e923"}.icon-arrow-down:before{content:"\e924"}.icon-arrow-left:before{content:"\e952"}.icon-arrow-right:before{content:"\e93e"}.icon-chevron-left:before{content:"\e934"}.icon-chevron-right:before{content:"\e935"}.icon-chevron-double-up:before{content:"\e959"}.icon-chevron-double-down:before{content:"\e95a"}.icon-folder-account:before{content:"\e942"}.icon-folder-move:before{content:"\e960"}.icon-folder-plus:before{content:"\e961"}.icon-folder-upload:before{content:"\e962"}.icon-folder-star:before{content:"\e963"}.icon-folder-edit:before{content:"\e964"}.icon-folder-download:before{content:"\e965"}.icon-folder-outline:before{content:"\e966"}.icon-folder:before{content:"\e944"}.icon-folder-o:before{content:"\e945"}.icon-folder-open-o:before{content:"\e946"}.icon-folder-open:before{content:"\e947"}.icon-color:before{content:"\e93c"}.icon-paper-clip:before{content:"\e93d"}.icon-text:before{content:"\e929"}.icon-share:before{content:"\f064"}.icon-list:before{content:"\e9a8"}.icon-format-list-bulleted:before{content:"\e9a8"}.icon-format-bold:before{content:"\e953"}.icon-format-header-pound:before{content:"\e954"}.icon-format-italic:before{content:"\e955"}.icon-format-list-numbers:before{content:"\e969"}.icon-format-quote-close:before{content:"\e96a"}.icon-image:before{content:"\e96c"}.icon-table-large:before{content:"\e96d"}.icon-aiux:before{content:"\e99e"}.icon-qc:before{content:"\e986"}.icon-qc-q:before{content:"\e985"}.icon-qc-c:before{content:"\e987"}.icon-menu-my:before{content:"\e97a"}.icon-home:before{content:"\e97a"}.icon-program:before{content:"\e9aa"}.icon-lightbulb-alt:before{content:"\e98f"}.icon-product:before{content:"\e98f"}.icon-rocket:before{content:"\e99c"}.icon-project:before{content:"\e99c"}.icon-run:before{content:"\e9a9"}.icon-test:before{content:"\e956"}.icon-infinite:before{content:"\e9a3"}.icon-devops:before{content:"\e9a3"}.icon-ops:before{content:"\e903"}.icon-doc:before{content:"\e99b"}.icon-menu-doc:before{content:"\e99b"}.icon-statistic:before{content:"\e999"}.icon-menu-backend:before{content:"\e993"}.icon-assets:before{content:"\e9ae"}.icon-diamond:before{content:"\e9ae"}.icon-feedback:before{content:"\e991"}.icon-flow:before{content:"\e994"}.icon-oa:before{content:"\e9a1"}.icon-more-circle:before{content:"\e988"}.icon-controls:before{content:"\e995"}.icon-account:before{content:"\e992"}.icon-about:before{content:"\e996"}.icon-cog-outline:before{content:"\e997"}.icon-backend:before{content:"\e997"}.icon-exit:before{content:"\e99a"}.icon-theme:before{content:"\e9a0"}.icon-globe:before{content:"\f0ac"}.icon-lang:before{content:"\f0ac"}.icon-usecase:before{content:"\e99d"}.icon-code:before{content:"\e990"}.icon-summary:before{content:"\e9ad"}.icon-more-alt:before{content:"\e9a7"}.icon-waterfall:before{content:"\e9a4"}.icon-manual:before{content:"\e98d"}.icon-kanban:before{content:"\e983"}.icon-lane:before{content:"\e9b1"}.icon-thumbs-up:before{content:"\f087"}.icon-thumbs-down:before{content:"\f088"}.icon-hash:before{content:"\e9ab"}.icon-version:before{content:"\e9ab"}.icon-p-square:before{content:"\e97b"}.icon-video-play:before{content:"\e97f"}.icon-plus-solid-circle:before{content:"\e974"}.icon-s:before{content:"\e975"}.icon-c:before{content:"\e976"}.icon-t:before{content:"\e977"}.icon-guide:before{content:"\e978"}.icon-todo:before{content:"\e979"}.icon-side-left:before{content:"\e9b3"}.icon-side-right:before{content:"\e9b2"}.icon-fullscreen-exit:before{content:"\e972"}.icon-alert:before{content:"\e99f"}.icon-back:before{content:"\e93f"}.icon-swap:before{content:"\e9b0"}.icon-clock:before{content:"\e97c"}.icon-cost:before{content:"\e97d"}.icon-pencil-alt:before{content:"\e984"}.icon-rich-text:before{content:"\e913"}.icon-markdown:before{content:"\e916"}.icon-excel:before{content:"\e933"}.icon-text-link:before{content:"\e94d"}.icon-ppt:before{content:"\e957"}.icon-word:before{content:"\e958"}.icon-doc-lib:before{content:"\e96f"}.icon-file:before{content:"\f016"}.icon-file-empty:before{content:"\f016"}.icon-file-text:before{content:"\f0f6"}.icon-file-alt:before{content:"\f15b"}.icon-file-text-alt:before{content:"\f15c"}.icon-file-pdf:before{content:"\f1c1"}.icon-file-word:before{content:"\f1c2"}.icon-file-excel:before{content:"\f1c3"}.icon-file-powerpoint:before{content:"\f1c4"}.icon-file-image:before{content:"\f1c5"}.icon-file-archive:before{content:"\f1c6"}.icon-file-audio:before{content:"\f1c7"}.icon-file-video:before{content:"\f1c8"}.icon-file-code:before{content:"\f1c9"}.icon-menu-collapse:before{content:"\e980"}.icon-menu-expand:before{content:"\e981"}.icon-group:before{content:"\e97e"}.icon-menu-users:before{content:"\e97e"}.icon-persons:before{content:"\e97e"}.icon-team:before{content:"\e97e"}.icon-estimate:before{content:"\e9ac"}.icon-sprint:before{content:"\e9a2"}.icon-shield-check:before{content:"\e9a5"}.icon-ok:before{content:"\e9a6"}.icon-printer:before{content:"\e906"}.icon-bullhorn:before{content:"\e910"}.icon-person:before{content:"\e941"}.icon-fields:before{content:"\e989"}.icon-trigger:before{content:"\e98a"}.icon-layout:before{content:"\e98b"}.icon-audit:before{content:"\e98c"}.icon-cancel:before{content:"\e951"}.icon-ban-circle:before{content:"\e951"}.icon-eye:before{content:"\e94e"}.icon-eye-off:before{content:"\e96e"}.icon-unlock:before{content:"\e94f"}.icon-lock:before{content:"\e950"}.icon-private:before{content:"\e950"}.icon-move:before{content:"\e94c"}.icon-hand-right:before{content:"\e907"}.icon-checked:before{content:"\e908"}.icon-off:before{content:"\e909"}.icon-start:before{content:"\e90a"}.icon-play:before{content:"\e90a"}.icon-time:before{content:"\e90b"}.icon-edit:before{content:"\e90c"}.icon-trash:before{content:"\e90d"}.icon-link:before{content:"\e90e"}.icon-unlink:before{content:"\e90f"}.icon-bug:before{content:"\e911"}.icon-list-alt:before{content:"\e912"}.icon-change:before{content:"\e970"}.icon-alter:before{content:"\e970"}.icon-glasses:before{content:"\e914"}.icon-review:before{content:"\e914"}.icon-sitemap:before{content:"\e915"}.icon-testcase:before{content:"\e915"}.icon-pluses:before{content:"\e917"}.icon-report-list:before{content:"\e918"}.icon-magic:before{content:"\e919"}.icon-active:before{content:"\e919"}.icon-treemap:before{content:"\e91a"}.icon-confirm:before{content:"\e91b"}.icon-split:before{content:"\e98e"}.icon-delay:before{content:"\e91d"}.icon-calendar:before{content:"\e91d"}.icon-pause:before{content:"\e91e"}.icon-ban:before{content:"\e91f"}.icon-plus-bold:before{content:"\e920"}.icon-copy:before{content:"\e921"}.icon-refresh:before{content:"\e922"}.icon-sm:before{font-size:14px;vertical-align:10%}.icon-qc{position:relative}.icon-qc:before{width:1em;color:#7cb938;content:"\e985"}.icon-qc:after{position:absolute;top:0;left:0;width:1em;height:1em;font-family:ZentaoIcon;font-size:14px;font-size:inherit;font-style:normal;font-weight:400;font-variant:normal;line-height:1;color:#36a742;text-transform:none;content:"\e987";speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-message.has-dot{position:relative}.icon-message.has-dot::after{position:absolute;top:-3px;right:-5px;display:block;width:6px;height:6px;content:' ';background-color:#ff5d5d;border-radius:50%}.icon-project{-webkit-transform:scale(1.2);-ms-transform:scale(1.2);-o-transform:scale(1.2);transform:scale(1.2)}.icon-product{-webkit-transform:scale(1.15);-ms-transform:scale(1.15);-o-transform:scale(1.15);transform:scale(1.15)}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:13px;font-weight:400;line-height:18px;text-align:center;white-space:nowrap;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border:1px solid transparent;border-radius:4px;-webkit-transition:.4s cubic-bezier(.175,.885,.32,1);-o-transition:.4s cubic-bezier(.175,.885,.32,1);transition:.4s cubic-bezier(.175,.885,.32,1);-webkit-transition-property:background,border,outline,opacity,-webkit-box-shadow;-o-transition-property:background,border,box-shadow,outline,opacity;transition-property:background,border,outline,opacity,-webkit-box-shadow;transition-property:background,border,box-shadow,outline,opacity;transition-property:background,border,box-shadow,outline,opacity,-webkit-box-shadow}.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:focus,.btn:hover{color:#3c4353;text-decoration:none}.btn:active{text-decoration:none;background-image:none;outline:0;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.1)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{pointer-events:none;cursor:not-allowed;filter:grayscale(1);-webkit-box-shadow:none;box-shadow:none;opacity:.5;-webkit-filter:grayscale(1)}.btn{color:#3c4353;background-color:#fff;border-color:#d6dae3}.btn.active,.btn.hover,.btn:active,.btn:focus,.btn:hover,.open .dropdown-toggle.btn{color:#3c4353;background-color:rgba(255,255,255,.8);border-color:#b8bfce}.btn.active,.btn:active,.open .dropdown-toggle.btn{background-color:#f2f2f2;background-image:none;border-color:#b8bfce}.btn.disabled,.btn.disabled.active,.btn.disabled:active,.btn.disabled:focus,.btn.disabled:hover,.btn[disabled],.btn[disabled].active,.btn[disabled]:active,.btn[disabled]:focus,.btn[disabled]:hover,fieldset[disabled] .btn,fieldset[disabled] .btn.active,fieldset[disabled] .btn:active,fieldset[disabled] .btn:focus,fieldset[disabled] .btn:hover{color:rgba(60,67,83,.3);background-color:#fff;border-color:#d6dae3}.btn-gray{color:#82899f;background-color:#f1f1f1;border-color:#f1f1f1}.btn-gray.active,.btn-gray.hover,.btn-gray:active,.btn-gray:focus,.btn-gray:hover,.open .dropdown-toggle.btn-gray{color:#82899f;background-color:rgba(241,241,241,.8);border-color:#d8d8d8}.btn-gray.active,.btn-gray:active,.open .dropdown-toggle.btn-gray{background-color:#e4e4e4;background-image:none;border-color:#d8d8d8}.btn-gray.disabled,.btn-gray.disabled.active,.btn-gray.disabled:active,.btn-gray.disabled:focus,.btn-gray.disabled:hover,.btn-gray[disabled],.btn-gray[disabled].active,.btn-gray[disabled]:active,.btn-gray[disabled]:focus,.btn-gray[disabled]:hover,fieldset[disabled] .btn-gray,fieldset[disabled] .btn-gray.active,fieldset[disabled] .btn-gray:active,fieldset[disabled] .btn-gray:focus,fieldset[disabled] .btn-gray:hover{color:rgba(130,137,159,.3);background-color:#f1f1f1;border-color:#f1f1f1}.btn-primary{color:#fff;background-color:#0c64eb;border-color:transparent}.btn-primary.active,.btn-primary.hover,.btn-primary:active,.btn-primary:focus,.btn-primary:hover,.open .dropdown-toggle.btn-primary{color:#fff;background-color:rgba(12,100,235,.8);border-color:rgba(0,0,0,0)}.btn-primary.active,.btn-primary:active,.open .dropdown-toggle.btn-primary{background-color:#0b5ad3;background-image:none;border-color:rgba(0,0,0,0)}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{color:rgba(255,255,255,.3);background-color:#0c64eb;border-color:transparent}.btn-secondary{color:#fff;background-color:#16a8f8;border-color:transparent}.btn-secondary.active,.btn-secondary.hover,.btn-secondary:active,.btn-secondary:focus,.btn-secondary:hover,.open .dropdown-toggle.btn-secondary{color:#fff;background-color:rgba(22,168,248,.8);border-color:rgba(0,0,0,0)}.btn-secondary.active,.btn-secondary:active,.open .dropdown-toggle.btn-secondary{background-color:#079ced;background-image:none;border-color:rgba(0,0,0,0)}.btn-secondary.disabled,.btn-secondary.disabled.active,.btn-secondary.disabled:active,.btn-secondary.disabled:focus,.btn-secondary.disabled:hover,.btn-secondary[disabled],.btn-secondary[disabled].active,.btn-secondary[disabled]:active,.btn-secondary[disabled]:focus,.btn-secondary[disabled]:hover,fieldset[disabled] .btn-secondary,fieldset[disabled] .btn-secondary.active,fieldset[disabled] .btn-secondary:active,fieldset[disabled] .btn-secondary:focus,fieldset[disabled] .btn-secondary:hover{color:rgba(255,255,255,.3);background-color:#16a8f8;border-color:transparent}.btn-warning{color:#fff;background-color:#ff9800;border-color:transparent}.btn-warning.active,.btn-warning.hover,.btn-warning:active,.btn-warning:focus,.btn-warning:hover,.open .dropdown-toggle.btn-warning{color:#fff;background-color:rgba(255,152,0,.8);border-color:rgba(0,0,0,0)}.btn-warning.active,.btn-warning:active,.open .dropdown-toggle.btn-warning{background-color:#e68900;background-image:none;border-color:rgba(0,0,0,0)}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{color:rgba(255,255,255,.3);background-color:#ff9800;border-color:transparent}.btn-danger{color:#fff;background-color:#ff5d5d;border-color:transparent}.btn-danger.active,.btn-danger.hover,.btn-danger:active,.btn-danger:focus,.btn-danger:hover,.open .dropdown-toggle.btn-danger{color:#fff;background-color:rgba(255,93,93,.8);border-color:rgba(0,0,0,0)}.btn-danger.active,.btn-danger:active,.open .dropdown-toggle.btn-danger{background-color:#ff4343;background-image:none;border-color:rgba(0,0,0,0)}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{color:rgba(255,255,255,.3);background-color:#ff5d5d;border-color:transparent}.btn-success{color:#fff;background-color:#00da88;border-color:transparent}.btn-success.active,.btn-success.hover,.btn-success:active,.btn-success:focus,.btn-success:hover,.open .dropdown-toggle.btn-success{color:#fff;background-color:rgba(0,218,136,.8);border-color:rgba(0,0,0,0)}.btn-success.active,.btn-success:active,.open .dropdown-toggle.btn-success{background-color:#00c178;background-image:none;border-color:rgba(0,0,0,0)}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{color:rgba(255,255,255,.3);background-color:#00da88;border-color:transparent}.btn-info{color:#0c64eb;background-color:#e9f2fb;border-color:transparent}.btn-info.active,.btn-info.hover,.btn-info:active,.btn-info:focus,.btn-info:hover,.open .dropdown-toggle.btn-info{color:#0c64eb;background-color:rgba(233,242,251,.8);border-color:rgba(0,0,0,0)}.btn-info.active,.btn-info:active,.open .dropdown-toggle.btn-info{background-color:#d3e5f7;background-image:none;border-color:rgba(0,0,0,0)}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{color:rgba(12,100,235,.3);background-color:#e9f2fb;border-color:transparent}.btn-link{padding-right:6px;padding-left:6px;font-weight:400;color:#3c495c;text-shadow:none;cursor:pointer;background:0 0;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover,.btn-link[disabled],fieldset[disabled] .btn-link{border-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link:focus,.btn-link:hover{color:#222;background:#f1f1f1;background:rgba(0,0,0,.075)}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#cbd0db;text-decoration:none}.btn-lg{padding:11px 16px;font-size:14px;line-height:18px;border-radius:4px}.btn-mini,.btn-sm{padding:3px 8px;font-size:12px;line-height:18px;border-radius:4px}.btn-mini,.btn-xs{padding:0 5px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.btn-wide{min-width:120px}.btn-limit{max-width:180px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.btn-limit>.caret{position:absolute;top:14px;right:8px}.btn-circle{border-radius:17px}.btn>.label-icon{top:3px;padding:3px;margin:-2px;background-color:rgba(0,0,0,.2);border-radius:12px}.btn>.label-icon>.icon{font-size:16px;line-height:18px}.btn>.icon+.text{margin-left:5px}.btn.btn-sm.btn-circle{border-radius:12px}.btn.btn-sm>.label-icon{top:2px;width:20px;height:20px;padding:1px;line-height:20px}.btn.btn-sm>.label-icon>.icon{position:relative;top:-1px;display:inline-block;font-size:14px;line-height:18px}.btn-icon-left{position:relative;padding-left:35px;overflow:hidden;text-align:right}.btn-icon-left>.label-icon{position:absolute;left:5px;margin:0}.btn-icon-left>.icon{position:absolute;top:0;bottom:0;left:0;display:block;width:30px;line-height:30px;color:#16a8f8;text-align:center;background:#e9f2fb}.btn-icon-left.btn-sm{padding-left:28px}.btn-icon-left.btn-sm>.label-icon{left:2px}.btn-icon-left.btn-sm>.icon{width:24px;line-height:24px}.btn-icon-right{position:relative;padding-right:35px;text-align:left}.btn-icon-right>.label-icon{position:absolute;right:5px;margin:0}.btn-icon-right.btn-sm{padding-right:28px}.btn-icon-right.btn-sm>.label-icon{right:2px}.btn-icon{min-width:32px;padding-right:0;padding-left:0}.btn-icon.btn-sm{width:24px;min-width:24px;height:24px}.btn-group{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group>.btn-group{float:left}.btn-group>.btn{border-radius:0}.btn-group>.btn:first-child{border-top-left-radius:2px;border-bottom-left-radius:2px}.btn-group>.btn:last-child{border-top-right-radius:2px;border-bottom-right-radius:2px}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.dropdown{float:left;margin-right:10px}.btn-toolbar>.btn-group:last-child,.btn-toolbar>.btn:last-child,.btn-toolbar>.dropdown:last-child{margin-right:0}.btn-toolbar>.divider{float:left;height:20px;margin:7px 5px 4px 10px;border-left:1px solid rgba(0,0,0,.1)}.btn-toolbar .space{float:left;min-height:1px;margin:0 10px 0 10px}.btn-toolbar .input-control{float:left;width:120px}.btn-toolbar .page-title{float:left;line-height:34px}.btn-toolbar .page-title .text{font-size:14px;font-weight:700}.btn-toolbar .page-title .label{top:-2px;margin-right:10px}.btn-toolbar .divider+.page-title{margin-left:15px}.btn-active-line{position:relative;font-weight:700;color:#0c64eb}.btn-active-line:after{position:absolute;right:5px;bottom:1px;left:5px;display:block;content:' ';border-bottom:2px solid #0c64eb}.btn-active-line:hover{color:#0c64eb}.btn-active-text .text{position:relative;top:-1px;display:inline-block;font-weight:700;color:#0c64eb}.btn-active-text .text:after{position:absolute;bottom:-5px;display:block;width:100%;content:' ';border-bottom:2px solid #0c64eb}.angle-btn{position:relative;padding:1px;background:#fff;border:1px solid #cbd0db;border-right:none}.angle-btn:first-child{border-radius:2px 0 0 2px}.btn-toolbar>.angle-btn{margin-right:8px}.angle-btn:after,.angle-btn:before{position:absolute;top:-1px;right:-8px;display:block;width:0;height:0;content:' ';border-color:transparent transparent transparent #cbd0db;border-style:solid;border-width:17px 0 17px 8px}.angle-btn:after{right:-7px;border-color:transparent transparent transparent #fff;border-radius:2px}.angle-btn .btn{padding:6px;font-weight:700;background:#fff;border:none;border-radius:4px!important}.angle-btn .btn.btn-limit{padding-right:16px}.angle-btn .btn.btn-limit>.caret{right:4px}.angle-btn+.angle-btn{border-left:none}.angle-btn+.angle-btn>.btn-group:first-child{padding-left:8px}.angle-btn+.angle-btn>.btn-group:first-child:after,.angle-btn+.angle-btn>.btn-group:first-child:before{position:absolute;top:-2px;left:0;display:block;width:0;height:0;content:' ';border-color:transparent transparent transparent #cbd0db;border-style:solid;border-width:17px 0 17px 8px}.angle-btn+.angle-btn>.btn-group:first-child:after{left:-1px;border-color:transparent transparent transparent #fff;border-width:17px 0 17px 8px}.btn-toolbar>.angle-btn.active,.btn-toolbar>.angle-btn:last-child{border-color:#0c64eb}.btn-toolbar>.angle-btn.active .btn,.btn-toolbar>.angle-btn:last-child .btn{color:#0c64eb}.btn-toolbar>.angle-btn.active:after,.btn-toolbar>.angle-btn.active:before,.btn-toolbar>.angle-btn:last-child:after,.btn-toolbar>.angle-btn:last-child:before{border-color:transparent transparent transparent #0c64eb}.btn-toolbar>.angle-btn.active:after,.btn-toolbar>.angle-btn:last-child:after{border-color:transparent transparent transparent #fff}.btn-toolbar>.angle-btn+.angle-btn:last-child>.btn-group:first-child:after,.btn-toolbar>.angle-btn+.angle-btn:last-child>.btn-group:first-child:before,.btn-toolbar>.angle-btn.active+.angle-btn>.btn-group:first-child:after,.btn-toolbar>.angle-btn.active+.angle-btn>.btn-group:first-child:before{border-color:transparent transparent transparent #0c64eb}.btn-toolbar>.angle-btn+.angle-btn:last-child>.btn-group:first-child:after,.btn-toolbar>.angle-btn.active+.angle-btn>.btn-group:first-child:after{border-color:transparent transparent transparent #fff}.btn-toolbar>.angle-btn.active+.angle-btn,.btn-toolbar>.angle-btn.normal{border-color:#cbd0db}.btn-toolbar>.angle-btn.active+.angle-btn .btn,.btn-toolbar>.angle-btn.normal .btn{color:#3c4353}.btn-toolbar>.angle-btn.active+.angle-btn:after,.btn-toolbar>.angle-btn.active+.angle-btn:before,.btn-toolbar>.angle-btn.normal:after,.btn-toolbar>.angle-btn.normal:before{border-color:transparent transparent transparent #cbd0db}.btn-toolbar>.angle-btn.active+.angle-btn:after,.btn-toolbar>.angle-btn.normal:after{border-color:transparent transparent transparent #fff}.btn-toolbar>.angle-btn.active+.angle-btn>.btn-group:first-child:before,.btn-toolbar>.angle-btn.normal>.btn-group:first-child:before{border-color:transparent transparent transparent #cbd0db!important}.nav>li>.btn.btn-primary{color:#fff}.nav>li>.btn.btn-primary:focus,.nav>li>.btn.btn-primary:hover{background:rgba(12,100,235,.8)}.btn.btn-action,.c-actions .btn{display:inline-block;width:26px;padding:2px;overflow:hidden;line-height:20px;color:#16a8f8;background:0 0;border-color:transparent}.btn.btn-action>i,.c-actions .btn>i{position:relative;top:1px;font-size:18px}.btn.btn-action:hover,.c-actions .btn:hover{color:#0c64eb;background-color:#d3e5f7}.c-actions .btn+.btn{margin-left:-4px}.label{position:relative;display:inline-block;padding:3px 5px;font-size:12px;font-weight:400;vertical-align:middle;border-radius:2px}.label+.label{margin-left:4px}.label-pale{background:#bed8f3!important}.label-badge{border-radius:9px}.label-light{color:#3c4353;background-color:#ddd}.label-primary{background:#0c64eb!important}.label-gray{color:#878da0;background:#e8ebef}.label-outline.label-danger{color:#ff5d5d;background:#ffebee;border-color:rgba(255,93,93,.25)}.label-outline.label-light{color:#838a9d;background:#f2f5fb;border-color:#e1e5ee}.label-primary.label-outline{background:#e9f2fb!important;border-color:rgba(12,100,235,.25)}.label-outline.label-success{background:#e8f5e9;border-color:rgba(0,218,136,.25)}.label-outline.label-info{border-color:rgba(33,150,243,.25)}.label-outline.label-warning{border-color:rgba(255,152,0,.25)}.label-dot{position:relative;top:-1px;padding:0;border-radius:50%}.label-dot+.status-text{display:inline-block;margin-left:5px}.label-icon{min-width:18px;padding:0;line-height:18px;border-radius:10px}.label-id{display:inline-block;min-width:30px;padding:0 5px;font-size:12px;line-height:16px;color:#838a9d;text-align:center;vertical-align:middle;background-color:transparent;border:1px solid #838a9d;border-radius:2px}.pri-1,.todo-pri-1{color:#ff5d5d}[class*=" status-"],[class^=status-]{color:#3c4353}.status-changed,.status-delayed,.status-doing,.status-fail,.status-investigate{color:#ff5d5d}.status-changed>.label-dot,.status-delayed>.label-dot,.status-doing>.label-dot,.status-fail>.label-dot,.status-investigate>.label-dot{background-color:#ff5d5d}.status-wait{color:#838a9d}.status-wait>.label-dot{background-color:#7ec5ff}.status-unclosed{color:#838a9d}.status-unclosed>.label-dot{background-color:#0c64eb}.status-done,.status-normal,.status-pass,.status-resolved{color:#43a047}.status-done>.label-dot,.status-normal>.label-dot,.status-pass>.label-dot,.status-resolved>.label-dot{background-color:#00da88}.status-postpone{color:#838a9d}.status-postpone>.label-dot{background-color:#ff5d5d}.status-blocked{position:relative;left:-5px;display:inline-block;padding:0 5px;line-height:20px;color:#3c4353;background:#fff3e0;border-radius:10px}.status-blocked>.label-dot{background-color:#ff9800}.status-pause,.status-suspended{color:#ff9800}.status-pause>.label-dot,.status-suspended>.label-dot{background-color:#ff9800}.status-active.status-bug,.status-draft{color:#8666b8}.status-active.status-bug>.label-dot,.status-draft>.label-dot{background-color:#8666b8}.status-closed,.status-terminate{color:#838a9d}.status-closed>.label-dot,.status-terminate>.label-dot{background-color:#838a9d}.status-cancel{color:#838a9d}.status-cancel>.label-dot{background-color:#cbd0db}.label-pri{display:inline-block;min-width:18px;max-width:67px;height:18px;padding:0 4px;overflow:hidden;font-size:12px;line-height:16px;color:#838a9d;text-align:center;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle;border:1px solid #838a9d;border-radius:10px}.label-selector>.label-pri,[class*=label-pri-]{color:#158af1;border-color:#2098ee}.label-pri-1,.label-selector>.label-pri[data-value="1"]{color:#d50000;border-color:#d50000}.label-pri-2,.label-selector>.label-pri[data-value="2"]{color:#ff9800;border-color:#ff9800}.label-pri-3,.label-selector>.label-pri[data-value="3"]{color:#2098ee;border-color:#2098ee}.label-pri-4,.label-selector>.label-pri[data-value="4"]{color:#009688;border-color:#009688}.label-pri-5,.label-selector>.label-pri[data-value="5"]{color:#838a9d;border-color:#838a9d}.label-pri-0,.label-selector>.label-pri.active[data-value="0"]{color:#d5d9df;border-color:#d5d9df}.label-severity{position:relative;display:inline-block;width:24px;height:20px;font-weight:bolder;text-align:center;vertical-align:middle}.label-severity:before{position:absolute;top:-3px;left:0;z-index:0;display:block;font-family:ZentaoIcon;font-size:14px;font-size:24px;font-style:normal;font-weight:400;font-variant:normal;line-height:1;color:inherit;text-transform:none;content:"\e973";speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.label-severity:after{position:absolute;top:7px;left:0;z-index:1;display:block;width:24px;font-size:12px;line-height:12px;text-align:center;content:attr(data-severity)}.label-severity[data-severity],.label-severity[data-value]{color:#ccc}.label-severity[data-severity="0"],.label-severity[data-value="0"]{color:#ccc}.label-severity[data-severity="0"]:after,.label-severity[data-value="0"]:after{display:none}.label-severity[data-severity="1"],.label-severity[data-value="1"]{color:#c62828}.label-severity[data-severity="2"],.label-severity[data-value="2"]{color:#ff8f00}.label-severity[data-severity="3"],.label-severity[data-value="3"]{color:#fdd835}.label-severity[data-severity="4"],.label-severity[data-value="4"]{color:#cddc39}.label-severity[data-severity="5"],.label-severity[data-value="5"]{color:#8bc34a}.label-severity-custom[data-severity]{color:#d5d9df}.label-severity-custom[data-severity="1"]{color:#c62828}.label-severity-custom[data-severity="2"]{color:#ff8f00}.label-severity-custom[data-severity="3"]{color:#fdd835}.label-severity-custom[data-severity="4"]{color:#cddc39}.label-severity-custom[data-severity="5"]{color:#8bc34a}.label-selector{padding:0 10px}.label-selector>.label{display:inline-block;min-width:24px;height:24px;padding:0 5px;font-size:14px;line-height:20px;text-align:center;cursor:pointer;background:0 0;border:2px solid #d5d9df;border-radius:15px}.label-selector>.label+.label{margin-left:10px}.label-selector>.label.empty{border-color:transparent}.label-selector>.label.label-severity{font-size:12px;line-height:28px;border-color:transparent}.label-selector>.label.label-severity:before{top:-2px;left:-2px}.label-selector>.label.label-severity:after{display:none}.label-selector>.label.label-severity.active{background:0 0;filter:none;-webkit-filter:none}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{margin-top:2px}.ie .checkbox input[type=checkbox],.ie .checkbox-inline input[type=checkbox],.ie .radio input[type=radio],.ie .radio-inline input[type=radio]{margin-top:4px}.is-firefox .checkbox input[type=checkbox],.is-firefox .checkbox-inline input[type=checkbox],.is-firefox .radio input[type=radio],.is-firefox .radio-inline input[type=radio]{margin-top:3px}.checkbox-primary,.radio-primary{position:relative;display:block;vertical-align:middle}.checkbox-primary.inline-block,.radio-primary.inline-block{display:inline-block}.checkbox-primary.inline-block+.inline-block,.radio-primary.inline-block+.inline-block{margin-left:15px}.checkbox-primary>input,.radio-primary>input{position:absolute;top:0;left:0;z-index:3;width:100%;height:100%;margin:0;opacity:0}.checkbox-primary>label,.radio-primary>label{display:block;height:20px;padding-left:30px;margin:0;font-weight:400;line-height:20px;cursor:pointer}.checkbox-primary>label:after,.checkbox-primary>label:before,.radio-primary>label:after,.radio-primary>label:before{position:absolute;top:1px;right:0;left:0;display:block;width:18px;height:18px;line-height:18px;text-align:center;content:' ';border-radius:3px}.checkbox-primary>label:after,.radio-primary>label:after{z-index:1;border:2px solid #eee;border-color:rgba(0,0,0,.15);-webkit-transition:.4s cubic-bezier(.175,.885,.32,1);-o-transition:.4s cubic-bezier(.175,.885,.32,1);transition:.4s cubic-bezier(.175,.885,.32,1);-webkit-transition-property:border,background-color;-o-transition-property:border,background-color;transition-property:border,background-color}.checkbox-primary>label:before,.radio-primary>label:before{top:3px;z-index:2;font-family:ZentaoIcon;font-size:14px;font-style:normal;font-weight:400;font-weight:900;font-variant:normal;line-height:1;text-transform:none;content:"\e5ca";opacity:0;-webkit-transition:.2s cubic-bezier(.175,.885,.32,1);-o-transition:.2s cubic-bezier(.175,.885,.32,1);transition:.2s cubic-bezier(.175,.885,.32,1);-webkit-transition-property:opacity,-webkit-transform;-o-transition-property:opacity,-o-transform;transition-property:opacity,-webkit-transform;transition-property:opacity,transform;transition-property:opacity,transform,-webkit-transform,-o-transform;-webkit-transform:scale(0);-ms-transform:scale(0);-o-transform:scale(0);transform:scale(0);speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.checkbox-primary.checked>label:after,.checkbox-primary>input:checked+label:after,.radio-primary.checked>label:after,.radio-primary>input:checked+label:after{background-color:#00da88;border-color:#00da88;border-width:4px}.checkbox-primary.checked>label:before,.checkbox-primary>input:checked+label:before,.radio-primary.checked>label:before,.radio-primary>input:checked+label:before{color:#fff;opacity:1;-webkit-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}.checkbox-primary input:indeterminate+label:before,.checkbox-primary.indeterminate>label:before,.radio-primary input:indeterminate+label:before,.radio-primary.indeterminate>label:before{top:9px;left:5px;width:8px;height:2px;content:' ';background-color:#a3a2bc;opacity:1;-webkit-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}.checkbox-primary.focus>label:after,.checkbox-primary>input:focus+label:after,.radio-primary.focus>label:after,.radio-primary>input:focus+label:after{border-color:#00da88;-webkit-box-shadow:0 0 0 3px rgba(0,218,136,.2);box-shadow:0 0 0 3px rgba(0,218,136,.2)}.checkbox-primary:hover>label:after,.radio-primary:hover>label:after{border-color:#00da88}.checkbox-primary.checkbox-right>label,.radio-primary.checkbox-right>label{padding:0 30px 0 0}.checkbox-primary.checkbox-right>label:after,.checkbox-primary.checkbox-right>label:before,.radio-primary.checkbox-right>label:after,.radio-primary.checkbox-right>label:before{right:0;left:auto}.checkbox-primary input:disabled+label:after,.checkbox-primary.disabled>label:after,.radio-primary input:disabled+label:after,.radio-primary.disabled>label:after{background-color:#e5e5e5!important;border-color:#bbb!important}.checkbox-primary input:disabled:checked+label:after,.checkbox-primary.checked.disabled>label:after,.radio-primary input:disabled:checked+label:after,.radio-primary.checked.disabled>label:after{background-color:#bbb!important}.radio-primary>label:after{border-radius:50%}.radio-primary>label:before{top:7px;left:6px;width:6px;height:6px;content:' ';border:none;border-radius:50%}.radio-primary.checked>label:after,.radio-primary>input:checked+label:after{background-color:transparent;border-color:#00da88;border-width:2px}.radio-primary.checked>label:before,.radio-primary>input:checked+label:before{background-color:#00da88}.radio-primary input:disabled:checked+label:after,.radio-primary.checked.disabled>label:after{background-color:transparent;border-color:#bbb}.radio-primary input:disabled:checked+label:before,.radio-primary.checked.disabled>label:before{background-color:#bbb}.panel{position:relative;margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05),0 2px 6px 0 rgba(0,0,0,.045);box-shadow:0 1px 1px rgba(0,0,0,.05),0 2px 6px 0 rgba(0,0,0,.045)}.panel-body{padding:20px}.panel-body.has-table{padding:10px}.panel-body.has-table .table{margin-bottom:0;table-layout:fixed}.panel-heading{padding:12px 48px 12px 20px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading+.panel-body{padding-top:0}.panel-title{font-size:14px;font-weight:700;line-height:20px}.panel-title .label{top:-1px}.panel-actions{position:absolute;top:0;right:0;padding:7px 8px}.panel:hover .panel-actions{z-index:10}.panel-actions>li>a{display:inline-block;min-width:30px;padding:0 5px;line-height:30px;color:#a6aab8;text-align:center;border-radius:4px}.panel-actions>li>a:hover{color:#3c495c;text-decoration:initial;background-color:#f1f1f1}.panel-actions .btn-icon{color:#a6aab8}.panel-actions .btn.text-primary{color:#0c64eb}.panel .empty-tip{padding:30px 10px 50px;font-size:14px;color:#838a9d;text-align:center}.progress-text-left{position:relative;margin:7px 0;margin-left:35px;overflow:visible}.progress-text-left .progress-text{position:absolute;top:-7px;left:-35px;display:block;width:35px;height:20px;padding-right:5px;line-height:20px;color:#838a9d;text-align:right}.chart-color{width:20px}.chart-color-dot{display:inline-block;width:10px;height:10px;border-radius:50%}.chart-row{margin-top:10px}.chart-row+.chart-row{padding-top:10px;border-top:1px solid #eee}.chart-wrapper{padding:10px 5px;background:#eee}.chart-wrapper>h4{margin:5px 0 10px}.table-wrapper{max-height:250px;overflow:auto}.table-wrapper .table{margin:0}.progress-pie{position:relative}.progress-pie canvas{display:block}.progress-pie .progress-info{position:absolute;top:0;left:0;width:100%;height:100%;padding-top:25px;text-align:center}.progress-pie .progress-info>small{display:block;line-height:14px;color:#a6aab8}.progress-pie .progress-info>strong{display:block;font-size:36px;line-height:40px}.progress-pie .progress-info>strong>small{font-size:20px}.progress-pie-120 .progress-info{padding-top:30px}.progress-pie-120 .progress-info>small{line-height:18px}.progress-pie-50 .progress-info{padding-top:4px}.progress-pie-50 .progress-info>strong{font-size:20px;font-weight:400}.progress-pie-50 .progress-info>strong>small{font-size:14px}.progress-pie[data-value="100"] .progress-info>strong{-webkit-transform:scale(.7);-ms-transform:scale(.7);-o-transform:scale(.7);transform:scale(.7)}.progress-pie-24 .progress-info{right:-10px;left:-10px;width:auto;padding-top:0;font-size:12px;line-height:24px;-webkit-transform:scale(.9,1);-ms-transform:scale(.9,1);-o-transform:scale(.9,1);transform:scale(.9,1)}.progress-pie-24[data-value="100"] .progress-info{-webkit-transform:scale(.8,1);-ms-transform:scale(.8,1);-o-transform:scale(.8,1);transform:scale(.8,1)}.progress-pie-26 .progress-info{right:-10px;left:-10px;width:auto;padding-top:0;font-size:12px;line-height:26px;-webkit-transform:scale(.9,1);-ms-transform:scale(.9,1);-o-transform:scale(.9,1);transform:scale(.9,1)}.progress-pie-26[data-value="100"] .progress-info{-webkit-transform:scale(.8,1);-ms-transform:scale(.8,1);-o-transform:scale(.8,1);transform:scale(.8,1)}.status-bars{display:table;width:100%;height:140px;padding:5px;padding-top:50px;margin:0;overflow:hidden}.status-bars>li{position:relative;display:table-cell;text-align:center;vertical-align:bottom}.status-bars .bar{position:absolute;bottom:20px;left:50%;display:block;width:10px;margin-left:-5px;background:#0c64eb;border-radius:5px 5px 0 0}.status-bars .bar:after{position:absolute;right:-50px;bottom:0;left:-50px;display:block;height:1px;content:' ';background:#eee}.status-bars .title{font-size:12px;font-weight:400;color:#a6a8b6}.status-bars .value{position:relative;top:-20px;left:-20px;display:inline-block;width:50px;font-size:16px;font-weight:700;text-align:center}.status-bars-h{display:block;padding-right:50px;padding-left:60px;list-style:none}.status-bars-h>li{position:relative;height:40px;border-left:1px solid #eee}.status-bars-h .bar{position:relative;top:15px;display:block;height:10px;line-height:20px;background:#0c64eb;border-radius:0 5px 5px 0}.status-bars-h .title{position:absolute;top:-5px;left:-60px;width:60px;padding-right:10px;font-size:12px;color:#a6a8b6;text-align:right}.status-bars-h .value{position:absolute;top:-5px;right:-50px;display:block;width:40px;font-size:14px;font-weight:700;text-align:left;white-space:nowrap}.messager{border-radius:4px;-webkit-box-shadow:0 4px 16px rgba(0,0,0,.2),0 2px 8px rgba(0,0,0,.1);box-shadow:0 4px 16px rgba(0,0,0,.2),0 2px 8px rgba(0,0,0,.1)}.messager-icon{vertical-align:middle}.messager-icon>.icon{font-size:24px}.messager-content{padding:18px 20px;font-size:18px;line-height:30px}.messager-content>.icon{font-size:28px;line-height:30px}.messager-actions{vertical-align:middle}.messagger-zt{color:#3c4353;background-color:#fff!important}.messagger-zt .messager-icon>.icon{color:#0c64eb}.messagger-zt .messager-actions>.action{color:#838a9d}.messagger-zt.messager-success .messager-icon>.icon{color:#00da88}.messagger-zt.messager-danger .messager-icon>.icon{color:#ff5d5d}.messagger-zt.messager-warning .messager-icon>.icon{color:#ff9800}.messagger-zt.messager-info .messager-icon>.icon{color:#2196f3}.tree{padding-left:0;overflow:hidden}.tree ul{position:relative;display:none;padding-left:0}.tree li{position:relative;padding:2px 0 2px 15px;list-style:none}.tree li.heading{padding-left:5px;color:#3c495c}.tree li>a{display:block;max-width:90%;padding:2px 6px;color:#3c495c;word-break:break-all}.tree li>a:hover{color:#3c4353}.tree li>a.tree-toggle:hover{background:0 0}.tree li.active>a{position:relative;font-weight:700;color:#0c64eb}.tree li>.list-toggle{position:absolute;top:1px;left:1px;z-index:10;width:20px;font-size:14px;line-height:22px;color:#cbd0db;text-align:center;cursor:pointer;-webkit-transition:all .2s;-o-transition:all .2s;transition:all .2s}.tree li>.list-toggle:before{content:"\f0da"}.tree li>.list-toggle:active,.tree li>.list-toggle:hover{color:#0c64eb}.tree li.has-active-item>.list-toggle{color:#3c4353}.tree li.has-list.open>ul{display:block}.tree li.has-list.open>.list-toggle{-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.tree li.has-list.open:before{position:absolute;top:16px;bottom:-5px;left:10px;display:block;content:' ';border-left:1px solid #d8d8d8}.tree-actions{display:inline-block;margin-left:5px;vertical-align:middle}.tree-actions a{display:inline-block;margin-left:5px;font-size:13px;opacity:.6}.tree-actions a:hover{opacity:1}.tree li>.module-name{color:#3c495c;vertical-align:middle}.tree li>.module-name:hover{background-color:#f0f2f5}.tree li>.module-name:hover>a{color:#3c4353}.treemap-node-fold-icon:before{position:relative;left:-4px;min-width:18px}.dropdown-menu{padding:5px 0;border-color:rgba(0,0,0,.1)}.dropdown-menu>li{padding:0 10px}.dropdown-menu>li>a{padding:2px 10px;margin:5px 0;border-radius:3px}.dropdown-menu>li>a>.icon{position:relative;left:-5px;opacity:.5}.dropdown-menu>li>a:hover>.icon{opacity:.8}.dropdown-menu>li.active>a,.dropdown-menu>li.selected>a{position:relative;color:#fff;background-color:#16a8f8}.dropdown-menu>li.selected>a:after{position:absolute;top:2px;right:4px;display:block;font-family:ZentaoIcon;font-size:14px;font-style:normal;font-weight:400;font-variant:normal;line-height:1;line-height:20px;text-transform:none;content:"\e5ca";speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.dropdown-menu>li.divider{margin:10px}.dropdown-submenu>a:after{margin-right:-5px}.dropdown-submenu>.dropdown-menu.pull-left{margin-left:-1px}.dropdown-submenu:focus>a,.dropdown-submenu:hover>a{color:#3c4353;background-color:#e9f2fb}.dropdown-submenu:hover>a:after{border-left-color:#0c64eb}.dropdown-submenu>a:hover:after{border-left-color:#fff}.pager .btn{padding:3px 10px}.pager .btn .caret{opacity:.7}.pager>li>.pager-label{padding:2px;line-height:20px}.pager>li>.pager-item{min-width:20px;padding:1px;margin:2px 0;font-size:16px;line-height:20px;text-align:center;background:0 0;border-color:transparent}.pager>li>.pager-item:hover{background-color:rgba(0,0,0,.1)}.pager>li>.pager-item>.icon{position:relative;top:-1px}.pager>li>.btn:hover,.pager>li>a:hover{background:rgba(0,0,0,.1)}.pager>li.disabled>a.pager-item{background:0 0;border-color:transparent;opacity:.5}.pager>li.active>a{background-color:#16a8f8}.pager>li .btn-group .btn{padding:1px;margin:1px 0;border-radius:4px}.pager .dropdown-menu{width:200px}.pager .dropdown-menu>li{float:left;width:33.333333%}.modal-dialog{width:900px;max-width:1360px;border:none;border-radius:0;-webkit-box-shadow:0 0 20px 0 rgba(0,0,0,.25);box-shadow:0 0 20px 0 rgba(0,0,0,.25)}.modal-dialog.modal-md{width:700px}.modal-dialog.modal-xs{width:400px}.modal-dialog.modal-sm{width:500px}.modal-dialog.modal-lg{width:1200px}.modal-dialog.modal-fullscreen{position:fixed;max-width:initial}.modal-header{padding:20px 0;margin:0 20px}.modal-header>.close{color:#838a9d;text-shadow:0 1px 0 rgba(255,255,255,.85);opacity:1}.modal-header>.close:hover{color:#222}.modal-footer{padding:20px 0;margin:0 20px}.modal-title{font-size:14px;font-weight:400;line-height:20px}.modal-actions{position:absolute;top:16px;right:16px}.modal-actions .divider{position:relative;top:5px;display:inline-block;width:0;height:20px;margin:0 10px;border-left:#eee 1px solid}.modal-actions>.dropdown{display:inline-block}.modal-body{padding:20px}.modal-iframe .modal-body>iframe{border-radius:0}.modal.fade .modal-dialog{-webkit-transform:translate(0,-3%);-ms-transform:translate(0,-3%);-o-transform:translate(0,-3%);transform:translate(0,-3%)}.modal.fade.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-simple .modal-footer{padding-top:0;border-top:none}.modal-iframe .modal-header{position:relative;z-index:10;min-height:0;padding:0;border:none}.modal-iframe .modal-title{display:none}.modal-iframe .modal-header .close{position:absolute;top:12px;right:10px;font-size:32px;font-weight:200}.modal-iframe .modal-dialog{overflow:hidden}.modal-inverse .modal-header>.close{color:rgba(255,255,255,.7);text-shadow:none}.modal-inverse .modal-header>.close:hover{color:#fff}.modal-scroll-inside>.modal-dialog{max-height:100%}.hide-modal-close .modal-iframe .modal-header .close{display:none}.tile{text-align:center}.tile-title{line-height:20px;color:#3c495c}.tile-amount{font-size:32px;font-weight:700;line-height:56px}.timeline>li{position:relative;list-style:none}.timeline>li:before,.timeline>li>a:after,.timeline>li>div:after{position:absolute;left:-20px;display:block;width:15px;height:15px;content:' ';border-radius:50%}.timeline>li:before{top:12px;left:-16px;z-index:3;width:7px;height:7px;background-color:#cbd0db;border:none;border:1px solid #cbd0db}.timeline>li>a:after,.timeline>li>div:after{top:11px;left:-17px;z-index:3;width:9px;height:9px;background-color:#0c64eb;border-radius:50%;opacity:0}.timeline>li+li:after{position:absolute;top:-12px;bottom:20px;left:-13px;z-index:1;display:block;content:' ';border-left:1px solid #eee}.timeline>li.active>a:after,.timeline>li.active>div:after{opacity:1}.timeline>li.active:before{top:8px;left:-20px;width:15px;height:15px;background-color:rgba(12,100,235,.2);border:none}.timeline>li>a,.timeline>li>div{display:block;padding:5px;line-height:20px}.timeline>li.active>a{color:#3c4353}.timeline-tag{position:absolute;top:5px;left:-115px;font-size:12px}.timeline-tag-left{padding-left:115px}.timeline-sm{font-size:12px}.timeline-sm>li:before,.timeline-sm>li>a:after,.timeline-sm>li>div:after{top:10px;left:-20px;width:11px;height:11px}.timeline-sm>li.active:before,.timeline-sm>li:before{top:10px;left:-18px;width:11px;height:11px;background:0 0;border:1px solid #eee}.timeline-sm>li>a,.timeline-sm>li>div{line-height:20px}.timeline-sm>li>a:after,.timeline-sm>li>div:after{top:13px;left:-15px;width:5px;height:5px}.form-control{-webkit-box-shadow:none;box-shadow:none}.form-horizontal .form-group>label{padding-right:0}.form-actions{margin-top:20px;margin-bottom:0}.form-actions .btn{margin-right:10px}form label{font-weight:400;color:#3c495c}.form-group .btn+.btn{margin-left:5px}.table-form{margin-bottom:0;table-layout:fixed}.table-form>thead>tr>th.required:after{position:relative;top:3px;right:auto;left:4px;display:inline-block;vertical-align:middle}.table-form>tbody>tr>td,.table-form>tbody>tr>th,.table-form>tfoot>tr>td,.table-form>thead>tr>th{padding:7px;vertical-align:middle;border-bottom:none}.table-form>tfoot>tr>td{padding:20px 7px 10px}.table-form>tbody>tr>th{width:100px;font-weight:700;text-align:right}.table-form .input-group{width:100%}.chosen-container-single .chosen-single{position:relative}.chosen-container-single .chosen-single>span{height:20px;line-height:20px;white-space:normal}.chosen-container-single .chosen-single div b{position:relative;top:1px;color:#cbd0db}.chosen-container-single .chosen-search:before{top:8px;right:15px}.chosen-container-multi .chosen-choices li.search-choice{font-size:13px;background:#eee;border-color:#cbd0db;-webkit-box-shadow:none;box-shadow:none}.chosen-container-single .chosen-search input[type=text]{height:30px;padding:3px 25px 3px 5px}.chosen-container-single .chosen-search{padding:3px 10px 0}.chosen-container-single .chosen-single{overflow:visible}.chosen-container .chosen-results{max-height:245px;padding:10px}.chosen-container .chosen-results>li{border-radius:4px}.chosen-container .chosen-results li.highlighted em{color:#fff}.table-responsive .chosen-container .chosen-results{max-height:200px}.chosen-compact.chosen-container-single .chosen-single>.chosen-search{top:-2px;right:-1px;bottom:-1px;left:-1px;display:none;height:auto;padding:0;opacity:0}.chosen-compact.chosen-container-single .chosen-single>.chosen-search>input{height:31px;padding:5px 26px 5px 8px;font-size:inherit;line-height:20px}.chosen-compact.chosen-container-single .chosen-single>.chosen-search:before{top:7px;right:8px}.datetimepicker{padding:10px}.datetimepicker td.day.today{background-color:#f77}.datetimepicker td.day.active{background-color:#16a8f8}.datetimepicker tfoot th,.datetimepicker thead th{color:#838a9d}.input-control .colorpicker{top:0;z-index:auto;opacity:1}.input-control .colorpicker .btn{padding:5px}.input-control .input-control-icon-right.btn{top:0}.colorpicker .dropdown-menu{min-width:232px;padding:5px 10px 10px 10px}.colorpicker .dropdown-menu>li{display:block;float:left;padding:5px}.colorpicker .dropdown-menu>li.heading{width:100%;margin-bottom:5px;font-size:16px;font-weight:700;text-align:left}.colorpicker .dropdown-menu>li.heading>.icon-close{position:relative;top:4px;float:right;cursor:pointer;opacity:.6}.colorpicker .dropdown-menu>li>a{position:relative;display:block;width:100%;height:100%;padding:0;margin:0;font-family:ZentaoIcon;font-size:14px;font-style:normal;font-weight:400;font-variant:normal;line-height:1;text-align:center;text-transform:none;border:1px solid transparent;border-radius:50%;speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.colorpicker .dropdown-menu>li>a:before{position:absolute;top:0;display:block;width:100%;height:20px;line-height:18px}.colorpicker .dropdown-menu>li>a:hover{-webkit-box-shadow:0 1px 4px rgba(0,0,0,.25);box-shadow:0 1px 4px rgba(0,0,0,.25)}.colorpicker .dropdown-menu>li>a.active:before{font-size:14px;content:"\e5ca"}.colorpicker .dropdown-menu>li>a.empty{color:#666;background:#fff}.colorpicker .dropdown-menu>li>a.empty:before{content:"\e90d"}.colorpicker .btn{position:relative}.colorpicker .btn .color-bar{position:absolute;right:5px;bottom:3px;left:5px;height:3px}.colorpicker .btn .color-bar[style*='background: ']+.ic{position:relative;top:-2px}.colorpicker .btn .ic{color:#cbd0db}.colorpicker .btn:hover .ic{color:#838a9d}.input-group .colorpicker{z-index:3}.input-group .chosen-container{display:table-cell}.input-group-addon{border-right-width:0;border-left-width:0}.input-group-addon:first-child{border-left-width:1px}.input-group-addon:last-child{border-right-width:1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin:0}.input-group-cell{display:table-cell;width:1%;padding:0 12px;white-space:nowrap;vertical-align:middle}.ke-container{border-color:#dcdcdc!important;border-radius:2px!important}.ke-container.focus{border-color:#0c64eb!important}.ke-toolbar{border-color:#dcdcdc!important}.required:after{top:6px;right:-10px;font-size:20px}td.required:after{top:12px;right:-5px}.input-group>.chosen-container.required:after,.input-group>.input-control.required:after{top:1px;right:1px;z-index:2}.input-group.required .required:after{display:none}.file-input{position:relative;width:-webkit-fit-content;width:-moz-fit-content;width:fit-content}.file-input .input-group{width:auto}.file-input .input-group>.input-group-cell:first-child{padding-right:0;padding-left:7px}.file-input input[type=file]{position:absolute;width:0;height:0;opacity:0}.file-input .file-title{display:inline-block;max-width:400px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle}.file-input .file-editbox{min-width:200px;max-width:100%}.file-input .file-size{display:inline-block;vertical-align:middle}.edit .file-input-empty,.file-input-edit,.file-input-normal,.normal .file-input-empty{display:none}.edit .file-input-edit,.normal .file-input-normal{display:block}.edit .file-input-edit.input-group,.normal .file-input-normal.input-group{display:table}.edit .file-input-normal{display:none!important}.file-input-normal>.input-group-btn{width:auto}.input-group .chosen-container-active .chosen-choices{border-color:#0c64eb!important}.input-group .chosen-container{min-width:100px}.input-group .input-group-btn .btn>.icon{line-height:17px}.os-mac select.form-control{-webkit-appearance:none;background-image:url(data:image/gif;base64,R0lGODlhCQAFAIAAAMvQ2////yH5BAEAAAEALAAAAAAJAAUAAAIKhH+BGYoNGWxgFgA7);background-image:url(data:image/gif;base64,R0lGODlhBwAEAIAAAMvQ2////yH5BAEAAAEALAAAAAAHAAQAAAIIhA+BGWoNWSgAOw==);background-repeat:no-repeat;background-position:right 5px top 12px;-moz-appearance:none}input::-webkit-contacts-auto-fill-button{position:absolute;right:0;display:none!important;pointer-events:none;visibility:hidden}.chosen-choices.has-error,.chosen-single.has-error,.form-control.has-error{border-color:#ff5d5d!important;-webkit-box-shadow:0 0 6px #ffc3c3!important;box-shadow:0 0 6px #ffc3c3!important}.popover-success.popover-form-result{font-weight:700;color:#fff;background:#00da88}.popover-success.popover-form-result.popover.right .arrow:after{border-right-color:#00da88}.form-unsaved{outline:2px solid #ff9800;-webkit-box-shadow:0 1px 12px #ff9800;box-shadow:0 1px 12px #ff9800;-webkit-transition:all .5s;-o-transition:all .5s;transition:all .5s}#mainHeader{height:50px;color:#fff;background:#1183fb -webkit-gradient(linear,right top,left top,from(#0a48d1),to(#1183fb));background:#1183fb -webkit-linear-gradient(right,#0a48d1 0,#1183fb 100%);background:#1183fb -o-linear-gradient(right,#0a48d1 0,#1183fb 100%);background:#1183fb linear-gradient(-90deg,#0a48d1 0,#1183fb 100%);background-color:#1183fb;border-top-color:#0c64eb;border-bottom-color:#e9f2fb}#mainHeader>.container{min-width:1200px;padding:0}#heading{position:absolute;top:10px;left:20px}@media (min-width:1400px){#heading{left:40px}}#heading h1{float:left;max-width:250px;margin:0;overflow:hidden;font-size:20px;font-weight:400;line-height:30px;text-overflow:ellipsis;white-space:nowrap}#heading h1 a{color:inherit;text-decoration:inherit}#heading h1.long-name{position:relative;top:-5px;display:table-cell;font-size:16px;line-height:20px;word-break:break-all;white-space:normal}#heading>.btn{display:block;float:left;height:20px;padding:1px 5px;margin:0;margin:5px 0 0 10px;font-size:12px;font-weight:lighter;line-height:18px;background-color:rgba(255,255,255,.2);border:none}#heading>.btn:hover{background-color:rgba(0,0,0,.1)}#navbar{margin:0 auto;text-align:center}#navbar .nav{display:inline-block}#navbar .nav>li>a{padding:10px;line-height:30px;color:#fff;border-radius:0;opacity:.9}@media (max-width:1400px){#navbar .nav>li>a{padding:10px 8px}}#navbar .nav>li>a:focus,#navbar .nav>li>a:hover{background:rgba(0,0,0,.15);opacity:1}#navbar .nav>li.active>a{font-weight:700;background:rgba(0,0,0,.1);opacity:1}#navbar .nav>li.divider{display:block;width:2px;height:20px;margin:15px 8px;background:rgba(255,255,255,.4)}@media (max-width:1400px){#navbar .nav>li.divider{margin:15px 5px}}@media (max-width:1300px){#navbar .nav>li.divider{margin:15px 3px}}#navbar .nav>li.divider:last-child{display:none}#navbar .nav .dropdown-menu li>a{text-align:left}#toolbar{position:absolute;top:12px;right:20px;font-size:12px;color:#fff}@media (min-width:1400px){#toolbar{right:40px}}#extraNav{text-align:right}#extraNav>li{display:inline-block;float:none;text-align:left}#extraNav>li>a{display:block;padding:0;color:#fff;opacity:.75}#extraNav>li>a:hover{text-decoration:unset;background-color:rgba(0,0,0,.1);opacity:1}#extraNav>li.open>a{background-color:rgba(0,0,0,.1)}#extraNav>li+li{margin-left:10px}#showSearchGo{color:#fff;background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.5)}#searchbox{position:relative;float:left;width:150px}#searchbox .input-group-btn .btn{position:relative;padding:1px 4px;font-size:12px;line-height:20px;color:#fff;background-color:rgba(255,255,255,.15);border-right:none;border-radius:2px}#searchbox .input-group-btn .btn:after{position:absolute;top:3px;right:0;bottom:3px;display:block;width:1px;content:' ';background-color:rgba(255,255,255,.15)}#searchbox .input-group-btn .btn:hover{background-color:rgba(255,255,255,.25)}#searchGo{position:absolute;top:0;right:-1px;z-index:9;min-width:24px;height:24px;padding:2px 3px;font-size:12px;line-height:20px;color:#fff;background-color:#16a8f8;border-radius:2px}#searchGo:hover{color:#fff!important;background-color:#0c64eb}#searchInput{height:24px;padding:2px 30px 2px 5px;color:#fff;text-align:left;background:rgba(255,255,255,.15);border-color:transparent;border-radius:0 12px 12px 0;-webkit-transition:background .2s,border .2s;-o-transition:background .2s,border .2s;transition:background .2s,border .2s}#searchInput:hover{background:rgba(255,255,255,.25)}#searchInput:focus{color:#333;background:#fff}#searchInput::-webkit-input-placeholder{font-size:12px;color:#fff;color:rgba(255,255,255,.5)}#searchInput::-moz-placeholder{font-size:12px;color:#fff;color:rgba(255,255,255,.5)}#searchInput:-ms-input-placeholder{font-size:12px;color:#fff;color:rgba(255,255,255,.5)}#searchInput::placeholder{font-size:12px;color:#fff;color:rgba(255,255,255,.5)}#searchInput:focus::-webkit-input-placeholder{color:#838a9d}#searchInput:focus::-moz-placeholder{color:#838a9d}#searchInput:focus:-ms-input-placeholder{color:#838a9d}#searchInput:focus::placeholder{color:#838a9d}#searchTypeMenu{min-width:220px}#searchTypeMenu>li{float:left;width:50%}#searchTypeMenu>li>a{margin:4px 0}#userNav .avatar{display:inline-block;vertical-align:middle}#userNav>li>a{padding:2px 6px;line-height:20px;color:#fff;opacity:.9}#userNav>li>a .user-name{max-width:100px;overflow:hidden;font-size:15px;text-overflow:ellipsis;white-space:nowrap}#userNav>li>a:hover{background-color:rgba(0,0,0,.1);opacity:1}#userNav>li>a:hover>i{opacity:1}#userNav>li>a span{display:inline-block;vertical-align:middle}#userNav>li.open>a{background-color:rgba(0,0,0,.1)}#userNav>li.has-new-items>a{position:relative}#userNav>li.has-new-items>a:before{position:absolute;top:3px;right:-1px;display:block;width:4px;height:4px;content:' ';background-color:#ff5d5d;border-radius:50%}#userNav .dropdown-menu{min-width:150px}#userNav .dropdown-menu>li>a>.icon{position:absolute;top:10px;right:5px;display:block;width:20px;height:20px;line-height:20px;text-align:center}#userNav .user-profile-item>a{position:relative;padding-left:45px}#userNav .user-profile-item .avatar{position:absolute;top:6px;left:5px}#userNav .user-profile-name{font-size:16px}#userNav .user-profile-role{font-size:12px;color:#a9abb8}#userNav .no-role .user-profile-role{display:none}#userNav .no-role .user-profile-name{line-height:40px}#subHeader{min-height:50px;background:#fff}#subHeader>.container{padding:0 20px}@media (min-width:1400px){#subHeader>.container{padding:0 40px}}#pageNav{position:absolute;top:8px;left:0;left:20px}@media (min-width:1400px){#pageNav{left:40px}}#subNavbar{margin-top:5px;font-size:14px;text-align:center}#subNavbar .nav{display:inline-block}#subNavbar .nav>li>a{padding:8px 12px;line-height:24px;color:#3c495c}#subNavbar .nav>li>a:hover{color:#3c495c;background-color:rgba(0,0,0,.075)}#subNavbar .nav>li.active>a{font-weight:700;color:#0c64eb}#subNavbar .nav>li.divider{display:block;width:2px;height:20px;margin:9px 5px;background-color:rgba(0,0,0,.05)}#subNavbar .dropdown-menu{text-align:left}[lang=en] #subNavbar>.nav>li>a{padding-right:8px;padding-left:8px}#pageActions{position:absolute;top:9px;right:20px}@media (min-width:1400px){#pageActions{right:40px}}.cell{padding:10px;background-color:#fff;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05),0 2px 6px 0 rgba(0,0,0,.045);box-shadow:0 1px 1px rgba(0,0,0,.05),0 2px 6px 0 rgba(0,0,0,.045)}.cell+.cell{margin-top:10px}.cell>.panel{margin:0;-webkit-box-shadow:none;box-shadow:none}.cell>.panel>.panel-heading{padding:5px 5px 10px}.cell>.panel>.panel-heading .panel-actions{padding:0}.cell>.panel>.panel-body{padding:5px}.cell>.table{margin:0}#main{min-width:1200px;padding:20px 0}#main>.container{padding:0 20px}@media (min-width:1400px){#main>.container{padding:0 40px}}#header,#header+#main{min-width:1200px}#mainMenu{margin:-10px 0 8px}.main-content{padding:20px;background-color:#fff;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05),0 2px 6px 0 rgba(0,0,0,.045);box-shadow:0 1px 1px rgba(0,0,0,.05),0 2px 6px 0 rgba(0,0,0,.045)}@media (min-width:1400px){.main-content>.center-block{max-width:1350px;padding:20px;border:1px solid #eee}.main-content>.center-block .main-header{background-color:#f1f1f1}}.main-content>h2{margin:0 0 20px}.main-content .cell{-webkit-box-shadow:none;box-shadow:none}.main-header{padding:5px 20px;border-bottom:1px solid #eee}.main-header:after,.main-header:before{display:table;content:" "}.main-header:after{clear:both}.main-header>h2{display:block;float:left;margin:0 10px 0 0;font-size:14px;line-height:34px}.main-header>h2 .label-id{margin-right:5px}.main-header>h2 small{font-size:14px;font-weight:400}.main-content .main-header{margin:-20px -20px 10px}.main-header .label{top:-1px}.main-row{display:table;width:100%;table-layout:fixed}.main-row>*{display:table-cell;vertical-align:top}@media (max-width:720px){.main-row{display:block}.main-row>*{display:block;width:100%}.main-row .side-col{width:100%;padding:0}.main-row .main-col+.side-col,.main-row .side-col+.main-col{margin-top:10px}}.main-row.hide-side .side-col{display:none}.main-form{margin:0}@media (min-width:720px){.main-content>.center-block .main-form{padding-right:20px}}#main .side-col .tabs{padding:5px}#main .side-col .nav-tabs{margin:0 5px 5px 5px;border-bottom:1px solid #ddd}#main .side-col .nav-tabs>li{margin:0}#main .side-col .nav-tabs>li+li{margin-left:10px}#main .side-col .nav-tabs>li>a{position:relative;padding:8px 5px;border:none;border-radius:2px!important}#main .side-col .nav-tabs>li.active>a{font-weight:700;color:#3c4353}#main .side-col .nav-tabs>li.active>a:before{position:absolute;right:0;bottom:-1px;left:0;display:block;height:2px;content:' ';background:#0c64eb}#main .side-col .tab-content .tab-pane table{border:none}.main-actions .btn-toolbar{display:inline-block;padding:4px 15px;color:#fff;pointer-events:auto;background:#717171;background-color:rgba(90,90,90,.85);border-radius:4px}.main-actions .btn-toolbar .divider{margin-right:15px;margin-left:15px;border-color:rgba(255,255,255,.1)}.main-actions .btn-toolbar .btn{height:30px;padding-right:10px;padding-left:10px;margin-right:0;color:#fff;background-color:transparent;border:none}.main-actions .btn-toolbar .btn+.btn{margin-left:10px}.main-actions .btn-toolbar .btn:focus,.main-actions .btn-toolbar .btn:hover{background-color:rgba(255,255,255,.2)}.main-actions .btn-toolbar .btn.btn-icon{min-width:32px;padding-right:0;padding-left:0}.main-actions .btn-toolbar .btn+.btn-group{margin-right:0;margin-left:10px}#mainContent .main-col>.main-actions{padding:30px 0 0 0;text-align:center}#mainContent .main-col>.main-actions>.btn-toolbar{visibility:visible;opacity:1;-webkit-transition:opacity .2s;-o-transition:opacity .2s;transition:opacity .2s}#mainActions{position:fixed;top:0;right:0;bottom:0;left:0;text-align:center;pointer-events:none}#mainActions .btn-toolbar{position:relative;top:-90px}#mainActions .dropdown-menu{text-align:left}#mainActions>.container{height:100%}.main-actions-holder{display:none}.main-actions-fixed .main-actions-holder{display:block}.main-actions-fixed #mainContent .main-col>.main-actions{position:fixed;bottom:10px}.main-actions-fixed.body-modal #mainContent .main-col>.main-actions{bottom:20px}#nextPage,#prevPage{position:absolute;top:50%;left:-10px;width:40px;height:60px;padding:10px 0;margin-top:-30px;line-height:40px;color:#fff;text-align:center;pointer-events:auto;background:#717171;background-color:rgba(90,90,90,.85);-webkit-box-shadow:0 2px 15px 2px rgba(0,0,0,.05);box-shadow:0 2px 15px 2px rgba(0,0,0,.05)}#nextPage:hover,#prevPage:hover{-webkit-box-shadow:0 2px 15px 2px rgba(0,0,0,.15);box-shadow:0 2px 15px 2px rgba(0,0,0,.15)}#nextPage>i,#prevPage>i{display:block;font-size:18px;line-height:36px}#nextPage{right:-10px;left:auto}@media (max-width:1800px){#prevPage{left:-3px}#nextPage{right:-3px}}#sidebarHeader{position:relative;float:left;width:180px;height:34px;padding-right:20px;margin-right:20px;background:#fff;border-left:4px solid #0c64eb;border-radius:4px 2px 2px 4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05),0 2px 6px 0 rgba(0,0,0,.045);box-shadow:0 1px 1px rgba(0,0,0,.05),0 2px 6px 0 rgba(0,0,0,.045)}#sidebarHeader:after{position:absolute;top:-1px;right:-8px;display:block;width:0;height:0;content:' ';border-color:transparent transparent transparent #fff;border-style:solid;border-width:18px 0 18px 8px}#sidebarHeader .title{padding:0 5px;overflow:hidden;font-size:14px;font-weight:700;line-height:32px;color:#0c64eb;text-align:center;text-overflow:ellipsis;white-space:nowrap}#sidebarHeader .title>a{position:absolute;top:0;right:0;width:20px;opacity:.5}#sidebarHeader .title>a:hover{opacity:1}#sidebar{position:relative;-webkit-transition:width .2s,padding .2s;-o-transition:width .2s,padding .2s;transition:width .2s,padding .2s}#sidebar>.sidebar-toggle{position:absolute;top:0;right:5px;bottom:0;width:10px;cursor:pointer;background:0 0;border-radius:5px;-webkit-transition:background-color .2s,opacity .5s;-o-transition:background-color .2s,opacity .5s;transition:background-color .2s,opacity .5s}#sidebar>.sidebar-toggle>.icon{position:absolute;top:50%;left:-1px;width:12px;height:30px;margin-top:-10px;line-height:30px;color:#fff;text-align:center;background:#79cdfb;border-radius:6px}#sidebar>.sidebar-toggle>.icon:before{position:relative;left:-1px}#sidebar>.sidebar-toggle:before{position:absolute;top:0;right:-5px;bottom:0;left:-5px;display:block;content:' '}#sidebar>.sidebar-toggle:hover{background:rgba(0,0,0,.075)}#sidebar>.cell{position:relative;left:0;width:180px;-webkit-transition:left .2s,opacity .2s;-o-transition:left .2s,opacity .2s;transition:left .2s,opacity .2s}#sidebar.no-animate>.cell{display:none;-webkit-transition:none;-o-transition:none;transition:none}.hide-sidebar #sidebar>.cell{position:absolute;left:-200px;visibility:hidden;opacity:0}.hide-sidebar #sidebar{position:relative;width:0;padding:0}.hide-sidebar #sidebar>.sidebar-toggle>.icon:before{content:"\e315"}@media (max-width:720px){#sidebar>.cell{width:100%}}#queryBox{max-height:0;padding:0;overflow:hidden;-webkit-transition:cubic-bezier(.175,.885,.32,1) .2s;-o-transition:cubic-bezier(.175,.885,.32,1) .2s;transition:cubic-bezier(.175,.885,.32,1) .2s;-webkit-transition-property:padding,max-height,margin;-o-transition-property:padding,max-height,margin;transition-property:padding,max-height,margin}#queryBox>form{visibility:hidden;-webkit-transition:visibility .2s .2s;-o-transition:visibility .2s .2s;transition:visibility .2s .2s}#queryBox.loading{height:50px}#queryBox.show{min-height:110px;max-height:300px;margin-bottom:10px;overflow:visible}#queryBox.show>form{visibility:visible}#queryBox.divider{border-bottom:1px solid #eee}#main .querybox-toggle.querybox-opened{position:relative;color:#0c64eb;background:0 0;border:none}#main .querybox-toggle.querybox-opened:before{position:absolute;bottom:-14px;left:50%;width:0;height:0;content:' ';border-color:transparent transparent #fff transparent;border-style:solid;border-width:0 10px 10px 10px}#contentNav{padding:5px;background:#fff;border-bottom:1px solid #eee}#contentNav .nav>li>a{position:relative;padding:6px 10px;color:#838a9d}#contentNav .nav>li.active>a{font-weight:700;color:#0c64eb}#contentNav .nav>li.active>a:before{position:absolute;right:10px;bottom:3px;left:10px;display:block;height:2px;content:' ';background:#0c64eb}.body-modal{padding-bottom:0}.body-modal #main,.body-modal .container{min-width:0!important}.body-modal #main{padding:0}.body-modal .main-header{position:fixed;top:0;right:20px;left:20px;z-index:100;padding:13px 48px 13px 0;margin:0;background:#fff}.body-modal #mainContent{padding-top:70px}.body-modal .main-header>h2{max-width:100%;overflow:hidden;font-size:14px;text-overflow:ellipsis;white-space:nowrap}.body-modal .cell,.body-modal .main-content{-webkit-box-shadow:none;box-shadow:none}.body-modal #mainMenu{position:fixed;top:0;right:0;left:0;z-index:100;padding:12px 60px 12px 10px;margin:0;background:#fff}.body-modal #mainMenu>.btn-toolbar.pull-left.divider{display:none}.body-modal #mainMenu>.btn-toolbar{width:100%;margin-left:20px}.body-modal #mainMenu>.btn-toolbar>.divider:first-child{display:none}.body-modal #mainMenu>.btn-toolbar .page-title{width:100%;margin-left:0}.body-modal #mainMenu>.btn-toolbar .page-title>.text{position:relative;top:-2px;display:inline-block;max-width:85%;max-width:-webkit-calc(100% - 100px);max-width:calc(100% - 100px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle}.body-modal #mainMenu+#mainContent.main-row{padding:60px 10px 0}.body-modal #mainMenu+#mainContent.main-row .cell{border:1px solid #efefef;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05),0 2px 6px 0 rgba(0,0,0,.045);box-shadow:0 1px 1px rgba(0,0,0,.05),0 2px 6px 0 rgba(0,0,0,.045)}.body-modal #mainActions{top:auto}.body-modal #mainActions .btn-toolbar{top:auto;bottom:10px}.body-modal.m-bug-view,.body-modal.m-story-view,.body-modal.m-task-view,.body-modal.m-testcase-view,.body-modal.m-testtask-view,.body-modal.m-todo-view{padding-bottom:20px;border-radius:3px}#tabsNav{position:relative}#tabsNav .tab-pane>.actions{position:absolute;top:-8px;right:0}#tabsNav .tab-pane>.cell,#tabsNav .tab-pane>.main-table{padding:0;border:1px solid #cbd0db;border-top:none;border-radius:0 0 4px 4px}#tabsNav .tab-pane>.cell .detail-title{padding-left:5px}#helpContent{position:fixed;top:50px;right:0;bottom:40px;left:0;display:none;background-color:#fff}#helpContent .load-error{display:none;padding:20px}#helpContent .show-error .load-error{display:block}.text-middle td,.text-middle th{vertical-align:middle}.text-center td,.text-center th{text-align:center}.c-sm{width:40px}.c-id{width:90px}.c-id-sm{width:70px}.c-id-xs{width:55px}.c-date{width:100px}.c-num,.c-pri,.c-type{width:80px;overflow:hidden}.c-begin,.c-end,.c-time{width:65px}.c-hours{width:60px}.c-actions-1{width:50px}.c-actions-2{width:75px}.c-actions-3{width:102px}.c-actions,.c-actions-4{width:128px}.c-actions-5{width:155px}.c-actions-6{width:180px}.c-product,.c-project{width:180px}.c-plan{width:130px}.c-datetime{width:120px}.c-stage,.c-status,.c-user{width:80px}.c-side{width:200px;border-right:10px solid #efefef}.c-assign,.c-assignedTo,.c-openedBy{width:130px}.c-progress{width:155px}.c-assign,.c-assignedTo,.c-openedBy,.c-product,.c-project,.c-status,.c-url,.c-user{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}td.c-name,td.c-title{overflow:hidden;text-align:left!important;text-overflow:ellipsis;white-space:nowrap}td.c-actions{position:relative;padding-top:0;padding-bottom:0;overflow:hidden;white-space:nowrap;vertical-align:middle}td.c-actions .btn-link{color:#3c495c;background:0 0}td.c-actions .btn-link:hover{color:#0c64eb;background:#e9f2fb}td.c-actions .more{position:absolute;top:50%;right:100%;display:none;padding-right:4px;padding-left:20px;margin-top:-15px;margin-right:-6px;white-space:nowrap;background-color:#fafafa;-webkit-transition:opacity .3s,margin .3s;-o-transition:opacity .3s,margin .3s;transition:opacity .3s,margin .3s}tr:hover td.c-actions .more{display:block}td:hover+td.c-actions>.more{margin-right:-15px;pointer-events:none;opacity:.15}tr[data-url]{cursor:pointer}.table tbody>tr>td,.table thead>tr>th{vertical-align:middle}.table tbody>tr>td.has-btn,.table thead>tr>th.has-btn{padding-top:1px;padding-bottom:1px;overflow:visible}.table tbody>tr>td .progress,.table thead>tr>th .progress{height:6px}.table .em,.table em{color:#3c4353}.table .divider{border-bottom:10px solid #efefef}.table .divider-top{border-top:10px solid #efefef}.table .btn-icon-left{max-width:100%;padding-left:20px;overflow:hidden;line-height:18px;text-align:left;text-overflow:ellipsis;background:0 0;border-color:#eaf3fc}.table .btn-icon-left>.icon{width:20px;font-size:14px;background:0 0!important;opacity:0}.table .btn-icon-left.btn-sm{height:26px;font-size:13px}.table .btn-icon-left:active,.table .btn-icon-left:focus,.table .btn-icon-left:hover{border-color:rgba(0,0,0,.2)}.table .btn-icon-left:active>.icon,.table .btn-icon-left:focus>.icon,.table .btn-icon-left:hover>.icon{opacity:1}.table .btn-icon-left>.text{padding-left:25px}.table thead>tr>th.c-assign,.table thead>tr>th.c-assignedTo{padding-left:29px}.table a{vertical-align:middle}.table tbody>tr:last-child{border-bottom:none}.table caption{margin-bottom:5px;background:#f1f1f1;border:none}.is-firefox .table .btn-icon-left>.icon{line-height:22px}.main-table{border-radius:4px}.main-table>.table,.main-table>.table-footer,.main-table>.table-header,.main-table>.table-responsive{-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05),0 2px 6px 0 rgba(0,0,0,.045);box-shadow:0 1px 1px rgba(0,0,0,.05),0 2px 6px 0 rgba(0,0,0,.045)}.main-table .table{font-size:13px;table-layout:fixed;background-color:#fff;border-radius:4px 4px 0 0}.main-table .table.table-lg{font-size:14px}.main-table .table .btn-icon-left{border-color:transparent}.main-table .table .btn-icon-left>.icon{background:0 0;border-radius:4px}.main-table .table .btn-icon-left.btn-sm{height:26px}.main-table .table .btn-icon-left:hover{border-color:rgba(0,0,0,.2)}.main-table .table .btn-icon-left:hover>.icon{background:#e9f2fb;border-radius:4px 0 0 4px}.main-table tbody>tr>td,.main-table thead>tr>th{min-height:36px;padding:2px 8px;line-height:30px}.main-table tbody>tr>td:first-child,.main-table thead>tr>th:first-child{padding-right:4px;padding-left:15px}.main-table thead>tr>th{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border-bottom:1px solid #ddd}.main-table tbody>tr:nth-child(odd){background-color:#f5f5f5}.main-table tbody>tr:last-child>td{border-bottom:1px solid #ddd}.main-table tbody>tr>td{position:relative;border-bottom:none;border-bottom:1px solid #eee}.main-table tbody>tr>td .label{max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.main-table tbody>tr>td>a{line-height:28px;color:#0c60e1}.main-table tbody>tr>td>a:not(.btn):visited{color:#082999;opacity:.9}.main-table tbody>tr>td>a:hover,.main-table tbody>tr>td>a:visited:hover{color:#0c64eb}.main-table tbody>tr>td.c-actions{padding-right:10px}.main-table tbody>tr>td.c-side+td:before,.main-table tbody>tr>td:first-child:before{position:absolute;top:0;bottom:0;left:0;display:block;width:0;content:'';background:#0c64eb;opacity:0;-webkit-transition:.2s linear;-o-transition:.2s linear;transition:.2s linear;-webkit-transition-property:width,opacity,border-radius;-o-transition-property:width,opacity,border-radius;transition-property:width,opacity,border-radius}@-moz-document url-prefix(){.main-table tbody>tr>td.c-side+td:before,.main-table tbody>tr>td:first-child:before{bottom:-1px}}.main-table tbody>tr>td.c-side:before{display:none}.main-table tbody>tr{-webkit-transition:.2s cubic-bezier(.175,.885,.32,1);-o-transition:.2s cubic-bezier(.175,.885,.32,1);transition:.2s cubic-bezier(.175,.885,.32,1);-webkit-transition-property:background-color,-webkit-box-shadow;-o-transition-property:box-shadow,background-color;transition-property:background-color,-webkit-box-shadow;transition-property:box-shadow,background-color;transition-property:box-shadow,background-color,-webkit-box-shadow}.main-table tbody>tr:hover{background:#e9f2fb}.main-table .table-grouped tbody>tr:hover{background:#f2f7fd;-webkit-box-shadow:none;box-shadow:none}.main-table .table-grouped tbody>tr:hover td.c-actions .more{background:#f2f7fd}.main-table tbody>tr.checked{background:#fff3e0}.main-table tbody>tr.checked:hover{background:#ffebbc}.main-table tbody>tr.checked>td.c-side+td:before,.main-table tbody>tr.checked>td:first-child:before{width:4px;opacity:1}.main-table tbody>tr.checked.row-check-begin{border-top-left-radius:4px;border-top-right-radius:2px}.main-table tbody>tr.checked.row-check-begin>td:first-child:before{border-top-left-radius:4px}.main-table tbody>tr.checked.row-check-end{border-bottom-right-radius:2px;border-bottom-left-radius:4px}.main-table tbody>tr.checked.row-check-end>td:first-child:before{border-bottom-left-radius:4px}.main-table .checkbox-primary{display:inline-block;line-height:20px}.main-table .checkbox-primary label{margin:0}.main-table .table{margin:0}.table-header{padding:4px 0 12px}.table-header .table-statistic{color:#838a9d}.table-header .table-statistic strong{font-size:15px;color:#3c4353}.table-header .btn-toolbar{margin-top:-28px}.table-header.fixed-right{position:relative;z-index:5;padding:0}.table-header.fixed-right>.btn-toolbar{position:absolute;top:1px;right:1px;z-index:1;padding:1px;margin:0;background:#fff;border-radius:4px}.table-header.fixed-right>.btn-toolbar .btn{opacity:.65}.table-header.fixed-right>.btn-toolbar .btn:hover{opacity:1}.table-header-fixed .table-header{position:fixed;top:0}.table-header-fixed .table-header>.btn-toolbar{background-color:transparent}.table-header-fixed .table-header>.btn-toolbar .btn{color:#fff}.table-footer{position:relative;min-height:40px;padding:6px 15px;background:#fff;border-radius:0 0 4px 4px}.body-modal .table-footer{margin-bottom:20px}.talbe-lg+.table-footer{padding:11px 15px}.table-footer .btn-toolbar,.table-footer .checkbox-primary{float:left}.table-footer .btn-toolbar+.btn-toolbar{margin-left:8px}.table-footer .checkbox-primary{margin:5px 20px 0 0}.table-footer .checkbox-primary.checked label:after{border-color:#00da88!important}.table-footer .btn{padding:3px 10px;line-height:20px}.table-footer .pager{position:absolute;top:0;right:0;z-index:4;height:40px;padding:6px 5px 6px 10px;margin:0;background:#fff;opacity:1;-webkit-transition:opacity .4s;-o-transition:opacity .4s;transition:opacity .4s}.table-footer .pager:before{position:absolute;top:0;bottom:0;left:-50px;display:block;width:50px;content:' ';background:-webkit-gradient(linear,left top,right top,from(rgba(255,255,255,0)),to(#fff));background:-webkit-linear-gradient(left,rgba(255,255,255,0) 0,#fff 100%);background:-o-linear-gradient(left,rgba(255,255,255,0) 0,#fff 100%);background:linear-gradient(to right,rgba(255,255,255,0) 0,#fff 100%)}.table-footer .pager .btn,.table-footer .pager>li>.pager-item,.table-footer .pager>li>.pager-label{color:#838a9d;background:0 0;border-color:transparent}.table-footer .pager .btn,.table-footer .pager>li>a{border-radius:3px}.table-footer .pager .btn:hover,.table-footer .pager>li>a:hover{background:rgba(0,0,0,.1)}.table-footer .pager>li.disabled>a.pager-item{opacity:1}.table-footer .form-control{height:28px;padding:3px 8px}.table-footer .table-statistic{position:relative;z-index:2;float:left;padding-right:30px;line-height:28px;color:#838a9d;background:#fff}.table-footer .table-statistic:hover{z-index:4}.table-footer .table-statistic:hover+.pager{z-index:2;opacity:.3}.table-footer .btn-toolbar+.table-statistic,.table-footer .btn-toolbar+.text{margin-left:10px}.table-footer .text{float:left;line-height:28px}.table-footer.fixed-footer{position:fixed;z-index:10;margin:0;background:rgba(75,75,75,.85);border-top-color:transparent}.table-footer.fixed-footer .checkbox-primary label{color:#fff}.table-footer.fixed-footer .checkbox-primary label:after{border-color:rgba(255,255,255,.8)}.table-footer.fixed-footer .table-statistic{color:#fff;background:0 0}.table-footer.fixed-footer .pager{background:#666}.table-footer.fixed-footer .pager:before{background:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,0)),to(#666));background:-webkit-linear-gradient(left,rgba(0,0,0,0) 0,#666 100%);background:-o-linear-gradient(left,rgba(0,0,0,0) 0,#666 100%);background:linear-gradient(to right,rgba(0,0,0,0) 0,#666 100%)}.table-footer.fixed-footer .pager .btn,.table-footer.fixed-footer .pager>li>.pager-item,.table-footer.fixed-footer .pager>li>.pager-label{color:#fff}.table-footer.fixed-footer .pager .btn:hover,.table-footer.fixed-footer .pager>li>a:hover{background:rgba(255,255,255,.3)}.table-footer.fixed-footer .pager>li.disabled>a.pager-item{opacity:.5}.table-actions{width:0;height:28px;visibility:hidden;opacity:0;-webkit-transition:opacity cubic-bezier(.175,.885,.32,1) .8s;-o-transition:opacity cubic-bezier(.175,.885,.32,1) .8s;transition:opacity cubic-bezier(.175,.885,.32,1) .8s}.table-actions.show-always{width:auto;pointer-events:none;cursor:not-allowed;visibility:visible;opacity:.75}.has-row-checked .table-actions{width:auto;pointer-events:auto!important;cursor:default;visibility:visible;opacity:1}.table-lg tbody>tr>td{padding:9px 10px}.table-lg tbody>tr>td .btn+.btn{margin-left:5px}.table.has-sort-head thead>tr>th{padding-right:0}.table.has-sort-head thead>tr>th>a{position:relative;display:inline-block;padding-right:16px;color:#3c4353}.table.has-sort-head thead>tr>th>a:after,.table.has-sort-head thead>tr>th>a:before{position:absolute;top:0;right:0;font-family:ZentaoIcon;font-size:14px;font-style:normal;font-weight:400;font-variant:normal;line-height:1;line-height:30px;color:#3c495c;text-transform:none;content:"\f0de";opacity:.5;speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.table.has-sort-head thead>tr>th>a:after{content:"\f0dd"}.table.has-sort-head thead>tr>th>a.sort-down,.table.has-sort-head thead>tr>th>a.sort-up{color:#000;text-decoration:none}.table.has-sort-head thead>tr>th>a:hover,.table.has-sort-head thead>tr>th>a:hover:after,.table.has-sort-head thead>tr>th>a:hover:before{color:#0c64eb;opacity:1}.table.has-sort-head thead>tr>th>a.sort-down:after,.table.has-sort-head thead>tr>th>a.sort-up:before{color:#000;opacity:1}.head-fixed .datatable-head-span .table,.table.fixed-header-copy{z-index:10;color:#fff;background:rgba(75,75,75,.85)}.head-fixed .datatable-head-span .table thead>tr>th,.table.fixed-header-copy thead>tr>th{color:#eee}.head-fixed .datatable-head-span .table thead>tr>th>a,.table.fixed-header-copy thead>tr>th>a{color:#eee}.head-fixed .datatable-head-span .table thead>tr>th>a:hover,.table.fixed-header-copy thead>tr>th>a:hover{color:#fff}.head-fixed .datatable-head-span .table thead>tr>th>a:after,.head-fixed .datatable-head-span .table thead>tr>th>a:before,.table.fixed-header-copy thead>tr>th>a:after,.table.fixed-header-copy thead>tr>th>a:before{color:#eee}.head-fixed .datatable-head-span .table thead>tr>th>a.sort-down,.head-fixed .datatable-head-span .table thead>tr>th>a.sort-down:after,.head-fixed .datatable-head-span .table thead>tr>th>a.sort-up,.head-fixed .datatable-head-span .table thead>tr>th>a.sort-up:before,.head-fixed .datatable-head-span .table thead>tr>th>a:hover,.head-fixed .datatable-head-span .table thead>tr>th>a:hover:after,.head-fixed .datatable-head-span .table thead>tr>th>a:hover:before,.table.fixed-header-copy thead>tr>th>a.sort-down,.table.fixed-header-copy thead>tr>th>a.sort-down:after,.table.fixed-header-copy thead>tr>th>a.sort-up,.table.fixed-header-copy thead>tr>th>a.sort-up:before,.table.fixed-header-copy thead>tr>th>a:hover,.table.fixed-header-copy thead>tr>th>a:hover:after,.table.fixed-header-copy thead>tr>th>a:hover:before{color:#fff}.head-fixed .datatable-head-span .table thead>tr>th>.dropdown>a,.table.fixed-header-copy thead>tr>th>.dropdown>a{color:#eee}.head-fixed .datatable-head-span .table thead>tr>th>.dropdown>a:hover,.table.fixed-header-copy thead>tr>th>.dropdown>a:hover{color:#fff}.head-fixed .datatable-head-span .table .checkbox-primary,.table.fixed-header-copy .checkbox-primary{z-index:1}.head-fixed .datatable-head-span .table .checkbox-primary label,.table.fixed-header-copy .checkbox-primary label{color:#fff}.head-fixed .datatable-head-span .table .checkbox-primary label:after,.table.fixed-header-copy .checkbox-primary label:after{border-color:rgba(255,255,255,.8)}.head-fixed .datatable-head-span .table .checkbox-primary.checked label:after,.table.fixed-header-copy .checkbox-primary.checked label:after{border-color:#00da88!important}.table-data{margin:0;table-layout:fixed}.table-data tbody>tr>td,.table-data tbody>tr>th{padding:6px 8px;word-break:break-all;border:none}.table-data tbody>tr>th{width:70px;padding-left:0;font-weight:400;color:#838a9d;text-align:right;vertical-align:middle}.table-data tbody>tr>td{padding-right:0}.table-data tbody>tr>td>a{color:#0c60e1}.table-data tbody>tr>td>a:not(.btn):visited{color:#082999}.table-data tbody>tr>td>a:hover,.table-data tbody>tr>td>a:visited:hover{color:#0c64eb}.table-data ol,.table-data ul{margin:0}.fixed-head-table{background:rgba(0,0,0,.7);border-bottom:1px solid #ddd}.fixed-head-table thead>tr>th{color:#fff}.table-empty-tip{padding:80px 10px;text-align:center;background:#fff}.not-firefox .table-grouped>tbody>tr>td.c-side{background:#fff!important}.table-grouped .group-toggle{cursor:pointer}.table-grouped .group-toggle.group-summary{border-top:10px solid #efefef}.table-grouped tbody>tr>td:first-child,.table-grouped thead>tr>th:first-child{padding-left:8px}.group-expand-all,.table-group-collapsed .group-collapse-all{display:none}.table-group-collapsed .group-expand-all{display:inline-block}.table-auto{table-layout:auto}.datatable .table>tbody>tr.checked.hover>td,.datatable .table>tbody>tr.checked>td.col-hover{background:#ffebbc}body.has-fixed-footer{padding-bottom:60px}.table.with-footer-fixed{margin-bottom:20px}.table-nest-hide{display:none!important}th.table-nest-title{position:relative;padding-left:30px!important}.table-nest-icon{position:relative;display:inline-block;width:22px;height:22px;font-size:16px;color:#a6aab8;text-align:center;border-radius:4px}.table-nest-toggle:before{line-height:22px;content:"\e6f2"}.table-nest-toggle:hover{color:#0c64eb;background-color:rgba(0,0,0,.1)}.table-nest-child-hide .table-nest-toggle:before{font-size:16px;content:"\e6f1"}th.table-nest-title .table-nest-toggle{position:absolute!important;top:7px;left:8px}.table-nest-toggle.table-nest-toggle-global{width:22px;height:22px;padding:0!important;line-height:22px;text-align:center;border-radius:4px}.table-nest-toggle.table-nest-toggle-global:before{position:static!important;font-size:16px!important;line-height:22px!important;content:"\e6f2"!important;opacity:1!important}.table-nest-toggle.table-nest-toggle-global:after{display:none!important}.table-nest-collapsed .table-nest-toggle.table-nest-toggle-global:before{font-size:16px!important;content:"\e6f1"!important}.disable-empty-nest-row .is-nest-child .table-nest-icon:before,.disable-empty-nest-row .no-nest .table-nest-icon:before{position:relative;top:-1px;width:6px;min-width:6px;height:6px;content:' ';background-color:#cbd0db;border-radius:1px}.table-nest-child-hover>td:first-child,.table-nest-hover>td:first-child{-webkit-box-shadow:inset 3px 0 0 #cbd0db;box-shadow:inset 3px 0 0 #cbd0db}.article-content{overflow:auto}.article-content img{margin-top:0}.article-content table{margin:10px 0}.article-content table td,.article-content table th{border:1px solid #cbd0db}.article-content table th{background:#eee}.article-content a{color:#0c64eb}.article-content a:focus,.article-content a:hover{color:#16a8f8}.article-content,.article>.content{word-wrap:break-word}.detail{padding:10px 0;margin:0 10px}.detail+.detail{padding-top:25px;border-top:1px solid #eee}.detail-title{font-size:14px;font-weight:700;line-height:20px}.detail-title>.pull-right{position:relative;top:-8px}h2.detail-title{margin:0;font-size:15px;font-weight:700}h2.detail-title .label,h2.detail-title .label-id{position:relative;top:-1px}.detail-content{padding:0;margin-top:10px}.detail-content em{color:#3c4353}.detail-content .list-unstyled>li+li{margin-top:5px}.side-col .detail-content{padding-left:0}details.detail{padding:10px 0}details.detail summary{position:relative;cursor:pointer;outline:0}details.detail summary::-webkit-details-marker{display:none}details.detail summary:after{position:absolute;top:0;right:0;font-family:ZentaoIcon;font-size:14px;font-style:normal;font-weight:400;font-variant:normal;line-height:1;text-transform:none;content:"\e316";opacity:.4;speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}details.detail[open] summary:after{content:"\e313"}.files-list{padding-left:0;list-style:none}.files-list>li>a{display:block;line-height:24px}.files-list>li>a>.icon{display:inline-block;margin-right:5px;opacity:.7}.files-list>li>a:hover{color:#0c64eb}.files-list>li>.right-icon{opacity:0;-webkit-transition:opacity .2s;-o-transition:opacity .2s;transition:opacity .2s}.files-list>li:hover>.right-icon{opacity:1}.histories-list{padding-left:15px;margin-bottom:0}.histories-list>li{position:relative}.histories-list>li+li{margin-top:5px}.histories-list>li strong{color:#3c4353}.histories-list .comment,.histories-list .show-form .comment-edit-form{padding:5px 5px 5px 10px;margin:5px 0 0;background-color:rgba(0,0,0,.025);border:1px solid #eee}.histories-list .btn-edit-comment{position:absolute;top:28px;right:2px}.histories-list .comment-edit-form,.histories-list .show-form .btn-edit-comment,.histories-list .show-form .comment{display:none}.histories-list .show-form .comment-edit-form{display:block;padding:10px;border:1px solid #eee}.histories .btn-mini{width:16px;min-width:16px;height:16px;overflow:hidden;line-height:16px;color:#cbd0db;vertical-align:-8%;border-radius:1px}.histories .btn-mini:focus,.histories .btn-mini:hover{color:#0c64eb;border-color:#0c64eb}.histories .show-changes .btn-expand>.icon:before{content:"\e926"}.histories .btn-strip{display:none}.histories .show-changes .btn-strip{display:inline-block}.history-changes{display:none;padding:5px;margin-bottom:-5px;margin-left:5px;font-size:12px;line-height:20px}.history-changes blockquote{padding:5px 5px 5px 10px;margin:5px 0 0;font-size:12px;background-color:rgba(0,0,0,.05);border-left:3px solid #eee}.history-changes blockquote.original{display:none}.show-changes .history-changes,.show-original .history-changes blockquote.original{display:block}.show-original .history-changes blockquote.textdiff{display:none}.syntaxhighlighter{overflow:auto}.list-group{overflow-y:auto}.list-group>a{display:block;padding:2px 10px 2px 5px;overflow:hidden;line-height:20px;text-overflow:ellipsis;white-space:nowrap;border-radius:4px}.list-group>a+a{margin-top:5px}.list-group>a>.icon{display:inline-block;margin-right:3px;opacity:.5}.list-group>a.selected{color:#e9f2fb;background-color:#0c64eb}.list-group>a.active{color:#0c64eb;background-color:#e9f2fb}.list-group>a.active:hover,.list-group>a:hover{color:#fff;background-color:#0c64eb}.list-group>.heading{padding:2px 5px;line-height:20px;color:#838a9d}.list-group>a+.heading{margin-top:4px}.dropup .search-box-sink{padding-top:5px;padding-bottom:45px}.dropup .search-box-sink .search-box{position:absolute;right:10px;bottom:10px;left:10px;margin:0}.dropup .search-box-sink .search-box+.list-group{height:auto;max-height:171px}.search-list{min-width:200px;max-width:300px;padding:0}.search-list .search-box{float:none;width:auto;margin:10px}.search-list .search-box .icon-search{opacity:.5}.search-list .list-group{max-height:248px;padding:5px 10px;margin:5px 0}.dropup .search-list .search-box+.list-group{height:171px;padding-top:0}.search-list .search-input{height:30px}.search-list .input-control-icon-right{height:28px;line-height:28px}.search-list .list-group>a.active{color:inherit;background-color:inherit}.search-list.searchbox-focus .list-group>a.active{color:#0c64eb;background-color:#e9f2fb}.search-list .list-group>a.active:hover,.search-list.searchbox-focus .list-group>a.active:hover{color:#fff;background-color:#0c64eb}#dropMenu{width:initial;max-width:initial}#dropMenu>.search-box{width:100%;padding:10px 10px 0;margin:0}#dropMenu>.search-box .icon-search{color:#333}#dropMenu>.search-box.has-icon-right>.form-control{padding-left:26px}#dropMenu .input-control-icon-left{top:10px;left:10px}#dropMenu .input-control-icon-right{top:11px;right:11px}#dropMenu .input-control-icon-right .icon{position:relative;top:2px}#dropMenu .list-group{max-height:initial;margin:0}#dropMenu .table-row{margin:0 -10px;table-layout:auto}#dropMenu .table-col{position:relative;width:100%;min-width:250px;max-width:450px}#dropMenu .table-col .list-group{max-height:300px;padding:0 10px 5px}#dropMenu .col-left{padding-bottom:30px}#dropMenu .col-right{display:none}#dropMenu .col-footer{position:absolute;right:0;bottom:0;left:0;padding:5px 10px;border-top:1px solid #eee}#dropMenu .col-footer>a{opacity:.8}#dropMenu .col-footer>a:hover{opacity:1}#dropMenu.show-right-col .table-col{width:50%}#dropMenu.show-right-col .col-right{display:table-cell;border-left:1px solid #eee}#dropMenu.show-right-col .col-right>.list-group{max-height:335px;margin:0}#dropMenu.show-right-col .col-right>.list-group>a{opacity:.7}#dropMenu.show-right-col .col-right>.list-group>a:hover{opacity:1}#dropMenu.show-right-col .toggle-right-col>.icon-angle-right:before{content:"\e314"}#dropMenu.has-search-text .list-group{overflow-x:hidden}#dropMenu.has-search-text>.search-box{width:100%!important}#dropMenu.has-search-text>.list-group>.table-row{display:block}#dropMenu.has-search-text>.list-group>.table-row>.table-col{display:block;width:100%}#dropMenu.has-search-text .col-left{padding-bottom:0}#dropMenu.has-search-text .pull-right.toggle-right-col{display:none}#dropMenu.has-search-text .col-left .list-group{margin-bottom:0}#dropMenu.has-search-text .col-right .list-group{margin-bottom:0}#dropMenu.has-search-text .col-right .list-group>a{opacity:.7}#dropMenu.has-search-text .col-footer,#dropMenu.has-search-text .hide-in-search{display:none}#swapper{position:relative}#swapper #dropMenu .list-group>a.active:hover,#swapper #dropMenu .search-box .list-group>a.active,#swapper #dropMenu.searchbox-focus .list-group>a.active:hover{color:#fff!important;background:#0c64eb!important}#swapper #dropMenu .tree{margin:0}.release-path{overflow:hidden}.release-line{display:table;width:100%;padding:0;table-layout:fixed}.release-line>li{display:table-cell;list-style:none}.release-line>li>a{position:relative;display:block}.release-line>li>a:before{position:absolute;left:0;display:block;width:13px;height:13px;content:' ';background:#fff;border:2px solid #838a9d;border-radius:50%}.release-line>li>a:after{position:absolute;left:5px;display:block;width:2px;height:30px;content:' ';background:#cbe0f6}.release-line>li>a>.icon{position:absolute;left:4px;font-size:24px}.release-line>li>a .title{display:block;font-size:14px;white-space:nowrap}.release-line>li>a .date,.release-line>li>a .info{display:block;max-height:18px;overflow:hidden;font-size:12px;color:#838a9d;text-overflow:ellipsis;white-space:nowrap}.release-line>li>a:hover:before{background-color:#e9f2fb}.release-line>li>a:hover:after{background-color:#838a9d}.release-line>li>a:hover .title{color:#0c64eb}.release-line>li>a:hover .date,.release-line>li>a:hover .info{color:#838a9d}.release-line>li:nth-child(odd){padding-top:80px;vertical-align:top}.release-line>li:nth-child(odd)>a{height:85px;padding-top:36px;border-top:5px solid #cbe0f6}.release-line>li:nth-child(odd)>a:before{top:-9px}.release-line>li:nth-child(odd)>a:after{top:6px}.release-line>li:nth-child(odd)>a>.icon{top:-26px}.release-line>li:nth-child(even){padding-bottom:80px;vertical-align:bottom}.release-line>li:nth-child(even)>a{height:85px;padding-bottom:36px;border-bottom:5px solid #cbe0f6}.release-line>li:nth-child(even)>a:before{bottom:-9px}.release-line>li:nth-child(even)>a:after{bottom:6px}.release-line>li:nth-child(even)>a>.icon{bottom:-2px}.release-line>li:last-child>a{border-color:transparent}.release-line>li.active>a:before{border-color:#0c64eb}.release-line>li+li>a>.date,.release-line>li+li>a>.info,.release-line>li+li>a>.title{position:relative;left:-36%}#footer{position:fixed;right:0;bottom:0;left:0;z-index:1010;height:40px;background:#fff;border-top:1px solid #eff1f7}#footer .breadcrumb{padding:10px 0;margin:0}#footer .breadcrumb>li{max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#footer .breadcrumb>.active,#footer .breadcrumb>li>a{color:#838a9e}#footer .breadcrumb>.active>.icon,#footer .breadcrumb>li>a>.icon{display:none}#footer .breadcrumb>.active:hover,#footer .breadcrumb>li>a:hover{color:#16a8f8}#footer .breadcrumb>li+li:before{content:'>'}#footer>.container{padding:0 20px}@media (min-width:1400px){#footer>.container{padding:0 40px}}#poweredBy{position:absolute;top:4px;right:0;padding:5px 10px}#poweredBy .icon-zentao{color:#0097fd}#poweredBy a{color:#3c4353}#poweredBy a:hover{color:#0c64eb}#poweredBy a:hover .icon-zentao{color:#0c64eb}#poweredBy a.text-important{color:#bd7b46}#poweredBy a.text-important:hover{color:#ff5d5d}#poweredBy a.text-primary{color:#0c64eb}#poweredBy a.text-primary:hover{color:#16a8f8}#poweredBy #aiux{color:#cbd0dc}#noticeBox .alert{-webkit-box-shadow:rgba(0,0,0,.15) 0 3px 10px,rgba(0,0,0,.25) 0 3px 10px;box-shadow:rgba(0,0,0,.15) 0 3px 10px,rgba(0,0,0,.25) 0 3px 10px}#heading{top:0}.header-btn{position:relative;padding:8px 0}.header-btn .btn{position:relative;height:34px;padding:1px 6px;margin:0;overflow:visible;font-size:13px;font-weight:400;line-height:28px;color:#fff;background-color:transparent;border-color:transparent!important;border-right:none;-webkit-transition:none;-o-transition:none;transition:none}.header-btn .btn>.caret{margin-left:0;border-width:4px}.header-btn .btn>.text{display:inline-block;max-width:150px;overflow:hidden;text-overflow:ellipsis;vertical-align:middle}.header-btn .btn:hover{-webkit-box-shadow:none;box-shadow:none}.header-btn .btn:hover,.header-btn.active .btn{color:#fff;background:rgba(0,0,0,.15)}.header-btn .btn:hover>.caret,.header-btn.active .btn>.caret{opacity:1}.header-btn+.header-btn{margin-left:10px}.header-btn+.header-btn:before{position:absolute;top:16px;left:-13px;display:block;font-family:ZentaoIcon;font-size:14px;font-size:16px;font-style:normal;font-weight:400;font-variant:normal;line-height:1;text-transform:none;content:"\e315";opacity:.6;speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.header-btn .dropdown-menu{margin-top:-10px}#toolbar{top:0;height:50px}#userNav>li{margin-right:0}#userNav>li>a{padding:10px 5px}#userNav>li>a>.icon{font-size:30px;filter:brightness(1.2) hue-rotate(30deg);opacity:.9;-webkit-filter:brightness(1.2) hue-rotate(30deg)}#userNav>li:hover>a{background-color:rgba(0,0,0,.1)}#userNav .dropdown-menu>li>a{position:relative;padding-left:24px}#userNav .dropdown-menu>li>a>.icon{top:1px;left:0}#userNav .dropdown-menu>li.user-profile-item>a{padding-left:45px} diff --git a/xuanxuan/module/im/ext/config/xuanxuan.php b/xuanxuan/module/im/ext/config/xuanxuan.php index 617c1a8b09..2f3e26a02e 100644 --- a/xuanxuan/module/im/ext/config/xuanxuan.php +++ b/xuanxuan/module/im/ext/config/xuanxuan.php @@ -14,3 +14,4 @@ $config->im->cards['task']['view'] = array('width' => '700px', 'height' => ' $config->im->cards['doc']['view'] = array('width' => '700px', 'height' => '500px'); $config->im->cards['story']['view'] = array('width' => '700px', 'height' => '500px'); $config->im->cards['testcase']['view'] = array('width' => '700px', 'height' => '500px'); +$config->im->cards['productplan']['view'] = array('width' => '700px', 'height' => '447px'); diff --git a/xuanxuan/module/index/ext/control/index.php b/xuanxuan/module/index/ext/control/index.php new file mode 100644 index 0000000000..aff59ef5da --- /dev/null +++ b/xuanxuan/module/index/ext/control/index.php @@ -0,0 +1,39 @@ +view->pageBodyClass = 'xxc-embed'; + } + else + { + $this->loadModel('im'); + + $xuanConfig = new stdclass(); + $token = $this->im->userGetAuthToken($this->app->user->id, 'zentaoweb'); + $clientUrl = isset($this->config->webClientUrl) ? $this->config->webClientUrl : 'data/xuanxuan/web/'; + + $xuanConfig->clientUrl = $clientUrl; + $xuanConfig->server = $this->im->getServer('zentao'); + $xuanConfig->account = $this->app->user->account; + $xuanConfig->authKey = $token->token; + $xuanConfig->debug = $this->config->debug; + $xuanConfig->serverTime = (int)(microtime(true) * 1000); + $xuanConfig->serverNowTime = (int)round(time() / $tokenAuthWindow); + + $this->view->xuanConfig = $xuanConfig; + } + + return parent::index($open); + } +} diff --git a/xuanxuan/module/index/ext/css/index/xuanxuan.css b/xuanxuan/module/index/ext/css/index/xuanxuan.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/xuanxuan/module/index/ext/js/index/xuanxuan.js b/xuanxuan/module/index/ext/js/index/xuanxuan.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/xuanxuan/module/index/ext/view/index.xuanxuan.html.hook.php b/xuanxuan/module/index/ext/view/index.xuanxuan.html.hook.php new file mode 100644 index 0000000000..196eb44690 --- /dev/null +++ b/xuanxuan/module/index/ext/view/index.xuanxuan.html.hook.php @@ -0,0 +1,73 @@ + + + + + + + + + + + + diff --git a/xuanxuan/www/data/xuanxuan/sdk/sdk.min.js b/xuanxuan/www/data/xuanxuan/sdk/sdk.min.js new file mode 100644 index 0000000000..7d4c3078be --- /dev/null +++ b/xuanxuan/www/data/xuanxuan/sdk/sdk.min.js @@ -0,0 +1,4 @@ +(()=>{var u=Object.defineProperty;var m=Object.getOwnPropertySymbols;var _=Object.prototype.hasOwnProperty,g=Object.prototype.propertyIsEnumerable;var b=(t,e,i)=>e in t?u(t,e,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[e]=i,r=(t,e)=>{for(var i in e||(e={}))_.call(e,i)&&b(t,i,e[i]);if(m)for(var i of m(e))g.call(e,i)&&b(t,i,e[i]);return t};function y(t){return typeof t=="number"?t===0?0:`${t}px`:t}function f(t,e,i){i=i||document;let o=e.querySelector("style");o||(o=i.createElement("style"),e.appendChild(o)),o.innerHTML="",o.appendChild(i.createTextNode(t))}function w(){let t=document.getElementById("xx-embed-container");return t||(t=document.createElement("div"),t.id="xx-embed-container",f(["#xx-embed-container {position:fixed;z-index:1200;top:0;bottom:0;right:0;left:0;justify-content:center;align-items:center;pointer-events:none}","#xx-embed-container .xx-embed {position:absolute;pointer-events:auto;background-color:#fff;border:1px solid #ddd;box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);display:flex;flex-direction:column;}","#xx-embed-container .xx-embed-has-animation {transition:.2s;transition-property:width,height,left,top,bottom,right;}","#xx-embed-container .xx-embed-header {flex:none;display:flex;flex-direction:row;align-items:center;justify-content:space-between;background-color:#f1f1f1;user-select:none;border-bottom: 1px solid #ddd;}","#xx-embed-container .xx-embed-hide-header .xx-embed-header {display: none!important}","#xx-embed-container .xx-embed-title {padding:0 5px;font-size:13px;opacity:.7;font-weight:bold; display: flex; align-items:center;}","#xx-embed-container .xx-embed-notice-badge {line-height: 14px; min-width: 14px; padding: 0 4px; border-radius: 7px; background-color: #ff0040; color: #fff; font-size:12px;box-sizing: border-box;text-align:center;margin-right:4px}","#xx-embed-container .xx-embed-nav {display:flex;flex-direction:row;align-items:center;}","#xx-embed-container .xx-embed-btn {display:flex;width:24px;height:24px;align-items:center;justify-content:center;opacity:.5;cursor:pointer}","#xx-embed-container .xx-embed-btn:hover {opacity:1;background-color:rgba(0,0,0,.1)}","#xx-embed-container .xx-embed-btn-expand {display:none}","#xx-embed-container .xx-embed-collapsed .xx-embed-btn-expand {display:block}","#xx-embed-container .xx-embed-collapsed .xx-embed-btn-collapse {display:none}","#xx-embed-container .xx-embed-collapsed {height:24px!important;width:200px!important}","#xx-embed-container .xx-embed-collapsed .xx-embed-body {display:none!important}","#xx-embed-container .xx-embed-hidden {display:none!important}","#xx-embed-container .xx-embed-body {flex:auto;position:relative}","#xx-embed-container .xx-embed-iframe {width:100%;height:100%;position:absolute}","#xx-embed-container .xx-embed-close-confirm {display:none;position:absolute;left:0;top:0;right:0;bottom:0;z-index:10;background-color:rgba(0,0,0,0.5);}","#xx-embed-container .xx-embed-close-confirm-shown .xx-embed-close-confirm {display:flex;justify-content:center;align-items:center;}","#xx-embed-container .xx-embed-close-confirm-dialog {background-color:#fff;padding:20px 20px;box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);border-radius:4px}","#xx-embed-container .xx-embed-close-confirm-text {margin-bottom: 15px}","#xx-embed-container .xx-embed-close-confirm-btn {display: inline-block; padding: 5px 10px; margin-right: 10px; background-color: #f1f1f1; color: #A3A2BC; border: 1px solid #A3A2BC; min-width: 80px; text-align: center;border-radius:4px; cursor:pointer}","#xx-embed-container .xx-embed-close-confirm-btn-confirm {background-color: #6129c4; color: #fff; border-color: #6129c4;}"].join(` +`),t),document.body.appendChild(t)),t}function L(t,e,i){let o={width:e,height:i};t==="bottom-right"?Object.assign(o,{bottom:0,right:0}):t==="bottom-left"?Object.assign(o,{bottom:0,left:0}):t==="bottom-center"?Object.assign(o,{bottom:0,left:Math.floor((window.innerWidth-e)/2)}):t==="bottom"?Object.assign(o,{bottom:0,left:0,width:"100%"}):t==="left"?Object.assign(o,{bottom:0,left:0,height:"100%"}):t==="right"?Object.assign(o,{bottom:0,right:0,height:"100%"}):t==="top"?Object.assign(o,{top:0,left:0,width:"100%"}):t==="top-center"?Object.assign(o,{top:0,left:Math.floor((window.innerWidth-e)/2)}):t==="center"?Object.assign(o,{left:Math.floor((window.innerWidth-e)/2),top:Math.floor((window.innerHeight-i)/2)}):t&&typeof t=="object"&&Object.assign(o,t);let n=[];for(let s in o)n.push(`${s}:${y(o[s])}`);return n.join(";")}var a=0;function v(){if(document.body.clientWidth>=window.innerWidth)return 0;if(!a){let t=document.createElement("div");t.className="scrollbar-measure",t.style.position="absolute",t.style.top="-9999px",t.style.width="50px",t.style.height="50px",t.style.overflow="scroll",document.body.appendChild(t),a=t.offsetWidth-t.clientWidth,document.body.removeChild(t)}return a}function C(){if(v()){let t=parseInt(getComputedStyle(document.body)["padding-right"]||0,10);return a&&(document.body.style.paddingRight=`${t+a}px`,document.body.style.overflowY="hidden"),a}return 0}function S(){document.body.style.paddingRight="",document.body.style.overflowY=""}var T=Date.now(),d={clientUrl:"./client/",showHeader:!0,width:800,height:480,position:"bottom-right",debug:!1,lang:"zh-cn",animation:!0,autoUpdateTitle:!1,showNoticeBadge:!0,hideOnCollapse:!1,closeAsCollapse:!1,showCollapseButtons:!0,closeBtnHtml:'',expandBtnHtml:'',collapseBtnHtml:''},j=100,c=new Map,h={"zh-cn":{title:"\u55A7\u55A7",closeConfirm:"\u8981\u9000\u51FA\u804A\u5929\u5417\uFF1F",confirm:"\u786E\u5B9A",cancel:"\u53D6\u6D88",collapse:"\u6536\u8D77",expand:"\u5C55\u5F00",close:"\u5173\u95ED"},"zh-tw":{title:"\u55A7\u55A7",closeConfirm:"\u8981\u9000\u51FA\u804A\u5929\u55CE\uFF1F",confirm:"\u78BA\u5B9A",cancel:"\u53D6\u6D88",collapse:"\u6536\u8D77",expand:"\u5C55\u958B",close:"\u95DC\u9589"},en:{title:"Xuanxuan",closeConfirm:"Are you sure to quit chat?",confirm:"Confirm",cancel:"Cancel",collapse:"Collapse",expand:"Expand",close:"Quit"}},x=class{constructor(e){this._options=r(r({},d),e),this._id=`xx-${(T++).toString(36)}`,this._zIndex=j++,this._element=null,this._showed=!1,this._collapsed=!1,this._iframeWindow=null,this._noticeCount=0,this._noticeInfo={},this.disableScrollbar=this.disableScrollbar.bind(this),this.enableScrollbar=this.enableScrollbar.bind(this),this._lang=this._options.lang&&typeof this._options.lang=="object"?Object.assign({},h[d.lang],this._options.lang):(typeof this._options.lang=="string"?h[this._options.lang]:null)||h[d.lang],(this._options.show||this._options.preload)&&this.show(),c.set(this._id,this),this._createTime=Date.now(),this._options.onCreated&&this._options.onCreated.call(this,this)}get id(){return this._id}get shown(){return this._showed}get collapsed(){return!this._collapsed}get url(){let e=[this._options.clientUrl,this._options.clientUrl.indexOf("?")<0?"?":"&","&origin=",encodeURIComponent(window.location.origin),"&embedId=",encodeURIComponent(this._id)];return this._options.account&&e.push("&account=",encodeURIComponent(this._options.account)),this._options.server&&e.push("&server=",encodeURIComponent(this._options.server)),this._options.authKey&&e.push("&authKey=",encodeURIComponent(this._options.authKey)),this._options.injectCss&&e.push("&embedCss=",encodeURIComponent(this._options.injectCss)),this._options.injectStyle&&e.push("&embedStyle=",encodeURIComponent(this._options.injectStyle)),this._options.injectScript&&e.push("&embedScript=",encodeURIComponent(this._options.injectScript)),this._options.chat?e.push("#/chats/recents/",this._options.chat):this._options.path&&e.push(this._options.path[0]==="#"?"":"#",this._options.path),e.join("")}get hasAccountInfo(){return this._options.server&&this._options.account&&this._options.authKey}get createTime(){return this._createTime}get loadedTime(){return this._loadedTime}get startTime(){return this._startTime}get firstLoginTime(){return this._firstLoginTime}get lastLoginTime(){return this._lastLoginTime}get noticeCount(){return this._noticeCount}get noticeInfo(){return this._noticeInfo}handleMessage(e,i){switch(e){case"titleUpdated":this._options.autoUpdateTitle&&i&&i[0]&&this._element&&(this._element.querySelector(".xx-embed-title-text").innerText=i[0]),this._options.onTitleUpdate&&this._options.onTitleUpdate.call(this,...i);break;case"login":this._lastLoginTime=Date.now(),this._firstLoginTime||(this._firstLoginTime=this._lastLoginTime),this._options.onLogin&&this._options.onLogin.call(this,...i);break;case"logout":this._options.onLogout&&this._options.onLogout.call(this,...i),this._options.closeOnLogout&&this.close(!0);break;case"loaded":this._loadedTime=Date.now(),this._options.onLoaded&&this._options.onLoaded.call(this,...i);break;case"start":this._startTime=Date.now(),this._options.onStart&&this._options.onStart.call(this,...i);break;case"sendMessage":this._options.onSendMessage&&this._options.onSendMessage.call(this,...i);break;case"receiveMessage":this._options.onReceiveMessage&&this._options.onReceiveMessage.call(this,...i);break;case"notice":if(this._options.showNoticeBadge&&this._element){let o=i[0].total,n=this._element.querySelector(".xx-embed-notice-badge");n.style.display=o&&o>0?"inline-block":"none",n.innerText=o||"",this._noticeCount=o,this._noticeInfo=i[0]}this._options.onNotice&&this._options.onNotice.call(this,...i);break;default:this._options.onClientEvent&&this._options.onClientEvent.call(this,e,...i);break}}postMessage(e,...i){let n=document.getElementById(`xx-embed-iframe-${this._id}`).contentWindow,s=new URL(this._options.clientUrl);try{n.postMessage([this._id,e,i],s.origin)}catch(p){if(console.info(`Cannot send message to client for type "${e}"`,i),Array.isArray(i)){n.postMessage([this._id,e,JSON.parse(JSON.stringify(i))],s.origin);return}console.error(`Cannot send message to client for type "${e}"`,i)}}executeCommand(e,...i){this.postMessage("executeCommand",e,...i)}executeCommandLine(e){this.postMessage("executeCommandLine",e)}show(e){if(this._options.preload&&this._createTime&&e&&typeof e=="object"){let s=!1;if(e.server&&e.server!==this._options.server&&(this._options.server=e.server,s=!0),e.account&&e.account!==this._options.account&&(this._options.account=e.account,s=!0),e.authKey&&e.authKey!==this._options.authKey&&(this._options.authKey=e.authKey,s=!0),s){this.postMessage("requestLogin",{server:this._options.server,account:this._options.account,authKey:this._options.authKey}),setTimeout(()=>{this.show()},500);return}}let i=this._id;if(!this._element){let s=document.createElement("div");s.id=i,s.classList.add("xx-embed"),s.classList.add("xx-embed-hidden"),this._options.showHeader||s.classList.add("xx-embed-hide-header");let p=['
    ',`
    ${this._options.title||this._lang.title}
    `,'","
    ",'
    ',``,"
    ",'
    ','
    ','
    ',this._lang.closeConfirm,"
    ",'","
    ","
    "];s.innerHTML=p.join(` +`),this._options.animation&&s.classList.add("xx-embed-has-animation"),w().appendChild(s),this._element=s,this._options.showCollapseButtons&&(s.querySelector(".xx-embed-btn-expand").addEventListener("click",l=>{this.expand(),l.stopPropagation()}),s.querySelector(".xx-embed-btn-collapse").addEventListener("click",l=>{this.collapse(),l.stopPropagation()})),s.querySelector(".xx-embed-btn-close").addEventListener("click",l=>{this._options.closeAsCollapse?this.collapse():this.close(),l.stopPropagation()}),s.querySelector(".xx-embed-header").addEventListener("dblclick",l=>{this.toggleCollapse()}),s.querySelector(".xx-embed-close-confirm-btn-confirm").addEventListener("click",()=>{this.close(!0)}),s.querySelector(".xx-embed-close-confirm-btn-cancel").addEventListener("click",()=>{this.cancelClose()}),s.addEventListener("mouseenter",this.disableScrollbar),s.addEventListener("mouseleave",this.enableScrollbar)}let o=L(this._options.position||"bottom-right",this._options.width,this._options.height),n=[`#${i} {${o}}`,`#${i} .xx-embed-body {min-width: ${this._options.width}px; min-height: ${this._options.height-24}px}`];typeof this._options.cssStyle=="string"&&n.push(this._options.cssStyle.replace(/#id/g,`#${this._id}`)),f(n.join(` +`),this._element),(!this._options.preload||this._createTime)&&(this._element.classList.remove("xx-embed-hidden"),this._collapsed&&this.expand(),this._showed=!0,this._options.onShow&&this._options.onShow(this))}redirect(e){e.startsWith("#")||(e=`#${e}`),this.postMessage("setRoute",e)}hide(){!this._showed||!this._element||(this._element.classList.add("xx-embed-hidden"),this._showed=!1,this._options.onHide&&this._options.onHide(this))}toggle(){this._showed?this.hide():this.show()}collapse(){if(!(!this._showed||!this._element||this._collapsed)){if(this._options.hideOnCollapse){this.hide(),this._options.onCollapse&&this._options.onCollapse(this);return}this._element.classList.add("xx-embed-collapsed"),this._collapsed=!0,this._options.onCollapse&&this._options.onCollapse(this)}}expand(){!this._showed||!this._element||!this._collapsed||(this._element.classList.remove("xx-embed-collapsed"),this._collapsed=!1,this._options.onExpand&&this._options.onExpand(this))}toggleCollapse(){this._collapsed?this.expand():this.collapse()}reload(){!this._element||(document.getElementById(`xx-embed-iframe-${this._id}`).src=this.url,this._options.onLoad&&this._options.onLoad(this))}close(e){if(!!this._element&&!(this._options.onClose&&this._options.onClose(this)===!1)){if(!e){this._showed?this.expand():this.show(),this._element.classList.add("xx-embed-close-confirm-shown");return}this.postMessage("logout"),this._element.classList.add("xx-embed-hidden"),this._showed=!1,setTimeout(()=>{this._element.removeEventListener("mouseenter",this.disableScrollbar),this._element.removeEventListener("mouseleave",this.enableScrollbar),this._element.remove(),this._element=null},1e3),this._options.onClosed&&this._options.onClosed(this),c.delete(this._id)}}cancelClose(){!this._element||this._element.classList.remove("xx-embed-close-confirm-shown")}disableScrollbar(){let e=C();e&&(this._element.style.transform=`translateX(-${e}px)`)}enableScrollbar(){S(),this._element.style.transform=""}};x.setGlobalOptions=t=>{Object.assign(d,t)};window.addEventListener("message",t=>{if(!Array.isArray(t.data))return;let e=c.get(t.data[0]);!e||e.handleMessage(t.data[1],t.data[2])},!1);window.Xuanxuan=x;})(); diff --git a/xuanxuan/www/data/xuanxuan/zentao-integrated.zip b/xuanxuan/www/data/xuanxuan/zentao-integrated.zip index 75d0f95e28..064a737660 100644 Binary files a/xuanxuan/www/data/xuanxuan/zentao-integrated.zip and b/xuanxuan/www/data/xuanxuan/zentao-integrated.zip differ