Commit c3707e8c authored by Nick Thomas's avatar Nick Thomas

Merge remote-tracking branch 'upstream/master' into nt/ce-to-ee-thursday

parents 81b9d927 e87728dc
...@@ -23,28 +23,29 @@ export default class ApproversSelect { ...@@ -23,28 +23,29 @@ export default class ApproversSelect {
$(document).on('click', '.js-approver-remove', e => ApproversSelect.removeApprover(e)); $(document).on('click', '.js-approver-remove', e => ApproversSelect.removeApprover(e));
} }
static getApprovers(fieldName, selector, key) { static getApprovers(fieldName, approverList) {
const input = $(`[name="${fieldName}"]`); const input = $(`[name="${fieldName}"]`);
const existingApprovers = $(approverList).map((i, el) =>
const existingApprovers = $(selector).map((i, el) =>
parseInt($(el).data('id'), 10), parseInt($(el).data('id'), 10),
); );
const selectedApprovers = input.val() const selectedApprovers = input.val()
.split(',') .split(',')
.filter(val => val !== ''); .filter(val => val !== '');
const approvers = { return [...existingApprovers, ...selectedApprovers];
[key]: [...existingApprovers, ...selectedApprovers],
};
return approvers;
} }
fetchGroups(term) { fetchGroups(term) {
const options = ApproversSelect.getApprovers(this.fieldNames[1], '.js-approver-group', 'skip_groups'); const options = {
skip_groups: ApproversSelect.getApprovers(this.fieldNames[1], '.js-approver-group'),
};
return Api.groups(term, options); return Api.groups(term, options);
} }
fetchUsers(term) { fetchUsers(term) {
const options = ApproversSelect.getApprovers(this.fieldNames[0], '.js-approver', 'skip_users'); const options = {
skip_users: ApproversSelect.getApprovers(this.fieldNames[0], '.js-approver'),
project_id: $('#project_id').val(),
};
return Api.users(term, options); return Api.users(term, options);
} }
......
/* eslint-disable no-new*/ /* eslint-disable no-new*/
import './smart_interval'; import './smart_interval';
const healthyClass = 'geo-node-icon-healthy'; const healthyClass = 'geo-node-healthy';
const unhealthyClass = 'geo-node-icon-unhealthy'; const unhealthyClass = 'geo-node-unhealthy';
const healthyIcon = 'fa-check';
const unhealthyIcon = 'fa-close';
class GeoNodeStatus { class GeoNodeStatus {
constructor(el) { constructor(el) {
this.$el = $(el); this.$el = $(el);
this.$icon = $('.js-geo-node-icon', this.$el); this.$icon = $('.js-geo-node-icon', this.$el);
this.$healthStatus = $('.js-health-status', this.$el);
this.$status = $('.js-geo-node-status', this.$el); this.$status = $('.js-geo-node-status', this.$el);
this.$repositoriesSynced = $('.js-repositories-synced', this.$status); this.$repositoriesSynced = $('.js-repositories-synced', this.$status);
this.$repositoriesFailed = $('.js-repositories-failed', this.$status); this.$repositoriesFailed = $('.js-repositories-failed', this.$status);
...@@ -29,11 +32,16 @@ class GeoNodeStatus { ...@@ -29,11 +32,16 @@ class GeoNodeStatus {
getStatus() { getStatus() {
$.getJSON(this.endpoint, (status) => { $.getJSON(this.endpoint, (status) => {
this.setStatusIcon(status.healthy); this.setStatusIcon(status.healthy);
this.setHealthStatus(status.healthy);
this.$repositoriesSynced.html(`${status.repositories_synced_count}/${status.repositories_count} (${status.repositories_synced_in_percentage})`); this.$repositoriesSynced.html(`${status.repositories_synced_count}/${status.repositories_count} (${status.repositories_synced_in_percentage})`);
this.$repositoriesFailed.html(status.repositories_failed_count); this.$repositoriesFailed.html(status.repositories_failed_count);
this.$lfsObjectsSynced.html(`${status.lfs_objects_synced_count}/${status.lfs_objects_count} (${status.lfs_objects_synced_in_percentage})`); this.$lfsObjectsSynced.html(`${status.lfs_objects_synced_count}/${status.lfs_objects_count} (${status.lfs_objects_synced_in_percentage})`);
this.$attachmentsSynced.html(`${status.attachments_synced_count}/${status.attachments_count} (${status.attachments_synced_in_percentage})`); this.$attachmentsSynced.html(`${status.attachments_synced_count}/${status.attachments_count} (${status.attachments_synced_in_percentage})`);
this.$health.html(status.health); if (status.health === 'Healthy') {
this.$health.html('');
} else {
this.$health.html(`<code class="geo-health">${status.health}</code>`);
}
this.$status.show(); this.$status.show();
}); });
...@@ -41,16 +49,26 @@ class GeoNodeStatus { ...@@ -41,16 +49,26 @@ class GeoNodeStatus {
setStatusIcon(healthy) { setStatusIcon(healthy) {
if (healthy) { if (healthy) {
this.$icon.removeClass(unhealthyClass) this.$icon.removeClass(`${unhealthyClass} ${unhealthyIcon}`)
.addClass(healthyClass) .addClass(`${healthyClass} ${healthyIcon}`)
.attr('title', 'Healthy'); .attr('title', 'Healthy');
} else { } else {
this.$icon.removeClass(healthyClass) this.$icon.removeClass(`${healthyClass} ${healthyIcon}`)
.addClass(unhealthyClass) .addClass(`${unhealthyClass} ${unhealthyIcon}`)
.attr('title', 'Unhealthy'); .attr('title', 'Unhealthy');
} }
}
this.$icon.tooltip('fixTitle'); setHealthStatus(healthy) {
if (healthy) {
this.$healthStatus.removeClass(unhealthyClass)
.addClass(healthyClass)
.html('Healthy');
} else {
this.$healthStatus.removeClass(healthyClass)
.addClass(unhealthyClass)
.html('Unhealthy');
}
} }
} }
......
...@@ -2,8 +2,9 @@ ...@@ -2,8 +2,9 @@
// MarkdownPreview // MarkdownPreview
// //
// Handles toggling the "Write" and "Preview" tab clicks, rendering the preview, // Handles toggling the "Write" and "Preview" tab clicks, rendering the preview
// and showing a warning when more than `x` users are referenced. // (including the explanation of slash commands), and showing a warning when
// more than `x` users are referenced.
// //
(function () { (function () {
var lastTextareaPreviewed; var lastTextareaPreviewed;
...@@ -17,32 +18,45 @@ ...@@ -17,32 +18,45 @@
// Minimum number of users referenced before triggering a warning // Minimum number of users referenced before triggering a warning
MarkdownPreview.prototype.referenceThreshold = 10; MarkdownPreview.prototype.referenceThreshold = 10;
MarkdownPreview.prototype.emptyMessage = 'Nothing to preview.';
MarkdownPreview.prototype.ajaxCache = {}; MarkdownPreview.prototype.ajaxCache = {};
MarkdownPreview.prototype.showPreview = function ($form) { MarkdownPreview.prototype.showPreview = function ($form) {
var mdText; var mdText;
var preview = $form.find('.js-md-preview'); var preview = $form.find('.js-md-preview');
var url = preview.data('url');
if (preview.hasClass('md-preview-loading')) { if (preview.hasClass('md-preview-loading')) {
return; return;
} }
mdText = $form.find('textarea.markdown-area').val(); mdText = $form.find('textarea.markdown-area').val();
if (mdText.trim().length === 0) { if (mdText.trim().length === 0) {
preview.text('Nothing to preview.'); preview.text(this.emptyMessage);
this.hideReferencedUsers($form); this.hideReferencedUsers($form);
} else { } else {
preview.addClass('md-preview-loading').text('Loading...'); preview.addClass('md-preview-loading').text('Loading...');
this.fetchMarkdownPreview(mdText, (function (response) { this.fetchMarkdownPreview(mdText, url, (function (response) {
preview.removeClass('md-preview-loading').html(response.body); var body;
if (response.body.length > 0) {
body = response.body;
} else {
body = this.emptyMessage;
}
preview.removeClass('md-preview-loading').html(body);
preview.renderGFM(); preview.renderGFM();
this.renderReferencedUsers(response.references.users, $form); this.renderReferencedUsers(response.references.users, $form);
if (response.references.commands) {
this.renderReferencedCommands(response.references.commands, $form);
}
}).bind(this)); }).bind(this));
} }
}; };
MarkdownPreview.prototype.fetchMarkdownPreview = function (text, success) { MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) {
if (!window.preview_markdown_path) { if (!url) {
return; return;
} }
if (text === this.ajaxCache.text) { if (text === this.ajaxCache.text) {
...@@ -51,7 +65,7 @@ ...@@ -51,7 +65,7 @@
} }
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: window.preview_markdown_path, url: url,
data: { data: {
text: text text: text
}, },
...@@ -83,6 +97,22 @@ ...@@ -83,6 +97,22 @@
} }
}; };
MarkdownPreview.prototype.hideReferencedCommands = function ($form) {
$form.find('.referenced-commands').hide();
};
MarkdownPreview.prototype.renderReferencedCommands = function (commands, $form) {
var referencedCommands;
referencedCommands = $form.find('.referenced-commands');
if (commands.length > 0) {
referencedCommands.html(commands);
referencedCommands.show();
} else {
referencedCommands.html('');
referencedCommands.hide();
}
};
return MarkdownPreview; return MarkdownPreview;
}()); }());
...@@ -137,6 +167,8 @@ ...@@ -137,6 +167,8 @@
$form.find('.md-write-holder').show(); $form.find('.md-write-holder').show();
$form.find('textarea.markdown-area').focus(); $form.find('textarea.markdown-area').focus();
$form.find('.md-preview-holder').hide(); $form.find('.md-preview-holder').hide();
markdownPreview.hideReferencedCommands($form);
}); });
$(document).on('markdown-preview:toggle', function (e, keyboardEvent) { $(document).on('markdown-preview:toggle', function (e, keyboardEvent) {
......
.geo-node-icon-healthy { .geo-node-healthy {
color: $gl-success; color: $gl-success;
} }
.geo-node-icon-unhealthy { .geo-node-unhealthy {
color: $gl-danger; color: $gl-danger;
} }
.geo-node-icon-disabled { .geo-node-disabled {
color: $gray-darkest; color: $gray-darkest;
} }
.well-list.geo-nodes {
li {
position: relative;
&:hover {
background: $white-light;
}
&.node-disabled,
&.node-disabled:hover {
background-color: $gray-lightest;
}
}
}
.node-info {
color: $gl-text-color;
}
.geo-health {
display: inline-block;
margin-top: 5px;
}
.node-badge {
color: $white-light;
display: inline-block;
margin-left: 5px;
padding: 0 5px;
border-radius: 3px;
&.primary-node {
background-color: $blue-300;
}
&.current-node {
background-color: $green-400;
}
}
.node-actions {
margin-top: 10px;
@media (min-width: $screen-md-min) {
position: absolute;
right: 15px;
top: 0;
}
.btn:not(:first-of-type) {
margin-left: 10px;
}
}
module MarkdownPreview
private
def render_markdown_preview(text, markdown_context = {})
render json: {
body: view_context.markdown(text, markdown_context),
references: {
users: preview_referenced_users(text)
}
}
end
def preview_referenced_users(text)
extractor = Gitlab::ReferenceExtractor.new(@project, current_user)
extractor.analyze(text, author: current_user)
extractor.users.map(&:username)
end
end
class Projects::WikisController < Projects::ApplicationController class Projects::WikisController < Projects::ApplicationController
include MarkdownPreview
before_action :authorize_read_wiki! before_action :authorize_read_wiki!
before_action :authorize_create_wiki!, only: [:edit, :create, :history] before_action :authorize_create_wiki!, only: [:edit, :create, :history]
before_action :authorize_admin_wiki!, only: :destroy before_action :authorize_admin_wiki!, only: :destroy
...@@ -103,9 +101,14 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -103,9 +101,14 @@ class Projects::WikisController < Projects::ApplicationController
end end
def preview_markdown def preview_markdown
context = { pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id] } result = PreviewMarkdownService.new(@project, current_user, params).execute
render_markdown_preview(params[:text], context) render json: {
body: view_context.markdown(result[:text], pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id]),
references: {
users: result[:users]
}
}
end end
private private
......
class ProjectsController < Projects::ApplicationController class ProjectsController < Projects::ApplicationController
include IssuableCollections include IssuableCollections
include ExtractsPath include ExtractsPath
include MarkdownPreview
before_action :authenticate_user!, except: [:index, :show, :activity, :refs] before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
before_action :project, except: [:index, :new, :create] before_action :project, except: [:index, :new, :create]
...@@ -241,7 +240,15 @@ class ProjectsController < Projects::ApplicationController ...@@ -241,7 +240,15 @@ class ProjectsController < Projects::ApplicationController
end end
def preview_markdown def preview_markdown
render_markdown_preview(params[:text]) result = PreviewMarkdownService.new(@project, current_user, params).execute
render json: {
body: view_context.markdown(result[:text]),
references: {
users: result[:users],
commands: view_context.markdown(result[:commands])
}
}
end end
private private
......
...@@ -3,7 +3,6 @@ class SnippetsController < ApplicationController ...@@ -3,7 +3,6 @@ class SnippetsController < ApplicationController
include ToggleAwardEmoji include ToggleAwardEmoji
include SpammableActions include SpammableActions
include SnippetsActions include SnippetsActions
include MarkdownPreview
include RendersBlob include RendersBlob
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
...@@ -90,7 +89,14 @@ class SnippetsController < ApplicationController ...@@ -90,7 +89,14 @@ class SnippetsController < ApplicationController
end end
def preview_markdown def preview_markdown
render_markdown_preview(params[:text], skip_project_check: true) result = PreviewMarkdownService.new(@project, current_user, params).execute
render json: {
body: view_context.markdown(result[:text], skip_project_check: true),
references: {
users: result[:users]
}
}
end end
protected protected
......
module EE module EE
module GeoHelper module GeoHelper
def node_status_icon(node) def node_status_icon(node)
if node.primary? unless node.primary?
icon 'star fw', class: 'has-tooltip', title: 'Primary node'
else
status = node.enabled? ? 'healthy' : 'disabled' status = node.enabled? ? 'healthy' : 'disabled'
icon = status == 'healthy' ? 'check' : 'times'
icon 'globe fw', icon "#{icon} fw",
class: "js-geo-node-icon geo-node-icon-#{status} has-tooltip", class: "js-geo-node-icon geo-node-#{status}",
title: status.capitalize title: status.capitalize
end end
end end
def node_class(node)
klass = []
klass << 'js-geo-secondary-node' if node.secondary?
klass << 'node-disabled' unless node.enabled?
klass
end
def toggle_node_button(node) def toggle_node_button(node)
btn_class, title, data = btn_class, title, data =
if node.enabled? if node.enabled?
['warning', 'Disable node', { confirm: 'Disabling a node stops the sync process. Are you sure?' }] ['warning', 'Disable', { confirm: 'Disabling a node stops the sync process. Are you sure?' }]
else else
['success', 'Enable node'] %w[success Enable]
end end
link_to icon('power-off fw', text: title), link_to title,
toggle_admin_geo_node_path(node), toggle_admin_geo_node_path(node),
method: :post, method: :post,
class: "btn btn-sm btn-#{btn_class} prepend-left-10 has-tooltip", class: "btn btn-sm btn-#{btn_class}",
title: title, title: title,
data: data data: data
end end
......
...@@ -122,6 +122,10 @@ module GitlabRoutingHelper ...@@ -122,6 +122,10 @@ module GitlabRoutingHelper
namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args) namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args)
end end
def preview_markdown_path(project, *args)
preview_markdown_namespace_project_path(project.namespace, project, *args)
end
def toggle_subscription_path(entity, *args) def toggle_subscription_path(entity, *args)
if entity.is_a?(Issue) if entity.is_a?(Issue)
toggle_subscription_namespace_project_issue_path(entity.project.namespace, entity.project, entity) toggle_subscription_namespace_project_issue_path(entity.project.namespace, entity.project, entity)
......
...@@ -35,6 +35,10 @@ class GeoNode < ActiveRecord::Base ...@@ -35,6 +35,10 @@ class GeoNode < ActiveRecord::Base
mode: :per_attribute_iv, mode: :per_attribute_iv,
encode: true encode: true
def current?
Gitlab::Geo.current_node == self
end
def secondary? def secondary?
!primary !primary
end end
......
...@@ -5,7 +5,7 @@ class GeoNodeStatusEntity < Grape::Entity ...@@ -5,7 +5,7 @@ class GeoNodeStatusEntity < Grape::Entity
expose :healthy?, as: :healthy expose :healthy?, as: :healthy
expose :health do |node| expose :health do |node|
node.healthy? ? 'No Health Problems Detected' : node.health node.healthy? ? 'Healthy' : node.health
end end
expose :attachments_count expose :attachments_count
......
class PreviewMarkdownService < BaseService
def execute
text, commands = explain_slash_commands(params[:text])
users = find_user_references(text)
success(
text: text,
users: users,
commands: commands.join(' ')
)
end
private
def explain_slash_commands(text)
return text, [] unless %w(Issue MergeRequest).include?(commands_target_type)
slash_commands_service = SlashCommands::InterpretService.new(project, current_user)
slash_commands_service.explain(text, find_commands_target)
end
def find_user_references(text)
extractor = Gitlab::ReferenceExtractor.new(project, current_user)
extractor.analyze(text, author: current_user)
extractor.users.map(&:username)
end
def find_commands_target
if commands_target_id.present?
finder = commands_target_type == 'Issue' ? IssuesFinder : MergeRequestsFinder
finder.new(current_user, project_id: project.id).find(commands_target_id)
else
collection = commands_target_type == 'Issue' ? project.issues : project.merge_requests
collection.build
end
end
def commands_target_type
params[:slash_commands_target_type]
end
def commands_target_id
params[:slash_commands_target_id]
end
end
...@@ -115,22 +115,22 @@ class DynamicPathValidator < ActiveModel::EachValidator ...@@ -115,22 +115,22 @@ class DynamicPathValidator < ActiveModel::EachValidator
# this would map to the activity-page of it's parent. # this would map to the activity-page of it's parent.
GROUP_ROUTES = %w[ GROUP_ROUTES = %w[
activity activity
analytics
audit_events
avatar avatar
edit edit
group_members group_members
hooks
issues issues
labels labels
merge_requests
milestones
projects
subgroups
analytics
audit_events
hooks
ldap ldap
ldap_group_links ldap_group_links
merge_requests
milestones
notification_setting notification_setting
pipeline_quota pipeline_quota
projects
subgroups
].freeze ].freeze
CHILD_ROUTES = (WILDCARD_ROUTES | GROUP_ROUTES).freeze CHILD_ROUTES = (WILDCARD_ROUTES | GROUP_ROUTES).freeze
......
...@@ -16,42 +16,46 @@ ...@@ -16,42 +16,46 @@
Geo nodes (#{@nodes.count}) Geo nodes (#{@nodes.count})
%ul.well-list.geo-nodes %ul.well-list.geo-nodes
- @nodes.each do |node| - @nodes.each do |node|
%li{ id: dom_id(node), class: ('js-geo-secondary-node' if node.secondary?), data: { status_url: status_admin_geo_node_path(node) } } %li{ id: dom_id(node), class: node_class(node), data: { status_url: status_admin_geo_node_path(node) } }
.list-item-name .node-block
%span
= node_status_icon(node) = node_status_icon(node)
%strong= node.url %strong= node.url
- if node.current?
.node-badge.current-node Current node
- if node.primary? - if node.primary?
.node-badge.primary-node Primary
%span.help-block Primary node %span.help-block Primary node
- else - else
.js-geo-node-status{ style: 'display: none' } .js-geo-node-status{ style: 'display: none' }
- if node.enabled?
%p
%span.help-block
Health Status:
%span.js-health-status
%p %p
%span.help-block %span.help-block
Repositories synced: Repositories synced:
%span.js-repositories-synced %strong.node-info.js-repositories-synced
%p %p
%span.help-block %span.help-block
Repositories failed: Repositories failed:
%span.js-repositories-failed %strong.node-info.js-repositories-failed
%p %p
%span.help-block %span.help-block
LFS objects synced: LFS objects synced:
%span.js-lfs-objects-synced %strong.node-info.js-lfs-objects-synced
%p %p
%span.help-block %span.help-block
Attachments synced: Attachments synced:
%span.js-attachments-synced %strong.node-info.js-attachments-synced
%p %p
%span.help-block.js-health .js-health
.pull-right - if Gitlab::Geo.primary?
.node-actions
- if Gitlab::Geo.license_allows? - if Gitlab::Geo.license_allows?
- if node.missing_oauth_application? - if node.missing_oauth_application?
= link_to repair_admin_geo_node_path(node), method: :post, title: 'OAuth application is missing', class: 'btn btn-default btn-sm prepend-left-10' do = link_to "Repair authentication", repair_admin_geo_node_path(node), method: :post, title: 'OAuth application is missing', class: 'btn btn-default btn-sm'
= icon('exclamation-triangle fw')
Repair authentication
- if node.secondary? - if node.secondary?
= toggle_node_button(node) = toggle_node_button(node)
= link_to admin_geo_node_path(node), data: { confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-remove btn-sm prepend-left-10' do = link_to "Remove", admin_geo_node_path(node), data: { confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-remove btn-sm'
= icon 'trash'
Remove
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
%h3.page-title %h3.page-title
Pre-defined push rules. Pre-defined push rules.
%p.light %p.light
Rules that define what git pushes are accepted for a project. All newly created projects will use this settings. Request new rules for free by creating an issue on the <a href="https://gitlab.com/gitlab-org/gitlab-ee/issues/">GitLab EE issue tracker</a> and labeling it 'Feature proposal'. Or if you can please contribute a tested merge request. Rules that define what git pushes are accepted for a project. All newly created projects will use this settings.
%hr.clearfix %hr.clearfix
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
.form-group.milestone-description .form-group.milestone-description
= f.label :description, "Description", class: "control-label" = f.label :description, "Description", class: "control-label"
.col-sm-10 .col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do = render layout: 'projects/md_preview', locals: { url: '' } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...' = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...'
.clearfix .clearfix
.error-alert .error-alert
......
...@@ -5,10 +5,6 @@ ...@@ -5,10 +5,6 @@
- content_for :project_javascripts do - content_for :project_javascripts do
- project = @target_project || @project - project = @target_project || @project
- if @project_wiki && @page
- preview_markdown_path = namespace_project_wiki_preview_markdown_path(project.namespace, project, @page.slug)
- else
- preview_markdown_path = preview_markdown_namespace_project_path(project.namespace, project)
- if current_user - if current_user
:javascript :javascript
window.uploads_path = "#{namespace_project_uploads_path project.namespace,project}"; window.uploads_path = "#{namespace_project_uploads_path project.namespace,project}";
......
- referenced_users = local_assigns.fetch(:referenced_users, nil)
.md-area .md-area
.md-header .md-header
%ul.nav-links.clearfix %ul.nav-links.clearfix
...@@ -28,9 +30,10 @@ ...@@ -28,9 +30,10 @@
.md-write-holder .md-write-holder
= yield = yield
.md.md-preview-holder.js-md-preview.hide{ class: (preview_class if defined?(preview_class)) } .md.md-preview-holder.js-md-preview.hide.md-preview{ data: { url: url } }
.referenced-commands.hide
- if defined?(referenced_users) && referenced_users - if referenced_users
.referenced-users.hide .referenced-users.hide
%span %span
= icon("exclamation-triangle") = icon("exclamation-triangle")
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
.form-group.milestone-description .form-group.milestone-description
= f.label :description, "Description", class: "control-label" = f.label :description, "Description", class: "control-label"
.col-sm-10 .col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project) } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...' = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...'
= render 'projects/notes/hints' = render 'projects/notes/hints'
.clearfix .clearfix
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
= form_tag '#', method: :put, class: 'edit-note common-note-form js-quick-submit' do = form_tag '#', method: :put, class: 'edit-note common-note-form js-quick-submit' do
= hidden_field_tag :target_id, '', class: 'js-form-target-id' = hidden_field_tag :target_id, '', class: 'js-form-target-id'
= hidden_field_tag :target_type, '', class: 'js-form-target-type' = hidden_field_tag :target_type, '', class: 'js-form-target-type'
= render layout: 'projects/md_preview', locals: { preview_class: 'md-preview', referenced_users: true } do = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(project), referenced_users: true } do
= render 'projects/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..." = render 'projects/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..."
= render 'projects/notes/hints' = render 'projects/notes/hints'
......
- supports_slash_commands = note_supports_slash_commands?(@note) - supports_slash_commands = note_supports_slash_commands?(@note)
- if supports_slash_commands
- preview_url = preview_markdown_path(@project, slash_commands_target_type: @note.noteable_type, slash_commands_target_id: @note.noteable_id)
- else
- preview_url = preview_markdown_path(@project)
= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form", "data-noteable-iid" => @note.noteable.try(:iid), }, authenticity_token: true do |f| = form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form", "data-noteable-iid" => @note.noteable.try(:iid), }, authenticity_token: true do |f|
= hidden_field_tag :view, diff_view = hidden_field_tag :view, diff_view
...@@ -18,7 +22,7 @@ ...@@ -18,7 +22,7 @@
-# DiffNote -# DiffNote
= f.hidden_field :position = f.hidden_field :position
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do
= render 'projects/zen', f: f, = render 'projects/zen', f: f,
attr: :note, attr: :note,
classes: 'note-textarea js-note-text', classes: 'note-textarea js-note-text',
......
%ul#notes-list.notes.main-notes-list.timeline %ul#notes-list.notes.main-notes-list.timeline
= render "shared/notes/notes" = render "shared/notes/notes"
= render 'projects/notes/edit_form' = render 'projects/notes/edit_form', project: @project
%ul.notes.notes-form.timeline %ul.notes.notes-form.timeline
%li.timeline-entry %li.timeline-entry
......
.row.prepend-top-default.append-bottom-default .row.prepend-top-default.append-bottom-default
.col-lg-3 .col-lg-3
%h4.prepend-top-0 %h4.prepend-top-0
Push Rules Push Rules
%p.light %p.light
Push Rules outline what is accepted for this project. You can request new rules (for free) by creating an issue on our Push Rules outline what is accepted for this project.
= succeed '.' do
%a{ href: "https://gitlab.com/gitlab-org/gitlab-ee/issues/" }GitLab EE issue tracker
Alternatively, submit a merge request to GitLab EE.
.col-lg-9 .col-lg-9
%h5.prepend-top-0 %h5.prepend-top-0
Add new push rule Add new push rule
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
= form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal common-note-form release-form js-quick-submit' }) do |f| = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal common-note-form release-form js-quick-submit' }) do |f|
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..." = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..."
= render 'projects/notes/hints' = render 'projects/notes/hints'
.error-alert .error-alert
......
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
.form-group .form-group
= label_tag :release_description, 'Release notes', class: 'control-label' = label_tag :release_description, 'Release notes', class: 'control-label'
.col-sm-10 .col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do
= render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..." = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..."
= render 'projects/notes/hints' = render 'projects/notes/hints'
.help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page. .help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
.form-group .form-group
= f.label :content, class: 'control-label' = f.label :content, class: 'control-label'
.col-sm-10 .col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do = render layout: 'projects/md_preview', locals: { url: namespace_project_wiki_preview_markdown_path(@project.namespace, @project, @page.slug) } do
= render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: 'Write your content or drag files here...' = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: 'Write your content or drag files here...'
= render 'projects/notes/hints' = render 'projects/notes/hints'
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
.col-sm-10 .col-sm-10
- author = issuable.author || current_user - author = issuable.author || current_user
- skip_users = issuable.all_approvers_including_groups + [author] - skip_users = issuable.all_approvers_including_groups + [author]
= users_select_tag("merge_request[approver_ids]", multiple: true, class: 'input-large', scope: :all, email_user: true, skip_users: skip_users) = users_select_tag("merge_request[approver_ids]", multiple: true, class: 'input-large', email_user: true, skip_users: skip_users)
.help-block .help-block
This merge request must be approved by these users. This merge request must be approved by these users.
You can override the project settings by setting your own list of approvers. You can override the project settings by setting your own list of approvers.
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
= render 'shared/issuable/form/template_selector', issuable: issuable = render 'shared/issuable/form/template_selector', issuable: issuable
= render 'shared/issuable/form/title', issuable: issuable, form: form, has_wip_commits: commits && commits.detect(&:work_in_progress?) = render 'shared/issuable/form/title', issuable: issuable, form: form, has_wip_commits: commits && commits.detect(&:work_in_progress?)
= render 'shared/issuable/form/description', issuable: issuable, form: form = render 'shared/issuable/form/description', issuable: issuable, form: form, project: project
- if issuable.respond_to?(:confidential) - if issuable.respond_to?(:confidential)
.form-group .form-group
......
- project = local_assigns.fetch(:project)
- issuable = local_assigns.fetch(:issuable) - issuable = local_assigns.fetch(:issuable)
- form = local_assigns.fetch(:form) - form = local_assigns.fetch(:form)
- supports_slash_commands = issuable.new_record?
- if supports_slash_commands
- preview_url = preview_markdown_path(project, slash_commands_target_type: issuable.class.name)
- else
- preview_url = preview_markdown_path(project)
.form-group.detail-page-description .form-group.detail-page-description
= form.label :description, 'Description', class: 'control-label' = form.label :description, 'Description', class: 'control-label'
.col-sm-10 .col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do
= render 'projects/zen', f: form, attr: :description, = render 'projects/zen', f: form, attr: :description,
classes: 'note-textarea', classes: 'note-textarea',
placeholder: "Write a comment or drag your files here...", placeholder: "Write a comment or drag your files here...",
supports_slash_commands: !issuable.persisted? supports_slash_commands: supports_slash_commands
= render 'projects/notes/hints', supports_slash_commands: !issuable.persisted? = render 'projects/notes/hints', supports_slash_commands: supports_slash_commands
.clearfix .clearfix
.error-alert .error-alert
---
title: 'Remove superfluous wording on push rules'
merge_request: 1811
---
title: Display slash commands outcome when previewing Markdown
merge_request: 10054
author: Rares Sfirlogea
...@@ -36,18 +36,18 @@ class RenameReservedDynamicPaths < ActiveRecord::Migration ...@@ -36,18 +36,18 @@ class RenameReservedDynamicPaths < ActiveRecord::Migration
DISSALLOWED_GROUP_PATHS = %w[ DISSALLOWED_GROUP_PATHS = %w[
activity activity
avatar
group_members
labels
milestones
subgroups
analytics analytics
audit_events audit_events
avatar
group_members
hooks hooks
labels
ldap ldap
ldap_group_links ldap_group_links
milestones
notification_setting notification_setting
pipeline_quota pipeline_quota
subgroups
] ]
def up def up
......
...@@ -8,10 +8,6 @@ GitLab Enterprise Edition offers a user-friendly interface for such cases. ...@@ -8,10 +8,6 @@ GitLab Enterprise Edition offers a user-friendly interface for such cases.
Push Rules are defined per project so you can have different rules applied to different projects depends on your needs. Push Rules are defined per project so you can have different rules applied to different projects depends on your needs.
Push Rules settings can be found at Project settings -> Push Rules page. Push Rules settings can be found at Project settings -> Push Rules page.
## New hooks
If you are a subscriber and need a hook that is not there yet we would be glad to add it for free, please contact support to request one.
## How to use ## How to use
Let's assume you have the following requirements for your workflow: Let's assume you have the following requirements for your workflow:
......
...@@ -11,7 +11,7 @@ merge request in a project. ...@@ -11,7 +11,7 @@ merge request in a project.
## Configuring Approvals ## Configuring Approvals
You can configure the approvals in the project settings, under merge requests. You can configure the approvals in the project settings, under merge requests.
To enable it, set **Approvals required** to 1 or higher and search for the To enable it, turn on **Activate merge request approvals** and search for the
users you want to be approvers. users you want to be approvers.
![Merge Request Approvals in Project Settings](img/approvals_settings.png) ![Merge Request Approvals in Project Settings](img/approvals_settings.png)
...@@ -19,8 +19,6 @@ users you want to be approvers. ...@@ -19,8 +19,6 @@ users you want to be approvers.
### Approvals Required ### Approvals Required
This sets the amount of approvals required before being able to merge a merge request. This sets the amount of approvals required before being able to merge a merge request.
At 0, this disables the feature. Any value above 0 requires that amount of different
users to approve the merge request.
The number of approvers can be higher than the required approvals. The number of approvers can be higher than the required approvals.
......
module Gitlab module Gitlab
module SlashCommands module SlashCommands
class CommandDefinition class CommandDefinition
attr_accessor :name, :aliases, :description, :params, :condition_block, :action_block attr_accessor :name, :aliases, :description, :explanation, :params,
:condition_block, :parse_params_block, :action_block
def initialize(name, attributes = {}) def initialize(name, attributes = {})
@name = name @name = name
@aliases = attributes[:aliases] || [] @aliases = attributes[:aliases] || []
@description = attributes[:description] || '' @description = attributes[:description] || ''
@explanation = attributes[:explanation] || ''
@params = attributes[:params] || [] @params = attributes[:params] || []
@condition_block = attributes[:condition_block] @condition_block = attributes[:condition_block]
@parse_params_block = attributes[:parse_params_block]
@action_block = attributes[:action_block] @action_block = attributes[:action_block]
end end
...@@ -28,14 +31,20 @@ module Gitlab ...@@ -28,14 +31,20 @@ module Gitlab
context.instance_exec(&condition_block) context.instance_exec(&condition_block)
end end
def explain(context, opts, arg)
return unless available?(opts)
if explanation.respond_to?(:call)
execute_block(explanation, context, arg)
else
explanation
end
end
def execute(context, opts, arg) def execute(context, opts, arg)
return if noop? || !available?(opts) return if noop? || !available?(opts)
if arg.present? execute_block(action_block, context, arg)
context.instance_exec(arg, &action_block)
elsif action_block.arity == 0
context.instance_exec(&action_block)
end
end end
def to_h(opts) def to_h(opts)
...@@ -52,6 +61,23 @@ module Gitlab ...@@ -52,6 +61,23 @@ module Gitlab
params: params params: params
} }
end end
private
def execute_block(block, context, arg)
if arg.present?
parsed = parse_params(arg, context)
context.instance_exec(parsed, &block)
elsif block.arity == 0
context.instance_exec(&block)
end
end
def parse_params(arg, context)
return arg unless parse_params_block
context.instance_exec(arg, &parse_params_block)
end
end end
end end
end end
...@@ -44,6 +44,22 @@ module Gitlab ...@@ -44,6 +44,22 @@ module Gitlab
@params = params @params = params
end end
# Allows to give an explanation of what the command will do when
# executed. This explanation is shown when rendering the Markdown
# preview.
#
# Example:
#
# explanation do |arguments|
# "Adds label(s) #{arguments.join(' ')}"
# end
# command :command_key do |arguments|
# # Awesome code block
# end
def explanation(text = '', &block)
@explanation = block_given? ? block : text
end
# Allows to define conditions that must be met in order for the command # Allows to define conditions that must be met in order for the command
# to be returned by `.command_names` & `.command_definitions`. # to be returned by `.command_names` & `.command_definitions`.
# It accepts a block that will be evaluated with the context given to # It accepts a block that will be evaluated with the context given to
...@@ -61,6 +77,24 @@ module Gitlab ...@@ -61,6 +77,24 @@ module Gitlab
@condition_block = block @condition_block = block
end end
# Allows to perform initial parsing of parameters. The result is passed
# both to `command` and `explanation` blocks, instead of the raw
# parameters.
# It accepts a block that will be evaluated with the context given to
# `CommandDefintion#to_h`.
#
# Example:
#
# parse_params do |raw|
# raw.strip
# end
# command :command_key do |parsed|
# # Awesome code block
# end
def parse_params(&block)
@parse_params_block = block
end
# Registers a new command which is recognizeable from body of email or # Registers a new command which is recognizeable from body of email or
# comment. # comment.
# It accepts aliases and takes a block. # It accepts aliases and takes a block.
...@@ -77,8 +111,10 @@ module Gitlab ...@@ -77,8 +111,10 @@ module Gitlab
name, name,
aliases: aliases, aliases: aliases,
description: @description, description: @description,
explanation: @explanation,
params: @params, params: @params,
condition_block: @condition_block, condition_block: @condition_block,
parse_params_block: @parse_params_block,
action_block: block action_block: block
) )
...@@ -89,8 +125,14 @@ module Gitlab ...@@ -89,8 +125,14 @@ module Gitlab
end end
@description = nil @description = nil
@explanation = nil
@params = nil @params = nil
@condition_block = nil @condition_block = nil
@parse_params_block = nil
end
def definition_by_name(name)
command_definitions_by_name[name.to_sym]
end end
end end
end end
......
...@@ -31,6 +31,7 @@ feature 'Merge request approvals', js: true, feature: true do ...@@ -31,6 +31,7 @@ feature 'Merge request approvals', js: true, feature: true do
context 'when creating an MR' do context 'when creating an MR' do
let(:other_user) { create(:user) } let(:other_user) { create(:user) }
let(:non_member) { create(:user) }
before do before do
project.team << [user, :developer] project.team << [user, :developer]
...@@ -49,6 +50,10 @@ feature 'Merge request approvals', js: true, feature: true do ...@@ -49,6 +50,10 @@ feature 'Merge request approvals', js: true, feature: true do
it 'does not allow setting the current user as an approver' do it 'does not allow setting the current user as an approver' do
expect(find('.select2-results')).not_to have_content(user.name) expect(find('.select2-results')).not_to have_content(user.name)
end end
it 'filters non members from approvers list' do
expect(find('.select2-results')).not_to have_content(non_member.name)
end
end end
context "Group approvers" do context "Group approvers" do
......
...@@ -7,13 +7,14 @@ describe 'Project settings > [EE] Merge Requests', feature: true, js: true do ...@@ -7,13 +7,14 @@ describe 'Project settings > [EE] Merge Requests', feature: true, js: true do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:empty_project, approvals_before_merge: 1) } let(:project) { create(:empty_project, approvals_before_merge: 1) }
let(:group) { create(:group) } let(:group) { create(:group) }
let(:approver) { create(:user) } let(:group_member) { create(:user) }
let(:non_member) { create(:user) }
before do before do
login_as(user) login_as(user)
project.team << [user, :master] project.team << [user, :master]
group.add_developer(approver)
group.add_developer(user) group.add_developer(user)
group.add_developer(group_member)
end end
scenario 'adds approver' do scenario 'adds approver' do
...@@ -28,6 +29,18 @@ describe 'Project settings > [EE] Merge Requests', feature: true, js: true do ...@@ -28,6 +29,18 @@ describe 'Project settings > [EE] Merge Requests', feature: true, js: true do
click_button 'Add' click_button 'Add'
expect(find('.js-current-approvers')).to have_content(user.name) expect(find('.js-current-approvers')).to have_content(user.name)
find('.js-select-user-and-group').click
expect(find('.select2-results')).not_to have_content(user.name)
end
scenario 'filter approvers' do
visit edit_project_path(project)
find('.js-select-user-and-group').click
expect(find('.select2-results')).to have_content(user.name)
expect(find('.select2-results')).not_to have_content(non_member.name)
end end
scenario 'adds approver group' do scenario 'adds approver group' do
......
...@@ -167,6 +167,58 @@ describe Gitlab::SlashCommands::CommandDefinition do ...@@ -167,6 +167,58 @@ describe Gitlab::SlashCommands::CommandDefinition do
end end
end end
end end
context 'when the command defines parse_params block' do
before do
subject.parse_params_block = ->(raw) { raw.strip }
subject.action_block = ->(parsed) { self.received_arg = parsed }
end
it 'executes the command passing the parsed param' do
subject.execute(context, {}, 'something ')
expect(context.received_arg).to eq('something')
end
end
end
end
end
describe '#explain' do
context 'when the command is not available' do
before do
subject.condition_block = proc { false }
subject.explanation = 'Explanation'
end
it 'returns nil' do
result = subject.explain({}, {}, nil)
expect(result).to be_nil
end
end
context 'when the explanation is a static string' do
before do
subject.explanation = 'Explanation'
end
it 'returns this static string' do
result = subject.explain({}, {}, nil)
expect(result).to eq 'Explanation'
end
end
context 'when the explanation is dynamic' do
before do
subject.explanation = proc { |arg| "Dynamic #{arg}" }
end
it 'invokes the proc' do
result = subject.explain({}, {}, 'explanation')
expect(result).to eq 'Dynamic explanation'
end end
end end
end end
......
...@@ -11,67 +11,99 @@ describe Gitlab::SlashCommands::Dsl do ...@@ -11,67 +11,99 @@ describe Gitlab::SlashCommands::Dsl do
end end
params 'The first argument' params 'The first argument'
command :one_arg, :once, :first do |arg1| explanation 'Static explanation'
arg1 command :explanation_with_aliases, :once, :first do |arg|
arg
end end
desc do desc do
"A dynamic description for #{noteable.upcase}" "A dynamic description for #{noteable.upcase}"
end end
params 'The first argument', 'The second argument' params 'The first argument', 'The second argument'
command :two_args do |arg1, arg2| command :dynamic_description do |args|
[arg1, arg2] args.split
end end
command :cc command :cc
explanation do |arg|
"Action does something with #{arg}"
end
condition do condition do
project == 'foo' project == 'foo'
end end
command :cond_action do |arg| command :cond_action do |arg|
arg arg
end end
parse_params do |raw_arg|
raw_arg.strip
end
command :with_params_parsing do |parsed|
parsed
end
end end
end end
describe '.command_definitions' do describe '.command_definitions' do
it 'returns an array with commands definitions' do it 'returns an array with commands definitions' do
no_args_def, one_arg_def, two_args_def, cc_def, cond_action_def = DummyClass.command_definitions no_args_def, explanation_with_aliases_def, dynamic_description_def,
cc_def, cond_action_def, with_params_parsing_def =
DummyClass.command_definitions
expect(no_args_def.name).to eq(:no_args) expect(no_args_def.name).to eq(:no_args)
expect(no_args_def.aliases).to eq([:none]) expect(no_args_def.aliases).to eq([:none])
expect(no_args_def.description).to eq('A command with no args') expect(no_args_def.description).to eq('A command with no args')
expect(no_args_def.explanation).to eq('')
expect(no_args_def.params).to eq([]) expect(no_args_def.params).to eq([])
expect(no_args_def.condition_block).to be_nil expect(no_args_def.condition_block).to be_nil
expect(no_args_def.action_block).to be_a_kind_of(Proc) expect(no_args_def.action_block).to be_a_kind_of(Proc)
expect(no_args_def.parse_params_block).to be_nil
expect(one_arg_def.name).to eq(:one_arg) expect(explanation_with_aliases_def.name).to eq(:explanation_with_aliases)
expect(one_arg_def.aliases).to eq([:once, :first]) expect(explanation_with_aliases_def.aliases).to eq([:once, :first])
expect(one_arg_def.description).to eq('') expect(explanation_with_aliases_def.description).to eq('')
expect(one_arg_def.params).to eq(['The first argument']) expect(explanation_with_aliases_def.explanation).to eq('Static explanation')
expect(one_arg_def.condition_block).to be_nil expect(explanation_with_aliases_def.params).to eq(['The first argument'])
expect(one_arg_def.action_block).to be_a_kind_of(Proc) expect(explanation_with_aliases_def.condition_block).to be_nil
expect(explanation_with_aliases_def.action_block).to be_a_kind_of(Proc)
expect(explanation_with_aliases_def.parse_params_block).to be_nil
expect(two_args_def.name).to eq(:two_args) expect(dynamic_description_def.name).to eq(:dynamic_description)
expect(two_args_def.aliases).to eq([]) expect(dynamic_description_def.aliases).to eq([])
expect(two_args_def.to_h(noteable: "issue")[:description]).to eq('A dynamic description for ISSUE') expect(dynamic_description_def.to_h(noteable: 'issue')[:description]).to eq('A dynamic description for ISSUE')
expect(two_args_def.params).to eq(['The first argument', 'The second argument']) expect(dynamic_description_def.explanation).to eq('')
expect(two_args_def.condition_block).to be_nil expect(dynamic_description_def.params).to eq(['The first argument', 'The second argument'])
expect(two_args_def.action_block).to be_a_kind_of(Proc) expect(dynamic_description_def.condition_block).to be_nil
expect(dynamic_description_def.action_block).to be_a_kind_of(Proc)
expect(dynamic_description_def.parse_params_block).to be_nil
expect(cc_def.name).to eq(:cc) expect(cc_def.name).to eq(:cc)
expect(cc_def.aliases).to eq([]) expect(cc_def.aliases).to eq([])
expect(cc_def.description).to eq('') expect(cc_def.description).to eq('')
expect(cc_def.explanation).to eq('')
expect(cc_def.params).to eq([]) expect(cc_def.params).to eq([])
expect(cc_def.condition_block).to be_nil expect(cc_def.condition_block).to be_nil
expect(cc_def.action_block).to be_nil expect(cc_def.action_block).to be_nil
expect(cc_def.parse_params_block).to be_nil
expect(cond_action_def.name).to eq(:cond_action) expect(cond_action_def.name).to eq(:cond_action)
expect(cond_action_def.aliases).to eq([]) expect(cond_action_def.aliases).to eq([])
expect(cond_action_def.description).to eq('') expect(cond_action_def.description).to eq('')
expect(cond_action_def.explanation).to be_a_kind_of(Proc)
expect(cond_action_def.params).to eq([]) expect(cond_action_def.params).to eq([])
expect(cond_action_def.condition_block).to be_a_kind_of(Proc) expect(cond_action_def.condition_block).to be_a_kind_of(Proc)
expect(cond_action_def.action_block).to be_a_kind_of(Proc) expect(cond_action_def.action_block).to be_a_kind_of(Proc)
expect(cond_action_def.parse_params_block).to be_nil
expect(with_params_parsing_def.name).to eq(:with_params_parsing)
expect(with_params_parsing_def.aliases).to eq([])
expect(with_params_parsing_def.description).to eq('')
expect(with_params_parsing_def.explanation).to eq('')
expect(with_params_parsing_def.params).to eq([])
expect(with_params_parsing_def.condition_block).to be_nil
expect(with_params_parsing_def.action_block).to be_a_kind_of(Proc)
expect(with_params_parsing_def.parse_params_block).to be_a_kind_of(Proc)
end end
end end
end end
...@@ -111,6 +111,22 @@ describe GeoNode, type: :model do ...@@ -111,6 +111,22 @@ describe GeoNode, type: :model do
end end
end end
describe '#current?' do
subject { described_class.new }
it 'returns true when node is the current node' do
allow(Gitlab::Geo).to receive(:current_node) { subject }
expect(subject.current?).to eq true
end
it 'returns false when node is not the current node' do
allow(Gitlab::Geo).to receive(:current_node) { double }
expect(subject.current?).to eq false
end
end
describe '#uri' do describe '#uri' do
context 'when all fields are filled' do context 'when all fields are filled' do
it 'returns an URI object' do it 'returns an URI object' do
......
...@@ -61,7 +61,7 @@ describe GeoNodeStatusEntity do ...@@ -61,7 +61,7 @@ describe GeoNodeStatusEntity do
describe '#health' do describe '#health' do
context 'when node is healthy' do context 'when node is healthy' do
it 'exposes the health message' do it 'exposes the health message' do
expect(subject[:health]).to eq 'No Health Problems Detected' expect(subject[:health]).to eq 'Healthy'
end end
end end
......
require 'spec_helper'
describe PreviewMarkdownService do
let(:user) { create(:user) }
let(:project) { create(:empty_project) }
before do
project.add_developer(user)
end
describe 'user references' do
let(:params) { { text: "Take a look #{user.to_reference}" } }
let(:service) { described_class.new(project, user, params) }
it 'returns users referenced in text' do
result = service.execute
expect(result[:users]).to eq [user.username]
end
end
context 'new note with slash commands' do
let(:issue) { create(:issue, project: project) }
let(:params) do
{
text: "Please do it\n/assign #{user.to_reference}",
slash_commands_target_type: 'Issue',
slash_commands_target_id: issue.id
}
end
let(:service) { described_class.new(project, user, params) }
it 'removes slash commands from text' do
result = service.execute
expect(result[:text]).to eq 'Please do it'
end
it 'explains slash commands effect' do
result = service.execute
expect(result[:commands]).to eq "Assigns #{user.to_reference}."
end
end
context 'merge request description' do
let(:params) do
{
text: "My work\n/estimate 2y",
slash_commands_target_type: 'MergeRequest'
}
end
let(:service) { described_class.new(project, user, params) }
it 'removes slash commands from text' do
result = service.execute
expect(result[:text]).to eq 'My work'
end
it 'explains slash commands effect' do
result = service.execute
expect(result[:commands]).to eq 'Sets time estimate to 2y.'
end
end
end
...@@ -825,4 +825,222 @@ describe SlashCommands::InterpretService, services: true do ...@@ -825,4 +825,222 @@ describe SlashCommands::InterpretService, services: true do
end end
end end
end end
describe '#explain' do
let(:service) { described_class.new(project, developer) }
let(:merge_request) { create(:merge_request, source_project: project) }
describe 'close command' do
let(:content) { '/close' }
it 'includes issuable name' do
_, explanations = service.explain(content, issue)
expect(explanations).to eq(['Closes this issue.'])
end
end
describe 'reopen command' do
let(:content) { '/reopen' }
let(:merge_request) { create(:merge_request, :closed, source_project: project) }
it 'includes issuable name' do
_, explanations = service.explain(content, merge_request)
expect(explanations).to eq(['Reopens this merge request.'])
end
end
describe 'title command' do
let(:content) { '/title This is new title' }
it 'includes new title' do
_, explanations = service.explain(content, issue)
expect(explanations).to eq(['Changes the title to "This is new title".'])
end
end
describe 'assign command' do
let(:content) { "/assign @#{developer.username} do it!" }
it 'includes only the user reference' do
_, explanations = service.explain(content, merge_request)
expect(explanations).to eq(["Assigns @#{developer.username}."])
end
end
describe 'unassign command' do
let(:content) { '/unassign' }
let(:issue) { create(:issue, project: project, assignee: developer) }
it 'includes current assignee reference' do
_, explanations = service.explain(content, issue)
expect(explanations).to eq(["Removes assignee @#{developer.username}."])
end
end
describe 'milestone command' do
let(:content) { '/milestone %wrong-milestone' }
let!(:milestone) { create(:milestone, project: project, title: '9.10') }
it 'is empty when milestone reference is wrong' do
_, explanations = service.explain(content, issue)
expect(explanations).to eq([])
end
end
describe 'remove milestone command' do
let(:content) { '/remove_milestone' }
let(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) }
it 'includes current milestone name' do
_, explanations = service.explain(content, merge_request)
expect(explanations).to eq(['Removes %"9.10" milestone.'])
end
end
describe 'label command' do
let(:content) { '/label ~missing' }
let!(:label) { create(:label, project: project) }
it 'is empty when there are no correct labels' do
_, explanations = service.explain(content, issue)
expect(explanations).to eq([])
end
end
describe 'unlabel command' do
let(:content) { '/unlabel' }
it 'says all labels if no parameter provided' do
merge_request.update!(label_ids: [bug.id])
_, explanations = service.explain(content, merge_request)
expect(explanations).to eq(['Removes all labels.'])
end
end
describe 'relabel command' do
let(:content) { '/relabel Bug' }
let!(:bug) { create(:label, project: project, title: 'Bug') }
let(:feature) { create(:label, project: project, title: 'Feature') }
it 'includes label name' do
issue.update!(label_ids: [feature.id])
_, explanations = service.explain(content, issue)
expect(explanations).to eq(["Replaces all labels with ~#{bug.id} label."])
end
end
describe 'subscribe command' do
let(:content) { '/subscribe' }
it 'includes issuable name' do
_, explanations = service.explain(content, issue)
expect(explanations).to eq(['Subscribes to this issue.'])
end
end
describe 'unsubscribe command' do
let(:content) { '/unsubscribe' }
it 'includes issuable name' do
merge_request.subscribe(developer, project)
_, explanations = service.explain(content, merge_request)
expect(explanations).to eq(['Unsubscribes from this merge request.'])
end
end
describe 'due command' do
let(:content) { '/due April 1st 2016' }
it 'includes the date' do
_, explanations = service.explain(content, issue)
expect(explanations).to eq(['Sets the due date to Apr 1, 2016.'])
end
end
describe 'wip command' do
let(:content) { '/wip' }
it 'includes the new status' do
_, explanations = service.explain(content, merge_request)
expect(explanations).to eq(['Marks this merge request as Work In Progress.'])
end
end
describe 'award command' do
let(:content) { '/award :confetti_ball: ' }
it 'includes the emoji' do
_, explanations = service.explain(content, issue)
expect(explanations).to eq(['Toggles :confetti_ball: emoji award.'])
end
end
describe 'estimate command' do
let(:content) { '/estimate 79d' }
it 'includes the formatted duration' do
_, explanations = service.explain(content, merge_request)
expect(explanations).to eq(['Sets time estimate to 3mo 3w 4d.'])
end
end
describe 'spend command' do
let(:content) { '/spend -120m' }
it 'includes the formatted duration and proper verb' do
_, explanations = service.explain(content, issue)
expect(explanations).to eq(['Substracts 2h spent time.'])
end
end
describe 'target branch command' do
let(:content) { '/target_branch my-feature ' }
it 'includes the branch name' do
_, explanations = service.explain(content, merge_request)
expect(explanations).to eq(['Sets target branch to my-feature.'])
end
end
describe 'board move command' do
let(:content) { '/board_move ~bug' }
let!(:bug) { create(:label, project: project, title: 'bug') }
let!(:board) { create(:board, project: project) }
it 'includes the label name' do
_, explanations = service.explain(content, issue)
expect(explanations).to eq(["Moves issue to ~#{bug.id} column in the board."])
end
end
# EE-specific tests
describe 'weight command' do
let(:content) { '/weight 4' }
it 'includes the number' do
_, explanations = service.explain(content, issue)
expect(explanations).to eq(['Sets weight to 4.'])
end
end
end
end end
...@@ -257,4 +257,19 @@ shared_examples 'issuable record that supports slash commands in its description ...@@ -257,4 +257,19 @@ shared_examples 'issuable record that supports slash commands in its description
end end
end end
end end
describe "preview of note on #{issuable_type}" do
it 'removes slash commands from note and explains them' do
visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable)
page.within('.js-main-target-form') do
fill_in 'note[note]', with: "Awesome!\n/assign @bob "
click_on 'Preview'
expect(page).to have_content 'Awesome!'
expect(page).not_to have_content '/assign @bob'
expect(page).to have_content 'Assigns @bob.'
end
end
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment