diff --git a/config/zentaopms.php b/config/zentaopms.php index 5d1ef5ed25..b7b187ba3a 100644 --- a/config/zentaopms.php +++ b/config/zentaopms.php @@ -245,6 +245,7 @@ define('TABLE_KANBANCOLUMN', '`' . $config->db->prefix . 'kanbancolumn`'); define('TABLE_KANBANORDER', '`' . $config->db->prefix . 'kanbanorder`'); define('TABLE_KANBANGROUP', '`' . $config->db->prefix . 'kanbangroup`'); define('TABLE_KANBANCARD', '`' . $config->db->prefix . 'kanbancard`'); +define('TABLE_KANBANCELL', '`' . $config->db->prefix . 'kanbancell`'); if(!defined('TABLE_LANG')) define('TABLE_LANG', '`' . $config->db->prefix . 'lang`'); if(!defined('TABLE_PROJECTSPEC')) define('TABLE_PROJECTSPEC', '`' . $config->db->prefix . 'projectspec`'); diff --git a/db/update16.1.sql b/db/update16.1.sql new file mode 100644 index 0000000000..64a39a5490 --- /dev/null +++ b/db/update16.1.sql @@ -0,0 +1,15 @@ +CREATE TABLE `zt_kanbancell` ( + `id` int(8) NOT NULL AUTO_INCREMENT, + `kanban` mediumint(8) NOT NULL, + `lane` mediumint(8) NOT NULL, + `column` mediumint(8) NOT NULL, + `type` char(30) NOT NULL, + `cards` text NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `card_group` (`kanban`,`type`,`lane`,`column`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +ALTER TABLE `zt_kanban` ADD `displayCards` smallint(6) NOT NULL default '0' AFTER `order`; +ALTER TABLE `zt_project` ADD `displayCards` smallint(6) NOT NULL default '0' AFTER `order`; + +UPDATE `zt_grouppriv` SET `method` = 'taskKanban' WHERE `module` = 'execution' AND `method` = 'kanban'; diff --git a/db/zentao.sql b/db/zentao.sql index c733278613..6d76a6175f 100644 --- a/db/zentao.sql +++ b/db/zentao.sql @@ -677,6 +677,7 @@ CREATE TABLE `zt_kanban` ( `archived` enum('0', '1') NOT NULL DEFAULT '0', `status` enum('active','closed') NOT NULL default 'active', `order` mediumint(8) NOT NULL DEFAULT '0', + `displayCards` smallint(6) NOT NULL default '0', `createdBy` char(30) NOT NULL, `createdDate` datetime NOT NULL, `lastEditedBy` char(30) NOT NULL, @@ -706,8 +707,6 @@ CREATE TABLE `zt_kanbancard` ( `kanban` mediumint(8) unsigned NOT NULL, `region` mediumint(8) unsigned NOT NULL, `group` mediumint(8) unsigned NOT NULL, - `lane` mediumint(8) unsigned NOT NULL, - `column` mediumint(8) unsigned NOT NULL, `name` varchar(255) NOT NULL, `pri` mediumint(8) unsigned NOT NULL, `assignedTo` text NOT NULL, @@ -731,6 +730,17 @@ CREATE TABLE `zt_kanbancard` ( `deleted` enum('0', '1') NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; +-- DROP TABLE IF EXISTS `zt_kanbancell`; +CREATE TABLE `zt_kanbancell` ( + `id` int(8) NOT NULL AUTO_INCREMENT, + `kanban` mediumint(8) NOT NULL, + `lane` mediumint(8) NOT NULL, + `column` mediumint(8) NOT NULL, + `type` char(30) NOT NULL, + `cards` text NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `card_group` (`kanban`,`type`,`lane`,`column`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; -- DROP TABLE IF EXISTS `zt_kanbangroup`; CREATE TABLE `zt_kanbangroup` ( `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, @@ -1010,6 +1020,7 @@ CREATE TABLE IF NOT EXISTS `zt_project` ( `acl` char(30) NOT NULL DEFAULT 'open', `whitelist` text NOT NULL, `order` mediumint(8) unsigned NOT NULL, + `displayCards` smallint(6) NOT NULL default '0', `deleted` enum('0','1') NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `parent` (`parent`), diff --git a/module/action/model.php b/module/action/model.php index 638ff6d193..a99810be8b 100755 --- a/module/action/model.php +++ b/module/action/model.php @@ -863,7 +863,7 @@ class actionModel extends model /* Get actions. */ $actions = $this->dao->select('*')->from(TABLE_ACTION) - ->where(1) + ->where('objectType')->notIN('kanbanregion,kanbanlane,kanbancolumn') ->beginIF($period != 'all')->andWhere('date')->gt($begin)->fi() ->beginIF($period != 'all')->andWhere('date')->lt($end)->fi() ->beginIF($date)->andWhere('date' . ($direction == 'next' ? '<' : '>') . "'{$date}'")->fi() @@ -1291,6 +1291,12 @@ class actionModel extends model } $action->objectLink = helper::createLink($moduleName, $methodName, $params); + if($action->objectType == 'execution') + { + $execution = $this->loadModel('execution')->getById($action->objectID); + if(!empty($execution) and $execution->type == 'kanban') $action->objectLink = helper::createLink('execution', 'kanban', "executionID={$action->objectID}"); + } + if($action->objectType == 'doclib') { $docLib = $this->dao->select('type,product,project,execution,deleted')->from(TABLE_DOCLIB)->where('id')->eq($action->objectID)->fetch(); diff --git a/module/block/control.php b/module/block/control.php index 42f7a744d0..cbcc420032 100644 --- a/module/block/control.php +++ b/module/block/control.php @@ -905,7 +905,7 @@ class block extends control foreach($projects as $projectID => $project) { - if($project->model == 'scrum') + if($project->model == 'scrum' or $project->model == 'kanban') { $this->app->loadClass('pager', $static = true); $pager = pager::init(0, 3, 1); diff --git a/module/block/view/projectstatisticblock.html.php b/module/block/view/projectstatisticblock.html.php index 2fec19c385..a64091dab3 100644 --- a/module/block/view/projectstatisticblock.html.php +++ b/module/block/view/projectstatisticblock.html.php @@ -134,7 +134,7 @@ $(function()
- model == 'scrum'):?> + model == 'scrum' or $project->model == 'kanban'):?>

block->story;?>

diff --git a/module/bug/control.php b/module/bug/control.php index 5b92bfdbc9..a2b841db73 100644 --- a/module/bug/control.php +++ b/module/bug/control.php @@ -693,8 +693,9 @@ class bug extends control /* If executionID is setted, get builds and stories of this execution. */ if($executionID) { - $builds = $this->loadModel('build')->getBuildPairs($productID, $branch, 'noempty', $executionID, 'execution'); - $stories = $this->story->getExecutionStoryPairs($executionID); + $builds = $this->loadModel('build')->getBuildPairs($productID, $branch, 'noempty', $executionID, 'execution'); + $stories = $this->story->getExecutionStoryPairs($executionID); + $execution = $this->loadModel('execution')->getById($executionID); } else { @@ -737,7 +738,7 @@ class bug extends control $showFields = trim($showFields, ','); } - $projectID = $this->lang->navGroup->bug == 'project' ? $this->session->project : 0; + $projectID = $this->lang->navGroup->bug == 'project' ? $this->session->project : (isset($execution) ? $execution->project : 0); $this->view->customFields = $customFields; $this->view->showFields = $showFields; diff --git a/module/common/lang/common.php b/module/common/lang/common.php index 63a6bbf985..cb73ca4326 100644 --- a/module/common/lang/common.php +++ b/module/common/lang/common.php @@ -117,6 +117,7 @@ $lang->icons['mail'] = 'envelope'; $lang->icons['trash'] = 'trash'; $lang->icons['extension'] = 'th-large'; $lang->icons['app'] = 'th-large'; +$lang->icons['kanban'] = 'kanban'; $lang->icons['results'] = 'list-alt'; $lang->icons['create'] = 'plus'; diff --git a/module/common/lang/en.php b/module/common/lang/en.php index 0ffad68872..24cfef7f6d 100644 --- a/module/common/lang/en.php +++ b/module/common/lang/en.php @@ -157,6 +157,7 @@ $lang->repo->common = 'Code'; $lang->report->common = 'Statistic'; $lang->system->common = 'System'; $lang->admin->common = 'Admin'; +$lang->story->common = 'Story'; $lang->task->common = 'Task'; $lang->bug->common = 'Bug'; $lang->testcase->common = 'Testcase'; diff --git a/module/common/lang/menu.php b/module/common/lang/menu.php index 3bf77d2863..547d96b935 100644 --- a/module/common/lang/menu.php +++ b/module/common/lang/menu.php @@ -289,6 +289,11 @@ $lang->waterfall->menu->design['subMenu']->dbds = array('link' => "{$lang->d $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 . ''); +/* Kanban project menu. */ +$lang->kanban->menu = new stdclass(); +$lang->kanban->menuOrder = array(); +$lang->kanban->dividerMenu = ''; + /* Execution menu. */ $lang->execution->homeMenu = new stdclass(); $lang->execution->homeMenu->all = array('link' => "{$lang->execution->all}|execution|all|", 'alias' => 'batchedit'); @@ -296,7 +301,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->executionKanban|execution|kanban|executionID=%s"); +$lang->execution->menu->kanban = array('link' => "$lang->executionKanban|execution|taskkanban|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'); diff --git a/module/common/lang/zh-cn.php b/module/common/lang/zh-cn.php index ffde5b8940..8c2b961bad 100644 --- a/module/common/lang/zh-cn.php +++ b/module/common/lang/zh-cn.php @@ -157,6 +157,7 @@ $lang->repo->common = '代码'; $lang->report->common = '统计'; $lang->system->common = '组织'; $lang->admin->common = '后台'; +$lang->story->common = $lang->SRCommon; $lang->task->common = '任务'; $lang->bug->common = 'Bug'; $lang->testcase->common = '用例'; diff --git a/module/common/model.php b/module/common/model.php index fe1993ad7a..814602a66d 100644 --- a/module/common/model.php +++ b/module/common/model.php @@ -528,7 +528,7 @@ class commonModel extends model $attr = "class='iframe' data-width='650px'"; break; case 'project': - if(isset($config->maxVersion) and!defined('TUTORIAL')) + if(!defined('TUTORIAL')) { $params = "programID=0©ProjectID=0&extra=from=global"; $createMethod = 'createGuide'; diff --git a/module/doc/model.php b/module/doc/model.php index 3efa6e55f6..bfd4127b49 100644 --- a/module/doc/model.php +++ b/module/doc/model.php @@ -1377,7 +1377,7 @@ class docModel extends model $executions = $this->dao->select('*')->from(TABLE_EXECUTION) ->where('deleted')->eq(0) - ->andWhere('type')->in('sprint,stage') + ->andWhere('type')->in('sprint,stage,kanban') ->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->sprints)->fi() ->orderBy('order_asc') ->fetchAll('id'); diff --git a/module/execution/config.php b/module/execution/config.php index ec4ef74386..69eeeb5f7d 100644 --- a/module/execution/config.php +++ b/module/execution/config.php @@ -11,6 +11,9 @@ $config->execution->list->exportFields = 'id,name,projectName,code,PM,end,status $config->execution->modelList['scrum'] = 'sprint'; $config->execution->modelList['waterfall'] = 'stage'; +$config->execution->modelList['kanban'] = 'kanban'; + +$config->execution->statusActions = array('start', 'putoff', 'suspend', 'close', 'activate'); global $lang, $app; $app->loadLang('task'); diff --git a/module/execution/control.php b/module/execution/control.php index f4bb2ebc00..3b29ee4a69 100644 --- a/module/execution/control.php +++ b/module/execution/control.php @@ -124,7 +124,9 @@ class execution extends control $browseType = strtolower($status); /* Get products by execution. */ - $execution = $this->commonAction($executionID, $status); + $execution = $this->commonAction($executionID, $status); + if($execution->type == 'kanban') $this->locate($this->createLink('execution', 'kanban', "executionID=$executionID")); + $executionID = $execution->id; $products = $this->product->getProductPairsByProject($executionID); setcookie('preExecutionID', $executionID, $this->config->cookieLife, $this->config->webRoot, '', false, true); @@ -1262,6 +1264,15 @@ class execution extends control } $project = $this->project->getByID($projectID); + if(!empty($project) and $project->model == 'kanban') + { + global $lang; + $executionLang = $lang->execution->common; + $lang->executionCommon = $lang->execution->kanban; + $lang->execution->common = $lang->execution->kanban; + include $this->app->getModulePath('', 'execution') . 'lang/' . $this->app->getClientLang() . '.php'; + $lang->execution->common = $executionLang; + } $extra = str_replace(array(',', ' '), array('&', ''), $extra); parse_str($extra, $output); @@ -1288,6 +1299,7 @@ class execution extends control $this->view->tips = $this->fetch('execution', 'tips', "executionID=$executionID"); $this->view->executionID = $executionID; $this->view->projectID = $projectID; + $this->view->project = $project; $this->display(); exit; } @@ -1375,6 +1387,16 @@ class execution extends control } } + if(!empty($projectID) and $project->model == 'kanban') + { + $execution = $this->execution->getById($executionID); + $this->loadModel('kanban')->createRDKanban($execution); + + if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); + if($this->app->tab == 'project') return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => $this->createLink('project', 'index', "projectID=$projectID"))); + return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => inlink('kanban', "executionID=$executionID"))); + } + if(!empty($planID)) { return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => inlink('create', "projectID=$projectID&executionID=$executionID©ExecutionID=&planID=$planID&confirm=no"))); @@ -1429,6 +1451,7 @@ class execution extends control $this->view->copyExecution = isset($copyExecution) ? $copyExecution : ''; $this->view->from = $this->app->tab; $this->view->isStage = (isset($project->model) and $project->model == 'waterfall') ? true : false; + $this->view->project = $project; $this->display(); } @@ -1450,6 +1473,14 @@ class execution extends control $this->app->loadLang('stage'); $this->app->loadLang('programplan'); $browseExecutionLink = $this->createLink('execution', 'browse', "executionID=$executionID"); + $execution = $this->execution->getById($executionID); + + if($execution->type == 'kanban') + { + global $lang; + $lang->executionCommon = $lang->execution->kanban; + include $this->app->getModulePath('', 'execution') . 'lang/' . $this->app->getClientLang() . '.php'; + } if(!empty($_POST)) { @@ -1501,6 +1532,7 @@ class execution extends control $this->executeHooks($executionID); if($_POST['status'] == 'doing') $this->loadModel('common')->syncPPEStatus($executionID); + if($execution->type == 'kanban') return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => 'parent')); return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => inlink('view', "executionID=$executionID"))); } @@ -1508,10 +1540,8 @@ class execution extends control $this->execution->setMenu($executionID); $executions = array('' => '') + $this->executions; - $execution = $this->execution->getById($executionID); $managers = $this->execution->getDefaultManagers($executionID); - /* Remove current execution from the executions. */ unset($executions[$executionID]); @@ -1684,6 +1714,7 @@ class execution extends control { $execution = $this->commonAction($executionID); $executionID = $execution->id; + if($execution->type == 'kanban') $this->lang->executionCommon = $this->lang->execution->kanban; if(!empty($_POST)) { @@ -1756,6 +1787,7 @@ class execution extends control { $execution = $this->commonAction($executionID); $executionID = $execution->id; + if($execution->type == 'kanban') $this->lang->executionCommon = $this->lang->execution->kanban; if(!empty($_POST)) { @@ -1791,6 +1823,7 @@ class execution extends control { $execution = $this->commonAction($executionID); $executionID = $execution->id; + if($execution->type == 'kanban') $this->lang->executionCommon = $this->lang->execution->kanban; if(!empty($_POST)) { @@ -1833,6 +1866,7 @@ class execution extends control { $execution = $this->commonAction($executionID); $executionID = $execution->id; + if($execution->type == 'kanban') $this->lang->executionCommon = $this->lang->execution->kanban; if(!empty($_POST)) { @@ -1921,6 +1955,65 @@ class execution extends control /** * Kanban. * + * @param int $executionID + * @param string $browseType + * @param string $orderBy + * @param string $groupBy + * @access public + * @return void + */ + public function kanban($executionID, $browseType = 'all', $orderBy = 'id_asc', $groupBy = 'default') + { + if(empty($groupBy)) $groupBy = 'default'; + + $this->lang->execution->menu = new stdclass(); + $execution = $this->commonAction($executionID); + $kanbanData = $this->loadModel('kanban')->getRDKanban($executionID, $browseType, $orderBy); + $executionActions = array(); + + foreach($this->config->execution->statusActions as $action) + { + if($this->execution->isClickable($execution, $action)) $executionActions[] = $action; + } + + $userList = array(); + $users = $this->loadModel('user')->getPairs('noletter|nodeleted'); + $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; + } + + /* Get execution's product. */ + $productID = 0; + $products = $this->loadModel('product')->getProducts($execution->project); + if($products) $productID = key($products); + + $plans = $this->execution->getPlans($products); + $allPlans = array('' => ''); + if(!empty($plans)) + { + foreach($plans as $plan) $allPlans += $plan; + } + + $this->view->title = $this->lang->kanban->view; + $this->view->users = $users; + $this->view->regions = $kanbanData; + $this->view->execution = $execution; + $this->view->userList = $userList; + $this->view->browseType = $browseType; + $this->view->orderBy = $orderBy; + $this->view->groupBy = $groupBy; + $this->view->productID = $productID; + $this->view->allPlans = $allPlans; + $this->view->executionActions = $executionActions; + $this->display(); + } + + /** + * Task kanban. + * * @param int $executionID * @param string $browseType story|bug|task|all * @param string $orderBy @@ -1928,18 +2021,16 @@ class execution extends control * @access public * @return void */ - public function kanban($executionID, $browseType = '', $orderBy = 'order_asc', $groupBy = '') + public function taskKanban($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'; + if(empty($groupBy)) $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('kanbanType', $browseType, 'execution'); - $this->app->session->set('kanbanGroupBy' . $browseType, $groupBy, 'execution'); /* Load language. */ $this->app->loadLang('story'); @@ -2302,6 +2393,12 @@ class execution extends control if($tips) $tips = str_replace($this->lang->executionCommon, $this->lang->project->stage, $tips); $this->lang->execution->confirmDelete = str_replace($this->lang->executionCommon, $this->lang->project->stage, $this->lang->execution->confirmDelete); } + elseif($type == 'kanban') + { + global $lang; + $lang->executionCommon = $lang->execution->kanban; + include $this->app->getModulePath('', 'execution') . 'lang/' . $this->app->getClientLang() . '.php'; + } echo js::confirm($tips . sprintf($this->lang->execution->confirmDelete, $this->executions[$executionID]), $this->createLink('execution', 'delete', "executionID=$executionID&confirm=yes")); exit; @@ -2309,6 +2406,7 @@ class execution extends control else { /* Delete execution. */ + $execution = $this->execution->getByID($executionID); $this->dao->update(TABLE_EXECUTION)->set('deleted')->eq(1)->where('id')->eq($executionID)->exec(); $this->loadModel('action')->create('execution', $executionID, 'deleted', '', ACTIONMODEL::CAN_UNDELETED); $this->execution->updateUserView($executionID); @@ -2317,7 +2415,8 @@ class execution extends control $this->executeHooks($executionID); if($this->viewType == 'json') return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess)); - die(js::reload('parent')); + if($execution->type == 'kanban') return print(js::locate($this->createLink('execution', 'all'), 'parent')); + return print(js::reload('parent')); } } @@ -2536,7 +2635,7 @@ class execution extends control $this->loadModel('product'); /* Get projects, executions and products. */ - $object = $this->project->getByID($objectID, $this->app->tab == 'project' ? 'project' : 'sprint,stage'); + $object = $this->project->getByID($objectID, $this->app->tab == 'project' ? 'project' : 'sprint,stage,kanban'); $products = $this->product->getProducts($objectID); $browseLink = $this->createLink($this->app->tab == 'project' ? 'projectstory' : 'execution', 'story', "objectID=$objectID"); @@ -2871,7 +2970,7 @@ class execution extends control $projects = $this->loadModel('program')->getProjectList(0, 'all', 0, 'order_asc', null, 0, 0, true); $executionGroups = $this->dao->select('*')->from(TABLE_EXECUTION) ->where('deleted')->eq(0) - ->andWhere('type')->in('sprint,stage') + ->andWhere('type')->in('sprint,stage,kanban') ->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->sprints)->fi() ->beginIF($this->config->systemMode == 'new')->andWhere('project')->in(array_keys($projects))->fi() ->orderBy('id_desc') @@ -3439,7 +3538,7 @@ class execution extends control $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) + if($lastEditedTime > $enterTime or $groupBy != 'default') { $kanbanGroup = $this->loadModel('kanban')->getExecutionKanban($executionID, $browseType, $groupBy); die(json_encode($kanbanGroup)); diff --git a/module/execution/css/all.css b/module/execution/css/all.css index b01e13c585..ead807410e 100644 --- a/module/execution/css/all.css +++ b/module/execution/css/all.css @@ -22,3 +22,4 @@ td.flex span.project-type-label {margin-left: 5px; min-width: 36px;} .c-progress, .c-burn {width: 60px;} .c-percent, .c-realBegan, .c-end, .c-begin, .c-realEnd{width: 100px;} .c-action {text-align: center;} +.c-name > span {margin-right: 2px;} diff --git a/module/execution/css/kanban.css b/module/execution/css/kanban.css index 372183c4b4..c5534c0daa 100644 --- a/module/execution/css/kanban.css +++ b/module/execution/css/kanban.css @@ -1,5 +1,84 @@ -#main {padding-bottom: 10px;} -#main > .container {max-width: 1960px!important; padding: 0 10px} +#TRAction .icon {padding: 0px 6px; font-size: 14px} + +.has-error .form-control {border-color: #ff4268!important;} +.has-error .form-control:focus {box-shadow: inset 0 1px 1px rgb(0 0 0 / 8%), 0 0 6px #f5b2a5!important;} +.has-error .form-control::placeholder {color: #ff4268!important;} + +.label.text-red {background: #ff4268 !important; color: #ffffff;} + +.dropdown-menu > li {padding: 0 10px;} +.dropdown-menu > li > a > .icon {position: relative; left: -5px;} + +#kanbanContainer {padding-bottom: 0; margin-bottom: 0;} +#kanbanContainer.fullscreen {overflow: auto;} + +.region {border: 1px solid #dcdcdc; background-color: #fff;} +.region + .region {margin-top: 20px;} +.region .region-header {padding: 10px;} +.region .region-header span {font-size: 14px; color: #999EAB} +.region .region-header label {color: #999; background: transparent; border: 1px solid #ddd; margin-left: 10px; margin-right: 10px} +.region .region-header .action {float: right} +.region .region-header .icon-double-angle-up,.icon-double-angle-down {cursor: pointer;} +.region .kanban-header-sub-cols .kanban-header-col > .title {max-width: 100% !important;min-width:240px;} +.region .sort .region-header {cursor: move;} + +.region .kanban {padding: 0px 10px 10px 10px; min-height: unset; overflow: auto} +.region .kanban .form-actions {margin: 0;} +.region .kanban .form-actions .btn {margin-right: 10px; min-width: 50px;} + +.region .kanban-board + .kanban-board {margin-top: 20px;} +.region .kanban-board > .kanban-header > .kanban-group-header {padding: 8px 2px; width: 20px;} +.region .kanban-board > .kanban-header > .kanban-group-header:hover {cursor: move;} +.region .kanban-board.sort > .kanban-header {padding-left: 0;} + +.region .kanban-header {border-bottom: none!important; min-width: max-content; min-width: -moz-max-content;} +.region .kanban-header-col > .title {max-width: 80% !important;} +.region .kanban-header-col > .title {margin: 0} +.region .kanban-header-col > .title > span {display: inline-block; overflow: hidden; padding-right:2px; position: initial; max-width: 140px !important;} +.region .kanban-header-col > .title > .text-grey {opacity: .5; font-weight: bold; color: #8b91a2;} +.region .kanban-header-col > .title > .count {opacity: .5; font-weight: bold; color: #8b91a2;} +.region .kanban-header-col > .title > .error {color: #333333; padding-left: 2px; font-size: 10px; padding-top: 1px; padding-right: 2px;} +.kanban-affixed .kanban-header-col > .title > .text {color: #fff!important} +.region .kanban-header-col > .actions {position: relative; right: -30px;} +.region .kanban-header-parent-col > .actions {position: absolute; right: 0px;} +.region .kanban-header-sub-cols .actions {position: absolute; right: 0px;} + +.region .kanban-lane {position: relative; border-bottom: none!important; min-height: 200px !important} + +.region .kanban-lane > .kanban-lane-name > .text {margin: auto 0; max-height: 150px; overflow: hidden; text-overflow: ellipsis} +.region .kanban-lane > .kanban-lane-name > .actions {position: absolute; top: 0; left: 0; right: 0; opacity: 0;} +.region .kanban-lane > .kanban-lane-name > .actions > a {display: block; width: 20px; height: 20px; line-height: 20px; text-align: center; opacity: .7; color: #fff;} +.region .kanban-lane > .kanban-lane-name > .actions > a:hover {background-color: rgba(0,0,0,.2); opacity: 1;} +.region .kanban-lane > .kanban-lane-name:hover > .actions {opacity: 1;} +.region .kanban-lane.sort > .kanban-lane-name {cursor: move;} + +.region .kanban-lane-col {max-height: unset !important; overflow: auto!important; position: relative;} +.region .kanban-lane-col.has-scrollbar > .kanban-lane-actions {position: absolute; background-color: inherit; bottom: 0; left: 0; right: 0;} + +.region .kanban-lane-items {overflow: auto; padding-bottom: 10px;} + +.region .kanban-item {position: relative} + +.kanban-card {height: auto !important; padding: 8px 14px !important} + +.cardcolor {width:40px; height: 14px; float: left; margin-left: 5px; margin-right: 5px; margin-top: 2px; border-radius: 2px;} +#cardcolormenu {padding:2px 2px; width: 100px;} + +.kanban-col[data-type=ADD] {display: none;} +.kanban-col[data-type=EMPTY] {display: none;} +.kanban-item:hover .title {padding-right: 10px;} +.gray .actions {display:none;} + +.c-type {width: 150px !important; overflow: unset;} +.c-group {width: 190px !important; overflow: unset;} +.c-type {margin-left: 0px !important;} + +#kanbanScaleControl {width: 100px;} +#kanbanScaleControl .input-group-addon {background: #fff; padding: 5px;} +#kanbanScaleControl > .input-group-btn:first-child > .btn {border-radius: 16px 0 0 16px; border-right-color: transparent;} +#kanbanScaleControl > .input-group-btn:last-child > .btn {border-radius: 0 16px 16px 0; border-left-color: transparent;} +#kanbanScaleControl > .input-group-btn > .btn:hover {border-color: #b8bfce;} +#kanbanActionMenu {top: 24px; right: auto;} .kanban + .kanban {margin-top: 15px} .kanban-card > .title {display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis} @@ -16,43 +95,11 @@ .kanban-card > .actions > a {display: block; float: left; width: 20px; height: 20px; line-height: 20px; text-align: center; border-radius: 4px; opacity: .7;} .kanban-card > .actions > a:hover {background-color: rgba(0,0,0,.075); opacity: 1;} -.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;} - -.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; margin-top: 2px;} -#kanbans .kanban-affixed > .kanban-header {top: 100px;} -#kanbans .kanban-card[data-scale-size="4"] {padding: 8px;} -#kanbans .kanban-card[data-scale-size="3"] {padding: 3px 4px;} -#kanbans .kanban-card[data-scale-size="3"] > .title {white-space: normal; max-height: 100%;} -#kanbans .kanban-card[data-scale-size="2"] {padding: 3px 4px; overflow: hidden;} -#kanbans .kanban-card[data-scale-size="2"] > .title {display: inline; line-height: 18px; white-space: normal; max-height: 100%; word-break: break-all;} -#kanbans .kanban-card[data-scale-size="2"] > .infos {display: inline-block; height: 18px; vertical-align: top; margin: 0;} -#kanbans .kanban-card[data-scale-size="2"] > .infos > .info-pri {transform: scale(.8); vertical-align: top;} -#kanbans .kanban-card[data-scale-size="2"] > .infos > .avatar {display: inline-block; transform: scale(.8); position: relative; top: -2px; vertical-align: top; margin-right: -1px;} - -#kanbanScaleControl {width: 100px;} -#kanbanScaleControl .input-group-addon {background: #fff; padding: 5px;} -#kanbanScaleControl > .input-group-btn:first-child > .btn {border-radius: 16px 0 0 16px; border-right-color: transparent;} -#kanbanScaleControl > .input-group-btn:last-child > .btn {border-radius: 0 16px 16px 0; border-left-color: transparent;} -#kanbanScaleControl > .input-group-btn > .btn:hover {border-color: #b8bfce;} - -#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;} +#kanban .kanban-card[data-scale-size="4"] {padding: 8px;} +#kanban .kanban-card[data-scale-size="3"] {padding: 3px 4px;} +#kanban .kanban-card[data-scale-size="3"] > .title {white-space: normal; max-height: 100%;} +#kanban .kanban-card[data-scale-size="2"] {padding: 3px 4px; overflow: hidden;} +#kanban .kanban-card[data-scale-size="2"] > .title {display: inline; line-height: 18px; white-space: normal; max-height: 100%; word-break: break-all;} +#kanban .kanban-card[data-scale-size="2"] > .infos {display: inline-block; height: 18px; vertical-align: top; margin: 0;} +#kanban .kanban-card[data-scale-size="2"] > .infos > .info-pri {transform: scale(.8); vertical-align: top;} +#kanban .kanban-card[data-scale-size="2"] > .infos > .avatar {display: inline-block; transform: scale(.8); position: relative; top: -2px; vertical-align: top; margin-right: -1px;} diff --git a/module/execution/css/taskkanban.css b/module/execution/css/taskkanban.css new file mode 100644 index 0000000000..372183c4b4 --- /dev/null +++ b/module/execution/css/taskkanban.css @@ -0,0 +1,58 @@ +#main {padding-bottom: 10px;} +#main > .container {max-width: 1960px!important; padding: 0 10px} + +.kanban + .kanban {margin-top: 15px} +.kanban-card > .title {display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis} +.kanban-card > .infos {position: relative; margin-top: 5px} +.kanban-card > .infos > .info + .info {margin-left: 8px} +.kanban-card > .infos > .info-id, +.kanban-card > .infos > .info-deadline, +.kanban-card > .infos > .info-estimate {font-size: 12px; position: relative; top: 2px} +.kanban-card > .infos > .label-pri {min-width: 16px; line-height: 14px; height: 16px; padding: 0} +.kanban-card > .infos > .label-severity {transform: scale(.75)} +.kanban-card > .infos > .avatar {position: absolute; right: 0; top: 0} +.kanban-card > .actions {position: absolute; top: 4px; right: 4px; opacity: 0;} +.kanban-card:hover > .actions {opacity: 1;} +.kanban-card > .actions > a {display: block; float: left; width: 20px; height: 20px; line-height: 20px; text-align: center; border-radius: 4px; opacity: .7;} +.kanban-card > .actions > a:hover {background-color: rgba(0,0,0,.075); opacity: 1;} + +.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;} + +.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; margin-top: 2px;} +#kanbans .kanban-affixed > .kanban-header {top: 100px;} +#kanbans .kanban-card[data-scale-size="4"] {padding: 8px;} +#kanbans .kanban-card[data-scale-size="3"] {padding: 3px 4px;} +#kanbans .kanban-card[data-scale-size="3"] > .title {white-space: normal; max-height: 100%;} +#kanbans .kanban-card[data-scale-size="2"] {padding: 3px 4px; overflow: hidden;} +#kanbans .kanban-card[data-scale-size="2"] > .title {display: inline; line-height: 18px; white-space: normal; max-height: 100%; word-break: break-all;} +#kanbans .kanban-card[data-scale-size="2"] > .infos {display: inline-block; height: 18px; vertical-align: top; margin: 0;} +#kanbans .kanban-card[data-scale-size="2"] > .infos > .info-pri {transform: scale(.8); vertical-align: top;} +#kanbans .kanban-card[data-scale-size="2"] > .infos > .avatar {display: inline-block; transform: scale(.8); position: relative; top: -2px; vertical-align: top; margin-right: -1px;} + +#kanbanScaleControl {width: 100px;} +#kanbanScaleControl .input-group-addon {background: #fff; padding: 5px;} +#kanbanScaleControl > .input-group-btn:first-child > .btn {border-radius: 16px 0 0 16px; border-right-color: transparent;} +#kanbanScaleControl > .input-group-btn:last-child > .btn {border-radius: 0 16px 16px 0; border-left-color: transparent;} +#kanbanScaleControl > .input-group-btn > .btn:hover {border-color: #b8bfce;} + +#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/js/create.js b/module/execution/js/create.js index 4420527143..2e76285f06 100644 --- a/module/execution/js/create.js +++ b/module/execution/js/create.js @@ -1,6 +1,6 @@ function setCopyProject(executionID) { - location.href = createLink('execution', 'create', 'projectID=&executionID=0©ExecutionID=' + executionID); + location.href = createLink('execution', 'create', 'projectID=' + projectID + '&executionID=0©ExecutionID=' + executionID); } $(function() diff --git a/module/execution/js/kanban.js b/module/execution/js/kanban.js index daf1724e18..7cd0b16307 100644 --- a/module/execution/js/kanban.js +++ b/module/execution/js/kanban.js @@ -1,7 +1,282 @@ -function changeView(view) +/** + * Display the kanban in full screen. + * + * @access public + * @return void + */ +function fullScreen() { - var link = createLink('execution', 'kanban', "executionID=" + executionID + '&type=' + view); + var element = document.getElementById('kanbanContainer'); + var requestMethod = element.requestFullScreen || element.webkitRequestFullScreen || element.mozRequestFullScreen || element.msRequestFullscreen; + + if(requestMethod) + { + var afterEnterFullscreen = function() + { + $('#kanbanContainer').addClass('fullscreen') + .on('scroll', tryUpdateKanbanAffix); + $('.actions').hide(); + $('.action').hide(); + $('.kanban-group-header').hide(); + $(".title").attr("disabled", true).css("pointer-events", "none"); + $.cookie('isFullScreen', 1); + }; + + var whenFailEnterFullscreen = function(error) + { + exitFullScreen(); + }; + + try + { + var result = requestMethod.call(element); + if(result && (typeof result.then === 'function' || result instanceof window.Promise)) + { + result.then(afterEnterFullscreen).catch(whenFailEnterFullscreen); + } + else + { + afterEnterFullscreen(); + } + } + catch (error) + { + whenFailEnterFullscreen(error); + } + } +} + +/** + * Exit full screen. + * + * @access public + * @return void + */ +function exitFullScreen() +{ + $('#kanbanContainer').removeClass('fullscreen') + .off('scroll', tryUpdateKanbanAffix); + $('.actions').show(); + $('.action').show(); + $('.kanban-group-header').show(); + $(".title").attr("disabled", false).css("pointer-events", "auto"); + $.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(); +}); + +/** Change kanban scale size */ +function changeKanbanScaleSize(newScaleSize) +{ + var newScaleSize = Math.max(1, Math.min(4, newScaleSize)); + if(newScaleSize === window.kanbanScaleSize) return; + + window.kanbanScaleSize = newScaleSize; + $.zui.store.set('executionKanbanScaleSize', newScaleSize); + $('#kanbanScaleSize').text(newScaleSize); + $('#kanbanScaleControl .btn[data-type="+"]').attr('disabled', newScaleSize >= 4 ? 'disabled' : null); + $('#kanbanScaleControl .btn[data-type="-"]').attr('disabled', newScaleSize <= 1 ? 'disabled' : null); + + $('#kanban').children('.region').children("div[id^='kanban']").each(function() + { + var kanban = $(this).data('zui.kanban'); + if(!kanban) return; + kanban.setOptions({cardsPerRow: newScaleSize, cardHeight: getCardHeight()}); + }); + + return newScaleSize; +} + +/** Get card height */ +function getCardHeight() +{ + return [59, 59, 62, 62, 47][window.kanbanScaleSize]; +} + +$('#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 + '&browseType=' + type); location.href = link; +}); + +/** + * Create lane menu. + * + * @param object $options + * @access public + * @return void + */ +function createLaneMenu(options) +{ + var lane = options.$trigger.closest('.kanban-lane').data('lane'); + var privs = lane.actions; + if(!privs.length) return []; + + var items = []; + if(privs.includes('setLane')) items.push({label: kanbanLang.setLane, icon: 'edit', url: createLink('kanban', 'setLane', 'laneID=' + lane.id + '&executionID=0&from=kanban'), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '635px'}}); + if(privs.includes('deleteLane')) items.push({label: kanbanLang.deleteLane, icon: 'trash', url: createLink('kanban', 'deleteLane', 'lane=' + lane.id), attrs: {'target': 'hiddenwin'}}); + + var bounds = options.$trigger[0].getBoundingClientRect(); + items.$options = {x: bounds.right, y: bounds.top}; + return items; +} + +function createColumnMenu(options) +{ + var column = options.$trigger.closest('.kanban-col').data('col'); + var privs = column.actions; + if(!privs.length) return []; + + var items = []; + if(privs.includes('setColumn')) items.push({label: kanbanLang.editColumn, icon: 'edit', url: createLink('kanban', 'setColumn', 'columnID=' + column.id, '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal'}}); + if(privs.includes('setWIP')) items.push({label: kanbanLang.setWIP, icon: 'alert', url: createLink('kanban', 'setWIP', 'columnID=' + column.id), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width' : '500px'}}); + + var bounds = options.$trigger[0].getBoundingClientRect(); + items.$options = {x: bounds.right, y: bounds.top}; + 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 = []; + var laneID = col.$kanbanData.lanes[0].id ? col.$kanbanData.lanes[0].id : 0; + + if(col.type == 'backlog') + { + if(priv.canCreateStory) items.push({label: storyLang.create, url: $.createLink('story', 'create', 'productID=' + productID, '', true), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '80%'}}); + 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-toggle': 'modal', 'data-width': '90%'}}); + if(priv.canLinkStory) items.push({label: executionLang.linkStory, url: $.createLink('execution', 'linkStory', 'executionID=' + executionID, '', true), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '90%'}}); + if(priv.canLinkStoryByPlane) items.push({label: executionLang.linkStoryByPlan, url: '#linkStoryByPlan', 'attrs' : {'data-toggle': 'modal'}}); + } + else if(col.type == 'unconfirmed') + { + if(priv.canCreateBug) items.push({label: bugLang.create, url: $.createLink('bug', 'create', 'productID=0&moduleID=0&extra=executionID=' + executionID, '', true), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '80%'}}); + if(priv.canBatchCreateBug) items.push({label: bugLang.batchCreate, url: $.createLink('bug', 'batchcreate', 'productID=' + productID + '&moduleID=0&executionID=' + executionID, '', true), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '90%'}}); + } + else if(col.type == 'wait') + { + if(priv.canCreateTask) items.push({label: taskLang.create, url: $.createLink('task', 'create', 'executionID=' + executionID + "&storyID=0&moduleID=0&taskID=0&todoID=0&extra=laneID=" + laneID + ",columnID=" + col.id, '', true), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '80%'}}); + if(priv.canBatchCreateTask) items.push({label: taskLang.batchCreate, url: $.createLink('task', 'batchcreate', 'executionID=' + executionID, '', true), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '80%'}}); + } + return items; +} + +/** + * Hide kanban action + */ +function hideKanbanAction() +{ + $('.kanban').attr('data-action-enabled', null); + $('.contextmenu').removeClass('contextmenu-show'); + $('.contextmenu .contextmenu-menu').removeClass('open').removeClass('in'); + $('#moreTasks, #moreColumns').animate({right: -400}, 500); +} + +/** + * Handle finish drop task + */ +function handleFinishDrop() +{ + $('.kanban').find('.can-drop-here').removeClass('can-drop-here'); +} + +/* Define drag and drop rules */ +if(!window.kanbanDropRules) +{ + window.kanbanDropRules = + { + story: + { + backlog: ['ready', 'backlog'], + ready: ['backlog', 'ready'], + }, + bug: + { + 'unconfirmed': ['confirmed', 'fixing', 'fixed'], + 'confirmed': ['fixing', 'fixed'], + 'fixing': ['fixed'], + 'fixed': ['testing', 'tested', 'fixing'], + 'testing': ['tested', 'closed', 'fixing'], + 'tested': ['closed', 'fixing'], + 'closed': ['fixing'], + }, + task: + { + 'wait': ['wait', 'developing', 'developed', 'canceled', 'closed'], + 'developing': ['developing', 'developed', 'pause'], + 'developed': ['developed', 'canceled', 'closed'], + 'pause': ['pause', 'developing'], + 'canceled': ['canceled', 'developing'], + 'closed': ['closed', 'developing'], + } + } +} + +/* + * 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 laneType = $element.closest('.kanban-lane').data().lane.type; + var kanbanRules = window.kanbanDropRules ? window.kanbanDropRules[laneType] : null; + + if(!kanbanRules) return $root.find('.kanban-lane-col:not([data-type="' + col.type + '"])'); + + var colRules = kanbanRules[col.type]; + var groupID = $col.closest('.kanban-board').data().id; + return $root.find('.kanban-lane-col').filter(function() + { + if(!colRules) return false; + if(colRules === true) return true; + + var $newCol = $(this); + var newCol = $newCol.data(); + var newGroupID = $newCol.closest('.kanban-board').data().id; + + var canDropHere = colRules.indexOf(newCol.type) > -1 && newGroupID.id === groupID.id; + if(canDropHere) $newCol.addClass('can-drop-here'); + return canDropHere; + }); } /** @@ -29,17 +304,17 @@ function renderUserAvatar(user, objectType, objectID, size) var link = createLink('bug', 'assignto', 'id=' + objectID, '', true); } - if(!user) return $(''); + if(!user) return $(''); if(typeof user === 'string') user = {account: user}; - if(!user.avatar && window.userList && window.userList[user.account]) user = window.userList[user.account]; + if(!user.avatar && window.users && window.users[user.account]) user = {avatar: users[user.account].avatar, account: user.account, realname: users[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}); + return $('').avatar({user: user}).attr('data-toggle', 'modal').attr('data-width', '80%'); } /** @@ -81,7 +356,7 @@ function renderStoryItem(item, $item, col) if(!$title.length) { $title = $('' + (scaleSize <= 1 ? ' ' : '') + '') - .attr('href', $.createLink('story', 'view', 'storyID=' + item.id, '', true)); + .attr('href', $.createLink('story', 'view', 'storyID=' + item.id, '', true)).attr('data-toggle', 'modal').attr('data-width', '80%'); $title.appendTo($item); } $title.attr('title', item.title).find('.text').text(item.title); @@ -110,7 +385,7 @@ function renderStoryItem(item, $item, col) if(scaleSize <= 1) { var $actions = $item.find('.actions'); - if(!$actions.length && item.menus.length) + if(!$actions.length && item.menus && item.menus.length) { $actions = $([ '
', @@ -143,7 +418,7 @@ function renderBugItem(item, $item, col) if(!$title.length) { $title = $('' + (scaleSize <= 1 ? ' ' : '') + '') - .attr('href', $.createLink('bug', 'view', 'bugID=' + item.id, '', true)); + .attr('href', $.createLink('bug', 'view', 'bugID=' + item.id, '', true)).attr('data-toggle', 'modal').attr('data-width', '80%'); $title.appendTo($item); } $title.attr('title', item.title).find('.text').text(item.title); @@ -174,7 +449,7 @@ function renderBugItem(item, $item, col) if(scaleSize <= 1) { var $actions = $item.find('.actions'); - if(!$actions.length && item.menus.length) + if(!$actions.length && item.menus && item.menus.length) { $actions = $([ '
', @@ -207,7 +482,7 @@ function renderTaskItem(item, $item, col) if(!$title.length) { $title = $('' + (scaleSize <= 1 ? ' ' : '') + '') - .attr('href', $.createLink('task', 'view', 'taskID=' + item.id, '', true)); + .attr('href', $.createLink('task', 'view', 'taskID=' + item.id, '', true)).attr('data-toggle', 'modal').attr('data-width', '80%'); $title.appendTo($item); } $title.attr('title', item.name).find('.text').text(item.name); @@ -238,7 +513,7 @@ function renderTaskItem(item, $item, col) if(scaleSize <= 1) { var $actions = $item.find('.actions'); - if(!$actions.length && item.menus.length) + if(!$actions.length && item.menus && item.menus.length) { $actions = $([ '
', @@ -261,272 +536,202 @@ 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 + * Render items count of a column. */ -function renderColumnCount($count, count, col) +function renderCount($count, count, column) { - var text = count + '/' + (col.limit < 0 ? '' : col.limit); - $count.html(text + ''); + /* Render WIP. */ + var limit = !column.limit || column.limit == '-1' ? '' : column.limit; + if($count.parent().find('.limit').length) + { + $count.parent().find('.limit').html(limit); + } + else + { + $count.parent().find('.count').before("("); + $count.parent().find('.count').after("/" + limit + ")"); + } + + if(column.limit != -1 && column.limit < count) + { + $count.parents('.title').parent('.kanban-header-col').css('background-color', '#F6A1A1'); + $count.parents('.title').find('.text').css('max-width', $count.parents('.title').width() - 200); + $count.css('color', '#E33030'); + if(!$count.parent().find('.error').length) $count.parent().find('.include-last').after(""); + } + else + { + $count.parents('.title').parent('.kanban-header-col').css('background-color', 'transparent'); + $count.parents('.title').find('.text').css('max-width', $count.parents('.title').width() - 120); + $count.css('color', '#8B91A2'); + $count.parent().find('.error').remove(); + } } /** - * 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 + * Render header of a column. */ -function renderHeaderCol($col, col, $header, kanban) +function renderHeaderCol($column, column, $header, kanbanData) { - if(col.asParent) $col = $col.children('.kanban-header-col'); - 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; + /* Render group header. */ + var privs = kanbanData.actions; + var columnPrivs = kanbanData.columns[0].actions; + var $actions = $column.children('.actions'); - if((col.type === 'backlog' && printStoryButton) || (col.type === 'wait' && printTaskButton) || (col.type == 'unconfirmed' && printBugButton)) + if(privs.includes('sortGroup')) + { + var groups = regions[column.region].groups; + if($header.closest('.kanban').data('zui.kanban')) + { + groups = $header.closest('.kanban').data('zui.kanban').data; + } + if(groups.length > 1) + { + $column.closest('.kanban-board').addClass('sort'); + $column.closest('.kanban-header').find('.kanban-group-header').remove(); + $column.closest('.kanban-header').prepend('
'); + } + } + + var printMoreBtn = (columnPrivs.includes('setColumn') || columnPrivs.includes('setWIP')); + + /* Render more menu. */ + if(((column.type == 'backlog' && hasStoryButton) || (column.type == 'wait' && hasTaskButton) || (column.type == 'unconfirmed' && hasBugButton)) && $actions.children('.text-primary').length == 0) { $actions.append([ - '', + '', '', '' ].join('')); } - - $actions.append([ - '', - '', - '' - ].join('')); - $actions.appendTo($col); -} - -/** - * 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)) + if(printMoreBtn && $actions.children('.btn').length == 0) { - $([ - '
', - '', - '', - '', - '
' - ].join('')).appendTo($name); + $actions.append(' '); } } /** - * Updata kanban data - * @param {string} kanbanID Kanban 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 - * @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() - { - if($(this).hasClass('iframe')) - { - 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 - { - afterEnterFullscreen(); - } - } - catch (error) - { - whenFailEnterFullscreen(error); - } - } -} - -/** - * Exit full screen. + * Render lane name. * + * @param object $lane + * @param int lane + * @param object $kanban + * @param array columns + * @param object $kanban * @access public * @return void */ -function exitFullScreen() +function renderLaneName($lane, lane, $kanban, columns, kanban) { - $('#kanbanContainer').removeClass('scrollbar-hover'); - $('.actions').show(); - $('#kanbanContainer a').each(function() + var canSet = lane.actions.includes('setLane'); + var canSort = lane.actions.includes('sortLane') && kanban.lanes.length > 1; + var canDelete = lane.actions.includes('deleteLane'); + + $lane.parent().toggleClass('sort', canSort); + + if(!$lane.children('.actions').length && (canSet || canDelete)) { - 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: - { - backlog: ['ready'], - ready: ['backlog'], - }, - bug: - { - 'unconfirmed': ['confirmed', 'fixing', 'fixed'], - 'confirmed': ['fixing', 'fixed'], - 'fixing': ['fixed'], - 'fixed': ['testing', 'tested', 'fixing'], - 'testing': ['tested', 'closed', 'fixing'], - 'tested': ['closed', 'fixing'], - 'closed': ['fixing'], - }, - task: - { - 'wait': ['developing', 'developed', 'canceled', 'closed'], - 'developing': ['developed', 'pause'], - 'developed': ['canceled', 'closed'], - 'pause': ['developing'], - 'canceled': ['developing'], - 'closed': ['developing'], - } + $([ + '
', + '', + '', + '', + '
' + ].join('')).appendTo($lane); } } -/* - * Find drop columns - * @param {JQuery} $element Drag element - * @param {JQuery} $root Dnd root element +/** + * Update a region. + * + * @param int regionID + * @param array regionData + * @access public + * @return boolean */ -function findDropColumns($element, $root) +function updateRegion(regionID, regionData = []) { - var $col = $element.closest('.kanban-col'); - var col = $col.data(); - var kanbanID = $root.data('id'); - var kanbanRules = window.kanbanDropRules ? window.kanbanDropRules[kanbanID] : null; + if(!regionID) return false; - if(!kanbanRules) return $root.find('.kanban-lane-col:not([data-type="' + col.type + '"])'); + var $region = $('#kanban'+ regionID).kanban(); - 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; + if(!$region.length) return false; + if(!regionData) regionData = regions[regionID]; - 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; - }); + $region.data('zui.kanban').render(regionData.groups); + return true; } /** - * 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 + * Handle drop task. + * + * @param object $element + * @param object $event + * @param object $kanban + * @access public + * @return void */ -function changeCardColType(card, fromColType, toColType, kanbanID) +function handleDropTask($element, event, kanban) { - if(typeof card == 'undefined') return false; - var objectID = card.id; + if(!event.target || !event.isNew) return; + + var $card = $element; + var $oldCol = $card.closest('.kanban-col'); + var $newCol = $(event.target).closest('.kanban-col'); + var oldCol = $oldCol.data(); + var newCol = $newCol.data(); + var oldLane = $oldCol.closest('.kanban-lane').data('lane'); + var newLane = $newCol.closest('.kanban-lane').data('lane'); + var cardType = $card.find('.kanban-card').data('type'); + + if(oldCol.id === newCol.id && newLane.id === oldLane.id) return false; + + var cardID = $card.data().id; + var fromColType = $oldCol.data('type'); + var toColType = $newCol.data('type'); + var regionID = $card.closest('.region').data().id; + + changeCardColType(cardID, oldCol.id, newCol.id, oldLane.id, newLane.id, cardType, fromColType, toColType, regionID); +} + +var kanbanActionHandlers = +{ + dropItem: handleDropTask +}; + +/** + * Handle kanban action + */ +function handleKanbanAction(action, $element, event, kanban) +{ + $('.kanban').attr('data-action-enabled', action); + var handler = kanbanActionHandlers[action]; + if(handler) handler($element, event, kanban); +} + +/** + * changeCardColType + * + * @param int cardID + * @param int fromColID + * @param int toColID + * @param int fromLaneID + * @param int toLaneID + * @param string cardType + * @param string fromColType + * @param string toColType + * @param int regionID + * @access public + * @return void + */ +function changeCardColType(cardID, fromColID, toColID, fromLaneID, toLaneID, cardType, fromColType, toColType, regionID = 0) +{ + var objectID = cardID; var showIframe = false; var moveCard = false; /* Task lane. */ - if(kanbanID == 'task') + if(cardType == 'task') { if(toColType == 'developed') { @@ -576,7 +781,7 @@ function changeCardColType(card, fromColType, toColType, kanbanID) } /* Bug lane. */ - if(kanbanID == 'bug') + if(cardType == 'bug') { if(toColType == 'confirmed') { @@ -597,7 +802,7 @@ function changeCardColType(card, fromColType, toColType, kanbanID) } else if(toColType == 'fixed') { - if(fromColType == 'fixing' || fromColType == 'confirmed' || fromColType == 'unconfirmed') + if(fromColType == 'fixing' || fromColType == 'confirmed' || fromColType == 'unconfirmed') { var link = createLink('bug', 'resolve', 'bugID=' + objectID, '', true); showIframe = true; @@ -622,48 +827,44 @@ function changeCardColType(card, fromColType, toColType, kanbanID) if(moveCard) { - var colID = card.$col.columnID; - var link = createLink('kanban', 'ajaxMoveCard', 'cardID=' + objectID + '&colID=' + colID + '&toColType=' + toColType + '&execitionID=' + executionID + '&browseType=' + browseType + '&groupBy=' + groupBy); - $.get(link, function(data) + var link = createLink('kanban', 'ajaxMoveCard', 'cardID=' + objectID + '&fromColID=' + fromColID + '&toColID=' + toColID + '&fromLaneID=' + fromLaneID + '&toLaneID=' + toLaneID + '&execitionID=' + executionID + '&browseType=' + browseType + '&groupBy=' + groupBy + '®ionID=' + regionID + '&orderBy=' + orderBy ); + $.ajax( { - if(data) + method: 'post', + dataType: 'json', + url: link, + success: function(data) { - kanbanGroup = $.parseJSON(data); - if(groupBy == 'default') - { - updateKanban('bug', kanbanGroup.bug); - } - else - { - updateKanban(browseType, kanbanGroup[groupBy]); - } + updateRegion(regionID, data[regionID]); + }, + error: function(xhr, status, error) + { + showErrorMessager(error || lang.timeout); } - }) + }); } } /* Story lane. */ - if(kanbanID == 'story') + if(cardType == 'story') { if(toColType == 'ready' || toColType == 'backlog') { - var colID = card.$col.columnID; - var link = createLink('kanban', 'ajaxMoveCard', 'cardID=' + objectID + '&colID=' + colID + '&toColType=' + toColType + '&execitionID=' + executionID + '&browseType=' + browseType + '&groupBy=' + groupBy); - $.get(link, function(data) + var link = createLink('kanban', 'ajaxMoveCard', 'cardID=' + objectID + '&fromColID=' + fromColID + '&toColID=' + toColID + '&fromLaneID=' + fromLaneID + '&toLaneID=' + toLaneID + '&execitionID=' + executionID + '&browseType=' + browseType + '&groupBy=' + groupBy + '®ionID=' + regionID+ '&orderBy=' + orderBy ); + $.ajax( { - if(data) + method: 'post', + dataType: 'json', + url: link, + success: function(data) { - kanbanGroup = $.parseJSON(data); - if(groupBy == 'default') - { - updateKanban('story', kanbanGroup.story); - } - else - { - updateKanban(browseType, kanbanGroup[groupBy]); - } + updateRegion(regionID, data[regionID]); + }, + error: function(xhr, status, error) + { + showErrorMessager(error || lang.timeout); } - }) + }); } } @@ -674,313 +875,90 @@ function changeCardColType(card, fromColType, toColType, kanbanID) } } -/** - * Handle finish drop task - * @param {Object} event Event object - * @returns {void} - */ -function handleFinishDrop(event) +function processMinusBtn() { - 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 + '&from=execution'), className: 'iframe', attrs: {'data-width': '500px'}}) - if(priv.canSetWIP) items.push({label: executionLang.setWIP, url: $.createLink('kanban', 'setWIP', 'col=' + col.columnID + '&executionID=' + executionID + '&from=execution'), 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') + var columnCount = $('#splitTable .child-column').size(); + if(columnCount > 2 && columnCount < 10) { - 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'}}); + $('#splitTable .btn-plus').show(); + $('#splitTable .btn-close').show(); } - else if(col.laneType == 'bug') + else if(columnCount <= 2) { - 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'}); + $('#splitTable .btn-close').hide(); } - else + else if(columnCount >= 10) { - 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'}); + $('#splitTable .btn-plus').hide(); } - 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 + '&from=execution'), 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 - * @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, + column: createColumnMenu, + columnCreate: createColumnCreateMenu }; -/* Set kanban affix container */ -window.kanbanAffixContainer = '#kanbanContainer>.panel-body'; - -/* Overload kanban default options */ -$.extend($.fn.kanban.Constructor.DEFAULTS, +/** + * init Kanban + */ +/** + * Init kanban. + * + * @param object $kanban + * @access public + * @return void + */ +function initKanban($kanban) { - onRender: function() + var id = $kanban.data('id'); + var region = regions[id]; + var displayCards = window.displayCards == 'undefined' ? 2 : window.displayCards; + + $kanban.kanban( { - var maxWidth = 0; - $('#kanbans .kanban-board').each(function() - { - maxWidth = Math.max(maxWidth, $(this).outerWidth()); - }); - $('#kanbans').css('min-width', maxWidth); - } -}); - -/** Get card height */ -function getCardHeight() -{ - return [59, 59, 62, 62, 47][window.kanbanScaleSize]; -} - -/** Change kanban scale size */ -function changeKanbanScaleSize(newScaleSize) -{ - var newScaleSize = Math.max(1, Math.min(4, newScaleSize)); - if(newScaleSize === window.kanbanScaleSize) return; - - window.kanbanScaleSize = newScaleSize; - $.zui.store.set('executionKanbanScaleSize', newScaleSize); - $('#kanbanScaleSize').text(newScaleSize); - $('#kanbanScaleControl .btn[data-type="+"]').attr('disabled', newScaleSize >= 4 ? 'disabled' : null); - $('#kanbanScaleControl .btn[data-type="-"]').attr('disabled', newScaleSize <= 1 ? 'disabled' : null); - - $('#kanbans').children('.kanban').each(function() - { - var kanban = $(this).data('zui.kanban'); - if(!kanban) return; - kanban.setOptions({cardsPerRow: newScaleSize, cardHeight: getCardHeight()}); - }); - - return newScaleSize; -} - -/* Example code: */ -$(function() -{ - $.cookie('isFullScreen', 0); - - window.kanbanScaleSize = +$.zui.store.get('executionKanbanScaleSize', 1); - $('#kanbanScaleSize').text(window.kanbanScaleSize); - $('#kanbanScaleControl .btn[data-type="+"]').attr('disabled', window.kanbanScaleSize >= 4 ? 'disabled' : null); - $('#kanbanScaleControl .btn[data-type="-"]').attr('disabled', window.kanbanScaleSize <= 1 ? 'disabled' : null); - - /* Common options */  - var commonOptions = - { - maxColHeight: 'auto', - minColWidth: 240, - maxColWidth: 240, - cardHeight: getCardHeight(), - showCount: true, - showZeroCount: true, - fluidBoardWidth: false, - cardsPerRow: window.kanbanScaleSize, - virtualize: true, - virtualRenderOptions: {container: '#kanbanContainer>.panel-body'}, + data: region.groups, + maxColHeight: 510, + calcColHeight: calcColHeight, + fluidBoardWidth: false, + minColWidth: 300, + maxColWidth: 300, + cardHeight: 60, + displayCards: displayCards, + createColumnText: kanbanLang.createColumn, + addItemText: '', + cardHeight: getCardHeight(), + cardsPerRow: window.kanbanScaleSize, + onAction: handleKanbanAction, + onRenderLaneName: renderLaneName, + onRenderHeaderCol: renderHeaderCol, + onRenderCount: renderCount, 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) + $kanban.on('click', '.action-cancel', hideKanbanAction); + $kanban.on('scroll', function() { - var $trigger = $(this); - var menuType = $trigger.data('contextmenu'); - - var menuCreator = window.menuCreators[menuType]; - if(!menuCreator) return; - - var options = $.extend({event: event, $trigger: $trigger}, $trigger.data()); - var items = menuCreator(options); - if(!items || !items.length) return; - - $.zui.ContextMenu.show(items, items.$options || {event: event}); + $.zui.ContextMenu.hide(); }); +} + +/** + * Init when page ready + */ +$(function() +{ + window.kanbanScaleSize = +$.zui.store.get('executionKanbanScaleSize', 1); + $('#kanbanScaleSize').text(window.kanbanScaleSize); + $('#kanbanScaleControl .btn[data-type="+"]').attr('disabled', window.kanbanScaleSize >= 4 ? 'disabled' : null); + $('#kanbanScaleControl .btn[data-type="-"]').attr('disabled', window.kanbanScaleSize <= 1 ? 'disabled' : null); /* Make kanbanScaleControl works */ $('#kanbanScaleControl').on('click', '.btn', function() @@ -988,9 +966,48 @@ $(function() changeKanbanScaleSize(window.kanbanScaleSize + ($(this).data('type') === '+' ? 1 : -1)); }); - /* Resize kanban container on window resize */ - resizeKanbanContainer(); - $(window).on('resize', resizeKanbanContainer); + /* Init first kanban */ + $('.kanban').each(function() + { + initKanban($(this)); + }); + + $('.icon-chevron-double-up,.icon-chevron-double-down').on('click', function() + { + $(this).toggleClass('icon-chevron-double-up icon-chevron-double-down'); + $(this).parents('.region').find('.kanban').toggle(); + hideKanbanAction(); + }); + + $('.region-header').on('click', '.action', hideKanbanAction); + $('#TRAction').on('click', '.btn', hideKanbanAction); + + /* Hide action box when user click document */ + $(document).on('click', function(e) + { + $('.kanban').each(function() + { + var currentAction = $(this).kanban().attr('data-action-enabled'); + var canHideAction = (currentAction === 'headerMore' || currentAction === 'editLaneName') + && !$(e.target).closest('.action,.action-box').length; + if(canHideAction) hideKanbanAction(); + }); + }); + + /* Init contextmenu */ + $('#kanban').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: event, $trigger: $trigger}, $trigger.data()); + var items = menuCreator(options); + if(!items || !items.length) return; + + $.zui.ContextMenu.show(items, items.$options || {event: event}); + }); /* Hide contextmenu when page scroll */ $(window).on('scroll', function() @@ -1004,67 +1021,160 @@ $(function() if(planID) { location.href = createLink('execution', 'importPlanStories', 'executionID=' + executionID + '&planID=' + planID + '&productID=0&fromMethod=kanban'); + $.closeModal(); } }); - $('#type_chosen .chosen-single span').prepend(''); - $('#group_chosen .chosen-single span').prepend(kanbanLang.laneGroup + ': '); - - /* Ajax update kanban. */ - var lastUpdateData; - setInterval(function() + $(document).on('click', '#splitTable .btn-plus', function() { - $.get(createLink('execution', 'ajaxUpdateKanban', "executionID=" + executionID + "&entertime=" + entertime + "&browseType=" + browseType + "&groupBy=" + groupBy), function(data) + var tr = $(this).closest('tr'); + tr.after($('#childTpl').html().replace(/key/g, key)); + tr.next().find('input[name^=color]').colorPicker(); + key++; + processMinusBtn(); + return false; + }); + + /* Remove a trade detail item. */ + $(document).on('click', '#splitTable .btn-close', function() + { + $(this).closest('tr').remove(); + processMinusBtn(); + return false; + }); + + /* Mofidy dafault color's border color. */ + $(document).on('mouseout', '.color0', function() + { + $('.color0 .cardcolor').css('border', '1px solid #b0b0b0'); + }); + + /* Mofidy dafault color's border color. */ + $(document).on('mouseover', '.color0', function() + { + $('.color0 .cardcolor').css('border', '1px solid #fff'); + }); + + /* Init sortable */ + var sortType = ''; + var $cards = null; + $('#kanban').sortable( + { + selector: '.region, .kanban-board, .kanban-lane', + trigger: '.region.sort > .region-header, .kanban-board.sort > .kanban-header > .kanban-group-header, .kanban-lane.sort > .kanban-lane-name', + container: function($ele) { - if(data && lastUpdateData !== data) + return $ele.parent(); + }, + targetSelector: function($ele) + { + /* Sort regions */ + if($ele.hasClass('region')) { - lastUpdateData = 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]); - } + sortType = 'region'; + return $ele.parent().children('.region'); } - }); - }, 10000); -}); -$('#type').change(function() -{ - var type = $('#type').val(); - if(type != 'all') - { - $('.c-group').show(); - $.get(createLink('execution', 'ajaxGetGroup', 'type=' + type), function(data) + /* Sort boards */ + if($ele.hasClass('kanban-board')) + { + sortType = 'board'; + return $ele.parent().children('.kanban-board'); + } + + /* Sort lanes */ + if($ele.hasClass('kanban-lane')) + { + sortType = 'lane'; + $cards = $ele.find('.kanban-item'); + + return $ele.parent().children('.kanban-lane'); + } + + /* Sort lanes */ + if($ele.hasClass('kanban-item')) + { + sortType = 'item'; + return $ele.parent().children('.kanban-item'); + } + }, + start: function(e) { - $('#group_chosen').remove(); - $('#group').replaceWith(data); - $('#group').chosen(); - }) - } + if(sortType == 'region') + { + showRegionIdList = ''; + $('.icon-chevron-double-up').each(function() + { + showRegionIdList += $(this).attr('data-id') + ','; + $(this).attr('class', 'icon-chevron-double-down'); + }); - var link = createLink('execution', 'kanban', "executionID=" + executionID + '&type=' + type); - location.href = link; + $('.region').find('.kanban').hide(); + hideKanbanAction(); + } + }, + finish: function(e) + { + var url = ''; + var orders = []; + e.list.each(function(index, data) + { + orders.push(data.item.data('id')); + }); + + if(sortType == 'region') + { + $('.region').each(function() + { + if(showRegionIdList.includes($(this).attr('data-id'))) + { + $(this).find('.icon-chevron-double-down').attr('class', 'icon-chevron-double-up'); + $(this).find('.kanban').show(); + } + }) + + url = createLink('kanban', 'sortRegion', 'regions=' + orders.join(',')); + } + if(sortType == 'board') + { + var region = e.element.parent().data('id'); + url = createLink('kanban', 'sortGroup', 'region=' + region + '&groups=' + orders.join(',')); + } + if(sortType == 'lane') + { + var region = e.element.parent().parent().data('id'); + url = createLink('kanban', 'sortLane', 'region=' + region + '&lanes=' + orders.join(',')); + } + if(sortType == 'item') + { + url = createLink('task', 'sort', 'kanbanID=' + kanbanID + '&tasks=' + orders.join(',')); + } + if(!url) return true; + + $.getJSON(url, function(response) + { + if(response.result == 'fail' && response.message.length) + { + bootbox.alert(response.message); + setTimeout(function(){return location.reload()}, 3000); + } + }); + }, + always: function(e) + { + if(sortType == 'lane') $cards.show(); + } + }); }); -$('.c-group').change(function() +/** Calculate column height */ +function calcColHeight(col, lane, colCards, colHeight, kanban) { - $('.c-group').show(); + var options = kanban.options; + if(!options.displayCards) return 0; - 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; -}); + var displayCards = +(options.displayCards || 2); + + if (typeof displayCards !== 'number' || displayCards < 2) displayCards = 2; + return (displayCards * (options.cardHeight + options.cardSpace) + options.cardSpace); +} diff --git a/module/execution/js/taskkanban.js b/module/execution/js/taskkanban.js new file mode 100644 index 0000000000..6ae8207fd6 --- /dev/null +++ b/module/execution/js/taskkanban.js @@ -0,0 +1,1132 @@ +function changeView(view) +{ + var link = createLink('execution', 'taskKanban', "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, size) +{ + var avatarSizeClass = 'avatar-' + (size || 'sm'); + var $noPrivAndNoAssigned = $('
'); + if(objectType == 'task') + { + 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 scaleSize = window.kanbanScaleSize; + if(+$item.attr('data-scale-size') !== scaleSize) $item.empty().attr('data-scale-size', scaleSize); + + if(scaleSize <= 3) + { + var $title = $item.find('.title'); + if(!$title.length) + { + $title = $('' + (scaleSize <= 1 ? ' ' : '') + '') + .attr('href', $.createLink('story', 'view', 'storyID=' + item.id, '', true)); + $title.appendTo($item); + } + $title.attr('title', item.title).find('.text').text(item.title); + } + + if(scaleSize <= 2) + { + var idHtml = scaleSize <= 1 ? ('#' + item.id + '') : ''; + var priHtml = '' + item.pri + ''; + var hoursHtml = (item.estimate && scaleSize <= 1) ? ('' + item.estimate + 'h') : ''; + var avatarHtml = renderUserAvatar(item.assignedTo, 'story', item.id); + var $infos = $item.find('.infos'); + if(!$infos.length) $infos = $('
'); + $infos.html([idHtml, priHtml, hoursHtml].join('')); + + $infos[scaleSize <= 1 ? 'append' : 'prepend'](avatarHtml); + if(scaleSize <= 1) $infos.appendTo($item); + else if(scaleSize === 2) $infos.prependTo($item); + else $infos.prependTo($item.find('.title')); + } + else if(scaleSize === 4) + { + $item.html(renderUserAvatar(item.assignedTo, 'story', item.id, 'md')); + } + + if(scaleSize <= 1) + { + var $actions = $item.find('.actions'); + if(!$actions.length && item.menus && item.menus.length) + { + $actions = $([ + '
', + '', + '', + '', + '
' + ].join('')).appendTo($item); + } + } + + return $item.attr('data-type', 'story').addClass('kanban-item-story'); +} + +/** + * Render bug item + * @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 scaleSize = window.kanbanScaleSize; + if(+$item.attr('data-scale-size') !== scaleSize) $item.empty().attr('data-scale-size', scaleSize); + + if(scaleSize <= 3) + { + var $title = $item.find('.title'); + if(!$title.length) + { + $title = $('' + (scaleSize <= 1 ? ' ' : '') + '') + .attr('href', $.createLink('bug', 'view', 'bugID=' + item.id, '', true)); + $title.appendTo($item); + } + $title.attr('title', item.title).find('.text').text(item.title); + } + + if(scaleSize <= 2) + { + var idHtml = scaleSize <= 1 ? ('#' + item.id + '') : ''; + var severityHtml = scaleSize <= 1 ? ('') : ''; + var priHtml = '' + item.pri + ''; + var avatarHtml = renderUserAvatar(item.assignedTo, 'bug', item.id); + + var $infos = $item.find('.infos'); + if(!$infos.length) $infos = $('
'); + $infos.html([idHtml, severityHtml, priHtml].join('')); + if(item.deadline && scaleSize <= 1) $infos.append(renderDeadline(item.deadline)); + $infos[scaleSize <= 1 ? 'append' : 'prepend'](avatarHtml); + + if(scaleSize <= 1) $infos.appendTo($item); + else if(scaleSize === 2) $infos.prependTo($item); + else $infos.prependTo($item.find('.title')); + } + else if(scaleSize === 4) + { + $item.html(renderUserAvatar(item.assignedTo, 'bug', item.id, 'md')); + } + + if(scaleSize <= 1) + { + var $actions = $item.find('.actions'); + if(!$actions.length && item.menus && item.menus.length) + { + $actions = $([ + '
', + '', + '', + '', + '
' + ].join('')).appendTo($item); + } + } + + return $item.attr('data-type', 'bug').addClass('kanban-item-bug'); +} + +/** + * 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 scaleSize = window.kanbanScaleSize; + if(+$item.attr('data-scale-size') !== scaleSize) $item.empty().attr('data-scale-size', scaleSize); + + if(scaleSize <= 3) + { + var $title = $item.find('.title'); + if(!$title.length) + { + $title = $('' + (scaleSize <= 1 ? ' ' : '') + '') + .attr('href', $.createLink('task', 'view', 'taskID=' + item.id, '', true)); + $title.appendTo($item); + } + $title.attr('title', item.name).find('.text').text(item.name); + } + + if(scaleSize <= 2) + { + var idHtml = scaleSize <= 1 ? ('#' + item.id + '') : ''; + var priHtml = '' + item.pri + ''; + var hoursHtml = (item.estimate && scaleSize <= 1) ? ('' + item.estimate + 'h') : ''; + var avatarHtml = renderUserAvatar(item.assignedTo, 'task', item.id); + + var $infos = $item.find('.infos'); + if(!$infos.length) $infos = $('
'); + $infos.html([idHtml, priHtml, hoursHtml].join('')); + if(item.deadline && scaleSize <= 1) $infos.append(renderDeadline(item.deadline)); + $infos[scaleSize <= 1 ? 'append' : 'prepend'](avatarHtml); + + if(scaleSize <= 1) $infos.appendTo($item); + else if(scaleSize === 2) $infos.prependTo($item); + else $infos.prependTo($item.find('.title')); + } + else if(scaleSize === 4) + { + $item.html(renderUserAvatar(item.assignedTo, 'task', item.id, 'md')); + } + + if(scaleSize <= 1) + { + var $actions = $item.find('.actions'); + if(!$actions.length && item.menus && 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'); + 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)) + { + $actions.append([ + '', + '', + '' + ].join('')); + } + + $actions.append([ + '', + '', + '' + ].join('')); + $actions.appendTo($col); +} + +/** + * 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 + * @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 + * @param {Object} data Kanban data + * @param {Object} options Kanban options + */ +function createKanban(kanbanID, data, options) +{ + var $kanban = $('#kanban-' + kanbanID); + var displayCards = window.displayCards == 'undefined' ? 2 : window.displayCards; + if($kanban.length) return updateKanban(kanbanID, data); + + $kanban = $('
').appendTo('#kanbans'); + $kanban.kanban($.extend({data: data, calcColHeight: calcColHeight, displayCards: displayCards}, 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() + { + if($(this).hasClass('iframe')) + { + 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 + { + afterEnterFullscreen(); + } + } + catch (error) + { + whenFailEnterFullscreen(error); + } + } +} + +/** + * 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: + { + backlog: ['ready'], + ready: ['backlog'], + }, + bug: + { + 'unconfirmed': ['confirmed', 'fixing', 'fixed'], + 'confirmed': ['fixing', 'fixed'], + 'fixing': ['fixed'], + 'fixed': ['testing', 'tested', 'fixing'], + 'testing': ['tested', 'closed', 'fixing'], + 'tested': ['closed', 'fixing'], + 'closed': ['fixing'], + }, + task: + { + 'wait': ['developing', 'developed', 'canceled', 'closed'], + 'developing': ['developed', 'pause'], + 'developed': ['canceled', 'closed'], + 'pause': ['developing'], + 'canceled': ['developing'], + 'closed': ['developing'], + } + } +} + +/* + * 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; + }); +} + +/** + * changeCardColType + * + * @param int $cardID + * @param int $fromColID + * @param int $toColID + * @param int $fromLaneID + * @param int $toLaneID + * @param string $cardType + * @param string $fromColType + * @param string $toColType + * @access public + * @return void + */ +function changeCardColType(cardID, fromColID, toColID, fromLaneID, toLaneID, cardType, fromColType, toColType) +{ + var objectID = cardID; + var showIframe = false; + var moveCard = false; + + /* Task lane. */ + if(cardType == '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; + } + } + } + + /* Bug lane. */ + if(cardType == 'bug') + { + if(toColType == 'confirmed') + { + if(fromColType == 'unconfirmed' && priv.canConfirmBug) + { + var link = createLink('bug', 'confirmBug', 'bugID=' + objectID, '', true); + showIframe = true; + } + } + else if(toColType == 'fixing') + { + if(fromColType == 'confirmed' || fromColType == 'unconfirmed') moveCard = true; + if((fromColType == 'closed' || fromColType == 'fixed' || fromColType == 'testing' || fromColType == 'tested') && priv.canActivateBug) + { + var link = createLink('bug', 'activate', 'bugID=' + objectID, '', true); + showIframe = true; + } + } + else if(toColType == 'fixed') + { + if(fromColType == 'fixing' || fromColType == 'confirmed' || fromColType == 'unconfirmed') + { + var link = createLink('bug', 'resolve', 'bugID=' + objectID, '', true); + showIframe = true; + } + } + else if(toColType == 'testing') + { + if(fromColType == 'fixed') moveCard = true; + } + else if(toColType == 'tested') + { + if(fromColType == 'fixed' || fromColType == 'testing') moveCard = true; + } + else if(toColType == 'closed') + { + if(fromColType == 'testing' || fromColType == 'tested') + { + var link = createLink('bug', 'close', 'bugID=' + objectID, '', true); + showIframe = true; + } + } + + if(moveCard) + { + var link = createLink('kanban', 'ajaxMoveCard', 'cardID=' + objectID + '&fromColID=' + fromColID + '&toColID=' + toColID + '&fromLaneID=' + fromLaneID + '&toLaneID=' + toLaneID + '&execitionID=' + executionID + '&browseType=' + browseType + '&groupBy=' + groupBy); + $.get(link, function(data) + { + if(data) + { + kanbanGroup = $.parseJSON(data); + if(groupBy == 'default') + { + updateKanban('bug', kanbanGroup.bug); + } + else + { + updateKanban(browseType, kanbanGroup[groupBy]); + } + } + }) + } + } + + /* Story lane. */ + if(cardType == 'story') + { + if(toColType == 'ready' || toColType == 'backlog') + { + var link = createLink('kanban', 'ajaxMoveCard', 'cardID=' + objectID + '&fromColID=' + fromColID + '&toColID=' + toColID + '&fromLaneID=' + fromLaneID + '&toLaneID=' + toLaneID + '&execitionID=' + executionID + '&browseType=' + browseType + '&groupBy=' + groupBy); + $.get(link, function(data) + { + if(data) + { + kanbanGroup = $.parseJSON(data); + if(groupBy == 'default') + { + updateKanban('story', kanbanGroup.story); + } + else + { + updateKanban(browseType, kanbanGroup[groupBy]); + } + } + }) + } + } + + if(showIframe) + { + var modalTrigger = new $.zui.ModalTrigger({type: 'iframe', width: '80%', url: link}); + modalTrigger.show(); + } +} + +/** + * Handle drop task. + * + * @param object $element + * @param object $event + * @param object $kanban + * @access public + * @return void + */ +function handleDropTask($element, event, kanban) +{ + if(!event.target) return; + + var $card = $element; + var $oldCol = $card.closest('.kanban-col'); + var $newCol = $(event.target).closest('.kanban-col'); + var oldCol = $oldCol.data(); + var newCol = $newCol.data(); + var oldLane = $oldCol.closest('.kanban-lane').data('lane'); + var newLane = $newCol.closest('.kanban-lane').data('lane'); + var cardType = $card.find('.kanban-card').data('type'); + + if(oldCol.id === newCol.id && newLane.id === oldLane.id) return false; + + var cardID = $card.data().id; + var fromColType = $oldCol.data('type'); + var toColType = $newCol.data('type'); + + changeCardColType(cardID, oldCol.id, newCol.id, oldLane.id, newLane.id, cardType, fromColType, toColType); +} + +var kanbanActionHandlers = +{ + dropItem: handleDropTask +}; + +/** + * Handle kanban action. + * + * @param string $action + * @param object $element + * @param object $event + * @param object $kanban + * @access public + * @return void + */ +function handleKanbanAction(action, $element, event, kanban) +{ + if(groupBy && groupBy != 'default') return false; + $('.kanban').attr('data-action-enabled', action); + var handler = kanbanActionHandlers[action]; + if(handler) handler($element, event, kanban); +} + +/** + * Handle finish drop task + * @param {Object} event Event object + * @returns {void} + */ +function handleFinishDrop(event) +{ + $('#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 + '&from=execution'), className: 'iframe', attrs: {'data-width': '500px'}}) + if(priv.canSetWIP) items.push({label: executionLang.setWIP, url: $.createLink('kanban', 'setWIP', 'col=' + col.columnID + '&executionID=' + executionID + '&from=execution'), 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 + '&from=execution'), 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 + * @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); + } +}); + +/** Get card height */ +function getCardHeight() +{ + return [59, 59, 62, 62, 47][window.kanbanScaleSize]; +} + +/** Change kanban scale size */ +function changeKanbanScaleSize(newScaleSize) +{ + var newScaleSize = Math.max(1, Math.min(4, newScaleSize)); + if(newScaleSize === window.kanbanScaleSize) return; + + window.kanbanScaleSize = newScaleSize; + $.zui.store.set('executionKanbanScaleSize', newScaleSize); + $('#kanbanScaleSize').text(newScaleSize); + $('#kanbanScaleControl .btn[data-type="+"]').attr('disabled', newScaleSize >= 4 ? 'disabled' : null); + $('#kanbanScaleControl .btn[data-type="-"]').attr('disabled', newScaleSize <= 1 ? 'disabled' : null); + + $('#kanbans').children('.kanban').each(function() + { + var kanban = $(this).data('zui.kanban'); + if(!kanban) return; + kanban.setOptions({cardsPerRow: newScaleSize, cardHeight: getCardHeight()}); + }); + + return newScaleSize; +} + +/* Example code: */ +$(function() +{ + $.cookie('isFullScreen', 0); + + window.kanbanScaleSize = +$.zui.store.get('executionKanbanScaleSize', 1); + $('#kanbanScaleSize').text(window.kanbanScaleSize); + $('#kanbanScaleControl .btn[data-type="+"]').attr('disabled', window.kanbanScaleSize >= 4 ? 'disabled' : null); + $('#kanbanScaleControl .btn[data-type="-"]').attr('disabled', window.kanbanScaleSize <= 1 ? 'disabled' : null); + + /* Common options */  + var commonOptions = + { + maxColHeight: 'auto', + minColWidth: 240, + maxColWidth: 240, + cardHeight: getCardHeight(), + showCount: true, + showZeroCount: true, + fluidBoardWidth: false, + cardsPerRow: window.kanbanScaleSize, + virtualize: true, + onAction: handleKanbanAction, + virtualRenderOptions: {container: '#kanbanContainer>.panel-body'}, + droppable: + { + target: findDropColumns, + finish: handleFinishDrop, + mouseButton: 'left' + }, + onRenderHeaderCol: renderHeaderCol, + onRenderLaneName: renderLaneName, + onRenderCount: renderColumnCount + }; + + if(groupBy != 'default') commonOptions.droppable = false; + + /* 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: event, $trigger: $trigger}, $trigger.data()); + var items = menuCreator(options); + if(!items || !items.length) return; + + $.zui.ContextMenu.show(items, items.$options || {event: event}); + }); + + /* Make kanbanScaleControl works */ + $('#kanbanScaleControl').on('click', '.btn', function() + { + changeKanbanScaleSize(window.kanbanScaleSize + ($(this).data('type') === '+' ? 1 : -1)); + }); + + /* 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. */ + var lastUpdateData; + setInterval(function() + { + $.get(createLink('execution', 'ajaxUpdateKanban', "executionID=" + executionID + "&entertime=" + entertime + "&browseType=" + browseType + "&groupBy=" + groupBy), function(data) + { + if(data && lastUpdateData !== data) + { + lastUpdateData = 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', 'taskKanban', "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', 'taskKanban', 'executionID=' + executionID + '&type=' + type + '&orderBy=order_asc' + '&groupBy=' + group); + location.href = link; +}); + +/** Calculate column height */ +function calcColHeight(col, lane, colCards, colHeight, kanban) +{ + var options = kanban.options; + if(!options.displayCards) return 0; + + var displayCards = +(options.displayCards || 2); + + if (typeof displayCards !== 'number' || displayCards < 2) displayCards = 2; + return (displayCards * (options.cardHeight + options.cardSpace) + options.cardSpace); +} diff --git a/module/execution/lang/en.php b/module/execution/lang/en.php index 6006065bfc..3f3905ff27 100644 --- a/module/execution/lang/en.php +++ b/module/execution/lang/en.php @@ -10,105 +10,106 @@ * @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->projectName = '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 = 'Planned Begin'; -$lang->execution->end = 'Planned 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->unfoldClosed = 'Unfold Closed'; -$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'; +$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->projectName = '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 = 'Planned Begin'; +$lang->execution->end = 'Planned 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->unfoldClosed = 'Unfold Closed'; +$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'; +$lang->execution->kanbanNoLinkProduct = "Kanban not linked {$lang->productCommon}"; /* Fields of zt_team. */ $lang->execution->root = 'Root'; @@ -163,7 +164,7 @@ global $config; if($config->systemMode == 'new') { $lang->execution->aclList['private'] = 'Private (for team members and execution stakeholders)'; - $lang->execution->aclList['open'] = 'Inherited Execution ACL (for who can access the current execution)'; + $lang->execution->aclList['open'] = 'Inherited Project ACL (for who can access the current project)'; } else { @@ -171,6 +172,9 @@ else $lang->execution->aclList['open'] = "Public (Users who can visit {$lang->executionCommon} can access it.)"; } +$lang->execution->kanbanAclList['private'] = 'Private'; +$lang->execution->kanbanAclList['open'] = 'Inherited Project'; + $lang->execution->storyPoint = 'Story Point'; $lang->execution->burnByList['left'] = 'View by remaining hours'; @@ -247,6 +251,8 @@ $lang->execution->iteration = 'Iterations'; $lang->execution->iterationInfo = '%s Iterations'; $lang->execution->viewAll = 'View All'; $lang->execution->testreport = 'Test Report'; +$lang->execution->taskKanban = 'Task Kanban'; +$lang->execution->RDKanban = 'Research & Development Kanban'; /* Group browsing. */ $lang->execution->allTasks = 'All'; @@ -422,6 +428,7 @@ $lang->printKanban->typeList['increment'] = 'Increment'; $lang->execution->typeList[''] = ''; $lang->execution->typeList['stage'] = 'Stage'; $lang->execution->typeList['sprint'] = $lang->executionCommon; +$lang->execution->typeList['kanban'] = 'Kanban'; $lang->execution->featureBar['task']['all'] = $lang->execution->allTasks; $lang->execution->featureBar['task']['unclosed'] = $lang->execution->unclosed; diff --git a/module/execution/lang/zh-cn.php b/module/execution/lang/zh-cn.php index eed1188d4c..148047ed53 100644 --- a/module/execution/lang/zh-cn.php +++ b/module/execution/lang/zh-cn.php @@ -10,105 +10,106 @@ * @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->projectName = '所属项目'; -$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 = '批量建任务'; +$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->projectName = '所属项目'; +$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 = '批量建任务'; +$lang->execution->kanbanNoLinkProduct = "看板没有关联{$lang->productCommon}"; /* Fields of zt_team. */ $lang->execution->root = '源ID'; @@ -171,6 +172,9 @@ else $lang->execution->aclList['open'] = "公开(有{$lang->executionCommon}视图权限即可访问)"; } +$lang->execution->kanbanAclList['private'] = '私有'; +$lang->execution->kanbanAclList['open'] = '继承项目'; + $lang->execution->storyPoint = '故事点'; $lang->execution->burnByList['left'] = '按剩余工时查看'; @@ -247,6 +251,8 @@ $lang->execution->iteration = '版本迭代'; $lang->execution->iterationInfo = '迭代%s次'; $lang->execution->viewAll = '查看所有'; $lang->execution->testreport = '测试报告'; +$lang->execution->taskKanban = '任务看板'; +$lang->execution->RDKanban = '研发看板'; /* 分组浏览。*/ $lang->execution->allTasks = '所有'; @@ -422,6 +428,7 @@ $lang->printKanban->typeList['increment'] = '增量'; $lang->execution->typeList[''] = ''; $lang->execution->typeList['stage'] = '阶段'; $lang->execution->typeList['sprint'] = $lang->executionCommon; +$lang->execution->typeList['kanban'] = '看板'; $lang->execution->featureBar['task']['all'] = $lang->execution->allTasks; $lang->execution->featureBar['task']['unclosed'] = $lang->execution->unclosed; diff --git a/module/execution/model.php b/module/execution/model.php index 6a7626aada..937402ee6b 100644 --- a/module/execution/model.php +++ b/module/execution/model.php @@ -65,13 +65,14 @@ class executionModel extends model { if(!$this->app->user->admin and strpos(",{$this->app->user->view->sprints},", ",$executionID,") === false and !defined('TUTORIAL') and $executionID != 0) die(js::error($this->lang->execution->accessDenied) . js::locate('back')); - $executions = $this->loadModel('execution')->getPairs(0, 'all', 'nocode'); + $executions = $this->getPairs(0, 'all', 'nocode'); if(!$executionID and $this->session->execution) $executionID = $this->session->execution; if(!$executionID or !in_array($executionID, array_keys($executions))) $executionID = key($executions); $this->session->set('execution', $executionID); /* Unset story, bug, build and testtask if type is ops. */ $execution = $this->getByID($executionID); + if($execution and $execution->type == 'kanban') $this->lang->execution->menu = new stdclass(); if($execution and $execution->type == 'stage' and $this->config->systemMode == 'new') { @@ -88,15 +89,6 @@ class executionModel extends model unset($this->lang->execution->menu->build); } - /* Hide story and qa menu when execution is story or design type. */ - /* - if($execution and ($execution->attribute == 'story' or $execution->attribute == 'design')) - { - unset($this->lang->execution->menu->story); - unset($this->lang->execution->menu->qa); - } - */ - if($executions and (!isset($executions[$executionID]) or !$this->checkPriv($executionID))) $this->accessDenied(); $moduleName = $this->app->getModuleName(); @@ -312,6 +304,12 @@ class executionModel extends model return false; } + if($type == 'kanban' and empty($this->post->products[0])) + { + dao::$errors['message'][] = $this->lang->execution->kanbanNoLinkProduct; + return false; + } + $this->config->execution->create->requiredFields .= ',project'; } @@ -381,7 +379,7 @@ class executionModel extends model $creatorExists = false; $teamMembers = array(); - $this->loadModel('kanban')->createExecutionLane($executionID); + if((isset($project) and $project->model != 'kanban') or empty($project)) $this->loadModel('kanban')->createExecutionLane($executionID); /* Save order. */ $this->dao->update(TABLE_EXECUTION)->set('`order`')->eq($executionID * 5)->where('id')->eq($executionID)->exec(); @@ -1054,7 +1052,7 @@ class executionModel extends model /* Order by status's content whether or not done */ $executions = $this->dao->select('*, IF(INSTR("done,closed", status) < 2, 0, 1) AS isDone, INSTR("doing,wait,suspended,closed", status) AS sortStatus')->from(TABLE_EXECUTION) ->where('deleted')->eq(0) - ->beginIF($type == 'all')->andWhere('type')->in('stage,sprint')->fi() + ->beginIF($type == 'all')->andWhere('type')->in('stage,sprint,kanban')->fi() ->beginIF($projectID and $this->config->systemMode == 'new')->andWhere('project')->eq($projectID)->fi() ->beginIF($type != 'all' and $this->config->systemMode == 'new')->andWhere('type')->eq($type)->fi() ->beginIF(strpos($mode, 'withdelete') === false)->andWhere('deleted')->eq(0)->fi() @@ -1202,7 +1200,7 @@ class executionModel extends model public function getIdList($projectID, $status = 'all') { return $this->dao->select('id')->from(TABLE_EXECUTION) - ->where('type')->in('sprint,stage') + ->where('type')->in('sprint,stage,kanban') ->andWhere('deleted')->eq('0') ->beginIF($projectID)->andWhere('project')->eq($projectID)->fi() ->beginIF($status == 'undone')->andWhere('status')->notIN('done,closed')->fi() @@ -1228,7 +1226,7 @@ class executionModel extends model $orderBy = (isset($project->model) and $project->model == 'waterfall') ? 'begin_asc,id_asc' : 'begin_desc,id_desc'; $executions = $this->dao->select('*')->from(TABLE_EXECUTION) - ->where('type')->in('stage,sprint') + ->where('type')->in('stage,sprint,kanban') ->andWhere('deleted')->eq('0') ->beginIF($projectID)->andWhere('project')->eq((int)$projectID)->fi() ->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->sprints)->fi() @@ -1319,7 +1317,7 @@ class executionModel extends model $module = 'execution'; $method = 'task'; } - if($module == 'testcase' and ($method == 'view' || $method == 'edit' || $method == 'batchedit')) + if($module == 'testcase' and ($method == 'view' or $method == 'edit' or $method == 'batchedit')) { $module = 'execution'; $method = 'testcase'; @@ -1329,7 +1327,7 @@ class executionModel extends model $module = 'execution'; $method = 'testtask'; } - if($module == 'build' and ($method == 'edit' || $method= 'view')) + if($module == 'build' and ($method == 'edit' or $method == 'view')) { $module = 'execution'; $method = 'build'; @@ -2479,6 +2477,23 @@ class executionModel extends model ->fetchAll('account'); } + /** + * Get members by execution id list. + * + * @param array $executionIdList + * @access public + * @return void + */ + public function getMembersByIdList($executionIdList) + { + return $this->dao->select("t1.root, t1.account, t2.realname")->from(TABLE_TEAM)->alias('t1') + ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account = t2.account') + ->where('t1.root')->in($executionIdList) + ->andWhere('t1.type')->eq('execution') + ->andWhere('t2.deleted')->eq('0') + ->fetchGroup('root'); + } + /** * Get the skip members of the team. * @@ -3022,13 +3037,16 @@ class executionModel extends model */ public static function isClickable($execution, $action) { - $action = strtolower($action); + $action = strtolower($action); + $clickable = commonModel::hasPriv('execution', $action); + if(!$clickable) return false; if($action == 'start') return $execution->status == 'wait'; if($action == 'close') return $execution->status != 'closed'; if($action == 'suspend') return $execution->status == 'wait' or $execution->status == 'doing'; if($action == 'putoff') return $execution->status == 'wait' or $execution->status == 'doing'; if($action == 'activate') return $execution->status == 'suspended' or $execution->status == 'closed'; + if($action == 'delete') return $execution->status == 'wait' or $execution->status == 'doing'; return true; } diff --git a/module/execution/view/ajaxgetdropmenu.html.php b/module/execution/view/ajaxgetdropmenu.html.php index e5ac0900c0..529e93b029 100644 --- a/module/execution/view/ajaxgetdropmenu.html.php +++ b/module/execution/view/ajaxgetdropmenu.html.php @@ -72,6 +72,11 @@ foreach($executions as $projectID => $projectExecutions) foreach($projectExecutions as $index => $execution) { + $kanbanLink = $this->createLink('execution', 'kanban', "executionID=%s"); + $taskLink = $this->createLink('execution', 'task', "executionID=%s"); + if($execution->type != 'kanban' and $link == $kanbanLink) $link = $taskLink; + if($execution->type == 'kanban' and $link != $kanbanLink) $link = $kanbanLink; + $selected = $execution->id == $executionID ? 'selected' : ''; if($execution->status != 'done' and $execution->status != 'closed' and ($execution->PM == $this->app->user->account or isset($execution->teams[$this->app->user->account]))) { diff --git a/module/execution/view/all.html.php b/module/execution/view/all.html.php index 4c46d38b2f..38e8118dfb 100644 --- a/module/execution/view/all.html.php +++ b/module/execution/view/all.html.php @@ -118,11 +118,10 @@ id);?> flex' title='name?>'> - config->maxVersion)):?> - '>execution->typeList[$execution->type]?> - + '>execution->typeList[$execution->type]?> children) ? $execution->name : html::a($this->createLink('execution', 'task', 'execution=' . $execution->id), $execution->name, '', "class='text-ellipsis'"); + $executionLink = $execution->projectModel == 'kanban' ? html::a($this->createLink('execution', 'kanban', 'executionID=' . $execution->id), $execution->name, '', "class='text-ellipsis'") : html::a($this->createLink('execution', 'task', 'execution=' . $execution->id), $execution->name, '', "class='text-ellipsis'"); + echo !empty($execution->children) ? $execution->name : $executionLink; if(isset($execution->delay)) echo "{$lang->execution->delayed} "; ?> children)):?> diff --git a/module/execution/view/create.html.php b/module/execution/view/create.html.php index 2fb95dbfc9..b4ea6625c5 100644 --- a/module/execution/view/create.html.php +++ b/module/execution/view/create.html.php @@ -42,6 +42,7 @@ project->common);?> systemMode);?> +
@@ -88,6 +89,7 @@
+ model != 'kanban'):?> systemMode == 'new')) ? $lang->execution->execType : $lang->execution->type;?> @@ -104,6 +106,7 @@
execution->typeDesc;?>
+ stage->percent;?> diff --git a/module/execution/view/edit.html.php b/module/execution/view/edit.html.php index dfd74c8305..0316288f4f 100644 --- a/module/execution/view/edit.html.php +++ b/module/execution/view/edit.html.php @@ -66,6 +66,7 @@
+ type != 'kanban'):?> execution->type;?> @@ -81,6 +82,7 @@ ?> + execution->teamname;?> team, "class='form-control'");?> diff --git a/module/execution/view/kanban.html.php b/module/execution/view/kanban.html.php index 28fb732228..41dad25b85 100644 --- a/module/execution/view/kanban.html.php +++ b/module/execution/view/kanban.html.php @@ -1,15 +1,90 @@ * @package execution - * @version $Id: kanban.html.php $ + * @version $Id: kanban.html.php 935 2022-01-11 16:49:24Z $ + * @link https://www.zentao.net */ ?> + +laneCount; + +js::set('regions', $regions); +js::set('browseType', $browseType); +js::set('orderBy', $orderBy); +js::set('groupBy', $groupBy); +js::set('execution', $execution); +js::set('productID', $productID); +js::set('kanbanLang', $lang->kanban); +js::set('kanbanlaneLang', $lang->kanbanlane); +js::set('storyLang', $lang->story); +js::set('executionLang', $lang->execution); +js::set('bugLang', $lang->bug); +js::set('taskLang', $lang->task); +js::set('deadlineLang', $lang->task->deadlineAB); +js::set('kanbancolumnLang', $lang->kanbancolumn); +js::set('kanbancardLang', $lang->kanbancard); +js::set('executionID', $execution->id); +js::set('laneCount', $laneCount); +js::set('userList', $userList); +js::set('noAssigned', $lang->kanbancard->noAssigned); +js::set('users', $users); +js::set('displayCards', $execution->displayCards); +js::set('colorListLang', $lang->kanbancard->colorList); +js::set('colorList', $this->config->kanban->cardColorList); + +$canSortRegion = commonModel::hasPriv('kanban', 'sortRegion') && count($regions) > 1; +$canEditRegion = commonModel::hasPriv('kanban', 'editRegion'); +$canDeleteRegion = commonModel::hasPriv('kanban', 'deleteRegion'); +$canCreateLane = commonModel::hasPriv('kanban', 'createLane'); +$canCreateTask = common::hasPriv('task', 'create'); +$canBatchCreateTask = common::hasPriv('task', 'batchCreate'); +$canCreateBug = common::hasPriv('bug', 'create'); +$canBatchCreateBug = common::hasPriv('bug', 'batchCreate'); +$canCreateStory = ($productID and common::hasPriv('story', 'create')); +$canBatchCreateStory = ($productID and common::hasPriv('story', 'batchCreate')); +$canLinkStory = ($productID and common::hasPriv('execution', 'linkStory')); +$canLinkStoryByPlane = ($productID and common::hasPriv('execution', 'importplanstories')); +$hasStoryButton = ($canCreateStory or $canBatchCreateStory or $canLinkStory or $canLinkStoryByPlane); +$hasTaskButton = ($canCreateTask or $canBatchCreateTask); +$hasBugButton = ($canCreateBug or $canBatchCreateBug); + +js::set('priv', + array( + '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'), + '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'), + 'canAssignBug' => common::hasPriv('bug', 'assignto'), + 'canConfirmBug' => common::hasPriv('bug', 'confirmBug'), + 'canActivateBug' => common::hasPriv('bug', 'activate') + ) +); +js::set('hasStoryButton', $hasStoryButton); +js::set('hasBugButton', $hasBugButton); +js::set('hasTaskButton', $hasTaskButton); +?> + -
-
+
+ +
+ +
+
+ +
- - - - - - - - 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'), - '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'), - 'canAssignBug' => common::hasPriv('bug', 'assignto'), - 'canConfirmBug' => common::hasPriv('bug', 'confirmBug'), - 'canActivateBug' => common::hasPriv('bug', 'activate') - ) -); -?> -execution);?> -story);?> -task);?> -bug);?> -execution->editName);?> -execution->setWIP);?> -execution->sortColumn);?> -kanban);?> -task->deadlineAB);?> -task->noAssigned);?> - - diff --git a/module/execution/view/taskheader.html.php b/module/execution/view/taskheader.html.php index c613a8bf5b..e7c2eea834 100644 --- a/module/execution/view/taskheader.html.php +++ b/module/execution/view/taskheader.html.php @@ -58,7 +58,7 @@ } } - echo "
  • "; common::printLink('execution', 'kanban', "executionID=$executionID", $lang->execution->kanban); echo '
  • '; + echo "
  • "; common::printLink('execution', 'taskKanban', "executionID=$executionID", $lang->execution->kanban); echo '
  • '; if($execution->type == 'sprint' or $execution->type == 'waterfall') { echo "
  • "; diff --git a/module/execution/view/taskkanban.html.php b/module/execution/view/taskkanban.html.php new file mode 100644 index 0000000000..5205158b97 --- /dev/null +++ b/module/execution/view/taskkanban.html.php @@ -0,0 +1,181 @@ + + + + +
  • + +
    +
    +
    +
    +
    + + + + + + + + + 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'), + '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'), + 'canAssignBug' => common::hasPriv('bug', 'assignto'), + 'canConfirmBug' => common::hasPriv('bug', 'confirmBug'), + 'canActivateBug' => common::hasPriv('bug', 'activate') + ) +); +?> +execution);?> +story);?> +task);?> +bug);?> +execution->editName);?> +execution->setWIP);?> +execution->sortColumn);?> +kanban);?> +task->deadlineAB);?> +task->noAssigned);?> + + +displayCards);?> + diff --git a/module/group/lang/resource.php b/module/group/lang/resource.php index bd9da48739..e72e4e7126 100644 --- a/module/group/lang/resource.php +++ b/module/group/lang/resource.php @@ -674,6 +674,7 @@ $lang->resource->kanban->cardsSort = 'cardsSort'; $lang->resource->kanban->viewArchivedColumn = 'viewArchivedColumn'; $lang->resource->kanban->viewArchivedCard = 'viewArchivedCard'; $lang->resource->kanban->restoreCard = 'restoreCard'; +$lang->resource->kanban->setLaneHeight = 'setLaneHeight'; $lang->kanban->methodOrder[5] = 'space'; $lang->kanban->methodOrder[10] = 'createSpace'; @@ -718,6 +719,7 @@ $lang->kanban->methodOrder[195] = 'viewArchivedColumn'; $lang->kanban->methodorder[200] = 'viewArchivedCard'; $lang->kanban->methodorder[205] = 'archiveColumn'; $lang->kanban->methodorder[210] = 'restoreCard'; +$lang->kanban->methodorder[215] = 'setLaneHeight'; /* Execution. */ $lang->resource->execution = new stdclass(); @@ -759,7 +761,7 @@ $lang->resource->execution->linkStory = 'linkStory'; $lang->resource->execution->unlinkStory = 'unlinkStory'; $lang->resource->execution->batchUnlinkStory = 'batchUnlinkStory'; $lang->resource->execution->updateOrder = 'updateOrder'; -$lang->resource->execution->kanban = 'kanban'; +$lang->resource->execution->taskKanban = 'taskKanban'; $lang->resource->execution->printKanban = 'printKanbanAction'; $lang->resource->execution->tree = 'treeAction'; $lang->resource->execution->treeTask = 'treeOnlyTask'; @@ -775,6 +777,7 @@ $lang->resource->execution->addWhitelist = 'addWhitelist'; $lang->resource->execution->unbindWhitelist = 'unbindWhitelist'; $lang->resource->execution->storyEstimate = 'storyEstimate'; $lang->resource->execution->executionkanban = 'kanbanAction'; +$lang->resource->execution->kanban = 'RDKanban'; //if($config->systemMode == 'classic') $lang->resource->project->list = 'list'; //$lang->execution->methodOrder[0] = 'index'; @@ -816,7 +819,7 @@ $lang->execution->methodOrder[170] = 'linkStory'; $lang->execution->methodOrder[175] = 'unlinkStory'; $lang->execution->methodOrder[180] = 'batchUnlinkStory'; $lang->execution->methodOrder[185] = 'updateOrder'; -$lang->execution->methodOrder[190] = 'kanban'; +$lang->execution->methodOrder[190] = 'taskKanban'; $lang->execution->methodOrder[195] = 'printKanban'; $lang->execution->methodOrder[200] = 'kanbanHideCols'; $lang->execution->methodOrder[205] = 'kanbanColsColor'; @@ -832,6 +835,7 @@ $lang->execution->methodOrder[250] = 'addWhitelist'; $lang->execution->methodOrder[255] = 'unbindWhitelist'; $lang->execution->methodOrder[260] = 'storyEstimate'; $lang->execution->methodOrder[265] = 'executionkanban'; +$lang->execution->methodOrder[270] = 'kanban'; /* Task. */ $lang->resource->task = new stdclass(); diff --git a/module/kanban/control.php b/module/kanban/control.php index a98155bc96..534586b70a 100644 --- a/module/kanban/control.php +++ b/module/kanban/control.php @@ -294,18 +294,19 @@ class kanban extends control * Create a region. * * @param int $kanbanID + * @param string $from kanban|execution * @access public * @return void */ - public function createRegion($kanbanID) + public function createRegion($kanbanID, $from = 'kanban') { if(!empty($_POST)) { - $kanban = $this->kanban->getByID($kanbanID); + $kanban = $from == 'execution' ? $this->loadModel('execution')->getByID($kanbanID) : $this->kanban->getByID($kanbanID); $copyRegionID = (int)$_POST['region']; unset($_POST['region']); - $regionID = $this->kanban->createRegion($kanban, '', $copyRegionID); + $regionID = $this->kanban->createRegion($kanban, '', $copyRegionID, $from); if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => 'parent')); @@ -417,10 +418,11 @@ class kanban extends control * * @param int $kanbanID * @param int $regionID + * @param string $from kanban|execution * @access public * @return void */ - public function createLane($kanbanID, $regionID) + public function createLane($kanbanID, $regionID, $from = 'kanban') { if(!empty($_POST)) { @@ -431,8 +433,12 @@ class kanban extends control return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => 'parent')); } - $this->view->lanes = $this->kanban->getLanePairsByRegion($regionID); - $this->display(); + $this->view->lanes = $this->kanban->getLanePairsByRegion($regionID, $from == 'kanban' ? 'all' : 'story'); + $this->view->from = $from; + $this->view->regionID = $regionID; + + if($from == 'kanban') $this->display(); + if($from == 'execution') $this->display('kanban', 'createexeclane'); } /** @@ -458,6 +464,34 @@ class kanban extends control return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess)); } + /** + * Set lane height. + * + * @param int $kanbanID + * @param string $from kanban|execution + * @access public + * @return void + */ + public function setLaneHeight($kanbanID, $from = 'kanban') + { + if(!empty($_POST)) + { + $this->kanban->setLaneHeight($kanbanID, $from); + + if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); + + return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => 'parent')); + + } + + $kanban = $from == 'execution' ? $this->loadModel('execution')->getByID($kanbanID) : $this->kanban->getByID($kanbanID); + + $this->view->heightType = $kanban->displayCards > 2 ? 'custom' : 'auto'; + $this->view->displayCards = $kanban->displayCards ? $kanban->displayCards : ''; + + $this->display(); + } + /** * Delete a lane. * @@ -470,7 +504,10 @@ class kanban extends control { if($confirm == 'no') { - die(js::confirm($this->lang->kanbanlane->confirmDelete, $this->createLink('kanban', 'deleteLane', "laneID=$laneID&confirm=yes"), '')); + $laneType = $this->kanban->getLaneById($laneID)->type; + $confirmTip = in_array($laneType, array('story', 'task', 'bug')) ? sprintf($this->lang->kanbanlane->confirmDeleteTip, $this->lang->{$laneType}->common) : $this->lang->kanbanlane->confirmDelete; + + die(js::confirm($confirmTip, $this->createLink('kanban', 'deleteLane', "laneID=$laneID&confirm=yes"), '')); } else { @@ -710,15 +747,17 @@ class kanban extends control * Move a card. * * @param int $cardID + * @param int $fromColID * @param int $toColID + * @param int $fromLaneID * @param int $toLaneID * @param int $kanbanID * @access public * @return void */ - public function moveCard($cardID, $toColID, $toLaneID, $kanbanID) + public function moveCard($cardID, $fromColID, $toColID, $fromLaneID, $toLaneID, $kanbanID) { - $this->kanban->moveCard($cardID, $toColID, $toLaneID); + $this->kanban->moveCard($cardID, $fromColID, $toColID, $fromLaneID, $toLaneID); if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); $kanbanGroup = $this->kanban->getKanbanData($kanbanID); die(json_encode($kanbanGroup)); @@ -992,26 +1031,48 @@ class kanban extends control * * @param int $cardID * @param int $fromColID - * @param string $toColType + * @param int $toColID + * @param int $fromLaneID + * @param int $toLaneID * @param int $executionID * @param string $browseType - * @param string $browseType + * @param string $groupBy + * @param int $regionID + * @param string $orderBy * @access public - * @return json + * @return void */ - public function ajaxMoveCard($cardID = 0, $fromColID = 0, $toColType = 'ready', $executionID = 0, $browseType = 'all', $groupBy = '') + public function ajaxMoveCard($cardID = 0, $fromColID = 0, $toColID = 0, $fromLaneID = 0, $toLaneID = 0, $executionID = 0, $browseType = 'all', $groupBy = '', $regionID = 0, $orderBy = '') { - $fromColumn = $this->dao->select('*')->from(TABLE_KANBANCOLUMN)->where('id')->eq($fromColID)->fetch(); - $toColumn = $this->dao->select('*')->from(TABLE_KANBANCOLUMN)->where('type')->eq($toColType)->andWhere('lane')->eq($fromColumn->lane)->fetch(); + $fromCell = $this->dao->select('id, cards')->from(TABLE_KANBANCELL) + ->where('kanban')->eq($executionID) + ->andWhere('lane')->eq($fromLaneID) + ->andWhere('`column`')->eq($fromColID) + ->fetch(); - $fromCards = str_replace(",$cardID,", ',', $fromColumn->cards); + $toCell = $this->dao->select('id, cards')->from(TABLE_KANBANCELL) + ->where('kanban')->eq($executionID) + ->andWhere('lane')->eq($toLaneID) + ->andWhere('`column`')->eq($toColID) + ->fetch(); + + $fromCards = str_replace(",$cardID,", ',', $fromCell->cards); $fromCards = $fromCards == ',' ? '' : $fromCards; - $toCards = ",$cardID," . ltrim($toColumn->cards, ','); + $toCards = ",$cardID," . ltrim($toCell->cards, ','); - $this->dao->update(TABLE_KANBANCOLUMN)->set('cards')->eq($fromCards)->where('id')->eq($fromColumn->id)->exec(); - $this->dao->update(TABLE_KANBANCOLUMN)->set('cards')->eq($toCards)->where('id')->eq($toColumn->id)->exec(); + $this->dao->update(TABLE_KANBANCELL)->set('cards')->eq($fromCards) + ->where('kanban')->eq($executionID) + ->andWhere('lane')->eq($fromLaneID) + ->andWhere('`column`')->eq($fromColID) + ->exec(); - $kanbanGroup = $this->kanban->getExecutionKanban($executionID, $browseType, $groupBy); + $this->dao->update(TABLE_KANBANCELL)->set('cards')->eq($toCards) + ->where('kanban')->eq($executionID) + ->andWhere('lane')->eq($toLaneID) + ->andWhere('`column`')->eq($toColID) + ->exec(); + + $kanbanGroup = $regionID == 0 ? $this->kanban->getExecutionKanban($executionID, $browseType, $groupBy) : $this->kanban->getRDKanban($executionID, $browseType, $orderBy, $groupBy, $regionID); die(json_encode($kanbanGroup)); } @@ -1084,4 +1145,20 @@ class kanban extends control $this->view->method = $methodName; $this->display(); } + + /** + * Ajax get lanes by region id. + * + * @param int $regionID + * @param string $type all|story|task|bug + * @access public + * @return string + */ + public function ajaxGetLanes($regionID, $type = 'all') + { + $lanes = $this->kanban->getLanePairsByRegion($regionID, $type); + + if(empty($lanes)) return; + return print(html::select('otherLane', $lanes, '', "class='form-control'")); + } } diff --git a/module/kanban/css/view.css b/module/kanban/css/view.css index 3d3351000f..b2e9f50509 100644 --- a/module/kanban/css/view.css +++ b/module/kanban/css/view.css @@ -58,8 +58,9 @@ .region .kanban-lane-items {overflow: auto; padding-bottom: 10px;} .region .kanban-item {position: relative} +.region .kanban-item > .kanban-card > .title {line-height: 18px;} .region .kanban-item > .kanban-card > .title:hover {color: #2272eb} -.region .kanban-item > .kanban-card > .info {margin-top: 5px; position: relative;} +.region .kanban-item > .kanban-card > .info {margin-top: 4px; position: relative;} .region .kanban-item > .kanban-card > .info > .pri {height: 14px; border-width: 1px; font-size: 12px; line-height: 12px; min-width: 14px; padding: 0 1px;} .region .kanban-item > .kanban-card > .info > .estimate{margin-left: 4px; color: #999; background-color: rgb(255, 255, 255); padding: 3px 0px} .region .kanban-item > .kanban-card > .info > .time {margin-left: 4px; font-size: 12px; background: rgb(242, 242, 242)} @@ -82,7 +83,7 @@ .region .kanban-item .has-color .info > .label-pri {border-color: #FFFFFF;} .region .kanban-item .item-more {position: absolute; right: 6px; top: 0px;} -.kanban-card {height: auto !important; padding: 8px 14px !important} +.kanban-card {height: auto !important; padding: 8px 14px !important; min-height: 60px;} .kanban-card > .title {word-break: break-all;} .kanban-card > .info {position: relative; margin-top: 5px} .kanban-card > .info > .info + .info {margin-left: 8px} diff --git a/module/kanban/js/createlane.js b/module/kanban/js/createlane.js index a2fe47e34c..c963df8dad 100644 --- a/module/kanban/js/createlane.js +++ b/module/kanban/js/createlane.js @@ -7,4 +7,3 @@ $(document).ready(function() $('#otherLane').parents('tr').toggle($(this).val() == 'sameAsOther'); }); }) - diff --git a/module/kanban/js/setlaneheight.js b/module/kanban/js/setlaneheight.js new file mode 100644 index 0000000000..4f37eb4763 --- /dev/null +++ b/module/kanban/js/setlaneheight.js @@ -0,0 +1,17 @@ +/** + * Set card count. + * + * @param string $heightType + * @access public + * @return void + */ +function setCardCount(heightType) +{ + heightType != 'custom' ? $('#cardBox').addClass('hidden') : $('#cardBox').removeClass('hidden'); +} + +$(function() +{ + var heightType = $("[name='heightType']:checked").val(); + setCardCount(heightType); +}) diff --git a/module/kanban/js/view.js b/module/kanban/js/view.js index 49ec25d957..7ec9e82ef2 100644 --- a/module/kanban/js/view.js +++ b/module/kanban/js/view.js @@ -374,16 +374,19 @@ function showErrorMessager(message) * Move a card. * * @param int $cardID + * @param int $fromColID * @param int $toColID + * @param int $fromLaneID + * @param int $toLaneID * @param int $kanbanID * @param int $regionID * @access public * @return string */ -function moveCard(cardID, toColID, toLaneID, kanbanID, regionID) +function moveCard(cardID, fromColID, toColID, fromLaneID, toLaneID, kanbanID, regionID) { if(!cardID) return false; - var url = createLink('kanban', 'moveCard', 'cardID=' + cardID + '&toColID=' + toColID + '&toLaneID=' + toLaneID + '&kanbanID=' + kanbanID); + var url = createLink('kanban', 'moveCard', 'cardID=' + cardID + '&fromColID='+ fromColID + '&toColID=' + toColID + '&fromLaneID='+ fromLaneID + '&toLaneID=' + toLaneID + '&kanbanID=' + kanbanID); return $.ajax( { method: 'post', @@ -460,6 +463,7 @@ function updateRegion(regionID, regionData = []) if(!regionData) regionData = regions[regionID]; $region.data('zui.kanban').render(regionData.groups); + resetRegionHeight('open'); return true; } @@ -586,7 +590,7 @@ function handleDropTask($element, event, kanban) if(oldCol.id === newCol.id && newLane.id === oldLane.id) return false; var cardID = $card.data().id; - moveCard(cardID, newCol.id, newLane.id, kanbanID, regionID); + moveCard(cardID, oldCol.id, newCol.id, oldLane.id, newLane.id, kanbanID, regionID); } /** @@ -762,6 +766,20 @@ function createColumnMenu(options) return items; } +/** Calculate column height */ +function calcColHeight(col, lane, colCards, colHeight, kanban) +{ + if(!isMultiLanes) return 0; + + var options = kanban.options; + + if(!options.displayCards) return 0; + var displayCards = +(options.displayCards || 2); + + if (typeof displayCards !== 'number' || displayCards < 2) displayCards = 2; + return (displayCards * (options.cardHeight + options.cardSpace) + options.cardSpace); +} + /* Define menu creators */ window.menuCreators = { @@ -775,16 +793,20 @@ window.menuCreators = */ function initKanban($kanban) { - var id = $kanban.data('id'); - var region = regions[id]; + var id = $kanban.data('id'); + var region = regions[id]; + var displayCards = window.displayCards == 'undefined' ? 2 : window.displayCards; $kanban.kanban( { data: region.groups, maxColHeight: 510, + calcColHeight: calcColHeight, fluidBoardWidth: false, - minColWidth: 300, - maxColWidth: 300, + minColWidth: 287, + maxColWidth: 287, + cardHeight: 60, + displayCards: displayCards, createColumnText: kanbanLang.createColumn, addItemText: '', itemRender: renderKanbanItem, @@ -813,7 +835,7 @@ function initKanban($kanban) */ $(function() { - if($.cookie('isFullScreen') == 1) fullScreen(); + window.isMultiLanes = laneCount > 1; /* Init first kanban */ $('.kanban').each(function() @@ -826,6 +848,7 @@ $(function() $(this).toggleClass('icon-chevron-double-up icon-chevron-double-down'); $(this).parents('.region').find('.kanban').toggle(); hideKanbanAction(); + resetRegionHeight($(this).hasClass('icon-chevron-double-up') ? 'open' : 'close'); }); $('.region-header').on('click', '.action', hideKanbanAction); @@ -1012,4 +1035,65 @@ $(function() if(sortType == 'lane') $cards.show(); } }); + + resetRegionHeight('open'); }); + +/** + * Reset region height according to window height. + * + * @param string fold + * @access public + * @return void + */ +function resetRegionHeight(fold) +{ + var regionCount = 0; + if($.isEmptyObject(regions)) return false; + for(var i in regions) + { + regionCount += 1; + if(regionCount > 1) return false; + } + + var regionID = Object.keys(regions)[0]; + var region = regions[regionID].groups; + var groupCount = 0; + + if($.isEmptyObject(region)) return false; + for(var j in region) + { + groupCount += 1; + if(groupCount > 1) return false; + } + + var group = region[0]; + var laneCount = 0; + + if($.isEmptyObject(group.lanes)) return false; + for(var h in group.lanes) + { + laneCount += 1; + if(laneCount > 1) return false; + } + + var regionHeaderHeight = $('.region-header').outerHeight(); + if(fold == 'open') + { + var windowHeight = $(window).height(); + var headerHeight = $('#mainHeader').outerHeight(); + var mainPadding = $('#main').css('padding-top'); + var panelBorder = $('.panel').css('border-top-width'); + var bodyPadding = $('.panel-body').css('padding-top'); + var height = windowHeight - (parseInt(mainPadding) * 2) - (parseInt(bodyPadding) * 2) - headerHeight - (parseInt(panelBorder) * 2); + var regionPadding = $('.kanban').css('padding-bottom'); + var columnHeight = $('.kanban-header').outerHeight(); + + $('.region').css('height', height); + $('.kanban-lane').css('height', height - regionHeaderHeight - parseInt(regionPadding) - columnHeight); + } + else + { + $('.region').css('height', regionHeaderHeight); + } +} diff --git a/module/kanban/lang/en.php b/module/kanban/lang/en.php index 7ba2b2d2c4..484c0196f3 100644 --- a/module/kanban/lang/en.php +++ b/module/kanban/lang/en.php @@ -18,6 +18,8 @@ $lang->kanban->deleteRegion = 'Delete Region'; $lang->kanban->createLane = 'Create Lane'; $lang->kanban->editLane = 'Edit Lane'; $lang->kanban->sortLane = 'Sort Lane'; +$lang->kanban->laneHeight = 'Lane Height'; +$lang->kanban->setLaneHeight = 'Set Lane Height'; $lang->kanban->deleteLane = 'Delete Lane'; $lang->kanban->createColumn = 'Create Column'; $lang->kanban->editColumn = 'Edit Column'; @@ -73,15 +75,18 @@ $lang->kanban->closedBy = 'Closed By'; $lang->kanban->closedDate = 'Closed Date'; $lang->kanban->empty = 'No Kanban'; $lang->kanban->teamSumCount = '%s people in total'; +$lang->kanban->cardCount = 'Card Count'; $lang->kanban->createColumnOnLeft = 'Create Column On Left'; $lang->kanban->createColumnOnRight = 'Create Column On Right'; $lang->kanban->accessDenied = "You have no access to the kanban."; $lang->kanban->confirmDelete = 'Do you want to delete this?'; +$lang->kanban->cardCountTip = 'Please enter the number of cards'; $lang->kanban->aclGroup['open'] = 'Open'; $lang->kanban->aclGroup['private'] = 'Private'; +$lang->kanban->aclGroup['extend'] = 'Extend'; $lang->kanban->aclList['extend'] = 'Extend (Accessible with space view permissions)'; $lang->kanban->aclList['private'] = 'Private (For the kanban team, whitelist members and space owner only)'; @@ -244,11 +249,20 @@ $lang->kanbanlane->default = 'Default Lane'; $lang->kanbanlane->column = 'Lane Kanban Column'; $lang->kanbanlane->otherlane = 'Select Existed Lane'; $lang->kanbanlane->color = 'Lane Color'; +$lang->kanbanlane->WIPType = 'Lane WIP Type'; -$lang->kanbanlane->confirmDelete = 'Are you sure to delete this lane? After deleting the lane, all data (columns and cards) in the lane will also be deleted.'; +$lang->kanbanlane->confirmDelete = 'Are you sure to delete this lane? After deleting the lane, all data (columns and cards) in the lane will also be deleted.'; +$lang->kanbanlane->confirmDeleteTip = 'Are you sure to delete this lane? After deleting the lane, all %s in the lane will be hidden.'; + +$lang->kanbanlane->modeList['sameAsOther'] = 'Use the same Kanban column'; +$lang->kanbanlane->modeList['independent'] = 'Independent Kanban column'; + +$lang->kanbanlane->heightTypeList['auto'] = 'Adaptive (Adaptive according to the card height)'; +$lang->kanbanlane->heightTypeList['custom'] = 'Custom (Customize lane height based on number of cards)'; + +$lang->kanbanlane->error = new stdclass(); +$lang->kanbanlane->error->mustBeInt = 'The number of cards must be a positive integer greater than 2.'; -$lang->kanbanlane->modeList['sameAsOther'] = 'Use the same Kanban column as other lanes'; -$lang->kanbanlane->modeList['independent'] = 'Independent Kanban column is adopted'; $lang->kanbanregion = new stdclass(); $lang->kanbanregion->name = 'Region Name'; diff --git a/module/kanban/lang/zh-cn.php b/module/kanban/lang/zh-cn.php index 62a58dec97..c5ccff65c5 100644 --- a/module/kanban/lang/zh-cn.php +++ b/module/kanban/lang/zh-cn.php @@ -18,6 +18,8 @@ $lang->kanban->deleteRegion = '删除区域'; $lang->kanban->createLane = '创建泳道'; $lang->kanban->editLane = '泳道设置'; $lang->kanban->sortLane = '泳道排序'; +$lang->kanban->laneHeight = '泳道高度'; +$lang->kanban->setLaneHeight = '设置泳道高度'; $lang->kanban->deleteLane = '删除泳道'; $lang->kanban->createColumn = '创建看板列'; $lang->kanban->editColumn = '编辑看板列'; @@ -73,15 +75,18 @@ $lang->kanban->closedBy = '由谁关闭'; $lang->kanban->closedDate = '关闭日期'; $lang->kanban->empty = '暂时没有看板'; $lang->kanban->teamSumCount = '共%s人'; +$lang->kanban->cardCount = '卡片数量'; $lang->kanban->createColumnOnLeft = '在左侧添加看板列'; $lang->kanban->createColumnOnRight = '在右侧添加看板列'; $lang->kanban->accessDenied = '您无权访问该看板'; $lang->kanban->confirmDelete = '您确认删除吗?'; +$lang->kanban->cardCountTip = '请输入卡片数量'; $lang->kanban->aclGroup['open'] = '公开'; $lang->kanban->aclGroup['private'] = '私有'; +$lang->kanban->aclGroup['extend'] = '继承空间'; $lang->kanban->aclList['extend'] = '继承空间访问权限(能访问当前空间,即可访问)'; $lang->kanban->aclList['private'] = '私有(看板团队成员、白名单、空间负责人可访问)'; @@ -244,12 +249,20 @@ $lang->kanbanlane->default = '默认泳道'; $lang->kanbanlane->column = '泳道看板列'; $lang->kanbanlane->otherlane = '选择共享看板列的泳道'; $lang->kanbanlane->color = '泳道颜色'; +$lang->kanbanlane->WIPType = '泳道在制品类型'; -$lang->kanbanlane->confirmDelete = '您确认删除该泳道吗?删除泳道后,该泳道中所有数据(列、卡片)也会被删除。'; +$lang->kanbanlane->confirmDelete = '您确认删除该泳道吗?删除泳道后,该泳道中所有数据(列、卡片)也会被删除。'; +$lang->kanbanlane->confirmDeleteTip = '您确认删除该泳道吗?删除泳道后,该泳道中所有的%s将被隐藏。'; $lang->kanbanlane->modeList['sameAsOther'] = '与其他泳道使用相同看板列'; $lang->kanbanlane->modeList['independent'] = '采用独立的看板列'; +$lang->kanbanlane->heightTypeList['auto'] = '自适应(根据卡片高度自适应)'; +$lang->kanbanlane->heightTypeList['custom'] = '自定义(根据卡片数量自定义泳道高度)'; + +$lang->kanbanlane->error = new stdclass(); +$lang->kanbanlane->error->mustBeInt = '卡片数量必须是大于2的正整数。'; + $lang->kanbanregion = new stdclass(); $lang->kanbanregion->name = '区域名称'; $lang->kanbanregion->default = '默认区域'; diff --git a/module/kanban/model.php b/module/kanban/model.php index afabc80307..f2cf659958 100644 --- a/module/kanban/model.php +++ b/module/kanban/model.php @@ -65,10 +65,11 @@ class kanbanModel extends model * @param object $kanban * @param object $region * @param int $copyRegionID + * @param string $from kanban|execution * @access public * @return int */ - public function createRegion($kanban, $region = null, $copyRegionID = 0) + public function createRegion($kanban, $region = null, $copyRegionID = 0, $from = 'kanban') { $account = $this->app->user->account; $order = 1; @@ -82,11 +83,11 @@ class kanbanModel extends model $order = $maxOrder ? $maxOrder + 1 : 1; $region = fixer::input('post') ->add('kanban', $kanban->id) - ->add('space', $kanban->space) ->add('createdBy', $account) ->add('createdDate', helper::now()) ->trim('name') ->get(); + if($from == 'kanban') $region->space = $kanban->space; } $region->order = $order; @@ -144,14 +145,14 @@ class kanbanModel extends model $copyColumn->parent = $parentColumns[$copyColumn->parent]; } - $parentColumnID = $this->createColumn($regionID, $copyColumn); + $parentColumnID = $this->createColumn($regionID, $copyColumn, 0, 0, $from); if($copyColumn->parent < 0) $parentColumns[$copyColumnID] = $parentColumnID; if(dao::isError()) return false; } } } - else + elseif($from == 'kanban') { $groupID = $this->createGroup($kanban->id, $regionID); if(dao::isError()) return false; @@ -229,10 +230,11 @@ class kanbanModel extends model * @param object $column * @param int $order * @param int $parent + * @param string $from kanban|execution * @access public * @return int */ - public function createColumn($regionID, $column = null, $order = 0, $parent = 0) + public function createColumn($regionID, $column = null, $order = 0, $parent = 0, $from = 'kanban') { if(empty($column)) { @@ -311,7 +313,12 @@ class kanbanModel extends model $columnID = $this->dao->lastInsertID(); - $this->dao->update(TABLE_KANBANCOLUMN)->set('type')->eq("column{$columnID}")->where('id')->eq($columnID)->exec(); + if($from == 'kanban') $this->dao->update(TABLE_KANBANCOLUMN)->set('type')->eq("column{$columnID}")->where('id')->eq($columnID)->exec(); + + /* Add kanban cell. */ + $lanes = $this->dao->select('id,type')->from(TABLE_KANBANLANE)->where('`group`')->eq($column->group)->fetchPairs(); + $kanbanID = $this->dao->select('kanban')->from(TABLE_KANBANREGION)->where('id')->eq($regionID)->fetch('kanban'); + foreach($lanes as $laneID => $laneType) $this->addKanbanCell($kanbanID, $laneID, $columnID, $laneType); return $columnID; } @@ -418,8 +425,6 @@ class kanbanModel extends model ->add('kanban', $kanbanID) ->add('region', $regionID) ->add('group', $groupID) - ->add('lane', $laneID) - ->add('column', $columnID) ->add('createdBy', $this->app->user->account) ->add('createdDate', $now) ->add('assignedDate', $now) @@ -444,6 +449,7 @@ class kanbanModel extends model $cardID = $this->dao->lastInsertID(); $this->file->saveUpload('kanbancard', $cardID); $this->file->updateObjectID($this->post->uid, $cardID, 'kanbancard'); + $this->addKanbanCell($kanbanID, $laneID, $columnID, 'common', $cardID); return $cardID; } @@ -614,6 +620,67 @@ class kanbanModel extends model return $kanbanData; } + /** + * Get a RD kanban data. + * + * @param int $executionID + * @param string $browseType all|story|task|bug + * @param string $orderBy + * @param int $regionID + * + * @access public + * @return array + */ + public function getRDKanban($executionID, $browseType = 'all', $orderBy = 'id_desc', $regionID = 0) + { + $kanbanData = array(); + $actions = array('sortGroup'); + $regions = $this->getRegionPairs($executionID); + $regionIDList = $regionID == 0 ? array_keys($regions) : array(0 => $regionID); + $groupGroup = $this->getGroupGroupByRegions($regionIDList); + $laneGroup = $this->getLaneGroupByRegions($regionIDList, $browseType); + $columnGroup = $this->getRDColumnGroupByRegions($regionIDList, array_keys($laneGroup)); + $cardGroup = $this->getCardGroupByExecution($executionID, $browseType, $orderBy); + + foreach($regions as $regionID => $regionName) + { + $region = new stdclass(); + $region->id = $regionID; + $region->name = $regionName; + $region->laneCount = 0; + + $groups = zget($groupGroup, $regionID, array()); + foreach($groups as $group) + { + $lanes = zget($laneGroup, $group->id, array()); + if(!$lanes) continue; + + foreach($lanes as $lane) + { + $this->refreshCards($lane); + $lane->items = isset($cardGroup[$lane->id]) ? $cardGroup[$lane->id] : array(); + $lane->defaultCardType = $lane->type; + } + + $group->columns = zget($columnGroup, $group->id, array()); + $group->lanes = $lanes; + $group->actions = array(); + + foreach($actions as $action) + { + if(commonModel::hasPriv('kanban', $action)) $group->actions[] = $action; + } + + $region->groups[] = $group; + $region->laneCount += count($lanes); + } + + $kanbanData[$regionID] = $region; + } + + return $kanbanData; + } + /** * Get region by id. * @@ -660,15 +727,17 @@ class kanbanModel extends model /** * Get lane group by regions. * - * @param array $regions + * @param array $regions + * @param string $browseType * @access public * @return array */ - public function getLaneGroupByRegions($regions) + public function getLaneGroupByRegions($regions, $browseType = 'all') { $laneGroup = $this->dao->select('*')->from(TABLE_KANBANLANE) ->where('deleted')->eq('0') ->andWhere('region')->in($regions) + ->beginIf($browseType != 'all')->andWhere('type')->eq($browseType)->fi() ->orderBy('order') ->fetchGroup('group'); @@ -758,10 +827,12 @@ class kanbanModel extends model */ public function getCardGroupByKanban($kanbanID) { - $cards = $this->dao->select('*')->from(TABLE_KANBANCARD) + $cards = $this->dao->select('t1.*,t2.kanban,t2.lane,t2.column')->from(TABLE_KANBANCARD)->alias('t1') + ->leftJoin(TABLE_KANBANCELL)->alias('t2')->on('t1.kanban=t2.kanban') ->where('deleted')->eq(0) + ->andWhere("INSTR(t2.cards, CONCAT(',',t1.id,','))")->gt(0) ->andWhere('archived')->eq(0) - ->andWhere('kanban')->eq($kanbanID) + ->andWhere('t2.kanban')->eq($kanbanID) //->orderBy('`order` asc') ->fetchAll('id'); @@ -781,6 +852,136 @@ class kanbanModel extends model return $cardGroup; } + /** + * Get RD column group by regions. + * + * @param array $regions + * @param array $groupIDList + * @access public + * @return array + */ + public function getRDColumnGroupByRegions($regions, $groupIDList = array()) + { + $columnGroup = $this->dao->select("*")->from(TABLE_KANBANCOLUMN) + ->where('deleted')->eq('0') + ->andWhere('region')->in($regions) + ->beginIF(!empty($groupIDList))->andWhere('`group`')->in($groupIDList)->fi() + ->orderBy('id_asc') + ->fetchGroup('group'); + + $actions = array('setColumn', 'setWIP', 'deleteColumn'); + + /* Group by parent. */ + $parentColumnGroup = array(); + foreach($columnGroup as $group => $columns) + { + foreach($columns as $column) + { + $column->actions = array(); + /* Judge column action priv. */ + foreach($actions as $action) + { + if($this->isClickable($column, $action)) $column->actions[] = $action; + } + + if($column->parent > 0) continue; + + $parentColumnGroup[$group][] = $column; + } + } + + $columnData = array(); + foreach($parentColumnGroup as $group => $parentColumns) + { + foreach($parentColumns as $parentColumn) + { + $columnData[$group][] = $parentColumn; + foreach($columnGroup[$group] as $column) + { + if($column->parent == $parentColumn->id) + { + $parentColumn->asParent = true; + + if(strpos(',developing,developed,', $column->type) !== false) $column->parentType = 'develop'; + if(strpos(',testing,tested,', $column->type) !== false) $column->parentType = 'test'; + if(strpos(',fixing,fixed,', $column->type) !== false) $column->parentType = 'resolving'; + + $columnData[$group][] = $column; + } + } + } + } + + return $columnData; + } + + /** + * Get card group by execution id. + * + * @param int $kanbanID + * @param string $browseType all|task|bug|story + * @param string $orderBy + * @access public + * @return array + */ + public function getCardGroupByExecution($executionID, $browseType = 'all', $orderBy = 'id_asc') + { + $cards = $this->dao->select('t1.*, t2.type as columnType') + ->from(TABLE_KANBANCELL)->alias('t1') + ->leftJoin(TABLE_KANBANCOLUMN)->alias('t2')->on('t1.column=t2.id') + ->where('t1.kanban')->eq($executionID) + ->beginIF($browseType != 'all')->andWhere('t1.type')->eq($browseType)->fi() + ->orderby($orderBy) + ->fetchgroup('lane', 'column'); + + /* 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"); + + $cardGroup = array(); + + foreach($cards as $laneID => $cells) + { + foreach($cells as $columnID => $cell) + { + $cardIdList = array_filter(explode(',', $cell->cards)); + $cardOrder = 1; + foreach($cardIdList as $cardID) + { + $cardData = array(); + $objects = zget($objectGroup, $cell->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'] = $cell->type == 'bug' ? '' : $object->estimate; + $cardData['assignedTo'] = $object->assignedTo; + $cardData['deadline'] = $cell->type == 'story' ? '' : $object->deadline; + $cardData['severity'] = $cell->type == 'bug' ? $object->severity : ''; + $cardData['acl'] = 'open'; + $cardData['lane'] = $laneID; + $cardData['column'] = $cell->column; + + if($cell->type == 'task') + { + $cardData['name'] = $object->name; + } + else + { + $cardData['title'] = $object->title; + } + $cardGroup[$laneID][$cell->columnType][] = $cardData; + } + } + } + + return $cardGroup; + } + /** * Get Kanban by execution id. * @@ -792,34 +993,26 @@ class kanbanModel extends model */ public function getExecutionKanban($executionID, $browseType = 'all', $groupBy = 'default') { - if($browseType != 'all' and $groupBy != 'default') $this->updateGroupLanes($executionID, $browseType, $groupBy); + if($groupBy != 'default') return $this->getKanban4Group($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); + foreach($lanes as $lane) $this->refreshCards($lane); - $columns = $this->dao->select('*')->from(TABLE_KANBANCOLUMN) - ->where('deleted')->eq(0) - ->andWhere('lane')->in(array_keys($lanes)) + $columns = $this->dao->select('t1.cards, t1.lane, t2.id, t2.type, t2.name, t2.color, t2.limit, t2.parent')->from(TABLE_KANBANCELL)->alias('t1') + ->leftJoin(TABLE_KANBANCOLUMN)->alias('t2')->on('t1.column = t2.id') + ->where('t2.deleted')->eq(0) + ->andWhere('t1.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); @@ -836,9 +1029,8 @@ class kanbanModel extends model { $laneData = array(); $columnData = array(); - $laneType = $groupBy == 'default' ? $lane->type : $lane->groupby; - $laneData['id'] = $groupBy == 'default' ? $lane->type : $lane->groupby . '-' . $lane->extra; + $laneData['id'] = $laneID; $laneData['laneID'] = $laneID; $laneData['name'] = $lane->name; $laneData['color'] = $lane->color; @@ -847,18 +1039,20 @@ class kanbanModel extends model foreach($columns[$laneID] as $columnID => $column) { - $columnData[$column->id]['id'] = $laneType . '-' . $column->type; - $columnData[$column->id]['columnID'] = $columnID; + $columnData[$column->id]['id'] = $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; + $columnData[$column->id]['parent'] = $column->parent; if($column->parent > 0) { - $columnData[$column->id]['parentType'] = zget($parentTypes, $column->parent, ''); + if($column->type == 'developing' or $column->type == 'developed') $columnData[$column->id]['parentType'] = 'develop'; + if($column->type == 'testing' or $column->type == 'tested') $columnData[$column->id]['parentType'] = 'test'; + if($column->type == 'fixing' or $column->type == 'fixed') $columnData[$column->id]['parentType'] = 'resolving'; } $cardOrder = 1; @@ -898,15 +1092,201 @@ class kanbanModel extends model 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; + $kanbanGroup[$lane->type]['id'] = $laneID; + $kanbanGroup[$lane->type]['columns'] = array_values($columnData); + $kanbanGroup[$lane->type]['lanes'][] = $laneData; + $kanbanGroup[$lane->type]['defaultCardType'] = $lane->type; } return $kanbanGroup; } + /** + * Get kanban for group view. + * + * @param int $executionID + * @param string $browseType + * @param string $groupBy + * @access public + * @return array + */ + public function getKanban4Group($executionID, $browseType, $groupBy) + { + /* Get card data. */ + if($browseType == 'story') $cardList = $this->loadModel('story')->getExecutionStories($executionID); + if($browseType == 'bug') $cardList = $this->loadModel('bug')->getExecutionBugs($executionID); + if($browseType == 'task') $cardList = $this->loadModel('execution')->getKanbanTasks($executionID, "id"); + + $lanes = $this->getLanes4Group($executionID, $browseType, $groupBy, $cardList); + if(empty($lanes)) return array(); + + $columns = $this->dao->select('t1.*, t2.`type` as columnType')->from(TABLE_KANBANCELL)->alias('t1') + ->leftJoin(TABLE_KANBANCOLUMN)->alias('t2')->on('t1.`column` = t2.id') + ->where('t1.kanban')->eq($executionID) + ->andWhere('t1.`type`')->eq($browseType) + ->fetchAll(); + + $cardGroup = array(); + foreach($columns as $column) + { + if(empty($column->cards)) continue; + foreach($cardList as $card) + { + if(strpos($column->cards, ",$card->id,") !== false) $cardGroup[$column->columnType][$card->id] = $card; + } + } + + /* Build kanban group data. */ + $kanbanGroup = array(); + foreach($lanes as $laneID => $lane) + { + $laneData = array(); + $columnData = array(); + $columnList = $this->lang->kanban->{$browseType . 'Column'}; + + $laneData['id'] = $groupBy . $laneID; + $laneData['laneID'] = $groupBy . $laneID; + $laneData['name'] = (($groupBy == 'pri' or $groupBy == 'severity') and $laneID) ? $this->lang->$browseType->$groupBy . ':' . $lane->name : $lane->name; + $laneData['color'] = $lane->color; + $laneData['order'] = $lane->order; + $laneData['defaultCardType'] = $browseType; + + /* Construct kanban column data. */ + foreach($columnList as $columnID => $columnName) + { + $parentColumn = ''; + if(in_array($columnID, array('developing', 'developed'))) $parentColumn = 'develop'; + if(in_array($columnID, array('testing', 'tested'))) $parentColumn = 'test'; + if(in_array($columnID, array('fixing', 'fixed'))) $parentColumn = 'resolving'; + + $columnData[$columnID]['id'] = $columnID; + $columnData[$columnID]['type'] = $columnID; + $columnData[$columnID]['name'] = $columnName; + $columnData[$columnID]['color'] = '#333'; + $columnData[$columnID]['limit'] = -1; + $columnData[$columnID]['laneType'] = $browseType; + $columnData[$columnID]['asParent'] = in_array($columnID, array('develop', 'test', 'resolving')) ? true : false; + $columnData[$columnID]['parentType'] = $parentColumn; + + $cardOrder = 1; + $objects = zget($cardGroup, $columnID, array()); + foreach($objects as $object) + { + if(empty($object)) continue; + + $cardData = array(); + if(in_array($groupBy, array('module', 'story', 'pri', 'severity')) and (int)$object->$groupBy !== $laneID) continue; + if(in_array($groupBy, array('assignedTo', 'type', 'category', 'source')) and $object->$groupBy != $laneID) continue; + + $cardData['id'] = $object->id; + $cardData['order'] = $cardOrder; + $cardData['pri'] = $object->pri ? $object->pri : ''; + $cardData['estimate'] = $browseType == 'bug' ? '' : $object->estimate; + $cardData['assignedTo'] = $object->assignedTo; + $cardData['deadline'] = $browseType == 'story' ? '' : $object->deadline; + $cardData['severity'] = $browseType == 'bug' ? $object->severity : ''; + + if($browseType == 'task') + { + $cardData['name'] = $object->name; + } + else + { + $cardData['title'] = $object->title; + } + + $laneData['cards'][$columnID][] = $cardData; + $cardOrder ++; + } + if(!isset($laneData['cards'][$columnID])) $laneData['cards'][$columnID] = array(); + } + + $kanbanGroup[$groupBy]['id'] = $groupBy . $laneID; + $kanbanGroup[$groupBy]['columns'] = array_values($columnData); + $kanbanGroup[$groupBy]['lanes'][] = $laneData; + $kanbanGroup[$groupBy]['defaultCardType'] = $browseType; + } + + return $kanbanGroup; + } + + /** + * Build lane data for group kanban. + * + * @access public + * @param int $executionID + * @param string $browseType + * @param string $groupBy + * @param array $cardList + * @return array + */ + public function getLanes4Group($executionID, $browseType, $groupBy, $cardList) + { + $lanes = array(); + $groupByList = array(); + $objectPairs = array(); + foreach($cardList as $item) + { + if(!isset($groupByList[$item->$groupBy])) $groupByList[$item->$groupBy] = $item->$groupBy; + } + + if(in_array($groupBy, array('module', 'story', 'pri', 'severity'))) $objectPairs[0] = $this->lang->$browseType->$groupBy . ': ' . $this->lang->kanban->noGroup; + if(in_array($groupBy, array('assignedTo', 'type', 'category', 'source'))) $objectPairs[''] = $this->lang->$browseType->$groupBy . ': ' . $this->lang->kanban->noGroup; + + if(in_array($groupBy, array('module', 'story', 'assignedTo'))) + { + if($groupBy == 'module') + { + $objectPairs += $this->dao->select('id,name')->from(TABLE_MODULE) + ->where('type')->in('story,task,bug') + ->andWhere('deleted')->eq('0') + ->andWhere('id')->in($groupByList) + ->fetchPairs(); + } + elseif($groupBy == 'story') + { + $objectPairs += $this->dao->select('id,title')->from(TABLE_STORY) + ->where('deleted')->eq(0) + ->andWhere('id')->in($groupByList) + ->fetchPairs(); + } + else + { + $objectPairs += $this->dao->select('account,realname')->from(TABLE_USER) + ->where('account')->in($groupByList) + ->fetchPairs(); + + if(isset($groupByList['closed'])) $objectPairs['closed'] = 'Closed'; + } + } + else + { + $objectPairs += $this->lang->$browseType->{$groupBy . 'List'}; + } + + $laneColor = 0; + $order = 1; + foreach($objectPairs as $objectID => $objectName) + { + if(!isset($groupByList[$objectID]) and $objectID) continue; + + $lane = new stdclass(); + $lane->id = $groupBy . $objectID; + $lane->type = $browseType; + $lane->execution = $executionID; + $lane->name = $objectName; + $lane->order = $order; + $lane->color = $this->config->kanban->laneColorList[$laneColor]; + + $order += 1; + $laneColor += 1; + if($laneColor == count($this->config->kanban->laneColorList)) $laneColor = 0; + $lanes[$objectID] = $lane; + } + + return $lanes; + } + /** * Get space list. * @@ -1118,14 +1498,16 @@ class kanbanModel extends model * Get lane pairs by region id. * * @param array $regionID + * @param string $type all|story|task|bug|common * @access public * @return array */ - public function getLanePairsByRegion($regionID) + public function getLanePairsByRegion($regionID, $type = 'all') { return $this->dao->select('id, name')->from(TABLE_KANBANLANE) ->where('deleted')->eq('0') ->andWhere('region')->eq($regionID) + ->beginIF($type != 'all')->andWhere('type')->eq($type)->fi() ->fetchPairs(); } @@ -1149,11 +1531,14 @@ class kanbanModel extends model ->add('region', $regionID) ->add('order', $maxOrder ? $maxOrder + 1 : 1) ->add('lastEditedTime', helper::now()) - ->add('type', 'common') + ->setIF(isset($_POST['laneType']), 'execution', $kanbanID) ->trim('name') ->setDefault('color', '#7ec5ff') + ->remove('laneType') ->get(); + $lane->type = isset($_POST['laneType']) ? $_POST['laneType'] : 'common'; + $mode = zget($lane, 'mode', ''); if($mode == 'sameAsOther') { @@ -1162,11 +1547,12 @@ class kanbanModel extends model } elseif($mode == 'independent') { - $groupID = $this->createGroup($kanbanID, $regionID); - $kanban = $this->getByID($kanbanID); - $this->createDefaultColumns($kanban, $regionID, $groupID); - - $lane->group = $groupID; + $lane->group = $this->createGroup($kanbanID, $regionID); + if($lane->type == 'common') + { + $kanban = $this->getByID($kanbanID); + $this->createDefaultColumns($kanban, $regionID, $lane->group); + } } } @@ -1177,6 +1563,18 @@ class kanbanModel extends model if(dao::isError()) return false; $laneID = $this->dao->lastInsertID(); + if($lane->type != 'common' and isset($mode) and $mode == 'independent') $this->createRDColumn($regionID, $lane->group, $laneID, $lane->type, $kanbanID); + + if(isset($mode) and ($mode == 'sameAsOther' or ($lane->type == 'common' and $mode == 'independent'))) + { + $columnIDList = $this->dao->select('id')->from(TABLE_KANBANCOLUMN)->where('deleted')->eq(0)->andWhere('archived')->eq(0)->andWhere('`group`')->eq($lane->group)->fetchPairs(); + foreach($columnIDList as $columnID) + { + $this->addKanbanCell($kanbanID, $laneID, $columnID, $lane->type); + + if(dao::isError()) return false; + } + } return $laneID; } @@ -1385,61 +1783,46 @@ class kanbanModel extends model } /** - * createColumn + * Create execution columns. * - * @param int $laneID - * @param string $type story|bug|task - * @param int $executionID - * @param string $groupBy - * @param string $groupValue + * @param int|array $laneID + * @param string $type story|bug|task + * @param int $executionID + * @param string $groupBy + * @param string $groupValue * @access public * @return void */ - public function createExecutionColumns($laneID, $type, $executionID, $groupBy = '', $groupValue = '') + public function createExecutionColumns($laneID, $type, $executionID) { - $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; - } + if(strpos(',testing,tested,', $colType) !== false) $data->parent = $testColumnID; + if(strpos(',develop,test,', $colType) !== false) $data->parent = -1; $this->dao->insert(TABLE_KANBANCOLUMN)->data($data)->exec(); - if($colType == 'develop') $devColumnID = $this->dao->lastInsertId(); - if($colType == 'test') $testColumnID = $this->dao->lastInsertId(); + + $colID = $this->dao->lastInsertId(); + if($colType == 'develop') $devColumnID = $colID; + if($colType == 'test') $testColumnID = $colID; + + if(is_array($laneID)) + { + foreach($laneID as $id) $this->addKanbanCell($executionID, $id, $colID, 'story'); + } + else + { + $this->addKanbanCell($executionID, $laneID, $colID, 'story'); + } } } elseif($type == 'bug') @@ -1447,37 +1830,27 @@ class kanbanModel extends model 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(',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(); + + $colID = $this->dao->lastInsertId(); + if($colType == 'resolving') $resolvingColumnID = $colID; + if($colType == 'test') $testColumnID = $colID; + + if(is_array($laneID)) + { + foreach($laneID as $id) $this->addKanbanCell($executionID, $id, $colID, 'bug'); + } + else + { + $this->addKanbanCell($executionID, $laneID, $colID, 'bug'); + } } } elseif($type == 'task') @@ -1485,29 +1858,186 @@ class kanbanModel extends model 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(); + + $colID = $this->dao->lastInsertId(); + if($colType == 'develop') $devColumnID = $colID; + + if(is_array($laneID)) + { + foreach($laneID as $id) $this->addKanbanCell($executionID, $id, $colID, 'task'); + } + else + { + $this->addKanbanCell($executionID, $laneID, $colID, 'task'); + } } } } + /** + * Add kanban cell for new lane. + * + * @param int $kanbanID + * @param int $laneID + * @param int $colID + * @param string $type story|task|bug|card + * @access public + * @return void + */ + public function addKanbanCell($kanbanID, $laneID, $colID, $type, $cardID = 0) + { + $cell = $this->dao->select('id, cards')->from(TABLE_KANBANCELL) + ->where('kanban')->eq($kanbanID) + ->andWhere('lane')->eq($laneID) + ->andWhere('`column`')->eq($colID) + ->andWhere('type')->eq($type) + ->fetch(); + + if(empty($cell)) + { + $cell = new stdclass(); + $cell->kanban = $kanbanID; + $cell->lane = $laneID; + $cell->column = $colID; + $cell->type = $type; + + $this->dao->insert(TABLE_KANBANCELL)->data($cell)->exec(); + } + else + { + $cell->cards = $cell->cards ? $cell->cards . "$cardID," : ",$cardID,"; + $this->dao->update(TABLE_KANBANCELL)->set('cards')->eq($cell->cards)->where('id')->eq($cell->id)->exec(); + } + } + + /** + * Create a default RD kanban. + * + * @param object $execution + * @access public + * @return void + */ + public function createRDKanban($execution) + { + $regionID = $this->createRDRegion($execution); + if(dao::isError()) return false; + + $groupID = $this->createGroup($execution->id, $regionID); + if(dao::isError()) return false; + + $this->createRDLane($execution->id, $regionID); + if(dao::isError()) return false; + } + + /** + * Create a default RD region. + * + * @param object $execution + * + * @access public + * @return int|bool + */ + public function createRDRegion($execution) + { + $region = new stdclass(); + $region->name = $this->lang->kanbanregion->default; + $region->kanban = $execution->id; + $region->createdBy = $this->app->user->account; + $region->createdDate = helper::today(); + $region->order = 1; + + $this->dao->insert(TABLE_KANBANREGION)->data($region) + ->check('name', 'unique', "kanban={$execution->id} AND deleted='0'") + ->autoCheck() + ->exec(); + + if(dao::isError()) return false; + return $this->dao->lastInsertId(); + } + + /** + * Create default RD lanes. + * + * @param int $executionID + * @param int $regionID + * + * @access public + * @return bool + */ + public function createRDLane($executionID, $regionID) + { + $laneIndex = 0; + foreach($this->lang->kanban->laneTypeList as $type => $name) + { + $groupID = $this->createGroup($executionID, $regionID); + if(dao::isError()) return false; + + $lane = new stdclass(); + $lane->execution = $executionID; + $lane->type = $type; + $lane->region = $regionID; + $lane->group = $groupID; + $lane->name = $name; + $lane->color = $this->config->kanban->laneColorList[$laneIndex]; + $lane->order = ++ $laneIndex * 5; + + $this->dao->insert(TABLE_KANBANLANE)->data($lane)->autoCheck()->exec(); + if(dao::isError()) return false; + + $this->createRDColumn($regionID, $groupID, $this->dao->lastInsertId(), $type, $executionID); + } + } + + /** + * Create default RD columns. + * + * @param int $regionID + * @param int $groupID + * @param int $laneID + * @param string $laneType + * + * @access public + * @return bool + */ + public function createRDColumn($regionID, $groupID, $laneID, $laneType, $executionID) + { + $devColumnID = $testColumnID = $resolvingColumnID = 0; + if($laneType == 'story') $columnList = $this->lang->kanban->storyColumn; + if($laneType == 'bug') $columnList = $this->lang->kanban->bugColumn; + if($laneType == 'task') $columnList = $this->lang->kanban->taskColumn; + + foreach($columnList as $type => $name) + { + $data = new stdClass(); + $data->name = $name; + $data->color = '#333'; + $data->type = $type; + $data->group = $groupID; + $data->region = $regionID; + + if(strpos(',developing,developed,', $type) !== false) $data->parent = $devColumnID; + if(strpos(',testing,tested,', $type) !== false) $data->parent = $testColumnID; + if(strpos(',fixing,fixed,', $type) !== false) $data->parent = $resolvingColumnID; + if(strpos(',develop,test,resolving,', $type) !== false) $data->parent = -1; + + $this->dao->insert(TABLE_KANBANCOLUMN)->data($data)->exec(); + if(dao::isError()) return false; + + if($type == 'develop') $devColumnID = $this->dao->lastInsertId(); + if($type == 'test') $testColumnID = $this->dao->lastInsertId(); + if($type == 'resolving') $resolvingColumnID = $this->dao->lastInsertId(); + + $this->addKanbanCell($executionID, $laneID, $this->dao->lastInsertId(), $laneType); + } + } + /** * Update a region. * @@ -1551,24 +2081,27 @@ class kanbanModel extends model ->andWhere('type')->eq($laneType) ->fetchAll('id'); - foreach($lanes as $lane) $this->updateCards($lane); + foreach($lanes as $lane) $this->refreshCards($lane); } /** - * Update column cards. + * Refresh column cards. * * @param object $lane * @access public * @return void */ - public function updateCards($lane) + public function refreshCards($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'); + $cardPairs = $this->dao->select('t2.type, t1.cards')->from(TABLE_KANBANCELL)->alias('t1') + ->leftJoin(TABLE_KANBANCOLUMN)->alias('t2')->on('t1.`column` = t2.id') + ->where('t1.kanban')->eq($executionID) + ->andWhere('t1.lane')->eq($lane->id) + ->fetchPairs(); + + if(empty($cardPairs)) return; if($laneType == 'story') { @@ -1582,21 +2115,19 @@ class kanbanModel extends model $cardPairs[$colType] = str_replace(",$storyID,", ',', $cardPairs[$colType]); } - if(strpos(',ready,develop,test,', $colType) !== false) continue; + if(strpos(',ready,backlog,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 and $colType != 'backlog') + if($story->stage == $stage and strpos($cardPairs[$colType], ",$storyID,") === false) { $cardPairs[$colType] = empty($cardPairs[$colType]) ? ",$storyID," : ",$storyID" . $cardPairs[$colType]; } } + + if($story->stage == 'projected' and strpos($cardPairs['ready'], ",$storyID,") === false and strpos($cardPairs['backlog'], ",$storyID,") === false) + { + $cardPairs['backlog'] = empty($cardPairs['backlog']) ? ",$storyID," : ",$storyID" . $cardPairs['backlog']; + } + } } elseif($laneType == 'bug') @@ -1613,11 +2144,7 @@ class kanbanModel extends model if(strpos(',resolving,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 and $bug->activatedCount == 0) + if($colType == 'unconfirmed' and $bug->status == $status and $bug->confirmed == 0 and strpos($cardPairs['unconfirmed'], ",$bugID,") === false and strpos($cardPairs['fixing'], ",$bugID,") === false and $bug->activatedCount == 0) { $cardPairs['unconfirmed'] = empty($cardPairs['unconfirmed']) ? ",$bugID," : ",$bugID" . $cardPairs['unconfirmed']; if(strpos($cardPairs['closed'], ",$bugID,") !== false) $cardPairs['closed'] = str_replace(",$bugID,", ',', $cardPairs['closed']); @@ -1655,11 +2182,7 @@ class kanbanModel extends model { 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) + if($task->status == $status and strpos($cardPairs[$colType], ",$taskID,") === false) { $cardPairs[$colType] = empty($cardPairs[$colType]) ? ",$taskID," : ",$taskID". $cardPairs[$colType]; } @@ -1671,107 +2194,21 @@ class kanbanModel extends model } } + $colPairs = $this->dao->select('t2.type, t2.id')->from(TABLE_KANBANCELL)->alias('t1') + ->leftJoin(TABLE_KANBANCOLUMN)->alias('t2')->on('t1.`column` = t2.id') + ->where('t1.kanban')->eq($executionID) + ->andWhere('t1.lane')->eq($lane->id) + ->fetchPairs(); + foreach($cardPairs as $colType => $cards) { - $this->dao->update(TABLE_KANBANCOLUMN)->set('cards')->eq($cards)->where('lane')->eq($lane->id)->andWhere('type')->eq($colType)->exec(); + if(!isset($colPairs[$colType])) continue; + $this->dao->update(TABLE_KANBANCELL)->set('cards')->eq($cards)->where('lane')->eq($lane->id)->andWhere('`column`')->eq($colPairs[$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->createExecutionColumns($laneID, $type, $executionID, $groupBy, $groupKey); - } - - $this->resetLaneOrder($executionID, $type, $groupBy); - } - /** * Update lane column. * @@ -1972,6 +2409,36 @@ class kanbanModel extends model return dao::isError(); } + /** + * Set lane height. + * + * @param int $kanbanID + * @param string $from kanban|execution + * @access public + * @return bool + */ + public function setLaneHeight($kanbanID, $from = 'kanban') + { + $kanbanID = (int)$kanbanID; + $kanban = fixer::input('post') + ->setIF($this->post->heightType == 'auto', 'displayCards', 0) + ->get(); + + if($kanban->heightType == 'custom') + { + if(!preg_match("/^-?\d+$/", $kanban->displayCards) or $kanban->displayCards < 3) + { + dao::$errors['displayCards'] = $this->lang->kanbanlane->error->mustBeInt; + return false; + } + } + + $table = $this->config->objectTables[$from]; + $this->dao->update($table)->set('displayCards')->eq((int)$kanban->displayCards)->where('id')->eq($kanbanID)->exec(); + + if(dao::isError()) return false; + } + /** * Set kanban headerActions. * @@ -1981,24 +2448,45 @@ class kanbanModel extends model */ public function setHeaderActions($kanban) { + $printSetHeight = false; + if(common::hasPriv('kanban', 'setLaneHeight')) + { + $laneCount = $this->dao->select('COUNT(t2.id) as count')->from(TABLE_KANBANREGION)->alias('t1') + ->leftJoin(TABLE_KANBANLANE)->alias('t2')->on('t1.id=t2.region') + ->where('t1.kanban')->eq($kanban->id) + ->andWhere('t1.deleted')->eq(0) + ->andWhere('t2.deleted')->eq(0) + ->fetch('count'); + + if($laneCount > 1) $printSetHeight = true; + } + $actions = ''; $actions .= "
    "; $actions .= " {$this->lang->kanban->fullScreen}"; - $printSettingBtn = (common::hasPriv('kanban', 'createRegion') or common::hasPriv('kanban', 'edit') or common::hasPriv('kanban', 'close') or common::hasPriv('kanban', 'delete')); + + $printSettingBtn = (common::hasPriv('kanban', 'createRegion') or $printSetHeight or common::hasPriv('kanban', 'edit') or common::hasPriv('kanban', 'close') or common::hasPriv('kanban', 'delete')); + if($printSettingBtn) { $actions .= "" . ' ' . $this->lang->kanban->setting . ''; $actions .= ""; } @@ -2061,17 +2549,23 @@ class kanbanModel extends model * Move a card. * * @param int $cardID + * @param int $fromColID * @param int $toColID + * @param int $fromLaneID * @param int $toLaneID * @access public * @return void */ - public function moveCard($cardID, $toColID, $toLaneID) + public function moveCard($cardID, $fromColID, $toColID, $fromLaneID, $toLaneID) { - $this->dao->update(TABLE_KANBANCARD) - ->set('column')->eq($toColID) - ->beginIF($toLaneID)->set('lane')->eq($toLaneID)->fi() - ->where('id')->eq($cardID)->exec(); + $fromCellCards = $this->dao->select('cards')->from(TABLE_KANBANCELL)->where('lane')->eq($fromLaneID)->andWhere('`column`')->eq($fromColID)->fetch('cards'); + $toCellCards = $this->dao->select('cards')->from(TABLE_KANBANCELL)->where('lane')->eq($toLaneID)->andWhere('`column`')->eq($toColID)->fetch('cards'); + + $fromCardList = str_replace("$cardID,", '', $fromCellCards); + $toCardList = rtrim($toCellCards, ',') . ",$cardID,"; + + $this->dao->update(TABLE_KANBANCELL)->set('cards')->eq($fromCardList)->where('`column`')->eq($fromColID)->andWhere('lane')->eq($fromLaneID)->exec(); + $this->dao->update(TABLE_KANBANCELL)->set('cards')->eq($toCardList)->where('`column`')->eq($toColID)->andWhere('lane')->eq($toLaneID)->exec(); } /** @@ -2255,7 +2749,7 @@ class kanbanModel extends model 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') + ->leftjoin(TABLE_KANBANLANE)->alias('t2')->on('t1.group=t2.id') ->where('t1.id')->eq($columnID) ->fetch(); diff --git a/module/kanban/view/createexeclane.html.php b/module/kanban/view/createexeclane.html.php new file mode 100644 index 0000000000..c3d2e687fa --- /dev/null +++ b/module/kanban/view/createexeclane.html.php @@ -0,0 +1,111 @@ + + * @package kanban + * @version $Id: createexeclane.html.php 935 2022-01-12 17:22:24Z $ + * @link https://www.zentao.net + */ +?> + + +kanban->laneColorList);?> + + + +
    +
    +

    + kanban->createLane;?> +

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    kanbanlane->name;?> +
    + +
    kanbanlane->WIPType;?>kanban->laneTypeList, 'story');?>
    kanbanlane->column;?>kanbanlane->modeList, $mode, '', 'block');?>>
    kanbanlane->color;?> +
    + +
    + + cancel, "data-dismiss='modal'", 'btn btn-wide');?> +
    +
    +
    + + diff --git a/module/kanban/view/setlaneheight.html.php b/module/kanban/view/setlaneheight.html.php new file mode 100644 index 0000000000..62125573fd --- /dev/null +++ b/module/kanban/view/setlaneheight.html.php @@ -0,0 +1,38 @@ + + * @package kanban + * @version $Id: setlaneheight.html.php 935 2022-01-11 10:49:24Z $ + * @link https://www.zentao.net + */ +?> + +
    +
    +
    +

    kanban->setLaneHeight;?>

    +
    +
    + + + + + + + + + + + + +
    kanban->laneHeight;?>kanbanlane->heightTypeList, $heightType, "onclick='setCardCount(this.value);'"));?>
    + +
    +
    +
    +
    + diff --git a/module/kanban/view/space.html.php b/module/kanban/view/space.html.php index 7f469f7036..1354a492e7 100644 --- a/module/kanban/view/space.html.php +++ b/module/kanban/view/space.html.php @@ -141,9 +141,11 @@
    kanban->teamSumCount, $teamCount);?>
    - acl == 'open' ? 'unlock' : 'lock';?> + + acl == 'private') $icon = 'lock';?> + acl == 'extend') $icon = 'inherit-space';?> - kanban->aclGroup, $kanban->acl == 'open' ? 'open' : 'private', '');?> + kanban->aclGroup, $kanban->acl, '');?>
    diff --git a/module/kanban/view/view.html.php b/module/kanban/view/view.html.php index 01a5ebb44b..2855b0104d 100644 --- a/module/kanban/view/view.html.php +++ b/module/kanban/view/view.html.php @@ -30,6 +30,7 @@ js::set('noAssigned', $lang->kanbancard->noAssigned); js::set('users', $users); js::set('colorListLang', $lang->kanbancard->colorList); js::set('colorList', $this->config->kanban->cardColorList); +js::set('displayCards', $kanban->displayCards); js::set('priv', array( diff --git a/module/my/control.php b/module/my/control.php index 7be5a039d0..61f141c693 100644 --- a/module/my/control.php +++ b/module/my/control.php @@ -443,6 +443,7 @@ EOF; $this->view->tasks = $tasks; $this->view->summary = $this->loadModel('execution')->summary($tasks); $this->view->type = $type; + $this->view->kanbanList = $this->execution->getPairs(0, 'kanban'); $this->view->recTotal = $recTotal; $this->view->recPerPage = $recPerPage; $this->view->pageID = $pageID; diff --git a/module/my/view/task.html.php b/module/my/view/task.html.php index 42af633614..4432aea57a 100644 --- a/module/my/view/task.html.php +++ b/module/my/view/task.html.php @@ -131,14 +131,15 @@ } else { - if($task->status != 'pause') common::printIcon('task', 'start', "taskID=$task->id", $task, 'list', '', '', 'iframe', true, '', '', $task->project); - if($task->status == 'pause') common::printIcon('task', 'restart', "taskID=$task->id", $task, 'list', '', '', 'iframe', true, '', '', $task->project); - common::printIcon('task', 'close', "taskID=$task->id", $task, 'list', '', '', 'iframe', true, '', '', $task->project); - common::printIcon('task', 'finish', "taskID=$task->id", $task, 'list', '', '', 'iframe', true, '', '', $task->project); + $attr = isset($kanbanList[$task->execution]) ? "disabled" : ''; + if($task->status != 'pause') common::printIcon('task', 'start', "taskID=$task->id", $task, 'list', '', '', 'iframe', true); + if($task->status == 'pause') common::printIcon('task', 'restart', "taskID=$task->id", $task, 'list', '', '', 'iframe', true); + common::printIcon('task', 'close', "taskID=$task->id", $task, 'list', '', '', 'iframe', true); + common::printIcon('task', 'finish', "taskID=$task->id", $task, 'list', '', '', 'iframe', true); - common::printIcon('task', 'recordEstimate', "taskID=$task->id", $task, 'list', 'time', '', 'iframe', true, '', '', $task->project); - common::printIcon('task', 'edit', "taskID=$task->id", $task, 'list', '', '', 'iframe', true, "data-width='95%'", '', $task->project); - common::printIcon('task', 'batchCreate', "executionID=$task->execution&storyID=$task->story&moduleID=$task->module&taskID=$task->id&ifame=true", $task, 'list', 'split', '', 'iframe', true, "data-width='95%'", $this->lang->task->children, $task->project); + common::printIcon('task', 'recordEstimate', "taskID=$task->id", $task, 'list', 'time', '', 'iframe', true); + common::printIcon('task', 'edit', "taskID=$task->id", $task, 'list', '', '', 'iframe', true, "data-width='95%'"); + common::printIcon('task', 'batchCreate', "executionID=$task->execution&storyID=$task->story&moduleID=$task->module&taskID=$task->id&ifame=true", $task, 'list', 'split', '', 'iframe', true, "data-width='95%' $attr", $this->lang->task->children); } } ?> diff --git a/module/product/model.php b/module/product/model.php index 8abcccf230..0a4ef341f6 100644 --- a/module/product/model.php +++ b/module/product/model.php @@ -1465,7 +1465,7 @@ class productModel extends model ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id') ->where('t2.deleted')->eq(0) ->andWhere('t1.product')->eq($productID) - ->andWhere('t2.type')->in('sprint,stage') + ->andWhere('t2.type')->in('sprint,stage,kanban') ->fetch(); $product->stories = $stories; diff --git a/module/product/view/ajaxgetdropmenu.html.php b/module/product/view/ajaxgetdropmenu.html.php index 7dacf4a174..1809c88c97 100644 --- a/module/product/view/ajaxgetdropmenu.html.php +++ b/module/product/view/ajaxgetdropmenu.html.php @@ -161,7 +161,7 @@ $(function() } }) - $('#tabContent [data-ride="tree"]').tree('expand'); + $('#swapper [data-ride="tree"]').tree('expand'); $('#swapper #dropMenu .search-box').on('onSearchChange', function(event, value) { diff --git a/module/productplan/model.php b/module/productplan/model.php index 596c73f0cd..3a4202134b 100644 --- a/module/productplan/model.php +++ b/module/productplan/model.php @@ -99,7 +99,7 @@ class productplanModel extends model ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id') ->where('t1.product')->eq($product) ->andWhere('t1.plan')->in(array_keys($plans)) - ->andWhere('t2.type')->in('sprint,stage') + ->andWhere('t2.type')->in('sprint,stage,kanban') ->fetchPairs('plan', 'project'); $storyCountInTable = $this->dao->select('plan,count(story) as count')->from(TABLE_PLANSTORY)->where('plan')->in($planIdList)->groupBy('plan')->fetchPairs('plan', 'count'); diff --git a/module/program/css/project.css b/module/program/css/project.css new file mode 100644 index 0000000000..499e53ecc1 --- /dev/null +++ b/module/program/css/project.css @@ -0,0 +1,3 @@ +.project-type-label.label-outline {width: 50px; min-width: 50px;} +.project-type-label.label {overflow: unset !important; text-overflow: unset !important; white-space: unset !important;} +[lang^='en'] .project-name > .label-warning {width: 55px !important;} diff --git a/module/program/model.php b/module/program/model.php index 02b3f2e1be..70faddc9c5 100644 --- a/module/program/model.php +++ b/module/program/model.php @@ -205,7 +205,7 @@ class programModel extends model /* Get doing executions. */ $doingExecutions = $this->dao->select('id, project, name, end')->from(TABLE_EXECUTION) - ->where('type')->in('sprint,stage') + ->where('type')->in('sprint,stage,kanban') ->andWhere('status')->eq('doing') ->andWhere('deleted')->eq(0) ->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->sprints)->fi() @@ -1254,7 +1254,7 @@ class programModel extends model $executions = $this->dao->select('id')->from(TABLE_EXECUTION) ->where('deleted')->eq(0) ->andWhere('project')->in($projectKeys) - ->andWhere('type')->in('sprint,stage') + ->andWhere('type')->in('sprint,stage,kanban') ->fetchAll('id'); /* Get all tasks and compute totalEstimate, totalConsumed, totalLeft, progress according to them. */ diff --git a/module/program/view/project.html.php b/module/program/view/project.html.php index e532ca9ed0..f5f336d3d9 100644 --- a/module/program/view/project.html.php +++ b/module/program/view/project.html.php @@ -15,10 +15,6 @@ js::set('programID', $programID); js::set('browseType', $browseType); ?> -