diff --git a/rhodecode/apps/home/__init__.py b/rhodecode/apps/home/__init__.py --- a/rhodecode/apps/home/__init__.py +++ b/rhodecode/apps/home/__init__.py @@ -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) diff --git a/rhodecode/apps/home/views.py b/rhodecode/apps/home/views.py --- a/rhodecode/apps/home/views.py +++ b/rhodecode/apps/home/views.py @@ -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 '' diff --git a/rhodecode/public/css/main.less b/rhodecode/public/css/main.less --- a/rhodecode/public/css/main.less +++ b/rhodecode/public/css/main.less @@ -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; +} diff --git a/rhodecode/public/js/rhodecode/routes.js b/rhodecode/public/js/rhodecode/routes.js --- a/rhodecode/public/js/rhodecode/routes.js +++ b/rhodecode/public/js/rhodecode/routes.js @@ -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', []); diff --git a/rhodecode/public/js/src/rhodecode/codemirror.js b/rhodecode/public/js/src/rhodecode/codemirror.js --- a/rhodecode/public/js/src/rhodecode/codemirror.js +++ b/rhodecode/public/js/src/rhodecode/codemirror.js @@ -231,6 +231,68 @@ var initCodeMirror = function(textAreadI 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(e } } }; + + + /* 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 '
' + + '' + escapeMarkup(state.text) + ''; + }; + 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; +}); diff --git a/rhodecode/public/js/src/rhodecode/comments.js b/rhodecode/public/js/src/rhodecode/comments.js --- a/rhodecode/public/js/src/rhodecode/comments.js +++ b/rhodecode/public/js/src/rhodecode/comments.js @@ -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( diff --git a/rhodecode/public/js/src/rhodecode/pullrequests.js b/rhodecode/public/js/src/rhodecode/pullrequests.js --- a/rhodecode/public/js/src/rhodecode/pullrequests.js +++ b/rhodecode/public/js/src/rhodecode/pullrequests.js @@ -378,49 +378,6 @@ var editPullRequest = function(repo_name 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 diff --git a/rhodecode/templates/data_table/_dt_elements.mako b/rhodecode/templates/data_table/_dt_elements.mako --- a/rhodecode/templates/data_table/_dt_elements.mako +++ b/rhodecode/templates/data_table/_dt_elements.mako @@ -350,8 +350,7 @@ <%def name="pullrequest_title(title, description)"> - ${title}
- ${h.shorter(description, 40)} + ${title} <%def name="pullrequest_comments(comments_nr)"> @@ -375,3 +374,52 @@ <%def name="pullrequest_author(full_contact)"> ${base.gravatar_with_user(full_contact, 16)} + + +<%def name="markup_form(form_id, form_text='', help_text=None)"> + +
+
+
+ +
+ +
+
+ +
+ +
+ + +
+ + +
+ + + diff --git a/rhodecode/templates/pullrequests/pullrequest.mako b/rhodecode/templates/pullrequests/pullrequest.mako --- a/rhodecode/templates/pullrequests/pullrequest.mako +++ b/rhodecode/templates/pullrequests/pullrequest.mako @@ -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 @@
- ${h.textarea('pullrequest_desc',size=30, )} - ${_('Write a short description on this pull request')} + ${dt.markup_form('pullrequest_desc')}
- +
## TODO: johbo: Abusing the "content" class here to get the @@ -164,379 +164,384 @@
diff --git a/rhodecode/templates/pullrequests/pullrequest_show.mako b/rhodecode/templates/pullrequests/pullrequest_show.mako --- a/rhodecode/templates/pullrequests/pullrequest_show.mako +++ b/rhodecode/templates/pullrequests/pullrequest_show.mako @@ -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 @@
-
${h.urlify_commit_message(c.pull_request.description, c.repo_name)}
+
${h.render(c.pull_request.description, renderer=c.visual.default_renderer)}
@@ -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'),