pull-requests: allow markup rendered description.

This commit is contained in:
Marcin Kuzminski 2018-06-25 12:01:35 +02:00
parent e32681449c
commit bafbc2b040
10 changed files with 819 additions and 449 deletions

View file

@ -42,6 +42,10 @@ def includeme(config):
name='goto_switcher_data',
pattern='/_goto_data')
config.add_route(
name='markup_preview',
pattern='/_markup_preview')
# register our static links via redirection mechanism
routing_links.connect_redirection_links(config)

View file

@ -27,7 +27,8 @@ from pyramid.view import view_config
from rhodecode.apps._base import BaseAppView
from rhodecode.lib import helpers as h
from rhodecode.lib.auth import (
LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator)
LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator,
CSRFRequired)
from rhodecode.lib.index import searcher_from_config
from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
from rhodecode.lib.ext_json import json
@ -425,3 +426,21 @@ class HomeView(BaseAppView):
c.repo_groups_data = json.dumps(repo_group_data)
return self._get_template_context(c)
@LoginRequired()
@CSRFRequired()
@view_config(
route_name='markup_preview', request_method='POST',
renderer='string', xhr=True)
def markup_preview(self):
# Technically a CSRF token is not needed as no state changes with this
# call. However, as this is a POST is better to have it, so automated
# tools don't flag it as potential CSRF.
# Post is required because the payload could be bigger than the maximum
# allowed by GET.
text = self.request.POST.get('text')
renderer = self.request.POST.get('renderer') or 'rst'
if text:
return h.render(text, renderer=renderer, mentions=True)
return ''

View file

