From b4379937b58ebbdbe3b1d3ebccb290f6aa8a9cc7 Mon Sep 17 00:00:00 2001 From: wangyidong Date: Thu, 26 Oct 2017 08:55:43 +0800 Subject: [PATCH] =?UTF-8?q?Finish=20task#3278=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE->=E9=9C=80=E6=B1=82=E7=9C=8B=E6=9D=BF?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E3=80=82,cost:4=20left:0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- module/common/lang/en.php | 2 +- module/common/lang/zh-cn.php | 2 +- module/project/control.php | 41 +++++++ module/project/css/storykanban.css | 69 ++++++++++++ module/project/js/storykanban.js | 130 +++++++++++++++++++++++ module/project/view/story.html.php | 1 + module/project/view/storykanban.html.php | 101 ++++++++++++++++++ module/story/model.php | 15 +++ 8 files changed, 359 insertions(+), 2 deletions(-) create mode 100644 module/project/css/storykanban.css create mode 100644 module/project/js/storykanban.js create mode 100644 module/project/view/storykanban.html.php diff --git a/module/common/lang/en.php b/module/common/lang/en.php index 4be2bdbdcc..c8e93b21f4 100644 --- a/module/common/lang/en.php +++ b/module/common/lang/en.php @@ -214,7 +214,7 @@ $lang->project->menu = new stdclass(); $lang->project->menu->list = array('link' => '%s', 'fixed' => true); $lang->project->menu->task = array('link' => 'Task|project|task|projectID=%s', 'subModule' => 'task,tree', 'alias' => 'grouptask,importtask,burn,importbug,kanban,printkanban,tree'); -$lang->project->menu->story = array('link' => 'Story|project|story|projectID=%s', 'subModule' => 'story', 'alias' => 'linkstory'); +$lang->project->menu->story = array('link' => 'Story|project|story|projectID=%s', 'subModule' => 'story', 'alias' => 'linkstory,storykanban'); $lang->project->menu->bug = 'Bug|project|bug|projectID=%s'; $lang->project->menu->dynamic = 'Dynamic|project|dynamic|projectID=%s'; $lang->project->menu->build = array('link' => 'Build|project|build|projectID=%s', 'subModule' => 'build'); diff --git a/module/common/lang/zh-cn.php b/module/common/lang/zh-cn.php index 6cd145fdb4..4646b49e3b 100644 --- a/module/common/lang/zh-cn.php +++ b/module/common/lang/zh-cn.php @@ -214,7 +214,7 @@ $lang->project->menu = new stdclass(); $lang->project->menu->list = array('link' => '%s', 'fixed' => true); $lang->project->menu->task = array('link' => '任务|project|task|projectID=%s', 'subModule' => 'task,tree', 'alias' => 'grouptask,importtask,burn,importbug,kanban,printkanban,tree'); -$lang->project->menu->story = array('link' => '需求|project|story|projectID=%s', 'subModule' => 'story', 'alias' => 'linkstory'); +$lang->project->menu->story = array('link' => '需求|project|story|projectID=%s', 'subModule' => 'story', 'alias' => 'linkstory,storykanban'); $lang->project->menu->bug = 'Bug|project|bug|projectID=%s'; $lang->project->menu->dynamic = '动态|project|dynamic|projectID=%s'; $lang->project->menu->build = array('link' => '版本|project|build|projectID=%s', 'subModule' => 'build'); diff --git a/module/project/control.php b/module/project/control.php index f4eb10402b..47eff3bad5 100644 --- a/module/project/control.php +++ b/module/project/control.php @@ -1425,6 +1425,47 @@ class project extends control $this->display(); } + /** + * Story kanban. + * + * @param int $projectID + * @access public + * @return void + */ + public function storyKanban($projectID) + { + /* Save to session. */ + $uri = $this->app->getURI(true); + $this->app->session->set('storyList', $uri); + + /* Compatibility IE8*/ + if(strpos($this->server->http_user_agent, 'MSIE 8.0') !== false) header("X-UA-Compatible: IE=EmulateIE7"); + + $this->project->setMenu($this->projects, $projectID); + $project = $this->loadModel('project')->getById($projectID); + $stories = $this->loadModel('story')->getProjectStories($projectID); + $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'story', false); + + /* Get project's product. */ + $productID = 0; + $productPairs = $this->loadModel('product')->getProductsByProject($projectID); + if($productPairs) $productID = key($productPairs); + + $this->view->title = $this->lang->project->kanban; + $this->view->position[] = html::a($this->createLink('project', 'story', "projectID=$projectID"), $project->name); + $this->view->position[] = $this->lang->project->kanban; + $this->view->stories = $this->story->getKanbanGroupData($stories); + $this->view->realnames = $this->loadModel('user')->getPairs('noletter'); + $this->view->projectID = $projectID; + $this->view->project = $project; + $this->view->productID = $productID; + + $kanbanSetting = $this->project->getKanbanSetting($projectID); + $this->view->showOption = $kanbanSetting->showOption; + + $this->display(); + } + /** * Delete a project. * diff --git a/module/project/css/storykanban.css b/module/project/css/storykanban.css new file mode 100644 index 0000000000..2a46209874 --- /dev/null +++ b/module/project/css/storykanban.css @@ -0,0 +1,69 @@ +#kanbanHeader th{border: 1px solid #ddd; padding:8px;} +#kanbanHeader .dropdown {display: inline-block;} +#kanbanHeader .dropdown > a {color: #036} +#kanbanHeader th{position: relative} +#kanbanHeader .actions {position: absolute; right: 0; top: 0; padding: 0px; background: #f1f1f1; border-left: 1px solid #ccc} +#kanbanHeader .actions .btn {display: block; border-radius: 0; width: 34px; height: 36px; position: relative; text-align: center; padding: 5px 0; color: #036} +#kanbanHeader .actions .btn-text {display: none} + +#kanbanWrapper {table-layout: fixed; border-top: none} +#kanbanWrapper > tbody > tr:first-child > td {border-top: none} +#kanbanWrapper thead th, #kanbanWrapper thead, #kanbanWrapper thead tr {padding: 0!important; border: none!important; height: 0!important; border-bottom: none!important} +#kanbanWrapper tbody td {vertical-align: top; padding: 6px} +#kanbanWrapper tbody th {background: #f1f1f1} +#kanbanWrapper .board-shadow {height: 0; opacity: 0; transition: opacity 0.2s;} +#kanbanWrapper tbody tr.dragging td.col-droppable {background: #fff4e5} +#kanbanWrapper tbody tr.dragging td.col-droppable.drop-to {background: #ffeacc} +#kanbanWrapper tbody tr.dragging td.col-droppable.drop-to .board-story + .board-shadow {margin-top: 6px;} +#kanbanWrapper tbody tr.dragging td.col-droppable.drop-to .board-shadow {height: 28px; background: #999; background: rgba(0,0,0,0.35); opacity: 1;} +#kanbanWrapper tbody tr.dragging td.col-droppable.drop-to.drag-from .board-shadow {display: none} +#kanbanWrapper .board.drag-from.dragging {opacity: 0.8;} + +.board {background: #333; color: #f1f1f1; color: rgba(255,255,255,0.9); opacity: 1; padding: 3px 6px; transition: all 0.2s; position:relative;} +.board + .board {margin-top: 6px;} +.board:hover {opacity: 1} +.board a {opacity: 0.9; color: inherit} +.board a:hover {opacity: 1} +.board-title, .board-header, .board-footer {padding: 2px 0} +.board-title {padding-right:35px;} +.board-actions {position: absolute; right: 3px; top: 3px} +.board-actions .btn {color: inherit; margin: 0; min-width: 20px} +.board-actions .btn:active, .board-actions .btn:focus, .board-actions .btn:hover {background: #999; color: inherit; background: rgba(255,255,255,.2);} +.ie-8 .board-actions .btn {min-width: 10px} +.board-actions > .dropdown {display: inline-block; margin-left: -5px;} +.board .dropdown-menu {min-width: 80px;} +.board .dropdown .caret {border-top-color: #999} +.board .dropdown-menu > a {display: block; padding: 3px 15px; color: #333; font-size: 12px} +.board .dropdown-menu > a:hover {background: #f1f1f1; color: #1a53ff; text-decoration: none} +.board .dropdown-menu > a.disabled, .board .dropdown-menu > a.disabled:hover, .board .dropdown-menu > a.active {color: #aaa; background: none} +.board .board-footer {display: none; opacity: 0.6} +.board.show-info .board-footer {display: block; margin: 0 -6px -3px -6px; padding: 5px; background: rgba(0,0,0,0.07);} +.board-footer .board-id {display: inline-block; border: 1px solid #aaa; font-weight: normal; padding: 0px 4px; text-align: center; min-width: 20px; line-height: 16px; height: 17px; margin-right: 10px; font-size: 12px;} +.board-footer .story-pri {margin-right: 10px;} + +.pri-0:before, .pri-3:before {content: "3";} +.pri-1:before {content: "1";} +.pri-2:before {content: "2";} +.pri-4:before {content: "4";} +.pri-5:before {content: "5";} +.pri-6:before {content: "6";} + +.story-pri {display: inline-block; width: 16px; height: 16px; text-align: center; border-radius: 8px; line-height: 16px; font-size: 12px} +.story-pri.pri-0 {background-color:#b56a00;} +.story-pri.pri-1 {background-color:#E30413;} +.story-pri.pri-2 {background-color:#E84E0E;} +.story-pri.pri-3 {background-color:#b56a00;} +.story-pri.pri-4 {background-color:#795523;} + +.board-story {color: #dd7503; background-color: #FFF3E0;} +.board-story.stage-projected {color:#777;} +.board-story.stage-developing {color:#D2322D; background-color: #FFEBEE;} +.board-story.stage-developed {color:#229F24; background-color: #E8F5E9;} +.board-story.stage-testing {color:#EF6C00; background-color: #FFF3E0;} +.board-story.stage-tested {color:#827717; background-color: #F9FBE7} +.board-story.stage-released {color:#9C27B0; background-color: #F3E5F5} + +.affix {position:fixed; top:0px; width:95%;z-index:1030;} +#kanbanHeader.affix {box-shadow: 0 3px 10px rgba(0,0,0,.1);} +#kanbanHeader.affix th {border-bottom: 1px solid #bbb; background: #114f8e; color: #ccc; color: rgba(255,255,255,.8)} +#kanbanHeader.affix th {border-color: rgba(255,255,255,.2)} diff --git a/module/project/js/storykanban.js b/module/project/js/storykanban.js new file mode 100644 index 0000000000..e11db3f2c0 --- /dev/null +++ b/module/project/js/storykanban.js @@ -0,0 +1,130 @@ +$(function() +{ + var boardID = ''; + var onlybody = config.requestType == 'GET' ? "&onlybody=yes" : "?onlybody=yes"; + $(".kanbanFrame").modalTrigger({type: 'iframe', width: '80%', afterShow:function(){ $('#ajaxModal').data('cancel-reload', true)}, afterHidden: function(){refresh()}}); + + $.cookie('selfClose', 0, {expires:config.cookieLife, path:config.webRoot}); + + var $kanban = $('#kanban'); + var $kanbanWrapper = $('#kanbanWrapper'); + + initBoards(); + + var stageMap = + { + story: + { + projected : {verified: 'batchChangeStage'}, + developing : {verified: 'batchChangeStage'}, + developed : {verified: 'batchChangeStage'}, + testing : {verified: 'batchChangeStage'}, + tested : {verified: 'batchChangeStage'}, + } + }; + + var lastOperation; + + function dropTo(id, from, to, type) + { + if(stageMap[type][from] && stageMap[type][from][to]) + { + lastOperation = {id: id, from: from, to: to}; + $.post(createLink(type, stageMap[type][from][to], 'stage=' + to), {'storyIDList[]':[id]}); + return true; + } + return false; + } + + function initBoards() + { + $('.col-droppable').append('
'); + + var $boardTasks = $kanban.find('.board-story'); + $boardTasks.droppable( + { + target: '.col-droppable', + flex: true, + before: function(e) + { + if(e.element.find('.dropdown.open').length) return false; + }, + start: function(e) + { + e.element.closest('td').addClass('drag-from').closest('tr').addClass('dragging'); + $kanban.addClass('dragging').find('.board-item-shadow').height(e.element.outerHeight()); + }, + drag: function(e) + { + if(e.isNew) + { + var $dargShadow = $('.drag-shadow.board-story'); + for(var stage in stageMap['story']) + { + $dargShadow.removeClass('board-story-' + stage); + } + $dargShadow.addClass('board-story-' + e.target.data('id')); + } + }, + drop: function(e) + { + if(e.isNew && e.element.closest('tr').data('id') == e.target.closest('tr').data('id')) + { + var result = dropTo(e.element.data('id'), e.element.closest('td').data('id'), e.target.data('id'), 'story'); + if(result !== false) + { + for(var stage in stageMap['story']) + { + e.element.removeClass('board-story-' + stage); + } + e.element.addClass('board-story-' + e.target.data('id')).insertBefore(e.target.find('.board-shadow')); + } + } + }, + finish: function(e) + { + $kanban.removeClass('dragging drop-in'); + $kanbanWrapper.find('tr.dragging').removeClass('dragging').find('.drop-in, .drag-from').removeClass('drop-in drag-from'); + } + }); + } + + function refresh() + { + var selfClose = $.cookie('selfClose'); + $.cookie('selfClose', 0, {expires:config.cookieLife, path:config.webRoot}); + if(selfClose == 1) + { + $('#kanbanWrapper').wrap("
"); + $('#tempDIV').load(location.href + ' #kanbanWrapper', function() + { + $('#kanbanWrapper').unwrap(); + initBoards() + $(".kanbanFrame").modalTrigger({type: 'iframe', width: '80%', afterShow:function(){ $('#ajaxModal').data('cancel-reload', true)}, afterHidden: function(){refresh()}}); + }); + } + } + + var fixH = $("#kanbanHeader").offset().top; + $(window).scroll(function() + { + var scroH = $(this).scrollTop(); + if(scroH>=fixH) + { + $("#kanbanHeader").addClass('affix'); + $("#kanbanHeader").width($('#kanbanWrapper').width()); + } + else if(scroH
id", '', 'button', 'columns', '', '', '', '', $lang->project->kanban); common::printIcon('story', 'export', "productID=$productID&orderBy=id_desc", '', 'button', '', '', 'export'); $this->lang->story->create = $this->lang->project->createStory; diff --git a/module/project/view/storykanban.html.php b/module/project/view/storykanban.html.php new file mode 100644 index 0000000000..21fe7bb2f1 --- /dev/null +++ b/module/project/view/storykanban.html.php @@ -0,0 +1,101 @@ + + +project->confirmUnlinkStory)?> +
+
icons['story']);?> project->story;?>
+
+
+ id", '', 'button', 'list', '', '', '', '', $lang->project->story); + common::printIcon('story', 'export', "productID=$productID&orderBy=id_desc", '', 'button', '', '', 'export'); + + $this->lang->story->create = $this->lang->project->createStory; + if($productID and !$this->loadModel('story')->checkForceReview()) common::printIcon('story', 'create', "productID=$productID&branch=&moduleID=0&story=0&project=$project->id"); + + if(commonModel::isTutorialMode()) + { + $wizardParams = helper::safe64Encode("project=$project->id"); + echo html::a($this->createLink('tutorial', 'wizard', "module=project&method=linkStory¶ms=$wizardParams"), " {$lang->project->linkStory}",'', "class='btn link-story-btn'"); + } + else + { + common::printIcon('project', 'linkStory', "project=$project->id", '', 'button', 'link', '', 'link-story-btn'); + } + ?> +
+
+
+
+app->user->account +?> +
+ + + + + + + + + + +
story->stageList[$col];?>story->stageList[$endCol];?>
+ + + + + + + + + + + + + + +
+ + +
+
+ createLink('story', 'view', "story=$story->id", '', true), $story->title, '', 'class="kanbanFrame" title="' . $story->title . '"');?> +
+ + +
+
+ +
+ + +
+
+ + diff --git a/module/story/model.php b/module/story/model.php index 017dde597f..da7345bef5 100644 --- a/module/story/model.php +++ b/module/story/model.php @@ -2174,6 +2174,21 @@ class storyModel extends model ->groupBy('version')->orderBy('value')->fetchAll(); } + /** + * Get kanban group data. + * + * @param array $stories + * @access public + * @return array + */ + public function getKanbanGroupData($stories) + { + $storyGroup = array(); + foreach($stories as $story) $storyGroup[$story->stage][$story->id] = $story; + + return $storyGroup; + } + /** * Adjust the action clickable. *