From adf53d8754ac199d9212e5d4665ba35e6c24d79d Mon Sep 17 00:00:00 2001 From: liumengyi Date: Wed, 12 Jan 2022 10:08:15 +0800 Subject: [PATCH 1/2] * Finish task #47334.Add default view of professional RD Kanban. --- module/execution/control.php | 29 +- module/execution/css/kanban.css | 142 +- module/execution/js/kanban.js | 1690 ++++++++------------- module/execution/js/taskkanban.js | 8 +- module/execution/model.php | 2 +- module/execution/view/kanban.html.php | 188 +-- module/execution/view/taskheader.html.php | 2 +- module/execution/view/taskkanban.html.php | 4 +- module/kanban/model.php | 161 +- 9 files changed, 1000 insertions(+), 1226 deletions(-) diff --git a/module/execution/control.php b/module/execution/control.php index 407ab663c3..f934284e97 100644 --- a/module/execution/control.php +++ b/module/execution/control.php @@ -1263,8 +1263,7 @@ class execution extends control } $project = $this->project->getByID($projectID); - - if(!empty($projectID) and $project->model == 'kanban') + if(!empty($project) and $project->model == 'kanban') { $this->lang->execution->createExec = str_replace($this->lang->execution->common, $this->lang->execution->kanban, $this->lang->execution->createExec); $this->lang->execution->execName = str_replace($this->lang->execution->common, $this->lang->execution->kanban, $this->lang->execution->execName); @@ -1962,12 +1961,34 @@ class execution extends control * @param int $executionID * @param string $browseType * @param string $orderBy - * @param string $group + * @param string $groupBy * @access public * @return void */ - public function kanban($executionID, $browseType = 'all', $orderBy = 'id_asc', $group = 'all') + public function kanban($executionID, $browseType = 'all', $orderBy = 'id_asc', $groupBy = 'all') { + unset($this->lang->execution->menu); + + $users = $this->loadModel('user')->getPairs('noletter|nodeleted'); + $kanbanData = $this->loadModel('kanban')->getRDKanban($executionID, $browseType, $orderBy, $groupBy); + $execution = $this->execution->getById($executionID); + + $userList = array(); + $avatarPairs = $this->dao->select('account, avatar')->from(TABLE_USER)->where('deleted')->eq(0)->fetchPairs(); + foreach($avatarPairs as $account => $avatar) + { + if(!$avatar) continue; + $userList[$account]['avatar'] = $avatar; + } + + $this->view->title = $this->lang->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->display(); } diff --git a/module/execution/css/kanban.css b/module/execution/css/kanban.css index 372183c4b4..df132b1ab1 100644 --- a/module/execution/css/kanban.css +++ b/module/execution/css/kanban.css @@ -1,58 +1,110 @@ -#main {padding-bottom: 10px;} -#main > .container {max-width: 1960px!important; padding: 0 10px} +#TRAction .icon {padding: 0px 6px; font-size: 14px} -.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;} +.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} +.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 > .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)} +.region .kanban-item > .kanban-card > .info > .user {position: absolute; right: 0; top: -2px} +.region .kanban-item > .kanban-card > .info > .user > .avatar {display: inline-block; width: 24px; height: 24px; line-height: 24px; margin-right: 0px; margin-left: 2px} +.region .kanban-item > .kanban-card > .info > .user > span {display: inline-block; color: transparent; width: 2px; height: 2px; background-color: #8990a2; position: sticky; border-radius: 50%; top: 3px; margin: 0 7px;} +.region .kanban-item > .kanban-card > .info > .user > span:before, +.region .kanban-item > .kanban-card > .info > .user > span:after {content: ''; display: block; position: absolute; width: 2px; height: 2px; background-color: #8990a2; top: 0; border-radius: 50%} +.region .kanban-item > .kanban-card > .info > .user > span:before {left: -4px;} +.region .kanban-item > .kanban-card > .info > .user > span:after {right: -4px;} +.region .kanban-item > .kanban-card > .actions > a {display: block; float: left; width: 20px; height: 20px; line-height: 20px; text-align: center; border-radius: 4px; opacity: .7;} +.region .kanban-item > .kanban-card > .actions > a:hover {background-color: rgba(0,0,0,.075); opacity: 1;} +.region .kanban-item:hover > .actions {opacity: 1;} +.region .kanban-item .has-color .title, +.region .kanban-item .has-color .actions .icon-more-v, +.region .kanban-item .has-color .info > .label-pri, +.region .kanban-item .has-color .info > .estimate, +.region .kanban-item .has-color .info > .user .lable-teamSumCount, +.region .kanban-item .has-color .info > .label-light {color: #FFFFFF;} +.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 > .title {word-break: break-all;} +.kanban-card > .info {position: relative; margin-top: 5px} +.kanban-card > .info > .info + .info {margin-left: 8px} .kanban-card:hover > .actions {opacity: 1;} +.kanban-card > .actions {position: absolute; top: 4px; right: 4px; opacity: 0;} .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;} +.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-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-col[data-type=ADD] {display: none;} +.kanban-col[data-type=EMPTY] {display: none;} +.kanban-item:hover .title {padding-right: 10px;} +.gray .actions {display:none;} -.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;} +.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;} - -#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/kanban.js b/module/execution/js/kanban.js index daf1724e18..ccbd589e45 100644 --- a/module/execution/js/kanban.js +++ b/module/execution/js/kanban.js @@ -1,886 +1,3 @@ -function changeView(view) -{ - var link = createLink('execution', 'kanban', "executionID=" + executionID + '&type=' + view); - location.href = link; -} - -/** - * Render user avatar - * @param {String|{account: string, avatar: string}} user User account or user object - * @returns {string} - */ -function renderUserAvatar(user, objectType, objectID, 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.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.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.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); - 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. - * - * @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; - }); -} - -/** - * Change column type for a card - * @param {Object} card Card object - * @param {String} fromColType The column type before change - * @param {String} toColType The column type after change - * @param {String} kanbanID Kanban ID - */ -function changeCardColType(card, fromColType, toColType, kanbanID) -{ - if(typeof card == 'undefined') return false; - var objectID = card.id; - var showIframe = false; - var moveCard = false; - - /* Task lane. */ - if(kanbanID == 'task') - { - if(toColType == 'developed') - { - if((fromColType == 'developing' || fromColType == 'wait') && priv.canFinishTask) - { - var link = createLink('task', 'finish', 'taskID=' + objectID, '', true); - showIframe = true; - } - } - else if(toColType == 'pause') - { - if(fromColType == 'developing' && priv.canPauseTask) - { - var link = createLink('task', 'pause', 'taskID=' + objectID, '', true); - showIframe = true; - } - } - else if(toColType == 'developing') - { - if((fromColType == 'pause' || fromColType == 'cancel' || fromColType == 'closed' || fromColType == 'developed') && priv.canActivateTask) - { - var link = createLink('task', 'activate', 'taskID=' + objectID, '', true); - showIframe = true; - } - if(fromColType == 'wait' && priv.canStartTask) - { - var link = createLink('task', 'start', 'taskID=' + objectID, '', true); - showIframe = true; - } - } - else if(toColType == 'canceled') - { - if((fromColType == 'developing' || fromColType == 'wait' || fromColType == 'pause') && priv.canCancelTask) - { - var link = createLink('task', 'cancel', 'taskID=' + objectID, '', true); - showIframe = true; - } - } - else if(toColType == 'closed') - { - if((fromColType == 'developed' || fromColType == 'canceled') && priv.canCloseTask) - { - var link = createLink('task', 'close', 'taskID=' + objectID, '', true); - showIframe = true; - } - } - } - - /* Bug lane. */ - if(kanbanID == '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 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) - { - if(data) - { - kanbanGroup = $.parseJSON(data); - if(groupBy == 'default') - { - updateKanban('bug', kanbanGroup.bug); - } - else - { - updateKanban(browseType, kanbanGroup[groupBy]); - } - } - }) - } - } - - /* Story lane. */ - if(kanbanID == '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) - { - 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 finish drop task - * @param {Object} event Event object - * @returns {void} - */ -function handleFinishDrop(event) -{ - var $card = $(event.element); // The drag card - var $dragCol = $card.closest('.kanban-lane-col'); - var $dropCol = $(event.target); - - /* Get d-n-d(drag and drop) infos */ - var card = $card.data('item'); - var fromColType = $dragCol.data('type'); - var toColType = $dropCol.data('type'); - var kanbanID = $card.closest('.kanban').data('id'); - - changeCardColType(card, fromColType, toColType, kanbanID); - - $('#kanbans').find('.can-drop-here').removeClass('can-drop-here'); -} - -/** Handle sort cards in column */ -function handleSortColCards() -{ - /* TODO: handle sort cards from column contextmenu */ - return false; -} - -/** - * Create column menu - * @returns {Object[]} - */ -function createColumnMenu(options) -{ - var $col = options.$trigger.closest('.kanban-col'); - var col = $col.data('col'); - var kanbanID = options.kanban; - - var items = []; - if(priv.canEditName) items.push({label: executionLang.editName, url: $.createLink('kanban', 'setColumn', 'col=' + col.columnID + '&executionID=' + executionID + '&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) @@ -904,143 +21,6 @@ function changeKanbanScaleSize(newScaleSize) 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'}, - droppable: - { - target: findDropColumns, - finish: handleFinishDrop, - mouseButton: 'left' - }, - onRenderHeaderCol: renderHeaderCol, - onRenderLaneName: renderLaneName, - onRenderCount: renderColumnCount - }; - - /* Create kanban */ - if(groupBy == 'default') - { - var kanbanLane = ''; - for(var i in kanbanList) - { - if(kanbanList[i] == 'story') kanbanLane = kanbanGroup.story; - if(kanbanList[i] == 'bug') kanbanLane = kanbanGroup.bug; - if(kanbanList[i] == 'task') kanbanLane = kanbanGroup.task; - - if(browseType == kanbanList[i] || browseType == 'all') createKanban(kanbanList[i], kanbanLane, commonOptions); - } - } - else - { - /* Create kanban by group. */ - createKanban(browseType, kanbanGroup[groupBy], commonOptions); - } - - /* Init iframe modals */ - $(document).on('click', '#kanbans .iframe,.contextmenu-menu .iframe', function(event) - { - var $link = $(this); - if($link.data('zui.modaltrigger')) return; - $link.modalTrigger({show: true}); - event.preventDefault(); - }); - - /* Init contextmenu */ - $('#kanbans').on('click', '[data-contextmenu]', function(event) - { - var $trigger = $(this); - var menuType = $trigger.data('contextmenu'); - - var menuCreator = window.menuCreators[menuType]; - if(!menuCreator) return; - - var options = $.extend({event: 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(); @@ -1055,16 +35,672 @@ $('#type').change(function() }) } - var link = createLink('execution', 'kanban', "executionID=" + executionID + '&type=' + type); + var link = createLink('execution', 'kanban', "executionID=" + executionID + '&browseType=' + type); location.href = link; }); -$('.c-group').change(function() +/** + * Create lane menu. + * + * @param object $options + * @access public + * @return void + */ +function createLaneMenu(options) { - $('.c-group').show(); + var lane = options.$trigger.closest('.kanban-lane').data('lane'); + var privs = lane.actions; + if(!privs.length) return []; - 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 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; +} + +/** + * Create card menu. + * + * @param object $options + * @access public + * @return array + */ +function createCardMenu(options) +{ + var card = options.$trigger.closest('.kanban-item').data('item'); + var privs = card.actions; + if(!privs.length) return []; + + var items = []; + if(privs.includes('editCard')) items.push({label: kanbanLang.editCard, icon: 'edit', url: createLink('kanban', 'editCard', 'cardID=' + card.id, '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '80%'}}); + if(privs.includes('archiveCard') && kanban.archived == '1') items.push({label: kanbanLang.archiveCard, icon: 'card-archive', url: createLink('kanban', 'archiveCard', 'cardID=' + card.id), attrs: {'target': 'hiddenwin'}}); + if(privs.includes('copyCard')) items.push({label: kanbanLang.copyCard, icon: 'copy', url: createLink('kanban', 'copyCard', 'cardID=' + card.id, '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal'}}); + if(privs.includes('deleteCard')) items.push({label: kanbanLang.deleteCard, icon: 'trash', url: createLink('kanban', 'deleteCard', 'cardID=' + card.id), attrs: {'target': 'hiddenwin'}}); + if(privs.includes('moveCard')) + { + var moveCardItems = []; + var moveColumns = []; + var parentColumns = []; + var regionGroups = regions[options.$trigger.closest('.region').data('id')].groups; + for(let i = 0; i < regionGroups.length ; i ++ ) + { + if(regionGroups[i].id == options.$trigger.closest('.kanban-board').data('id')) + { + moveColumns = regionGroups[i].columns; + break; + } + } + for(let i = moveColumns.length-1 ; i >= 0 ; i -- ) + { + if(moveColumns[i].parent > 0) parentColumns.push(moveColumns[i].parent); + if(moveColumns[i].id == card.column || $.inArray(moveColumns[i].id, parentColumns) >= 0) continue; + moveCardItems.push({label: moveColumns[i].name, onClick: function(){moveCard(card.id, moveColumns[i].id, card.lane, card.kanban, card.region);}}); + } + moveCardItems = moveCardItems.reverse(); + items.push({label: kanbanLang.moveCard, icon: 'move', items: moveCardItems}); + } + if(privs.includes('setCardColor')) + { + var cardColoritems = []; + if(!card.color) color = "#fff"; + for(let i = 0 ; i < colorList.length ; i ++ ) + { + var attr = card.color == colorList[i] ? '' : ''; + var border = i == 0 ? 'border:1px solid #b0b0b0;' : ''; + cardColoritems.push({label: "
" + colorListLang[colorList[i]] + attr , + onClick: function(){setCardColor(card.id, colorList[i], card.kanban, card.region);}, html: true, attrs: {id: 'cardcolormenu'}, className: 'color' + i}); + }; + items.push({label: kanbanLang.cardColor, icon: 'color', items: cardColoritems}); + } + + 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'}}); + if(privs.includes('splitColumn')) items.push({label: kanbanLang.splitColumn, icon: 'col-split', url: createLink('kanban', 'splitColumn', 'columnID=' + column.id, '', true), className: 'iframe', attrs: {'data-toggle': 'modal'}}); + if(privs.includes('createColumn')) + { + items.push({label: kanbanLang.createColumnOnLeft, icon: 'col-add-left', url: createLink('kanban', 'createColumn', 'columnID=' + column.id + '&position=left'), className: 'iframe', attrs: {'data-toggle': 'modal'}}); + items.push({label: kanbanLang.createColumnOnRight, icon: 'col-add-right', url: createLink('kanban', 'createColumn', 'columnID=' + column.id + '&position=right'), className: 'iframe', attrs: {'data-toggle': 'modal'}}); + } + if(privs.includes('copyColumn')) items.push({label: kanbanLang.copyColumn, icon: 'copy', url: createLink('kanban', 'copyColumn', 'columnID=' + column.id), className: 'iframe', attrs: {'data-toggle': 'modal'}}); + if(privs.includes('archiveColumn') && kanban.archived == '1' && column.$kanbanData.columns.length > 1) items.push({label: kanbanLang.archiveColumn, icon: 'card-archive', url: createLink('kanban', 'archiveColumn', 'columnID=' + column.id), attrs: {'target': 'hiddenwin'}}); + if(privs.includes('deleteColumn') && column.$kanbanData.columns.length > 1) items.push({label: kanbanLang.deleteColumn, icon: 'trash', url: createLink('kanban', 'deleteColumn', 'columnID=' + column.id), attrs: {'target': 'hiddenwin'}}); + + var bounds = options.$trigger[0].getBoundingClientRect(); + items.$options = {x: bounds.right, y: bounds.top}; + 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'); +} + +/** + * Find drop columns + * @param {JQuery} $element Drag element + * @param {JQuery} $root Dnd root element + */ +function findDropColumns($element, $root) +{ + var $task = $element; + var task = $task.data('task'); + //var status = task.status; + var $col = $task.closest('.kanban-col'); + var col = $col.data(); + var lane = $col.closest('.kanban-lane').data('lane'); + var allStatusCanChange = statusChangeMap[status]; + + hideKanbanAction(); + + return $root.find('.kanban-lane-col:not([data-type="EMPTY"],[data-type=""])').filter(function() + { + var $newCol = $(this); + var newCol = $newCol.data(); + var $newLane = $newCol.closest('.kanban-lane'); + var newLane = $newLane.data('lane'); + + $newCol.addClass('can-drop-here'); + return true; + }); +} + +/** + * Render items count of a column. + */ +function renderCount($count, count, column) +{ + /* 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 of a column. + */ +function renderHeaderCol($column, column, $header, kanbanData) +{ + /* Render group header. */ + var privs = kanbanData.actions; + var columnPrivs = kanbanData.columns[0].actions; + + 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 regionID = $column.closest('.kanban').data('id'); + var groupID = $column.closest('.kanban-board').data('id'); + var laneID = column.$kanbanData.lanes[0].id ? column.$kanbanData.lanes[0].id : 0; + var columnID = $column.closest('.kanban-col').data('id'); + var printMoreBtn = (columnPrivs.includes('setColumn') || columnPrivs.includes('setWIP') || columnPrivs.includes('createColumn') || columnPrivs.includes('copyColumn') || columnPrivs.includes('archiveColumn') || columnPrivs.includes('deleteColumn') || columnPrivs.includes('splitColumn')); + + /* Render more menu. */ + if(columnPrivs.includes('createCard') || printMoreBtn) + { + var addItemBtn = ''; + var moreAction = ''; + + if(!$column.children('.actions').length) $column.append('
'); + var $actions = $column.children('.actions'); + + if(columnPrivs.includes('createCard') && column.parent != -1) + { + var cardUrl = createLink('kanban', 'createCard', 'kanbanID=' + kanbanID + '®ionID=' + regionID + '&groupID=' + groupID + '&laneID=' + laneID + '&columnID=' + columnID); + addItemBtn = ['', '', ''].join(''); + } + + var moreAction = ' '; + $actions.html(addItemBtn + moreAction); + } +} + +/** + * Render lane name. + * + * @param object $lane + * @param int lane + * @param object $kanban + * @param array columns + * @param object $kanban + * @access public + * @return void + */ +function renderLaneName($lane, lane, $kanban, columns, kanban) +{ + 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)) + { + $([ + '
', + '', + '', + '', + '
' + ].join('')).appendTo($lane); + } +} +/** + * Adjust add button postion in column + */ +function adjustAddBtnPosition($kanban) +{ + if(!$kanban) + { + $('.kanban').children('.kanban-board').each(function() + { + adjustAddBtnPosition($(this)); + }); + return; + } + + $kanban.find('.kanban-lane-col:not([data-type="EMPTY"])').each(function() + { + var $col = $(this); + var items = $col.children('.kanban-lane-items')[0]; + $col.toggleClass('has-scrollbar', items.scrollHeight > items.clientHeight); + }); +} +/** + * 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); +} + +function processMinusBtn() +{ + var columnCount = $('#splitTable .child-column').size(); + if(columnCount > 2 && columnCount < 10) + { + $('#splitTable .btn-plus').show(); + $('#splitTable .btn-close').show(); + } + else if(columnCount <= 2) + { + $('#splitTable .btn-close').hide(); + } + else if(columnCount >= 10) + { + $('#splitTable .btn-plus').hide(); + } +} +/** + * The function for rendering kanban item + */ +function renderKanbanItem(item, $item) +{ + var $title = $item.children('.title'); + var privs = item.actions; + var printMoreBtn = (privs.includes('editCard') || privs.includes('archiveCard') || privs.includes('copyCard') || privs.includes('deleteCard') || privs.includes('moveCard') || privs.includes('setCardColor')); + if(!$title.length) + { + if(privs.includes('viewCard')) $title = $('').appendTo($item).attr('href', createLink('kanban', 'viewCard', 'cardID=' + item.id, '', true)); + if(!privs.includes('viewCard')) $title = $('