@ -1418,9 +1418,6 @@ table.integrations {
margin-top: @textmargin;
margin-bottom: @textmargin;
}
.pr-description {
white-space:pre-wrap;
}
.pr-reviewer-rules {
padding: 10px 0px 20px 0px;
@ -2405,3 +2402,65 @@ input[type=radio] {
height: 16px;
width: 16px;
}
.markup-form .clearfix {
.border-radius(@border-radius);
margin: 0px;
}
.markup-form-area {
padding: 8px 12px;
border: 1px solid @grey4;
.border-radius(@border-radius);
}
.markup-form-area-header .nav-links {
display: flex;
flex-flow: row wrap;
-webkit-flex-flow: row wrap;
width: 100%;
}
.markup-form-area-footer {
display: flex;
}
.markup-form-area-footer .toolbar {
}
// markup Form
div.markup-form {
margin-top: 20px;
}
.markup-form strong {
display: block;
margin-bottom: 15px;
}
.markup-form textarea {
width: 100%;
height: 100px;
font-family: 'Monaco', 'Courier', 'Courier New', monospace;
}
form.markup-form {
margin-top: 10px;
margin-left: 10px;
}
.markup-form .comment-block-ta,
.markup-form .preview-box {
.border-radius(@border-radius);
.box-sizing(border-box);
background-color: white;
}
.markup-form .preview-box.unloaded {
height: 50px;
text-align: center;
padding: 20px;
background-color: white;
}

View file

@ -76,6 +76,7 @@ function registerRCRoutes() {
pyroutes.register('admin_settings_search', '/_admin/settings/search', []);
pyroutes.register('admin_settings_labs', '/_admin/settings/labs', []);
pyroutes.register('admin_settings_labs_update', '/_admin/settings/labs/update', []);
pyroutes.register('admin_settings_automation', '/_admin/_admin/settings/automation', []);
pyroutes.register('admin_permissions_application', '/_admin/permissions/application', []);
pyroutes.register('admin_permissions_application_update', '/_admin/permissions/application/update', []);
pyroutes.register('admin_permissions_global', '/_admin/permissions/global', []);
@ -141,6 +142,7 @@ function registerRCRoutes() {
pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
pyroutes.register('repo_list_data', '/_repos', []);
pyroutes.register('goto_switcher_data', '/_goto_data', []);
pyroutes.register('markup_preview', '/_markup_preview', []);
pyroutes.register('journal', '/_admin/journal', []);
pyroutes.register('journal_rss', '/_admin/journal/rss', []);
pyroutes.register('journal_atom', '/_admin/journal/atom', []);
@ -222,6 +224,7 @@ function registerRCRoutes() {
pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
pyroutes.register('edit_repo_advanced_hooks', '/%(repo_name)s/settings/advanced/hooks', ['repo_name']);
pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
pyroutes.register('edit_repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
@ -275,6 +278,7 @@ function registerRCRoutes() {
pyroutes.register('search', '/_admin/search', []);
pyroutes.register('search_repo', '/%(repo_name)s/search', ['repo_name']);
pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
pyroutes.register('user_group_profile', '/_profile_user_group/%(user_group_name)s', ['user_group_name']);
pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
pyroutes.register('my_account_update', '/_admin/my_account/update', []);

View file

@ -231,6 +231,68 @@ var initCodeMirror = function(textAreadId, resetUrl, focus, options) {
return myCodeMirror;
};
var initMarkupCodeMirror = function(textAreadId, focus, options) {
var initialHeight = 100;
var ta = $(textAreadId).get(0);
if (focus === undefined) {
focus = true;
}
// default options
var codeMirrorOptions = {
lineNumbers: false,
indentUnit: 4,
viewportMargin: 30,
// this is a trick to trigger some logic behind codemirror placeholder
// it influences styling and behaviour.
placeholder: " ",
lineWrapping: true,
autofocus: focus
};
if (options !== undefined) {
// extend with custom options
codeMirrorOptions = $.extend(true, codeMirrorOptions, options);
}
var cm = CodeMirror.fromTextArea(ta, codeMirrorOptions);
cm.setSize(null, initialHeight);
cm.setOption("mode", DEFAULT_RENDERER);
CodeMirror.autoLoadMode(cm, DEFAULT_RENDERER); // load rst or markdown mode
cmLog.debug('Loading codemirror mode', DEFAULT_RENDERER);
// start listening on changes to make auto-expanded editor
cm.on("change", function(instance, changeObj) {
var height = initialHeight;
var lines = instance.lineCount();
if ( lines > 6 && lines < 20) {
height = "auto";
}
else if (lines >= 20){
zheight = 20*15;
}
instance.setSize(null, height);
// detect if the change was trigger by auto desc, or user input
var changeOrigin = changeObj.origin;
if (changeOrigin === "setValue") {
cmLog.debug('Change triggered by setValue');
}
else {
cmLog.debug('user triggered change !');
// set special marker to indicate user has created an input.
instance._userDefinedValue = true;
}
});
return cm;
};
var initCommentBoxCodeMirror = function(CommentForm, textAreaId, triggerActions){
var initialHeight = 100;
@ -593,3 +655,196 @@ var CodeMirrorPreviewEnable = function(edit_mode) {
}
}
};
/* markup form */
(function(mod) {
if (typeof exports == "object" && typeof module == "object") {
// CommonJS
module.exports = mod();
}
else {
// Plain browser env
(this || window).MarkupForm = mod();
}
})(function() {
"use strict";
function MarkupForm(textareaId) {
if (!(this instanceof MarkupForm)) {
return new MarkupForm(textareaId);
}
// bind the element instance to our Form
$('#' + textareaId).get(0).MarkupForm = this;
this.withSelectorId = function(selector) {
var selectorId = textareaId;
return selector + '_' + selectorId;
};
this.previewButton = this.withSelectorId('#preview-btn');
this.previewContainer = this.withSelectorId('#preview-container');
this.previewBoxSelector = this.withSelectorId('#preview-box');
this.editButton = this.withSelectorId('#edit-btn');
this.editContainer = this.withSelectorId('#edit-container');
this.cmBox = textareaId;
this.cm = initMarkupCodeMirror('#' + textareaId);
this.previewUrl = pyroutes.url('markup_preview');
// FUNCTIONS and helpers
var self = this;
this.getCmInstance = function(){
return this.cm
};
this.setPlaceholder = function(placeholder) {
var cm = this.getCmInstance();
if (cm){
cm.setOption('placeholder', placeholder);
}
};
this.initStatusChangeSelector = function(){
var formatChangeStatus = function(state, escapeMarkup) {
var originalOption = state.element;
return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
'<span>' + escapeMarkup(state.text) + '</span>';
};
var formatResult = function(result, container, query, escapeMarkup) {
return formatChangeStatus(result, escapeMarkup);
};
var formatSelection = function(data, container, escapeMarkup) {
return formatChangeStatus(data, escapeMarkup);
};
$(this.submitForm).find(this.statusChange).select2({
placeholder: _gettext('Status Review'),
formatResult: formatResult,
formatSelection: formatSelection,
containerCssClass: "drop-menu status_box_menu",
dropdownCssClass: "drop-menu-dropdown",
dropdownAutoWidth: true,
minimumResultsForSearch: -1
});
$(this.submitForm).find(this.statusChange).on('change', function() {
var status = self.getCommentStatus();
if (status && !self.isInline()) {
$(self.submitButton).prop('disabled', false);
}
var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
self.setPlaceholder(placeholderText)
})
};
// reset the text area into it's original state
this.resetMarkupFormState = function(content) {
content = content || '';
$(this.editContainer).show();
$(this.editButton).parent().addClass('active');
$(this.previewContainer).hide();
$(this.previewButton).parent().removeClass('active');
this.setActionButtonsDisabled(true);
self.cm.setValue(content);
self.cm.setOption("readOnly", false);
};
this.previewSuccessCallback = function(o) {
$(self.previewBoxSelector).html(o);
$(self.previewBoxSelector).removeClass('unloaded');
// swap buttons, making preview active
$(self.previewButton).parent().addClass('active');
$(self.editButton).parent().removeClass('active');
// unlock buttons
self.setActionButtonsDisabled(false);
};
this.setActionButtonsDisabled = function(state) {
$(this.editButton).prop('disabled', state);
$(this.previewButton).prop('disabled', state);
};
// lock preview/edit/submit buttons on load, but exclude cancel button
var excludeCancelBtn = true;
this.setActionButtonsDisabled(true);
// anonymous users don't have access to initialized CM instance
if (this.cm !== undefined){
this.cm.on('change', function(cMirror) {
if (cMirror.getValue() === "") {
self.setActionButtonsDisabled(true)
} else {
self.setActionButtonsDisabled(false)
}
});
}
$(this.editButton).on('click', function(e) {
e.preventDefault();
$(self.previewButton).parent().removeClass('active');
$(self.previewContainer).hide();
$(self.editButton).parent().addClass('active');
$(self.editContainer).show();
});
$(this.previewButton).on('click', function(e) {
e.preventDefault();
var text = self.cm.getValue();
if (text === "") {
return;
}
var postData = {
'text': text,
'renderer': templateContext.visual.default_renderer,
'csrf_token': CSRF_TOKEN
};
// lock ALL buttons on preview
self.setActionButtonsDisabled(true);
$(self.previewBoxSelector).addClass('unloaded');
$(self.previewBoxSelector).html(_gettext('Loading ...'));
$(self.editContainer).hide();
$(self.previewContainer).show();
// by default we reset state of comment preserving the text
var previewFailCallback = function(data){
alert(
"Error while submitting preview.\n" +
"Error code {0} ({1}).".format(data.status, data.statusText)
);
self.resetMarkupFormState(text)
};
_submitAjaxPOST(
self.previewUrl, postData, self.previewSuccessCallback,
previewFailCallback);
$(self.previewButton).parent().addClass('active');
$(self.editButton).parent().removeClass('active');
});
}
return MarkupForm;
});

View file

@ -42,6 +42,29 @@ var bindToggleButtons = function() {
});
};
var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
failHandler = failHandler || function() {};
postData = toQueryString(postData);
var request = $.ajax({
url: url,
type: 'POST',
data: postData,
headers: {'X-PARTIAL-XHR': true}
})
.done(function (data) {
successHandler(data);
})
.fail(function (data, textStatus, errorThrown) {
failHandler(data, textStatus, errorThrown)
});
return request;
};
/* Comment form for main and inline comments */
(function(mod) {
@ -259,24 +282,7 @@ var bindToggleButtons = function() {
};
this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
failHandler = failHandler || function() {};
var postData = toQueryString(postData);
var request = $.ajax({
url: url,
type: 'POST',
data: postData,
headers: {'X-PARTIAL-XHR': true}
})
.done(function(data) {
successHandler(data);
})
.fail(function(data, textStatus, errorThrown){
alert(
"Error while submitting comment.\n" +
"Error code {0} ({1}).".format(data.status, data.statusText));
failHandler()
});
return request;
return _submitAjaxPOST(url, postData, successHandler, failHandler);
};
// overwrite a submitHandler, we need to do it for inline comments
@ -340,7 +346,11 @@ var bindToggleButtons = function() {
self.globalSubmitSuccessCallback();
};
var submitFailCallback = function(){
var submitFailCallback = function(data) {
alert(
"Error while submitting comment.\n" +
"Error code {0} ({1}).".format(data.status, data.statusText)
);
self.resetCommentFormState(text);
};
self.submitAjaxPOST(
@ -436,7 +446,11 @@ var bindToggleButtons = function() {
$(self.previewContainer).show();
// by default we reset state of comment preserving the text
var previewFailCallback = function(){
var previewFailCallback = function(data){
alert(
"Error while preview of comment.\n" +
"Error code {0} ({1}).".format(data.status, data.statusText)
);
self.resetCommentFormState(text)
};
self.submitAjaxPOST(
@ -763,7 +777,11 @@ var CommentsController = function() {
commentForm.setActionButtonsDisabled(false);
};
var submitFailCallback = function(){
var submitFailCallback = function(data){
alert(
"Error while submitting comment.\n" +
"Error code {0} ({1}).".format(data.status, data.statusText)
);
commentForm.resetCommentFormState(text)
};
commentForm.submitAjaxPOST(

View file

@ -378,49 +378,6 @@ var editPullRequest = function(repo_name, pull_request_id, title, description) {
ajaxPOST(url, postData, success);
};
var initPullRequestsCodeMirror = function (textAreaId) {
var ta = $(textAreaId).get(0);
var initialHeight = '100px';
// default options
var codeMirrorOptions = {
mode: "text",
lineNumbers: false,
indentUnit: 4,
theme: 'rc-input'
};
var codeMirrorInstance = CodeMirror.fromTextArea(ta, codeMirrorOptions);
// marker for manually set description
codeMirrorInstance._userDefinedDesc = false;
codeMirrorInstance.setSize(null, initialHeight);
codeMirrorInstance.on("change", function(instance, changeObj) {
var height = initialHeight;
var lines = instance.lineCount();
if (lines > 6 && lines < 20) {
height = "auto"
}
else if (lines >= 20) {
height = 20 * 15;
}
instance.setSize(null, height);
// detect if the change was trigger by auto desc, or user input
changeOrigin = changeObj.origin;
if (changeOrigin === "setValue") {
cmLog.debug('Change triggered by setValue');
}
else {
cmLog.debug('user triggered change !');
// set special marker to indicate user has created an input.
instance._userDefinedDesc = true;
}
});
return codeMirrorInstance
};
/**
* Reviewer autocomplete

View file

@ -350,8 +350,7 @@
</%def>
<%def name="pullrequest_title(title, description)">
${title} <br/>
${h.shorter(description, 40)}
${title}
</%def>
<%def name="pullrequest_comments(comments_nr)">
@ -375,3 +374,52 @@
<%def name="pullrequest_author(full_contact)">
${base.gravatar_with_user(full_contact, 16)}
</%def>
<%def name="markup_form(form_id, form_text='', help_text=None)">
<div class="markup-form">
<div class="markup-form-area">
<div class="markup-form-area-header">
<ul class="nav-links clearfix">
<li class="active">
<a href="#edit-text" tabindex="-1" id="edit-btn_${form_id}">${_('Write')}</a>
</li>
<li class="">
<a href="#preview-text" tabindex="-1" id="preview-btn_${form_id}">${_('Preview')}</a>
</li>
</ul>
</div>
<div class="markup-form-area-write" style="display: block;">
<div id="edit-container_${form_id}">
<textarea id="${form_id}" name="${form_id}" class="comment-block-ta ac-input">${form_text if form_text else ''}</textarea>
</div>
<div id="preview-container_${form_id}" class="clearfix" style="display: none;">
<div id="preview-box_${form_id}" class="preview-box"></div>
</div>
</div>
<div class="markup-form-area-footer">
<div class="toolbar">
<div class="toolbar-text">
${(_('Parsed using %s syntax') % (
('<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
)
)|n}
</div>
</div>
</div>
</div>
<div class="markup-form-footer">
% if help_text:
<span class="help-block">${help_text}</span>
% endif
</div>
</div>
<script type="text/javascript">
new MarkupForm('${form_id}');
</script>
</%def>

View file

@ -1,4 +1,5 @@
<%inherit file="/base/base.mako"/>
<%namespace name="dt" file="/data_table/_dt_elements.mako"/>
<%def name="title()">
${c.repo_name} ${_('New pull request')}
@ -54,14 +55,13 @@
<label for="pullrequest_desc">${_('Description')}:</label>
</div>
<div class="textarea text-area editor">
${h.textarea('pullrequest_desc',size=30, )}
<span class="help-block">${_('Write a short description on this pull request')}</span>
${dt.markup_form('pullrequest_desc')}
</div>
</div>
<div class="field">
<div class="label label-textarea">
<label for="pullrequest_desc">${_('Commit flow')}:</label>
<label for="commit_flow">${_('Commit flow')}:</label>
</div>
## TODO: johbo: Abusing the "content" class here to get the
@ -164,379 +164,384 @@
</div>
<script type="text/javascript">
$(function(){
var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
var $pullRequestForm = $('#pull_request_form');
var $pullRequestSubmit = $('#pr_submit', $pullRequestForm);
var $sourceRepo = $('#source_repo', $pullRequestForm);
var $targetRepo = $('#target_repo', $pullRequestForm);
var $sourceRef = $('#source_ref', $pullRequestForm);
var $targetRef = $('#target_ref', $pullRequestForm);
var sourceRepo = function() { return $sourceRepo.eq(0).val() };
var sourceRef = function() { return $sourceRef.eq(0).val().split(':') };
var targetRepo = function() { return $targetRepo.eq(0).val() };
var targetRef = function() { return $targetRef.eq(0).val().split(':') };
var calculateContainerWidth = function() {
var maxWidth = 0;
var repoSelect2Containers = ['#source_repo', '#target_repo'];
$.each(repoSelect2Containers, function(idx, value) {
$(value).select2('container').width('auto');
var curWidth = $(value).select2('container').width();
if (maxWidth <= curWidth) {
maxWidth = curWidth;
}
$.each(repoSelect2Containers, function(idx, value) {
$(value).select2('container').width(maxWidth + 10);
});
});
};
var initRefSelection = function(selectedRef) {
return function(element, callback) {
// translate our select2 id into a text, it's a mapping to show
// simple label when selecting by internal ID.
var id, refData;
if (selectedRef === undefined || selectedRef === null) {
id = element.val();
refData = element.val().split(':');
if (refData.length !== 3){
refData = ["", "", ""]
}
} else {
id = selectedRef;
refData = selectedRef.split(':');
}
var text = refData[1];
if (refData[0] === 'rev') {
text = text.substring(0, 12);
}
var data = {id: id, text: text};
callback(data);
};
};
var formatRefSelection = function(item) {
var prefix = '';
var refData = item.id.split(':');
if (refData[0] === 'branch') {
prefix = '<i class="icon-branch"></i>';
}
else if (refData[0] === 'book') {
prefix = '<i class="icon-bookmark"></i>';
}
else if (refData[0] === 'tag') {
prefix = '<i class="icon-tag"></i>';
}
var originalOption = item.element;
return prefix + item.text;
};
// custom code mirror
var codeMirrorInstance = initPullRequestsCodeMirror('#pullrequest_desc');
reviewersController = new ReviewersController();
var queryTargetRepo = function(self, query) {
// cache ALL results if query is empty
var cacheKey = query.term || '__';
var cachedData = self.cachedDataSource[cacheKey];
if (cachedData) {
query.callback({results: cachedData.results});
} else {
$.ajax({
url: pyroutes.url('pullrequest_repo_destinations', {'repo_name': templateContext.repo_name}),
data: {query: query.term},
dataType: 'json',
type: 'GET',
success: function(data) {
self.cachedDataSource[cacheKey] = data;
query.callback({results: data.results});
},
error: function(data, textStatus, errorThrown) {
alert(
"Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
}
});
}
};
var queryTargetRefs = function(initialData, query) {
var data = {results: []};
// filter initialData
$.each(initialData, function() {
var section = this.text;
var children = [];
$.each(this.children, function() {
if (query.term.length === 0 ||
this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
children.push({'id': this.id, 'text': this.text})
}
});
data.results.push({'text': section, 'children': children})
});
query.callback({results: data.results});
};
var loadRepoRefDiffPreview = function() {
var url_data = {
'repo_name': targetRepo(),
'target_repo': sourceRepo(),
'source_ref': targetRef()[2],
'source_ref_type': 'rev',
'target_ref': sourceRef()[2],
'target_ref_type': 'rev',
'merge': true,
'_': Date.now() // bypass browser caching
}; // gather the source/target ref and repo here
if (sourceRef().length !== 3 || targetRef().length !== 3) {
prButtonLock(true, "${_('Please select source and target')}");
return;
}
var url = pyroutes.url('repo_compare', url_data);
// lock PR button, so we cannot send PR before it's calculated
prButtonLock(true, "${_('Loading compare ...')}", 'compare');
if (loadRepoRefDiffPreview._currentRequest) {
loadRepoRefDiffPreview._currentRequest.abort();
}
loadRepoRefDiffPreview._currentRequest = $.get(url)
.error(function(data, textStatus, errorThrown) {
if (textStatus !== 'abort') {
alert(
"Error while processing request.\nError code {0} ({1}).".format(
data.status, data.statusText));
}
})
.done(function(data) {
loadRepoRefDiffPreview._currentRequest = null;
$('#pull_request_overview').html(data);
var commitElements = $(data).find('tr[commit_id]');
var prTitleAndDesc = getTitleAndDescription(
sourceRef()[1], commitElements, 5);
var title = prTitleAndDesc[0];
var proposedDescription = prTitleAndDesc[1];
var useGeneratedTitle = (
$('#pullrequest_title').hasClass('autogenerated-title') ||
$('#pullrequest_title').val() === "");
if (title && useGeneratedTitle) {
// use generated title if we haven't specified our own
$('#pullrequest_title').val(title);
$('#pullrequest_title').addClass('autogenerated-title');
}
var useGeneratedDescription = (
!codeMirrorInstance._userDefinedDesc ||
codeMirrorInstance.getValue() === "");
if (proposedDescription && useGeneratedDescription) {
// set proposed content, if we haven't defined our own,
// or we don't have description written
codeMirrorInstance._userDefinedDesc = false; // reset state
codeMirrorInstance.setValue(proposedDescription);
}
var msg = '';
if (commitElements.length === 1) {
msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
} else {
msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}";
}
msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
if (commitElements.length) {
var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
}
else {
prButtonLock(true, "${_('There are no commits to merge.')}", 'compare');
}
});
};
var Select2Box = function(element, overrides) {
var globalDefaults = {
dropdownAutoWidth: true,
containerCssClass: "drop-menu",
dropdownCssClass: "drop-menu-dropdown"
};
var initSelect2 = function(defaultOptions) {
var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
element.select2(options);
};
return {
initRef: function() {
var defaultOptions = {
minimumResultsForSearch: 5,
formatSelection: formatRefSelection
};
initSelect2(defaultOptions);
},
initRepo: function(defaultValue, readOnly) {
var defaultOptions = {
initSelection : function (element, callback) {
var data = {id: defaultValue, text: defaultValue};
callback(data);
}
};
initSelect2(defaultOptions);
element.select2('val', defaultSourceRepo);
if (readOnly === true) {
element.select2('readonly', true);
}
}
};
};
var initTargetRefs = function(refsData, selectedRef) {
Select2Box($targetRef, {
placeholder: "${_('Select commit reference')}",
query: function(query) {
queryTargetRefs(refsData, query);
},
initSelection : initRefSelection(selectedRef)
}).initRef();
if (!(selectedRef === undefined)) {
$targetRef.select2('val', selectedRef);
}
};
var targetRepoChanged = function(repoData) {
// generate new DESC of target repo displayed next to select
var prLink = pyroutes.url('pullrequest_new', {'repo_name': repoData['name']});
$('#target_repo_desc').html(
"<strong>${_('Target repository')}</strong>: {0}. <a href=\"{1}\">Switch base, and use as source.</a>".format(repoData['description'], prLink)
);
// generate dynamic select2 for refs.
initTargetRefs(repoData['refs']['select2_refs'],
repoData['refs']['selected_ref']);
};
var sourceRefSelect2 = Select2Box($sourceRef, {
placeholder: "${_('Select commit reference')}",
query: function(query) {
var initialData = defaultSourceRepoData['refs']['select2_refs'];
queryTargetRefs(initialData, query)
},
initSelection: initRefSelection()
}
);
var sourceRepoSelect2 = Select2Box($sourceRepo, {
query: function(query) {}
});
var targetRepoSelect2 = Select2Box($targetRepo, {
cachedDataSource: {},
query: $.debounce(250, function(query) {
queryTargetRepo(this, query);
}),
formatResult: formatRepoResult
});
sourceRefSelect2.initRef();
sourceRepoSelect2.initRepo(defaultSourceRepo, true);
targetRepoSelect2.initRepo(defaultTargetRepo, false);
$sourceRef.on('change', function(e){
loadRepoRefDiffPreview();
reviewersController.loadDefaultReviewers(
sourceRepo(), sourceRef(), targetRepo(), targetRef());
});
$targetRef.on('change', function(e){
loadRepoRefDiffPreview();
reviewersController.loadDefaultReviewers(
sourceRepo(), sourceRef(), targetRepo(), targetRef());
});
$targetRepo.on('change', function(e){
var repoName = $(this).val();
calculateContainerWidth();
$targetRef.select2('destroy');
$('#target_ref_loading').show();
$.ajax({
url: pyroutes.url('pullrequest_repo_refs',
{'repo_name': templateContext.repo_name, 'target_repo_name':repoName}),
data: {},
dataType: 'json',
type: 'GET',
success: function(data) {
$('#target_ref_loading').hide();
targetRepoChanged(data);
loadRepoRefDiffPreview();
},
error: function(data, textStatus, errorThrown) {
alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
}
})
});
$pullRequestForm.on('submit', function(e){
prButtonLock(true, null, 'all');
});
prButtonLock(true, "${_('Please select source and target')}", 'all');
// auto-load on init, the target refs select2
calculateContainerWidth();
targetRepoChanged(defaultTargetRepoData);
$('#pullrequest_title').on('keyup', function(e){
$(this).removeClass('autogenerated-title');
});
% if c.default_source_ref:
// in case we have a pre-selected value, use it now
$sourceRef.select2('val', '${c.default_source_ref}');
// diff preview load
loadRepoRefDiffPreview();
// default reviewers
reviewersController.loadDefaultReviewers(
sourceRepo(), sourceRef(), targetRepo(), targetRef());
% endif
ReviewerAutoComplete('#user');
});
$(function(){
var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
var $pullRequestForm = $('#pull_request_form');
var $pullRequestSubmit = $('#pr_submit', $pullRequestForm);
var $sourceRepo = $('#source_repo', $pullRequestForm);
var $targetRepo = $('#target_repo', $pullRequestForm);
var $sourceRef = $('#source_ref', $pullRequestForm);
var $targetRef = $('#target_ref', $pullRequestForm);
var sourceRepo = function() { return $sourceRepo.eq(0).val() };
var sourceRef = function() { return $sourceRef.eq(0).val().split(':') };
var targetRepo = function() { return $targetRepo.eq(0).val() };
var targetRef = function() { return $targetRef.eq(0).val().split(':') };
var calculateContainerWidth = function() {
var maxWidth = 0;
var repoSelect2Containers = ['#source_repo', '#target_repo'];
$.each(repoSelect2Containers, function(idx, value) {
$(value).select2('container').width('auto');
var curWidth = $(value).select2('container').width();
if (maxWidth <= curWidth) {
maxWidth = curWidth;
}
$.each(repoSelect2Containers, function(idx, value) {
$(value).select2('container').width(maxWidth + 10);
});
});
};
var initRefSelection = function(selectedRef) {
return function(element, callback) {
// translate our select2 id into a text, it's a mapping to show
// simple label when selecting by internal ID.
var id, refData;
if (selectedRef === undefined || selectedRef === null) {
id = element.val();
refData = element.val().split(':');
if (refData.length !== 3){
refData = ["", "", ""]
}
} else {
id = selectedRef;
refData = selectedRef.split(':');
}
var text = refData[1];
if (refData[0] === 'rev') {
text = text.substring(0, 12);
}
var data = {id: id, text: text};
callback(data);
};
};
var formatRefSelection = function(item) {
var prefix = '';
var refData = item.id.split(':');
if (refData[0] === 'branch') {
prefix = '<i class="icon-branch"></i>';
}
else if (refData[0] === 'book') {
prefix = '<i class="icon-bookmark"></i>';
}
else if (refData[0] === 'tag') {
prefix = '<i class="icon-tag"></i>';
}
var originalOption = item.element;
return prefix + item.text;
};
// custom code mirror
var codeMirrorInstance = $('#pullrequest_desc').get(0).MarkupForm.cm;
reviewersController = new ReviewersController();
var queryTargetRepo = function(self, query) {
// cache ALL results if query is empty
var cacheKey = query.term || '__';
var cachedData = self.cachedDataSource[cacheKey];
if (cachedData) {
query.callback({results: cachedData.results});
} else {
$.ajax({
url: pyroutes.url('pullrequest_repo_destinations', {'repo_name': templateContext.repo_name}),
data: {query: query.term},
dataType: 'json',
type: 'GET',
success: function(data) {
self.cachedDataSource[cacheKey] = data;
query.callback({results: data.results});
},
error: function(data, textStatus, errorThrown) {
alert(
"Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
}
});
}
};
var queryTargetRefs = function(initialData, query) {
var data = {results: []};
// filter initialData
$.each(initialData, function() {
var section = this.text;
var children = [];
$.each(this.children, function() {
if (query.term.length === 0 ||
this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
children.push({'id': this.id, 'text': this.text})
}
});
data.results.push({'text': section, 'children': children})
});
query.callback({results: data.results});
};
var loadRepoRefDiffPreview = function() {
var url_data = {
'repo_name': targetRepo(),
'target_repo': sourceRepo(),
'source_ref': targetRef()[2],
'source_ref_type': 'rev',
'target_ref': sourceRef()[2],
'target_ref_type': 'rev',
'merge': true,
'_': Date.now() // bypass browser caching
}; // gather the source/target ref and repo here
if (sourceRef().length !== 3 || targetRef().length !== 3) {
prButtonLock(true, "${_('Please select source and target')}");
return;
}
var url = pyroutes.url('repo_compare', url_data);
// lock PR button, so we cannot send PR before it's calculated
prButtonLock(true, "${_('Loading compare ...')}", 'compare');
if (loadRepoRefDiffPreview._currentRequest) {
loadRepoRefDiffPreview._currentRequest.abort();
}
loadRepoRefDiffPreview._currentRequest = $.get(url)
.error(function(data, textStatus, errorThrown) {
if (textStatus !== 'abort') {
alert(
"Error while processing request.\nError code {0} ({1}).".format(
data.status, data.statusText));
}
})
.done(function(data) {
loadRepoRefDiffPreview._currentRequest = null;
$('#pull_request_overview').html(data);
var commitElements = $(data).find('tr[commit_id]');
var prTitleAndDesc = getTitleAndDescription(
sourceRef()[1], commitElements, 5);
var title = prTitleAndDesc[0];
var proposedDescription = prTitleAndDesc[1];
var useGeneratedTitle = (
$('#pullrequest_title').hasClass('autogenerated-title') ||
$('#pullrequest_title').val() === "");
if (title && useGeneratedTitle) {
// use generated title if we haven't specified our own
$('#pullrequest_title').val(title);
$('#pullrequest_title').addClass('autogenerated-title');
}
var useGeneratedDescription = (
!codeMirrorInstance._userDefinedValue ||
codeMirrorInstance.getValue() === "");
if (proposedDescription && useGeneratedDescription) {
// set proposed content, if we haven't defined our own,
// or we don't have description written
codeMirrorInstance._userDefinedValue = false; // reset state
codeMirrorInstance.setValue(proposedDescription);
}
// refresh our codeMirror so events kicks in and it's change aware
codeMirrorInstance.refresh();
var msg = '';
if (commitElements.length === 1) {
msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
} else {
msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}";
}
msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
if (commitElements.length) {
var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
}
else {
prButtonLock(true, "${_('There are no commits to merge.')}", 'compare');
}
});
};
var Select2Box = function(element, overrides) {
var globalDefaults = {
dropdownAutoWidth: true,
containerCssClass: "drop-menu",
dropdownCssClass: "drop-menu-dropdown"
};
var initSelect2 = function(defaultOptions) {
var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
element.select2(options);
};
return {
initRef: function() {
var defaultOptions = {
minimumResultsForSearch: 5,
formatSelection: formatRefSelection
};
initSelect2(defaultOptions);
},
initRepo: function(defaultValue, readOnly) {
var defaultOptions = {
initSelection : function (element, callback) {
var data = {id: defaultValue, text: defaultValue};
callback(data);
}
};
initSelect2(defaultOptions);
element.select2('val', defaultSourceRepo);
if (readOnly === true) {
element.select2('readonly', true);
}
}
};
};
var initTargetRefs = function(refsData, selectedRef) {
Select2Box($targetRef, {
placeholder: "${_('Select commit reference')}",
query: function(query) {
queryTargetRefs(refsData, query);
},
initSelection : initRefSelection(selectedRef)
}).initRef();
if (!(selectedRef === undefined)) {
$targetRef.select2('val', selectedRef);
}
};
var targetRepoChanged = function(repoData) {
// generate new DESC of target repo displayed next to select
var prLink = pyroutes.url('pullrequest_new', {'repo_name': repoData['name']});
$('#target_repo_desc').html(
"<strong>${_('Target repository')}</strong>: {0}. <a href=\"{1}\">Switch base, and use as source.</a>".format(repoData['description'], prLink)
);
// generate dynamic select2 for refs.
initTargetRefs(repoData['refs']['select2_refs'],
repoData['refs']['selected_ref']);
};
var sourceRefSelect2 = Select2Box($sourceRef, {
placeholder: "${_('Select commit reference')}",
query: function(query) {
var initialData = defaultSourceRepoData['refs']['select2_refs'];
queryTargetRefs(initialData, query)
},
initSelection: initRefSelection()
}
);
var sourceRepoSelect2 = Select2Box($sourceRepo, {
query: function(query) {}
});
var targetRepoSelect2 = Select2Box($targetRepo, {
cachedDataSource: {},
query: $.debounce(250, function(query) {
queryTargetRepo(this, query);
}),
formatResult: formatRepoResult
});
sourceRefSelect2.initRef();
sourceRepoSelect2.initRepo(defaultSourceRepo, true);
targetRepoSelect2.initRepo(defaultTargetRepo, false);
$sourceRef.on('change', function(e){
loadRepoRefDiffPreview();
reviewersController.loadDefaultReviewers(
sourceRepo(), sourceRef(), targetRepo(), targetRef());
});
$targetRef.on('change', function(e){
loadRepoRefDiffPreview();
reviewersController.loadDefaultReviewers(
sourceRepo(), sourceRef(), targetRepo(), targetRef());
});
$targetRepo.on('change', function(e){
var repoName = $(this).val();
calculateContainerWidth();
$targetRef.select2('destroy');
$('#target_ref_loading').show();
$.ajax({
url: pyroutes.url('pullrequest_repo_refs',
{'repo_name': templateContext.repo_name, 'target_repo_name':repoName}),
data: {},
dataType: 'json',
type: 'GET',
success: function(data) {
$('#target_ref_loading').hide();
targetRepoChanged(data);
loadRepoRefDiffPreview();
},
error: function(data, textStatus, errorThrown) {
alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
}
})
});
$pullRequestForm.on('submit', function(e){
// Flush changes into textarea
codeMirrorInstance.save();
prButtonLock(true, null, 'all');
});
prButtonLock(true, "${_('Please select source and target')}", 'all');
// auto-load on init, the target refs select2
calculateContainerWidth();
targetRepoChanged(defaultTargetRepoData);
$('#pullrequest_title').on('keyup', function(e){
$(this).removeClass('autogenerated-title');
});
% if c.default_source_ref:
// in case we have a pre-selected value, use it now
$sourceRef.select2('val', '${c.default_source_ref}');
// diff preview load
loadRepoRefDiffPreview();
// default reviewers
reviewersController.loadDefaultReviewers(
sourceRepo(), sourceRef(), targetRepo(), targetRef());
% endif
ReviewerAutoComplete('#user');
});
</script>
</%def>

View file

@ -1,5 +1,6 @@
<%inherit file="/base/base.mako"/>
<%namespace name="base" file="/base/base.mako"/>
<%namespace name="dt" file="/data_table/_dt_elements.mako"/>
<%def name="title()">
${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
@ -169,10 +170,10 @@
<label>${_('Description')}:</label>
</div>
<div id="pr-desc" class="input">
<div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
<div class="pr-description">${h.render(c.pull_request.description, renderer=c.visual.default_renderer)}</div>
</div>
<div id="pr-desc-edit" class="input textarea editor" style="display: none;">
<textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
${dt.markup_form('pr-description-input', form_text=c.pull_request.description)}
</div>
</div>
@ -643,7 +644,7 @@
$(function(){
// custom code mirror
var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
var PRDetails = {
editButton: $('#open_edit_pullrequest'),