diff --git a/module/mr/control.php b/module/mr/control.php index b4c56e24d1..d5d7603126 100644 --- a/module/mr/control.php +++ b/module/mr/control.php @@ -106,6 +106,27 @@ class mr extends control $gitlabUsers = $this->gitlab->getUserAccountIdPairs($MR->gitlabID); + /* Import lang for required modules. */ + $this->loadModel('repo'); + $this->loadModel('job'); + $this->loadModel('compile'); + + $repoList = array(); + $rawRepoList = $this->repo->getGitLabRepoList($MR->gitlabID, $MR->sourceProject); + foreach($rawRepoList as $rawRepo) $repoList[$rawRepo->id] = "[$rawRepo->id] $rawRepo->name"; + + $jobList = array(); + $rawJobList = $this->job->getListByRepoID($MR->repoID); + foreach($rawJobList as $rawJob) $jobList[$rawJob->id] = "[$rawJob->id] $rawJob->name"; + + $compileList = array(); + $rawCompileList = $this->compile->getListByJobID($MR->jobID); + foreach($rawCompileList as $rawCompile) $compileList[$rawCompile->id] = "[$rawCompile->id] [{$this->lang->compile->statusList[$rawCompile->status]}] $rawCompile->name"; + + $this->view->repoList = $repoList; + $this->view->jobList = !empty($MR->repoID) ? $jobList : array(); + $this->view->compileList = !empty($MR->jobID) ? $compileList : array(); + $this->view->title = $this->lang->mr->edit; $this->view->MR = $MR; $this->view->targetBranchList = $targetBranchList; @@ -286,6 +307,8 @@ class mr extends control } } + $this->view->repo = $this->loadModel('repo')->getRepoByID($MR->repoID); + $this->view->repoID = $MR->repoID; $this->view->title = $this->lang->mr->viewDiff; $this->view->diffs = $diffs; $this->view->encoding = $encoding; @@ -353,6 +376,49 @@ class mr extends control return $this->send($this->mr->reopen($MR)); } + /** + * Add a Bug for this review. + * + * @param int $repoID + * @param string $file + * @param int $v1 + * @param int $v2 + * @access public + * @return void + */ + public function addBug($repoID, $file, $v1, $v2) + { + $this->loadModel('repo'); + if($this->get->repoPath) $file = $this->get->repoPath; + if(!empty($_POST)) + { + $result = $this->mr->saveBug($repoID, $file, $v1, $v2); + if(dao::isError()) die(json_encode($result)); + + $bugID = $result['id']; + $repo = $this->repo->getRepoById($repoID); + $entry = $repo->name . '/' . $this->repo->decodePath($file); + $location = sprintf($this->lang->repo->reviewLocation, $entry, $repo->SCM != 'Subversion' ? substr($v2, 0, 10) : $v2, $this->post->begin, $this->post->end); + if(empty($v1)) + { + $revision = $repo->SCM != 'Subversion' ? substr($v2, 0, 10) : $v2; + $link = $this->repo->createLink('view', "repoID=$repoID&objectID=0&entry={$file}&revision=$v2&showBug=true") . '#L' . $this->post->begin; + } + else + { + $revision = $repo->SCM != 'Subversion' ? substr($v1, 0, 10) : $v1; + $revision .= ' : '; + $revision .= $repo->SCM != 'Subversion' ? substr($v2, 0, 10) : $v2; + $link = $this->repo->createLink('diff', "repoID=$repoID&objectID=0&entry={$file}&oldRevision=$v1&newRevision=$v2&showBug=true") . '#L' . $this->post->begin; + } + + $actionID = $this->loadModel('action')->create('bug', $bugID, 'repoCreated', '', html::a($link, $location)); + $this->loadModel('mail')->sendmail($bugID, $actionID); + + echo json_encode($result); + } + } + /** * AJAX: Get MR target projects. * @@ -441,7 +507,7 @@ class mr extends control if(!$compileList) return $this->send(array('message' => array())); $options = ""; - foreach($compileList as $compile) $options .= ""; + foreach($compileList as $compile) $options .= ""; $this->send($options); } } diff --git a/module/mr/css/common.css b/module/mr/css/common.css new file mode 100644 index 0000000000..f53aab533d --- /dev/null +++ b/module/mr/css/common.css @@ -0,0 +1,187 @@ +a {color: #169;} +a:hover, a:active {text-decoration:underline; color: #C61A1A;} +#swapper a {text-decoration: none;} +#swapper .col-footer a:hover {color: #0c64eb;} +h2,h3 {font-size: 20px; margin: 0; clear: both;} +h3 {font-size: 16px;} +.revision {font-size: 12px; line-height: 20px; text-align: right; padding-right: 8px;} +.directory {background-image:url('theme/default/images/repo/dir.png');} +.file {background-image:url('theme/default/images/repo/txt.png');} +//.icon {width: 17px; padding-left: 10px; padding-right: 2px;} +.mini-icon {display: inline-block; height: 16px; width: 16px; background-color: transparent; background-position: 0 0; background-repeat: no-repeat; vertical-align: text-bottom;} +.action {float: left;} +.input-group select#encrypt {border-left: 0px;} +.arrange {float: right;} +.versions {position: relative;} +#diffRepo {position: absolute; left: 20px; z-index: 1000;} +#repoID {display: inline-block; width: auto;} +.repoCode a:hover {text-decoration: none;} +.commentButton +{ + background-repeat: no-repeat; + position: absolute; + left: -28px; + width: 40px; + z-index: 10; + cursor:pointer; + font-size: 18px; + color: #4183C4; + display: none; +} +.repoCode tr.over .commentButton {display: block;} +.bug +{ + background-repeat: no-repeat; + position: absolute; + left: -7px; + width: 20px; + z-index: 0; + cursor:pointer; + font-size: 18px; + color: #4183C4; + line-height: 18px; +} +.repoCode .icon {opacity: 1;} +.icon-comment-add:before {content: '\e74c'; transform: scale(-1, 1); display: inline-block; font-weight: normal;} +.icon-comment-add:after {content: '+'; display: block; font-weight: normal; position: absolute; left: 16px; top: 1px; font-family: Arial; font-weight: bold; font-size: 12px;} +.icon-comments:before {content: '\e750'; transform: scale(-1, 1); display: inline-block; font-weight: normal; font-size: 18px; line-height: 18px;} +.commentButton:hover,.bug:hover {color: #d20b0b;} +.commentBoard {border-top: 1px solid #E4E4E4; border-bottom: 1px solid #E4E4E4; padding: 0; white-space: normal; background-color: #eee; padding: 10px;} +.commentBoard .table-form th {background: none;} +.lines input {width: 40px;} +.commentSubmit, .commentCancel {margin-right: 10px;} +.commentFoot .optional {float: left;} +.commentBoard.using .bugContainer {border: 1px solid #ddd; background: #fff;} +.commentBoard.using .bugContainer > .commentHeader {background: #f1f1f1; padding: 0 10px; border-bottom: 1px solid #ddd;} +.commentHeaderAuthor {max-width: 600px; line-height: 33px; font-weight: bold; color: #222; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;} +.comment {width: 100%; word-break: keep-all; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;} +.commentContent {margin: 10px;} +.commentHeaderRight {float: right;} + +/* Sider */ +#mainContent > #sidebar > .side-body.affix {top: 0px; z-index: 10000;} + +/* Pre row */ +.repoCode table > tbody > tr.over td {background: #f8eec7;} +.repoCode table > tbody > tr.over th {background: #cdcdcd; color: #333;} + +/* Comment-btn */ +.comment-btn {position: relative; margin: 0; padding: 0; display: none;} +.repoCode tr.over .comment-btn {display: block;} +.comment-btn .icon-wrapper {display: block; position: absolute; background: #4183C4; border-radius: 2px; width: 24px; height: 20px; left: -6px; top: 0; line-height: 20px; text-align: center; color: #fff; cursor: pointer; transition: transform 0.2s;} +.comment-btn .icon-wrapper:before {display: block; content: ' '; right: -4px; top: 6px; position: absolute; border-left: 4px solid #4183C4; border-right: 0 solid transparent; border-bottom: 4px solid transparent; border-top: 4px solid transparent; width: 0; height: 0;} +.comment-btn .icon-wrapper:hover {background: #169; transform: scale(1.1);} +.comment-btn .icon-wrapper:hover:before {border-left-color: #169;} + +.repoCode tr.commented {cursor: pointer;} + +.repoCode tr.commented .comment-btn {display: block;} +.repoCode tr.commented .comment-btn .icon-wrapper {background: none; border: none; width: 24px; line-height: 18px; height: 18px; left: -6px; color: #4183c4;} +.repoCode tr.commented .comment-btn .icon-wrapper:hover {border-color: #169; color: #169;} +.repoCode tr.commented .comment-btn .icon-wrapper > i:before {font-size: 18px; transform: scale(-1, 1); display: inline-block;} +.repoCode tr.commented .comment-btn .icon-wrapper:before {display: none;} + +.repoCode tr.over.commented .comment-btn .icon-wrapper, .repoCode tr.selected.commented .comment-btn .icon-wrapper {line-height: 20px; height: 20px; background: #4183C4; color: #fff; left: -6px;} +.repoCode tr.over.commented .comment-btn .icon-wrapper > i:before {font-size: 14px;} +.repoCode tr.selected.commented .comment-btn .icon-wrapper > i:before {font-size: 14px;} +.repoCode tr.over.commented .comment-btn .icon-wrapper:before, .repoCode tr.selected.commented .comment-btn .icon-wrapper:before {display: block;} + +/* repo action form */ +.repoCode .comment-list, .repoCode .comment-actions {max-width: 900px;} +.repoCode .bugFormContainer {border: 1px solid #bbb; margin: 0 0 0 15px; padding: 10px 20px 10px 10px; max-width: 880px; background: #fff;} +.repoCode .bugFormContainer th {width: 70px;} + +.repoCode .action-row {display: none;} +.repoCode .with-action-row .action-row {display: table-row;} +.repoCode .action-cell {background: #EEE; white-space: normal; padding: 10px 15px 10px 0;} +.repoCode .with-action-row table tr.selected .comment-btn:last-child {display: block;} +.repoCode .with-action-row table tr.selected td {background: #f8eec7;} +.repoCode .with-action-row table tr.selected th {background: #cdcdcd;} +.repoCode .with-action-row table tr.selected .comment-btn .icon-wrapper > i:before, .repoCode #diff.with-action-row tr.selected .comment-btn .icon-wrapper > i:before {content: '\d7';} + +.repoCode .comment-row {display: none;} +.repoCode .comment-row.show {display: table-row !important;} +.repoCode .comment-cell {background: #eee; white-space: normal;} +.repoCode .comment-cell .panel {margin: 10px; border-color: #bbb;} +.repoCode .comment-cell .panel-body {padding: 6px 10px;} +.repoCode .comment-cell .panel-actions.pull-right {margin-right: 0; margin-top: 0;} +.repoCode .comment-cell .editing .panel-body, .repoCode .comment-cell .commentContainer.show-form .panel-body {display: none;} +.repoCode .comment-cell .bug-edit-form, {padding: 10px; display: none;} +.repoCode .comment-cell .editing .bug-edit-form, .repoCode .comment-cell .commentContainer.show-form .comment-edit-form {display: block;} + +.repoCode .comment {border: 1px solid #e5e5e5; background: #fafafa; padding: 5px 10px; margin-bottom: 10px;} +.repoCode .comment .comment-edit-form {margin-top: 10px;} +.repoCode .panel-bug .steps {background: #f1f1f1; padding: 5px 10px;} +.repoCode .panel-bug .bug-edit-form {margin-bottom: 10px;} +.repoCode .panel-bug .panel-body {display: none;} +.repoCode .panel-bug .panel-heading {cursor: pointer;} +.repoCode .panel-bug.show .panel-body {display: block;} +.repoCode .panel-bug.show .icon-chevron-sign-down:before {content: '\e711';} +.repoCode .panel-bug.show-edit-form .bug-edit-form, +.repoCode .panel-bug.show-form .commentForm, +.repoCode .comment.show-form .comment-edit-form {display: block;} +.repoCode .panel-bug .bug-edit-form, +.repoCode .panel-bug.show-form .addComment, +.repoCode .panel-bug .commentForm, +.repoCode .comment .comment-edit-form, +.repoCode .panel-bug.show-edit-form .panel-body .title, +.repoCode .panel-bug.show-edit-form .bug-date, +.repoCode .comment.show-form .comment-content, .repoCode .comment.show-form .comment-date {display: none;} + +.repoCode .text-content {white-space: normal; white-space: pre-line;} +.repoCode .text-muted {color: #aaa;} + +.repoCode tr {transition: all 1s;} +.repoCode tr.highlight {background: #fff4e5;} +.repoCode tr.highlight td, .repoCode tr.highlight th {background: none; border-top: 1px solid #e48600; border-bottom: 1px solid #e48600;} +.repoCode tr.highlight.commented th {color: #e48600;} +.repoCode tr.highlight.commented td, .repoCode tr.highlight.commented th {border-bottom: none;} +.repoCode tr.highlight + tr.highlight td, .repoCode tr.highlight + tr.highlight th {border-top: none;} + +.repoCode .row-tip {display: none;} +.repoCode tr.commented .row-tip {display: block; position: relative; right: -3px; bottom: -1px;} +.repoCode tr.commented .tip, .repoCode tr.commented.open .tip.on-collapse {display: block; position: absolute; right: 0; bottom: 0; color: #4183c4; opacity: 0; padding: 0 5px; background: #edf3ff; height: 20px; line-height: 20px; transition: opacity 0.2s;} +.repoCode tr.commented:hover .tip.on-expand {opacity: 1;} +.repoCode tr.commented.open .tip.on-collapse {opacity: 1;} +.repoCode tr.commented.open .tip.on-expand {display: none;} +.repoCode tr.commented.open .tip.on-collapse span {display: none;} +.repoCode tr.commented.open:hover .tip.on-collapse span {display: inline;} +.repoCode tr.commented.open {background: #f8fafe;} +.repoCode tr.commented .preview-icon {position: absolute; left: -6px; bottom: 0; width: 20px; height: 20px; line-height: 20px; text-align: center; color: #4183c4; background: #edf3ff; display: none;} +.repoCode tr.commented:hover .preview-icon {display: block; transform: scale(-1, 1);} +.repoCode tr.commented .preview-icon:before {font-size: 18px;} + +.repoCode #diff tr.commented .row-tip {right: 0;} +.repoCode #diff tr.commented .icon-chat-dot {left: 0;} + +.repoCode .panel, .bugFormContainer {transition: border 0.4s;} +.repoCode .panel.highlight, #bugForm.highlight .bugFormContainer {border-color: #e48600;} + +#bugsPreview {white-space: normal;} +#bugsPreview .dropdown-menu {top: -100%; left: 30%; padding-top: 0; min-width: 300px; max-width: 500px;} +#bugsPreview .dropdown-menu > li.dropdown-header {background: #f1f1f1; padding-top: 8px;} +#bugsPreview .dropdown-menu > li > a {border-top: 1px solid #e5e5e5; text-overflow : ellipsis; overflow: hidden;} +#bugsPreview .dropdown-menu.show {display: block;} + +.icon-comments {position: relative; left: -50px;} + +/* bug form */ +#bugForm, #bugForm table {margin: 0; padding: 0;} + +.panel .table + .panel-footer {border-top: 0; background: #fff;} + +.transparent {border-color:transparent; background: none repeat scroll 0 0 transparent;} +.transparent:hover {border-color:transparent; background: none repeat scroll 0 0 transparent;} + +.side-col {width: 600px;} +#sidebar > .side-body {width: 580px;} +.hide-sidebar #sidebar > .side-body {display: none;} + +#sidebar>.sidebar-toggle {left: 5px; right: auto;} +#sidebar>.sidebar-toggle>.icon {right: -4px; left: auto;} + +#logForm .fixed-footer a.allLogs {color: #fff !important;} + +#submitLabel {text-align: left;} + +.header-btn .btn > .text {text-overflow: unset !important;} diff --git a/module/mr/js/diff.js b/module/mr/js/diff.js index e44d3a65cb..1267f00a1d 100644 --- a/module/mr/js/diff.js +++ b/module/mr/js/diff.js @@ -10,3 +10,499 @@ function changeEncoding(encoding) $('#encoding').val(encoding); $('#encoding').parents('form').submit(); } + +$(document).ready(function() +{ + var $diffCode = $('.diff'); + var hidePreview; + var $bugsPreview = $('#bugsPreview'); + var $bugsPreviewMenu = $('#bugsPreview').children('.dropdown-menu'); + var $rows = $diffCode.find('tr'); + var rowTip = $('#rowTip').html(); + var lastLine; + $rows.each(function() + { + var $row = $(this); + if(!$row.hasClass('empty')) + { + $row.children('th').first().prepend("
"); + $row.children('td').first().append(rowTip); + } + + if(lastLine && !$row.data('line')) + { + $row.attr('data-line', lastLine); + } + else + { + lastLine = $row.data('line'); + } + }).hover(function() + { + var $this = $(this); + if($this.hasClass('empty')) return; + $this.addClass("over"); + }, + function() + { + $(this).removeClass("over"); + }); + + var isInline = $.cookie('arrange') == 'inline'; + var $bugFormRow = $('' + (isInline ? '' : '') + ''); + var $bugForm = $('#bugForm'); + var $commentCell = $('#commentCell'); + var $bugPanel = $('#bugPanel'); +// $bugForm.find('input[name="begin"], input[name="end"]').attr('max', lastLine); + $bugFormRow.find('td').append($bugForm.removeClass('hide')); + + var highlight = function($e) + { + $('.highlight').removeClass('highlight'); + $e.addClass('highlight'); + }; + + var createComment = function(comment, $comments) + { + console.log(); + var $comment = $commentCell.clone() + .removeClass('hide') + .attr('id', 'comment-' + comment.id) + .attr('data-comment', comment.id); + $comment.find('.realname').text(comment.realname); + $comment.find('.comment-content').text(comment.comment); + $comment.find('.date').text(comment.date); + $comment.find('.edit').toggle(comment.edit); + $comment.find('.comment-edit-form').attr('action', createLink('repo', 'editComment', 'commentID=' + comment.id)); + + if($comments) + { + if(typeof $comments !== 'object') $comments = $('#bug-' + $comments + ' .comments'); + ($comments.hasClass('comments') ? $comments : $comments.find('.comments')).append($comment); + } + + return $comment; + }; + + var createBug = function(bug, line, $commentRow, show) + { + var commentCount, j; + var $bug = $bugPanel.clone().removeClass('hide').attr('id', 'bug-' + bug.id).attr('data-bug', bug.id); + $bug.find('.bugid').text(bug.id); + $bug.find('.realname').text(bug.realname); + $bug.find('.openedDate').text(bug.openedDate); + $bug.find('.title').text(bug.title); + $bug.find('.steps').toggle(bug.steps != '').html(bug.steps); + $bug.find('.edit').toggle(bug.edit); + $bug.find('.code-lines').text(bug.lines); + $bug.find('.delete').toggle(bug.delete); + $bug.find('input[name="objectID"]').val(bug.id); + $bug.find('.bug-edit-form').attr('action', createLink('repo', 'editBug', 'bugID=' + bug.id)); + $bug.find('a.view-bug').attr('href', createLink('bug', 'view', "bugID=" + bug.id)); + $bug.data('data', bug); + $bug.toggleClass('show', show > 1); + if(show > 2) highlight($bug); + + if(bug.comments) + { + commentCount = bug.comments.length; + $bugComments = $bug.find('.comments'); + for(j = 0; j < commentCount; j++) + { + createComment(bug.comments[j], $bugComments); + } + } + + if(!line && bug.line) line = bug.line; + if(line) + { + if(!$commentRow) + { + var $row = $rows.filter('[data-line="' + line + '"]').last(); + $commentRow = $row.next('tr'); + if(!$commentRow.hasClass('comment-row')) + { + $commentRow = $('' + (isInline ? '
' : '
') + ''); + $row.addClass('commented').after($commentRow); + } + } + ($commentRow.hasClass('comment-list') ? $commentRow : $commentRow.find('.comment-list')).append($bug); + + if(show && $commentRow.hasClass('comment-row')) $commentRow.addClass('show'); + } + + return $bug; + }; + + var toggleComment = function($row, show) + { + var $commentRow; + if($row.hasClass('comment-row')) + { + $commentRow = $row; + $row = $commentRow.prev('tr'); + if($row.hasClass('action-row')) + { + $row = $row.prev('tr'); + } + } + else + { + $commentRow = $row.next('tr'); + if($commentRow.hasClass('action-row')) + { + $commentRow = $commentRow.next('tr'); + } + } + if(show === undefined) + { + show = !$row.hasClass('open'); + } + if($row.hasClass('commented') && $commentRow.hasClass('comment-row')) + { + $commentRow.toggleClass('show', show); + $row.toggleClass('open', show); + } + }; + + $diffCode.on('click', '.comment-btn', function(e) + { + $rows.removeClass('selected'); + var $row = $(this).closest('tr'); + if($diffCode.hasClass('with-action-row') && $row.hasClass('with-action-row')) + { + $diffCode.removeClass('with-action-row'); + } + else + { + $diffCode.addClass('with-action-row'); + var line = $row.data('line'); + if(!$row.hasClass('with-action-row')) + { + $rows.removeClass('with-action-row') + $row.addClass('with-action-row'); + + $bugForm.find('input[name="begin"]').val(line); + $bugForm.find('input[name="end"]').attr('min', line).val(line); + $bugForm.find('select#assignedTo').val(blamePairs[line]); + $bugForm.find('select#assignedTo').trigger("chosen:updated"); + + $row.after($bugFormRow); + + KindEditor.remove('#commentText'); + $('#commentText').kindeditor(); + + var getCommiterLink = createLink('repo', 'ajaxgetcommitter', 'repoID=' + repoID + "&entry=" + file + "&revision=" + revision + "&line=" + line); + var connector = getCommiterLink.indexOf('&') >= 0 ? '&' : '?'; + getCommiterLink = getCommiterLink + connector + 'entry=' + file; + $.ajax({url: getCommiterLink}).done(function(responseText) + { + $bugForm.find('#assignedTo').val(responseText).trigger("chosen:updated"); + }); + } + highlight($bugForm); + $bugForm.find('input[name="title"]').focus(); + $row.addClass('selected'); + } + e.stopPropagation(); + }).on('click', '.bugCancel', function() + { + $rows.removeClass('selected'); + $diffCode.removeClass('with-action-row'); + }).on('click', '.bugEdit', function(e) + { + var $panelBug = $(this).closest('.panel-bug'); + + if($panelBug.hasClass('show-edit-form')) + { + $panelBug.removeClass('show-edit-form'); + e.stopPropagation(); + return; + } + + $panelBug.addClass('show show-edit-form').find('input[name="commentText"]').val($panelBug.find('.title').first().text()).focus(); + e.stopPropagation(); + return false; + }).on('submit', '.bug-edit-form', function() + { + var $form = $(this); + $(this).ajaxSubmit( + { + success:function(text) + { + var $bug = $form.closest('.panel-bug'); + $bug.find('.title').text(text); + $bug.removeClass('show-edit-form'); + }, + beforeSubmit:function(formData, jqForm) + { + var form = jqForm[0]; + if(!form.commentText.value) + { + alert(contentError); + return false; + } + } + }); + return false; + }).on('click', '.bugEditCancel', function() + { + $(this).closest('.panel-bug').removeClass('show-edit-form'); + }).on('click', '.bugDelete', function(e) + { + var $bug = $(this).closest('.panel-bug'); + if(!$bug.length) return; + + if(confirm(confirmDelete)) + { + var link = createLink('repo', 'deleteBug', 'bugID=' + $bug.data('bug') + '&confirm=yes'); + $.get(link, function(data) + { + if(data == 'deleted') + { + var $commentRow = $bug.closest('.comment-row'); + if($commentRow.find('.panel-bug').length === 1) + { + $commentRow.removeClass('show').prev('tr').removeClass('commented'); + } + $bug.remove(); + } + }); + } + e.stopPropagation(); + return false; + }).on('click', '.addComment', function() + { + $(this).closest('.panel-bug').addClass('show-form').find('.commentForm textarea').focus(); + }).on('click', '.commentCancel', function() + { + $(this).closest('.panel-bug').removeClass('show-form'); + }).on('submit', '.commentForm', function() + { + var $form = $(this); + $form.ajaxSubmit( + { + success:function(json) + { + var $panelBug = $form.closest('.panel-bug'); + $form.find('textarea').val(''); + $panelBug.removeClass('show-form'); + createComment($.parseJSON(json), $panelBug.data('bug')); + }, + beforeSubmit:function(formData, jqForm) + { + var form = jqForm[0]; + if(!form.comment.value) + { + alert(commentError); + return false; + } + } + }); + return false; + }).on('click', '.commentEdit', function() + { + var $comment = $(this).closest('.comment'); + + if($comment.hasClass('show-form')) + { + $comment.removeClass('show-form'); + return; + } + $comment.addClass('show-form').find('textarea').val($comment.find('.comment-content').text()).focus(); + }).on('click', '.commentEditCancel', function() + { + $(this).closest('.comment').removeClass('show-form'); + }).on('submit', '.comment-edit-form', function() + { + var $form = $(this); + $form.ajaxSubmit( + { + success:function(html) + { + var $comment = $form.closest('.comment'); + $comment.find('.comment-content').html(html); + $comment.removeClass('show-form'); + }, + beforeSubmit:function(formData, jqForm) + { + var form = jqForm[0]; + if(!form.commentText.value) + { + alert(contentError); + return false; + } + } + }); + return false; + }).on('click', '.commentDelete', function() + { + var $container = $(this).closest('.commentContainer'); + if(!$container.length) return; + + if(confirm(confirmDeleteComment)) + { + var commentID = $container.data('comment'); + var link = createLink('repo', 'deleteComment', 'commentID=' + commentID + '&confirm=yes'); + + $.get(link, function(data) + { + if(data == 'deleted') + { + var $commentRow = $container.closest('.comment-row'); + if($commentRow.find('.bugContainer, .commentContainer').length === 1) + { + $commentRow.removeClass('show').prev('tr').removeClass('commented'); + } + $container.remove(); + } + }); + } + return false; + }).on('click', 'tr.commented', function() + { + toggleComment($(this)); + }).on('click', '.panel-bug > .panel-heading', function() + { + $(this).closest('.panel-bug').toggleClass('show'); + }).on('mouseenter', 'tr.commented td .preview-icon', function(e) + { + var $cell = $(this).closest('td'); + var $row = $cell.closest('tr'); + var $commentRow = $row.next('tr'); + + var $bugs = $commentRow.find('.panel-bug'), line = '?'; + $bugsPreviewMenu.children('li:not(.dropdown-header)').remove(); + $bugsPreviewMenu.find('.bug-count').text($bugs.length); + $bugsPreviewMenu.find('.comment-count').text($commentRow.find('.comment').length); + $bugs.each(function() + { + var bug = $(this).data('data'); + line = bug.line; + $bugsPreviewMenu.append('
  • #' + bug.id + ' ' + bug.title + '
  • '); + }); + $bugsPreviewMenu.find('.code-line').text(line); + + $bugsPreview.prependTo($cell); + clearTimeout(hidePreview); + $bugsPreviewMenu.css({top: 0-$bugsPreviewMenu.outerHeight(), left: Math.max(0, e.offsetX-$bugsPreviewMenu.outerWidth())}).addClass('show'); + setTimeout(function(){$bugsPreviewMenu.addClass('in');}, 50); + }).on('mouseleave', 'tr.commented td', function() + { + $bugsPreviewMenu.removeClass('in'); + hidePreview = setTimeout(function(){$bugsPreviewMenu.removeClass('show');}, 200); + }); + + $bugsPreviewMenu.on('click', 'li', function(e) + { + var $bug = $($(this).find('a').data('id')); + if($bug.length) + { + $bug.addClass('show'); + toggleComment($bug.closest('tr.comment-row'), true); + highlight($bug); + + $bugsPreviewMenu.removeClass('in'); + hidePreview = setTimeout(function(){$bugsPreviewMenu.removeClass('show');}, 200); + } + e.stopPropagation(); + }); + + $bugForm.submit(function() + { + $(this).ajaxSubmit( + { + success:function(json) + { + json = $.parseJSON(json); + if(json.result == 'fail') + { + alert(json.message); + return false; + } + + createBug(json, null, null, 3); + $diffCode.removeClass('with-action-row'); + $diffCode.find('tr.with-action-row.selected').removeClass('selected'); + $bugForm.find('#title').val(''); + KindEditor.html('#commentText', ''); + }, + beforeSubmit:function(formData, jqForm) + { + var form = jqForm[0]; + if(!form.product.value) + { + alert(productError); + return false; + } + if(!form.title.value) + { + alert(titleError); + $bugForm.find('input[name="title"]').focus(); + return false; + } + } + }); + return false; + }).on('change', 'input[name="begin"]', function() + { + var begin = $(this).val(); + var $end = $bugForm.find('input[name="end"]').attr('min', begin); + if(parseInt($end.val()) < parseInt(begin)) + { + $end.val(begin); + } + }); + + if(bugs) + { + var lineBugs, bugsCount, i; + for(var line in bugs) + { + if(line) + { + lineBugs = bugs[line]; + bugsCount = lineBugs.length; + + for(i = 0; i < bugsCount; i++) + { + createBug(lineBugs[i], line); + } + } + } + } + + setTimeout(anchor, 200); + + $(document).on('click', function() + { + $('.highlight').removeClass('highlight'); + }); + + function anchor() + { + var hash = window.location.hash; + if(hash) + { + var line = hash.substr(1).replace('L', ''); + var $row = $('.diff tr[data-line="' + line +'"]').first(); + if($row.length) + { + var anchor = $row.offset().top; + + $('body,html').animate({scrollTop:anchor - 50}, 500); + + $row.addClass('highlight'); + if($row.hasClass('commented')) + { + toggleComment($row, true); + var $commentRow = $row.next('tr'); + if($commentRow.hasClass('comment-row')) + { + $commentRow.addClass('highlight'); + } + } + } + } + } +}); diff --git a/module/mr/model.php b/module/mr/model.php index 4b3c52b943..de525ca26a 100644 --- a/module/mr/model.php +++ b/module/mr/model.php @@ -173,10 +173,29 @@ class mrModel extends model */ public function update($MRID) { - $MR = fixer::input('post') - ->setDefault('editedBy', $this->app->user->account) - ->setDefault('editedDate', helper::now()) - ->get(); + if (!empty($_POST['compile'])) + { + $repoID = $this->post->repo; + $jobID = $this->post->job; + $compileID = $this->post->compile; + $compileStatus = $this->loadModel('compile')->getByID($this->post->compile)->status; + + $MR = fixer::input('post') + ->setDefault('editedBy', $this->app->user->account) + ->setDefault('editedDate', helper::now()) + ->add('repoID', $repoID) + ->add('jobID', $jobID) + ->add('compileID', $compileID) + ->add('compileStatus', $compileStatus) + ->get(); + } + else + { + $MR = fixer::input('post') + ->setDefault('editedBy', $this->app->user->account) + ->setDefault('editedDate', helper::now()) + ->get(); + } /* Update MR in GitLab. */ $newMR = new stdclass; @@ -195,7 +214,7 @@ class mrModel extends model $MR->assignee = zget($gitlabUsers, $MR->assignee, ''); /* Update MR in Zentao database. */ - $this->dao->update(TABLE_MR)->data($MR) + $this->dao->update(TABLE_MR)->data($MR, $this->config->mr->edit->skippedFields) ->where('id')->eq($MRID) ->batchCheck($this->config->mr->edit->requiredFields, 'notempty') ->autoCheck() @@ -736,4 +755,197 @@ class mrModel extends model if(isset($rawMR->state) and $rawMR->state == 'opened') return array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => helper::createLink('mr', 'view', "mr={$MR->id}")); return array('result' => 'fail', 'message' => $this->lang->fail, 'locate' => helper::createLink('mr', 'view', "mr={$MR->id}")); } + + + /** + * Get review. + * + * @param int $repoID + * @param string $entry + * @param string $revision + * @access public + * @return array + */ + public function getReview($repoID, $entry, $revision) + { + $reviews = array(); + $bugs = $this->dao->select('t1.*, t2.realname')->from(TABLE_BUG)->alias('t1') + ->leftJoin(TABLE_USER)->alias('t2') + ->on('t1.openedBy = t2.account') + ->where('t1.repo')->eq($repoID) + ->andWhere('t1.entry')->eq($entry) + ->andWhere('t1.v2')->eq($revision) + ->andWhere('t1.deleted')->eq(0) + ->fetchAll('id'); + $comments = $this->dao->select('t1.*, t2.realname')->from(TABLE_ACTION)->alias('t1') + ->leftJoin(TABLE_USER)->alias('t2') + ->on('t1.actor = t2.account') + ->where('t1.objectType')->eq('bug') + ->andWhere('t1.objectID')->in(array_keys($bugs)) + ->andWhere('t1.action')->eq('commented') + ->fetchGroup('objectID', 'id'); + foreach($bugs as $bug) + { + if(common::hasPriv('bug', 'edit')) $bug->edit = true; + if(common::hasPriv('bug', 'delete')) $bug->delete = true; + $lines = explode(',', trim($bug->lines, ',')); + $line = $lines[0]; + $reviews[$line]['bugs'][$bug->id] = $bug; + + if(isset($comments[$bug->id])) + { + foreach($comments[$bug->id] as $key => $comment) + { + if($comment->actor == $this->app->user->account) $comment->edit = true; + } + $reviews[$line]['comments'] = $comments; + } + } + + return $reviews; + } + + /** + * Get bugs by repo. + * + * @param int $repoID + * @param string $browseType + * @param string $orderBy + * @param object $pager + * @access public + * @return array + */ + public function getBugsByRepo($repoID, $browseType, $orderBy, $pager) + { + /* Get execution that user can access. */ + $executions = $this->loadModel('execution')->getPairs($this->session->project, 'all', 'empty|withdelete'); + + $bugs = $this->dao->select('*')->from(TABLE_BUG) + ->where('repo')->eq($repoID) + ->andWhere('deleted')->eq('0') + ->beginIF(!$this->app->user->admin)->andWhere('product')->in($this->app->user->view->products)->fi() + ->beginIF(!$this->app->user->admin)->andWhere('execution')->in(array_keys($executions))->fi() + ->beginIF($browseType == 'assigntome')->andWhere('assignedTo')->eq($this->app->user->account)->fi() + ->beginIF($browseType == 'openedbyme')->andWhere('openedBy')->eq($this->app->user->account)->fi() + ->beginIF($browseType == 'resolvedbyme')->andWhere('resolvedBy')->eq($this->app->user->account)->fi() + ->beginIF($browseType == 'assigntonull')->andWhere('assignedTo')->eq('')->fi() + ->beginIF($browseType == 'unresolved')->andWhere('resolvedBy')->eq('')->fi() + ->beginIF($browseType == 'unclosed')->andWhere('status')->ne('closed')->fi() + ->orderBy($orderBy) + ->page($pager) + ->fetchAll(); + return $bugs; + } + + /** + * Get execution pairs. + * + * @param int $product + * @param int $branch + * @access public + * @return array + */ + public function getExecutionPairs($product, $branch = 0) + { + $pairs = array(); + $executions = $this->loadModel('execution')->getList(0, 'all', 'undone', 0, $product, $branch); + foreach($executions as $execution) $pairs[$execution->id] = $execution->name; + return $pairs; + } + + /** + * Save bug. + * + * @param int $repoID + * @param string $file + * @param int $v1 + * @param int $v2 + * @access public + * @return array + */ + public function saveBug($repoID, $file, $v1, $v2) + { + $now = helper::now(); + $data = fixer::input('post') + ->add('severity', 3) + ->add('openedBy', $this->app->user->account) + ->add('openedDate', $now) + ->add('openedBuild', 'trunk') + ->add('assignedDate', $now) + ->add('type', 'codeimprovement') + ->add('repo', $repoID) + ->add('entry', $file) + ->add('lines', $this->post->begin . ',' . $this->post->end) + ->add('v1', $v1) + ->add('v2', $v2) + ->remove('commentText,begin,end,uid') + ->get(); + + $data->steps = $this->loadModel('file')->pasteImage($this->post->commentText, $this->post->uid); + $this->dao->insert(TABLE_BUG)->data($data)->exec(); + + if(!dao::isError()) + { + $bugID = $this->dao->lastInsertID(); + $this->file->updateObjectID($this->post->uid, $bugID, 'bug'); + setcookie("repoPairs[$repoID]", $data->product); + + return array('result' => 'success', 'id' => $bugID, 'realname' => $this->app->user->realname, 'openedDate' => substr($now, 5, 11), 'edit' => true, 'delete' => true, 'lines' => $data->lines, 'line' => $this->post->begin, 'steps' => $data->steps, 'title' => $data->title); + } + + return array('result' => 'fail', 'message' => join("\n", dao::getError())); + } + + /** + * Update bug. + * + * @param int $bugID + * @param string $title + * @access public + * @return string + */ + public function updateBug($bugID, $title) + { + $this->dao->update(TABLE_BUG)->set('title')->eq($title)->where('id')->eq($bugID)->exec(); + return $title; + } + + /** + * Update comment. + * + * @param int $commentID + * @param string $comment + * @access public + * @return string + */ + public function updateComment($commentID, $comment) + { + $this->dao->update(TABLE_ACTION)->set('comment')->eq($comment)->where('id')->eq($commentID)->exec(); + return $comment; + } + + /** + * Delete comment. + * + * @param int $commentID + * @access public + * @return void + */ + public function deleteComment($commentID) + { + return $this->dao->delete()->from(TABLE_ACTION)->where('id')->eq($commentID)->exec(); + } + + /** + * Get last review info. + * + * @param string $entry + * @access public + * @return object + */ + public function getLastReviewInfo($entry) + { + return $this->dao->select('*')->from(TABLE_BUG)->where('entry')->eq($entry)->orderby('id_desc')->fetch(); + } + } diff --git a/module/mr/view/diff.html.php b/module/mr/view/diff.html.php index 185a6170e7..406aff76e7 100644 --- a/module/mr/view/diff.html.php +++ b/module/mr/view/diff.html.php @@ -10,6 +10,8 @@ ?> + +