').appendTo($item); + } + + $title.text(item.name).attr('title', item.name); + + if(printMoreBtn) + { + $( + [ + '
', + '', + '', + '', + '
' + ].join('')).appendTo($item); + } + + var $info = $item.children('.info'); + if(!$info.length) $info = $( + [ + '
', + '', + '', + '', + '
', + '
' + ].join('')).appendTo($item); + + $item.data('card', item); + + $info.children('.estimate').text(item.estimate + kanbancardLang.lblHour); + if(item.estimate == 0) $info.children('.estimate').hide(); + + $info.children('.pri') + .attr('class', 'pri label-pri label-pri-' + item.pri) + .text(item.pri); + + $item.css('background-color', item.color); + $item.toggleClass('has-color', item.color != '#fff' && item.color != ''); + $item.find('.info > .label-light').css('background-color', item.color); + + var $time = $info.children('.time'); + if(item.end == '0000-00-00' && item.begin == '0000-00-00') + { + $time.hide(); + } + else + { + var today = new Date(); + var begin = $.zui.createDate(item.begin); + var end = $.zui.createDate(item.end); + var needRemind = (begin.toLocaleDateString() == today.toLocaleDateString() || end.toLocaleDateString() == today.toLocaleDateString()); + if(item.end == '0000-00-00' && item.begin != '0000-00-00') + { + $time.text($.zui.formatDate(begin, 'MM/dd') + ' ' + kanbancardLang.beginAB).attr('title', $.zui.formatDate(begin, 'yyyy/MM/dd') + ' ' +kanbancardLang.beginAB).show(); + } + else if(item.begin == '0000-00-00' && item.end != '0000-00-00') + { + $time.text($.zui.formatDate(end, 'MM/dd') + ' ' + kanbancardLang.deadlineAB).attr('title', $.zui.formatDate(end, 'yyyy/MM/dd') + ' ' + kanbancardLang.deadlineAB).show(); + } + else if(item.begin != '0000-00-00' && item.end != '0000-00-00') + { + $time.text($.zui.formatDate(begin, 'MM/dd') + ' ' + kanbancardLang.to + ' ' + $.zui.formatDate(end, 'MM/dd')).attr('title', $.zui.formatDate(begin, 'yyyy/MM/dd') + kanbancardLang.to + $.zui.formatDate(end, 'yyyy/MM/dd')).show(); + } + + if(!$item.hasClass('has-color') && needRemind) $time.css('background-color', 'rgba(210, 50, 61, 0.3)'); + if($item.hasClass('has-color') && needRemind) $time.css('background-color', 'rgba(255, 255, 255, 0.3)'); + if(!needRemind) $time.css('background-color', 'rgba(0, 0, 0, 0.15)'); + } + + /* Display avatars of assignedTo. */ + var assignedTo = item.assignedTo.split(','); + var $user = $info.children('.user'); + var title = []; + for(i = 0; i < assignedTo.length; i++) title.push(users[assignedTo[i]]); + $user.html(renderUsersAvatar(assignedTo, item.id)).attr('title', title); +} +/* Define menu creators */ +window.menuCreators = +{ + card: createCardMenu, + lane: createLaneMenu, + column: createColumnMenu +}; + +/** + * init Kanban + */ +function initKanban($kanban) +{ + var id = $kanban.data('id'); + var region = regions[id]; + + $kanban.kanban( + { + data: region.groups, + maxColHeight: 510, + fluidBoardWidth: false, + minColWidth: 300, + maxColWidth: 300, + createColumnText: kanbanLang.createColumn, + addItemText: '', + itemRender: renderKanbanItem, + onAction: handleKanbanAction, + onRenderKanban: adjustAddBtnPosition, + onRenderLaneName: renderLaneName, + onRenderHeaderCol: renderHeaderCol, + onRenderCount: renderCount, + droppable: + { + target: findDropColumns, + finish: handleFinishDrop, + mouseButton: 'left' + } + }); + + $kanban.on('click', '.action-cancel', hideKanbanAction); + $kanban.on('scroll', function() + { + $.zui.ContextMenu.hide(); + }); +} + +/** + * Init when page ready + */ +$(function() +{ + if($.cookie('isFullScreen') == 1) fullScreen(); + + 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() + { + changeKanbanScaleSize(window.kanbanScaleSize + ($(this).data('type') === '+' ? 1 : -1)); + }); + + /* 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}); + }); + + /* Adjust the add button position on window resize */ + $(window).on('resize', function(a) + { + adjustAddBtnPosition(); + }); + + + /* Hide contextmenu when page scroll */ + $(window).on('scroll', function() + { + $.zui.ContextMenu.hide(); + }); + + $(document).on('click', '#splitTable .btn-plus', function() + { + 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) + { + return $ele.parent(); + }, + targetSelector: function($ele) + { + /* Sort regions */ + if($ele.hasClass('region')) + { + sortType = 'region'; + return $ele.parent().children('.region'); + } + + /* 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) + { + if(sortType == 'region') + { + showRegionIdList = ''; + $('.icon-chevron-double-up').each(function() + { + showRegionIdList += $(this).attr('data-id') + ','; + $(this).attr('class', 'icon-chevron-double-down'); + }); + + $('.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(); + } + }); }); diff --git a/module/execution/js/taskkanban.js b/module/execution/js/taskkanban.js index daf1724e18..76842aa51b 100644 --- a/module/execution/js/taskkanban.js +++ b/module/execution/js/taskkanban.js @@ -1,6 +1,6 @@ function changeView(view) { - var link = createLink('execution', 'kanban', "executionID=" + executionID + '&type=' + view); + var link = createLink('execution', 'taskKanban', "executionID=" + executionID + '&type=' + view); location.href = link; } @@ -597,7 +597,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; @@ -1055,7 +1055,7 @@ $('#type').change(function() }) } - var link = createLink('execution', 'kanban', "executionID=" + executionID + '&type=' + type); + var link = createLink('execution', 'taskKanban', "executionID=" + executionID + '&type=' + type); location.href = link; }); @@ -1065,6 +1065,6 @@ $('.c-group').change(function() var type = $('#type').val(); var group = $('#group').val(); - var link = createLink('execution', 'kanban', 'executionID=' + executionID + '&type=' + type + '&orderBy=order_asc' + '&groupBy=' + group); + var link = createLink('execution', 'taskKanban', 'executionID=' + executionID + '&type=' + type + '&orderBy=order_asc' + '&groupBy=' + group); location.href = link; }); diff --git a/module/execution/model.php b/module/execution/model.php index 81a958accc..e897bac7eb 100644 --- a/module/execution/model.php +++ b/module/execution/model.php @@ -2480,7 +2480,7 @@ class executionModel extends model /** * Get members by execution id list. - * + * * @param array $executionIdList * @access public * @return void diff --git a/module/execution/view/kanban.html.php b/module/execution/view/kanban.html.php index 28fb732228..2ecc5dc6dd 100644 --- a/module/execution/view/kanban.html.php +++ b/module/execution/view/kanban.html.php @@ -1,15 +1,42 @@ * @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('execution', $execution); +js::set('kanbanLang', $lang->kanban); +js::set('kanbanlaneLang', $lang->kanbanlane); +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('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'); +?> + -
-
-
-
- -