summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLin Jen-Shin <godfat@godfat.org>2016-12-12 23:20:08 +0800
committerLin Jen-Shin <godfat@godfat.org>2016-12-12 23:20:08 +0800
commite5acebd9b99e3207427700144d9c83bcec4c629c (patch)
tree2cbd03d35e4b97046c71aac8471545e4cbe3bf82
parentc0dfa0c609805558b30f11046eedadfb8a14886d (diff)
parent7e9a8bb760cf6f9c7b3aa6b2b98bff30b58bc325 (diff)
downloadgitlab-ce-e5acebd9b99e3207427700144d9c83bcec4c629c.tar.gz
Merge remote-tracking branch 'upstream/master' into fix-git-hooks-when-creating-file
* upstream/master: API: Fix groups filter fix: removed signed_out notification Fix typo in curl example request Fix typo Change docs title to represent the edition remove left_align setting from notification setting dropdown in favor of a pure css solution fix alignment for notification settings ajax response [ci skip] Update "Installation from source" guide for 8.15.0 Group links spec update Stop replacing `$your_email` with the user's email Updates the font weight of button styles because of the change to system fonts Updated JS based on review Fixed group links dropdown to match Allow branch names with dots on API endpoint Use a single query in Projects::ProjectMembersController to fetch members Use gitlab-workhose 1.1.1 Updated members dropdowns Use default btn styling for Housekeeping button on projects settings page fix display hook error message API: Endpoint to expose personal snippets as /snippets Removed leave buttons from settings dropdowns
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--app/assets/javascripts/gl_dropdown.js5
-rw-r--r--app/assets/javascripts/members.js.es665
-rw-r--r--app/assets/javascripts/merge_request_widget.js.es62
-rw-r--r--app/assets/stylesheets/framework/buttons.scss2
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss5
-rw-r--r--app/assets/stylesheets/pages/members.scss4
-rw-r--r--app/assets/stylesheets/pages/notifications.scss4
-rw-r--r--app/assets/stylesheets/pages/projects.scss4
-rw-r--r--app/controllers/projects/project_members_controller.rb11
-rw-r--r--app/controllers/sessions_controller.rb6
-rw-r--r--app/finders/snippets_finder.rb5
-rw-r--r--app/helpers/gitlab_routing_helper.rb5
-rw-r--r--app/policies/personal_snippet_policy.rb5
-rw-r--r--app/views/groups/group_members/update.js.haml1
-rw-r--r--app/views/help/show.html.haml2
-rw-r--r--app/views/layouts/nav/_group_settings.html.haml12
-rw-r--r--app/views/layouts/nav/_project.html.haml17
-rw-r--r--app/views/profiles/notifications/show.html.haml2
-rw-r--r--app/views/projects/edit.html.haml2
-rw-r--r--app/views/projects/group_links/update.js.haml1
-rw-r--r--app/views/projects/project_members/update.js.haml1
-rw-r--r--app/views/shared/members/_access_request_buttons.html.haml26
-rw-r--r--app/views/shared/members/_group.html.haml21
-rw-r--r--app/views/shared/members/_member.html.haml20
-rw-r--r--app/views/shared/notifications/_button.html.haml3
-rw-r--r--app/views/shared/notifications/_notification_dropdown.html.haml3
-rw-r--r--changelogs/unreleased/23305-leave-project-and-leave-group-should-be-buttons.yml5
-rw-r--r--changelogs/unreleased/25294-remove-signed-out-msg.yml4
-rw-r--r--changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml4
-rw-r--r--changelogs/unreleased/features-api-snippets.yml4
-rw-r--r--changelogs/unreleased/issue_24020.yml4
-rw-r--r--changelogs/unreleased/issue_25030.yml4
-rw-r--r--changelogs/unreleased/members-dropdowns.yml4
-rw-r--r--changelogs/unreleased/update-button-font-weight.yml4
-rw-r--r--doc/README.md2
-rw-r--r--doc/api/merge_requests.md2
-rw-r--r--doc/api/snippets.md232
-rw-r--r--doc/ci/review_apps/index.md2
-rw-r--r--doc/install/installation.md4
-rw-r--r--doc/update/8.14-to-8.15.md2
-rw-r--r--features/steps/group/members.rb7
-rw-r--r--features/steps/project/team_management.rb8
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/branches.rb16
-rw-r--r--lib/api/entities.rb13
-rw-r--r--lib/api/groups.rb11
-rw-r--r--lib/api/snippets.rb137
-rw-r--r--lib/gitlab/url_builder.rb2
-rw-r--r--spec/features/groups/members/last_owner_cannot_leave_group_spec.rb4
-rw-r--r--spec/features/groups/members/member_leaves_group_spec.rb2
-rw-r--r--spec/features/groups/members/user_requests_access_spec.rb2
-rw-r--r--spec/features/help_pages_spec.rb10
-rw-r--r--spec/features/projects/members/group_links_spec.rb9
-rw-r--r--spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb2
-rw-r--r--spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb2
-rw-r--r--spec/features/projects/members/member_leaves_project_spec.rb2
-rw-r--r--spec/features/projects/members/owner_cannot_leave_project_spec.rb4
-rw-r--r--spec/finders/snippets_finder_spec.rb59
-rw-r--r--spec/javascripts/merge_request_widget_spec.js12
-rw-r--r--spec/requests/api/branches_spec.rb31
-rw-r--r--spec/requests/api/groups_spec.rb11
-rw-r--r--spec/requests/api/snippets_spec.rb157
-rw-r--r--spec/support/login_helpers.rb3
64 files changed, 888 insertions, 130 deletions
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 9084fa2f716..524cb55242b 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-1.1.0
+1.1.1
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 969778dded7..9a91018a8e4 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -650,6 +650,11 @@
} else if(value) {
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']");
}
+
+ if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) {
+ return;
+ }
+
if (el.hasClass(ACTIVE_CLASS)) {
el.removeClass(ACTIVE_CLASS);
if (field && field.length) {
diff --git a/app/assets/javascripts/members.js.es6 b/app/assets/javascripts/members.js.es6
index 895bc10784f..e3f367a11eb 100644
--- a/app/assets/javascripts/members.js.es6
+++ b/app/assets/javascripts/members.js.es6
@@ -1,38 +1,81 @@
-/* eslint-disable */
-((w) => {
- w.gl = w.gl || {};
+/* eslint-disable class-methods-use-this */
+(() => {
+ window.gl = window.gl || {};
class Members {
constructor() {
this.addListeners();
+ this.initGLDropdown();
}
addListeners() {
$('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow);
- $('.js-member-update-control').off('change').on('change', this.formSubmit);
- $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess);
+ $('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this));
+ $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this));
gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
}
+ initGLDropdown() {
+ $('.js-member-permissions-dropdown').each((i, btn) => {
+ const $btn = $(btn);
+
+ $btn.glDropdown({
+ selectable: true,
+ isSelectable(selected, $el) {
+ return !$el.hasClass('is-active');
+ },
+ fieldName: $btn.data('field-name'),
+ id(selected, $el) {
+ return $el.data('id');
+ },
+ toggleLabel(selected, $el) {
+ return $el.text();
+ },
+ clicked: (selected, $link) => {
+ this.formSubmit(null, $link);
+ },
+ });
+ });
+ }
+
removeRow(e) {
const $target = $(e.target);
if ($target.hasClass('btn-remove')) {
$target.closest('.member')
- .fadeOut(function () {
+ .fadeOut(function fadeOutMemberRow() {
$(this).remove();
});
}
}
- formSubmit() {
- $(this).closest('form').trigger("submit.rails").end().disable();
+ formSubmit(e, $el = null) {
+ const $this = e ? $(e.currentTarget) : $el;
+ const { $toggle, $dateInput } = this.getMemberListItems($this);
+
+ $this.closest('form').trigger('submit.rails');
+
+ $toggle.disable();
+ $dateInput.disable();
}
- formSuccess() {
- $(this).find('.js-member-update-control').enable();
+ formSuccess(e) {
+ const { $toggle, $dateInput } = this.getMemberListItems($(e.currentTarget).closest('.member'));
+
+ $toggle.enable();
+ $dateInput.enable();
+ }
+
+ getMemberListItems($el) {
+ const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('el-id')}`);
+
+ return {
+ $memberListItem,
+ $toggle: $memberListItem.find('.dropdown-menu-toggle'),
+ $dateInput: $memberListItem.find('.js-access-expiration-date'),
+ };
}
}
gl.Members = Members;
-})(window);
+})();
diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6
index a55fe9df0b3..d9495e50388 100644
--- a/app/assets/javascripts/merge_request_widget.js.es6
+++ b/app/assets/javascripts/merge_request_widget.js.es6
@@ -101,7 +101,7 @@
urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
return window.location.href = window.location.pathname + urlSuffix;
} else if (data.merge_error) {
- return this.$widgetBody.html("<h4>" + data.merge_error + "</h4>");
+ return _this.$widgetBody.html("<h4>" + data.merge_error + "</h4>");
} else {
callback = function() {
return merge_request_widget.mergeInProgress(deleteSourceBranch);
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 8da3da2ad08..1c7b2f4df7c 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -1,7 +1,7 @@
@mixin btn-default {
border-radius: 3px;
font-size: $gl-font-size;
- font-weight: 500;
+ font-weight: 400;
padding: $gl-vert-padding $gl-btn-padding;
&:focus,
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 33de652c06f..d5914b900e2 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -42,6 +42,11 @@
border-radius: $border-radius-base;
white-space: nowrap;
+ &[disabled] {
+ background-color: $input-bg-disabled;
+ cursor: not-allowed;
+ }
+
&.no-outline {
outline: 0;
}
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index 756efa9c7fa..5f3bbb40ba0 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -54,6 +54,10 @@
@media (min-width: $screen-sm-min) {
width: 50%;
}
+
+ .dropdown-menu-toggle {
+ width: 100%;
+ }
}
.member-access-text {
diff --git a/app/assets/stylesheets/pages/notifications.scss b/app/assets/stylesheets/pages/notifications.scss
index 94fbbef3c77..7d61390a439 100644
--- a/app/assets/stylesheets/pages/notifications.scss
+++ b/app/assets/stylesheets/pages/notifications.scss
@@ -1,5 +1,9 @@
.notification-list-item {
line-height: 34px;
+
+ .dropdown-menu {
+ @extend .dropdown-menu-align-right;
+ }
}
.notification {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 72b6685d940..6e0f6b1cd81 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -188,6 +188,10 @@
margin-left: 10px;
}
+ .notification-dropdown .dropdown-menu {
+ @extend .dropdown-menu-align-right;
+ }
+
.download-button {
@media (max-width: $screen-md-max) {
margin-left: 0;
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 3fb8bba3cd0..53308948f62 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -35,13 +35,12 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
end
- member_ids = @project_members.pluck(:id)
+ wheres = ["id IN (#{@project_members.select(:id).to_sql})"]
+ wheres << "id IN (#{group_members.select(:id).to_sql})" if group_members
- if group_members
- member_ids += group_members.pluck(:id)
- end
-
- @project_members = Member.where(id: member_ids).order(access_level: :desc).page(params[:page])
+ @project_members = Member.
+ where(wheres.join(' OR ')).
+ order(access_level: :desc).page(params[:page])
@requesters = AccessRequestsFinder.new(@project).execute(current_user)
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 38e7c6f4a48..8c698695202 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -37,6 +37,12 @@ class SessionsController < Devise::SessionsController
end
end
+ def destroy
+ super
+ # hide the signed_out notice
+ flash[:notice] = nil
+ end
+
private
# Handle an "initial setup" state, where there's only one user, it's an admin,
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index 00ff1611039..0586a923a74 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -1,12 +1,15 @@
class SnippetsFinder
def execute(current_user, params = {})
filter = params[:filter]
+ user = params.fetch(:user, current_user)
case filter
when :all then
snippets(current_user).fresh
+ when :public then
+ Snippet.are_public.fresh
when :by_user then
- by_user(current_user, params[:user], params[:scope])
+ by_user(current_user, user, params[:scope])
when :by_project
by_project(current_user, params[:project])
end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index af9087d8326..99db73c9ee0 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -159,6 +159,11 @@ module GitlabRoutingHelper
resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
end
+ # Snippets
+ def personal_snippet_url(snippet, *args)
+ snippet_url(snippet)
+ end
+
# Groups
## Members
diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb
index 46c5aa1a5be..d3913986cd8 100644
--- a/app/policies/personal_snippet_policy.rb
+++ b/app/policies/personal_snippet_policy.rb
@@ -6,9 +6,14 @@ class PersonalSnippetPolicy < BasePolicy
if @subject.author == @user
can! :read_personal_snippet
can! :update_personal_snippet
+ can! :destroy_personal_snippet
can! :admin_personal_snippet
end
+ unless @user.external?
+ can! :create_personal_snippet
+ end
+
if @subject.internal? && !@user.external?
can! :read_personal_snippet
end
diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml
index de8f53b6b52..9d05bff6c4e 100644
--- a/app/views/groups/group_members/update.js.haml
+++ b/app/views/groups/group_members/update.js.haml
@@ -1,3 +1,4 @@
:plain
var $listItem = $('#{escape_javascript(render('shared/members/member', member: @group_member))}');
$("##{dom_id(@group_member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
+ gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@group_member)}"));
diff --git a/app/views/help/show.html.haml b/app/views/help/show.html.haml
index be257b51b9e..f6ebd76af9d 100644
--- a/app/views/help/show.html.haml
+++ b/app/views/help/show.html.haml
@@ -1,3 +1,3 @@
- page_title @path.split("/").reverse.map(&:humanize)
.documentation.wiki
- = markdown @markdown.gsub('$your_email', current_user.try(:email) || "email@example.com")
+ = markdown @markdown
diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml
index c0328fe8842..1579d8f1662 100644
--- a/app/views/layouts/nav/_group_settings.html.haml
+++ b/app/views/layouts/nav/_group_settings.html.haml
@@ -1,10 +1,8 @@
- if current_user
- can_admin_group = can?(current_user, :admin_group, @group)
- can_edit = can?(current_user, :admin_group, @group)
- - member = @group.members.find_by(user_id: current_user.id)
- - can_leave = member && can?(current_user, :destroy_group_member, member)
- - if can_admin_group || can_edit || can_leave
+ - if can_admin_group || can_edit
.controls
.dropdown.group-settings-dropdown
%a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'}
@@ -14,13 +12,7 @@
- if can_admin_group
= nav_link(path: 'groups#projects') do
= link_to 'Projects', projects_group_path(@group), title: 'Projects'
- - if (can_edit || can_leave) && can_admin_group
+ - if can_edit && can_admin_group
%li.divider
- - if can_edit
%li
= link_to 'Edit Group', edit_group_path(@group)
- - if can_leave
- %li
- = link_to polymorphic_path([:leave, @group, :members]),
- data: { confirm: leave_confirmation_message(@group) }, method: :delete, title: 'Leave group' do
- Leave Group
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 7bd11f5727a..904d11c2cf4 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -6,23 +6,14 @@
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
- can_edit = can?(current_user, :admin_project, @project)
- -# We don't use @project.team.find_member because it searches for group members too...
- - member = @project.members.find_by(user_id: current_user.id)
- - can_leave = member && can?(current_user, :destroy_project_member, member)
= render 'layouts/nav/project_settings', can_edit: can_edit
- - if can_edit || can_leave
+ - if can_edit
%li.divider
- - if can_edit
- %li
- = link_to edit_project_path(@project) do
- Edit Project
- - if can_leave
- %li
- = link_to polymorphic_path([:leave, @project, :members]),
- data: { confirm: leave_confirmation_message(@project) }, method: :delete, title: 'Leave project' do
- Leave Project
+ %li
+ = link_to edit_project_path(@project) do
+ Edit Project
.scrolling-tabs-container{ class: nav_control_class }
.fade-left
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index 844fce59704..d79a1a9f368 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -30,7 +30,7 @@
%br
.clearfix
.form-group.pull-left.global-notification-setting
- = render 'shared/notifications/button', notification_setting: @global_notification_setting, left_align: true
+ = render 'shared/notifications/button', notification_setting: @global_notification_setting
.clearfix
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 01cd8fa0938..38e7fc4279c 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -146,7 +146,7 @@
such as compressing file revisions and removing unreachable objects.
.col-lg-9
= link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project),
- method: :post, class: "btn btn-save"
+ method: :post, class: "btn btn-default"
%hr
.row.prepend-top-default
.col-lg-3
diff --git a/app/views/projects/group_links/update.js.haml b/app/views/projects/group_links/update.js.haml
index af9a5b19060..55520fda494 100644
--- a/app/views/projects/group_links/update.js.haml
+++ b/app/views/projects/group_links/update.js.haml
@@ -1,3 +1,4 @@
:plain
var $listItem = $('#{escape_javascript(render('shared/members/group', group_link: @group_link))}');
$("#group_member_#{@group_link.id} .list-item-name").replaceWith($listItem.find('.list-item-name'));
+ gl.utils.localTimeAgo($('.js-timeago'), $("#group_member_#{@group_link.id}"));
diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml
index 91927181efb..d15f4310ff5 100644
--- a/app/views/projects/project_members/update.js.haml
+++ b/app/views/projects/project_members/update.js.haml
@@ -1,3 +1,4 @@
:plain
var $listItem = $('#{escape_javascript(render('shared/members/member', member: @project_member))}');
$("##{dom_id(@project_member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
+ gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@project_member)}"));
diff --git a/app/views/shared/members/_access_request_buttons.html.haml b/app/views/shared/members/_access_request_buttons.html.haml
index eff914398bb..e166dfab710 100644
--- a/app/views/shared/members/_access_request_buttons.html.haml
+++ b/app/views/shared/members/_access_request_buttons.html.haml
@@ -1,10 +1,16 @@
-- if can?(current_user, :request_access, source)
- - if requester = source.requesters.find_by(user_id: current_user.id)
- = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]),
- method: :delete,
- data: { confirm: remove_member_message(requester) },
- class: 'btn'
- - else
- = link_to 'Request Access', polymorphic_path([:request_access, source, :members]),
- method: :post,
- class: 'btn'
+- model_name = source.model_name.to_s.downcase
+
+- if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id))
+ = link_to "Leave #{model_name}", polymorphic_path([:leave, source, :members]),
+ method: :delete,
+ data: { confirm: leave_confirmation_message(source) },
+ class: 'btn'
+- elsif requester = source.requesters.find_by(user_id: current_user.id)
+ = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]),
+ method: :delete,
+ data: { confirm: remove_member_message(requester) },
+ class: 'btn'
+- elsif source.request_access_enabled && can?(current_user, :request_access, source)
+ = link_to 'Request Access', polymorphic_path([:request_access, source, :members]),
+ method: :post,
+ class: 'btn'
diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml
index 1c0346bbc78..8928de9097b 100644
--- a/app/views/shared/members/_group.html.haml
+++ b/app/views/shared/members/_group.html.haml
@@ -1,7 +1,8 @@
- group_link = local_assigns[:group_link]
- group = group_link.group
- can_admin_member = can?(current_user, :admin_project_member, @project)
-%li.member.group_member{ id: "group_member_#{group_link.id}" }
+- dom_id = "group_member_#{group_link.id}"
+%li.member.group_member{ id: dom_id }
%span{ class: "list-item-name" }
= image_tag group_icon(group), class: "avatar s40", alt: ''
%strong
@@ -14,7 +15,23 @@
Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)}
.controls.member-controls
= form_tag namespace_project_group_link_path(@project.namespace, @project, group_link), method: :put, remote: true, class: 'form-horizontal js-edit-member-form' do
- = select_tag 'group_link[group_access]', options_for_select(ProjectGroupLink.access_options, group_link.group_access), class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{group.id}", disabled: !can_admin_member
+ = hidden_field_tag "group_link[group_access]", group_link.group_access
+ .member-form-control.dropdown.append-right-5
+ %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
+ disabled: !can_admin_member,
+ data: { toggle: "dropdown", field_name: "group_link[group_access]" } }
+ %span.dropdown-toggle-text
+ = group_link.human_access
+ = icon("chevron-down")
+ .dropdown-menu.dropdown-select.dropdown-menu-align-right.dropdown-menu-selectable
+ = dropdown_title("Change permissions")
+ .dropdown-content
+ %ul
+ - Gitlab::Access.options.each do |role, role_id|
+ %li
+ = link_to role, "javascript:void(0)",
+ class: ("is-active" if group_link.group_access == role_id),
+ data: { id: role_id, el_id: dom_id }
.prepend-left-5.clearable-input.member-form-control
= text_field_tag 'group_link[expires_at]', group_link.expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{group.id}", disabled: !can_admin_member
%i.clear-icon.js-clear-input
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index e67f7d5a352..659d4c905fc 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -48,9 +48,25 @@
- if show_controls && (member.respond_to?(:group) && @group) || (member.respond_to?(:project) && @project)
- if user != current_user
= form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f|
- = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{member.id}", disabled: !can_admin_member
+ = f.hidden_field :access_level
+ .member-form-control.dropdown.append-right-5
+ %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
+ disabled: !can_admin_member,
+ data: { toggle: "dropdown", field_name: "#{f.object_name}[access_level]" } }
+ %span.dropdown-toggle-text
+ = member.human_access
+ = icon("chevron-down")
+ .dropdown-menu.dropdown-select.dropdown-menu-align-right.dropdown-menu-selectable
+ = dropdown_title("Change permissions")
+ .dropdown-content
+ %ul
+ - Gitlab::Access.options.each do |role, role_id|
+ %li
+ = link_to role, "javascript:void(0)",
+ class: ("is-active" if member.access_level == role_id),
+ data: { id: role_id, el_id: dom_id(member) }
.prepend-left-5.clearable-input.member-form-control
- = f.text_field :expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{member.id}", disabled: !can_admin_member
+ = f.text_field :expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{member.id}", disabled: !can_admin_member, data: { el_id: dom_id(member) }
%i.clear-icon.js-clear-input
- else
%span.member-access-text= member.human_access
diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
index 1f7df0bcd19..fbad0d05de3 100644
--- a/app/views/shared/notifications/_button.html.haml
+++ b/app/views/shared/notifications/_button.html.haml
@@ -1,4 +1,3 @@
-- left_align = local_assigns[:left_align]
- if notification_setting
.dropdown.notification-dropdown
= form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f|
@@ -19,7 +18,7 @@
= notification_title(notification_setting.level)
= icon("caret-down")
- = render "shared/notifications/notification_dropdown", notification_setting: notification_setting, left_align: left_align
+ = render "shared/notifications/notification_dropdown", notification_setting: notification_setting
= content_for :scripts_body do
= render "shared/notifications/custom_notifications", notification_setting: notification_setting
diff --git a/app/views/shared/notifications/_notification_dropdown.html.haml b/app/views/shared/notifications/_notification_dropdown.html.haml
index d3258ee64cb..85ad74f9a39 100644
--- a/app/views/shared/notifications/_notification_dropdown.html.haml
+++ b/app/views/shared/notifications/_notification_dropdown.html.haml
@@ -1,5 +1,4 @@
-- left_align = local_assigns[:left_align]
-%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: [notifications_menu_identifier("dropdown", notification_setting), ("dropdown-menu-align-right" unless left_align)] }
+%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: [notifications_menu_identifier("dropdown", notification_setting)] }
- NotificationSetting.levels.each_key do |level|
- next if level == "custom"
- next if level == "global" && notification_setting.source.nil?
diff --git a/changelogs/unreleased/23305-leave-project-and-leave-group-should-be-buttons.yml b/changelogs/unreleased/23305-leave-project-and-leave-group-should-be-buttons.yml
new file mode 100644
index 00000000000..99dbe4a32a0
--- /dev/null
+++ b/changelogs/unreleased/23305-leave-project-and-leave-group-should-be-buttons.yml
@@ -0,0 +1,5 @@
+---
+title: Moved Leave Project and Leave Group buttons to access_request_buttons from
+ the settings dropdown
+merge_request: 7600
+author:
diff --git a/changelogs/unreleased/25294-remove-signed-out-msg.yml b/changelogs/unreleased/25294-remove-signed-out-msg.yml
new file mode 100644
index 00000000000..567294fe5f7
--- /dev/null
+++ b/changelogs/unreleased/25294-remove-signed-out-msg.yml
@@ -0,0 +1,4 @@
+---
+title: 'fix: removed signed_out notification'
+merge_request: 7958
+author: jnoortheen
diff --git a/changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml b/changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml
new file mode 100644
index 00000000000..0770f9752a0
--- /dev/null
+++ b/changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml
@@ -0,0 +1,4 @@
+---
+title: Changed Housekeeping button on project settings page to default styling
+merge_request:
+author: Ryan Harris
diff --git a/changelogs/unreleased/features-api-snippets.yml b/changelogs/unreleased/features-api-snippets.yml
new file mode 100644
index 00000000000..80c7bb75359
--- /dev/null
+++ b/changelogs/unreleased/features-api-snippets.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Endpoint to expose personal snippets as /snippets'
+merge_request: 6373
+author: Bernard Guyzmo Pratz
diff --git a/changelogs/unreleased/issue_24020.yml b/changelogs/unreleased/issue_24020.yml
new file mode 100644
index 00000000000..87310b75296
--- /dev/null
+++ b/changelogs/unreleased/issue_24020.yml
@@ -0,0 +1,4 @@
+---
+title: "fix display hook error message"
+merge_request: 7775
+author: basyura
diff --git a/changelogs/unreleased/issue_25030.yml b/changelogs/unreleased/issue_25030.yml
new file mode 100644
index 00000000000..e18b8d6a79b
--- /dev/null
+++ b/changelogs/unreleased/issue_25030.yml
@@ -0,0 +1,4 @@
+---
+title: Allow branch names with dots on API endpoint
+merge_request:
+author:
diff --git a/changelogs/unreleased/members-dropdowns.yml b/changelogs/unreleased/members-dropdowns.yml
new file mode 100644
index 00000000000..b15403d6d62
--- /dev/null
+++ b/changelogs/unreleased/members-dropdowns.yml
@@ -0,0 +1,4 @@
+---
+title: Updated members dropdowns
+merge_request:
+author:
diff --git a/changelogs/unreleased/update-button-font-weight.yml b/changelogs/unreleased/update-button-font-weight.yml
new file mode 100644
index 00000000000..ddb3c1c8da4
--- /dev/null
+++ b/changelogs/unreleased/update-button-font-weight.yml
@@ -0,0 +1,4 @@
+---
+title: Updates the font weight of button styles because of the change to system fonts
+merge_request:
+author:
diff --git a/doc/README.md b/doc/README.md
index 66c8c26e4f0..eba1e9845b1 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -1,4 +1,4 @@
-# Documentation
+# GitLab Community Edition documentation
## User documentation
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 81df55ab4ab..662cc9da733 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -429,7 +429,7 @@ DELETE /projects/:id/merge_requests/:merge_request_id
| `merge_request_id` | integer | yes | The ID of a project's merge request |
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_request/85
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_requests/85
```
## Accept MR
diff --git a/doc/api/snippets.md b/doc/api/snippets.md
new file mode 100644
index 00000000000..5a5dc162ffe
--- /dev/null
+++ b/doc/api/snippets.md
@@ -0,0 +1,232 @@
+# Snippets
+
+> [Introduced][ce-6373] in GitLab 8.15.
+
+### Snippet visibility level
+
+Snippets in GitLab can be either private, internal, or public.
+You can set it with the `visibility_level` field in the snippet.
+
+Constants for snippet visibility levels are:
+
+| Visibility | Visibility level | Description |
+| ---------- | ---------------- | ----------- |
+| Private | `0` | The snippet is visible only to the snippet creator |
+| Internal | `10` | The snippet is visible for any logged in user |
+| Public | `20` | The snippet can be accessed without any authentication |
+
+## List snippets
+
+Get a list of current user's snippets.
+
+```
+GET /snippets
+```
+
+## Single snippet
+
+Get a single snippet.
+
+```
+GET /snippets/:id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | Integer | yes | The ID of a snippet |
+
+``` bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets/1
+```
+
+Example response:
+
+``` json
+{
+ "id": 1,
+ "title": "test",
+ "file_name": "add.rb",
+ "author": {
+ "id": 1,
+ "username": "john_smith",
+ "email": "john@example.com",
+ "name": "John Smith",
+ "state": "active",
+ "created_at": "2012-05-23T08:00:58Z"
+ },
+ "expires_at": null,
+ "updated_at": "2012-06-28T10:52:04Z",
+ "created_at": "2012-06-28T10:52:04Z",
+ "web_url": "http://example.com/snippets/1",
+}
+```
+
+## Create new snippet
+
+Creates a new snippet. The user must have permission to create new snippets.
+
+```
+POST /snippets
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `title` | String | yes | The title of a snippet |
+| `file_name` | String | yes | The name of a snippet file |
+| `content` | String | yes | The content of a snippet |
+| `visibility_level` | Integer | yes | The snippet's visibility |
+
+
+``` bash
+curl --request POST --data '{"title": "This is a snippet", "content": "Hello world", "file_name": "test.txt", "visibility_level": 10 }' --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets
+```
+
+Example response:
+
+``` json
+{
+ "id": 1,
+ "title": "This is a snippet",
+ "file_name": "test.txt",
+ "author": {
+ "id": 1,
+ "username": "john_smith",
+ "email": "john@example.com",
+ "name": "John Smith",
+ "state": "active",
+ "created_at": "2012-05-23T08:00:58Z"
+ },
+ "expires_at": null,
+ "updated_at": "2012-06-28T10:52:04Z",
+ "created_at": "2012-06-28T10:52:04Z",
+ "web_url": "http://example.com/snippets/1",
+}
+```
+
+## Update snippet
+
+Updates an existing snippet. The user must have permission to change an existing snippet.
+
+```
+PUT /snippets/:id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | Integer | yes | The ID of a snippet |
+| `title` | String | no | The title of a snippet |
+| `file_name` | String | no | The name of a snippet file |
+| `content` | String | no | The content of a snippet |
+| `visibility_level` | Integer | no | The snippet's visibility |
+
+
+``` bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data '{"title": "foo", "content": "bar"}' https://gitlab.example.com/api/v3/snippets/1
+```
+
+Example response:
+
+``` json
+{
+ "id": 1,
+ "title": "test",
+ "file_name": "add.rb",
+ "author": {
+ "id": 1,
+ "username": "john_smith",
+ "email": "john@example.com",
+ "name": "John Smith",
+ "state": "active",
+ "created_at": "2012-05-23T08:00:58Z"
+ },
+ "expires_at": null,
+ "updated_at": "2012-06-28T10:52:04Z",
+ "created_at": "2012-06-28T10:52:04Z",
+ "web_url": "http://example.com/snippets/1",
+}
+```
+
+## Delete snippet
+
+Deletes an existing snippet.
+
+```
+DELETE /snippets/:id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | Integer | yes | The ID of a snippet |
+
+
+```
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/snippets/1"
+```
+
+upon successful delete a `204 No content` HTTP code shall be expected, with no data,
+but if the snippet is non-existent, a `404 Not Found` will be returned.
+
+## Explore all public snippets
+
+```
+GET /snippets/public
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `per_page` | Integer | no | number of snippets to return per page |
+| `page` | Integer | no | the page to retrieve |
+
+``` bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets/public?per_page=2&page=1
+```
+
+Example response:
+
+``` json
+[
+ {
+ "author": {
+ "avatar_url": "http://www.gravatar.com/avatar/edaf55a9e363ea263e3b981d09e0f7f7?s=80&d=identicon",
+ "id": 12,
+ "name": "Libby Rolfson",
+ "state": "active",
+ "username": "elton_wehner",
+ "web_url": "http://localhost:3000/elton_wehner"
+ },
+ "created_at": "2016-11-25T16:53:34.504Z",
+ "file_name": "oconnerrice.rb",
+ "id": 49,
+ "raw_url": "http://localhost:3000/snippets/49/raw",
+ "title": "Ratione cupiditate et laborum temporibus.",
+ "updated_at": "2016-11-25T16:53:34.504Z",
+ "web_url": "http://localhost:3000/snippets/49"
+ },
+ {
+ "author": {
+ "avatar_url": "http://www.gravatar.com/avatar/36583b28626de71061e6e5a77972c3bd?s=80&d=identicon",
+ "id": 16,
+ "name": "Llewellyn Flatley",
+ "state": "active",
+ "username": "adaline",
+ "web_url": "http://localhost:3000/adaline"
+ },
+ "created_at": "2016-11-25T16:53:34.479Z",
+ "file_name": "muellershields.rb",
+ "id": 48,
+ "raw_url": "http://localhost:3000/snippets/48/raw",
+ "title": "Minus similique nesciunt vel fugiat qui ullam sunt.",
+ "updated_at": "2016-11-25T16:53:34.479Z",
+ "web_url": "http://localhost:3000/snippets/48"
+ }
+]
+```
+
diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md
index a66165dc973..c679ea4e298 100644
--- a/doc/ci/review_apps/index.md
+++ b/doc/ci/review_apps/index.md
@@ -33,7 +33,7 @@ built and deployed under a dynamic environment and can be previewed with an
also dynamically URL.
The details of the Review Apps implementation depend widely on your real
-technology stack and on your deployment process. The simplest case it to
+technology stack and on your deployment process. The simplest case is to
deploy a simple static HTML website, but it will not be that straightforward
when your app is using a database for example. To make a branch be deployed
on a temporary instance and booting up this instance with all required software
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 9c6a9656557..2740b2982b9 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -271,9 +271,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-14-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-15-stable gitlab
-**Note:** You can change `8-14-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `8-15-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
diff --git a/doc/update/8.14-to-8.15.md b/doc/update/8.14-to-8.15.md
index 3f58493fa63..5556dae2551 100644
--- a/doc/update/8.14-to-8.15.md
+++ b/doc/update/8.14-to-8.15.md
@@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-15-stable-ee
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags
-sudo -u git -H git checkout v4.0.0
+sudo -u git -H git checkout v4.0.3
```
### 6. Update gitlab-workhorse
diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb
index cefc55d07ab..adaf375453c 100644
--- a/features/steps/group/members.rb
+++ b/features/steps/group/members.rb
@@ -117,7 +117,12 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
member = mary_jane_member
page.within "#group_member_#{member.id}" do
- select 'Developer', from: "member_access_level_#{member.id}"
+ click_button member.human_access
+
+ page.within '.dropdown-menu' do
+ click_link 'Developer'
+ end
+
wait_for_ajax
end
end
diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb
index b21d0849ad1..22d971fadfb 100644
--- a/features/steps/project/team_management.rb
+++ b/features/steps/project/team_management.rb
@@ -65,7 +65,11 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
user = User.find_by(name: 'Dmitriy')
project_member = project.project_members.find_by(user_id: user.id)
page.within "#project_member_#{project_member.id}" do
- select "Reporter", from: "member_access_level_#{project_member.id}"
+ click_button project_member.human_access
+
+ page.within '.dropdown-menu' do
+ click_link 'Reporter'
+ end
end
end
@@ -144,7 +148,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
step 'I should see "Opensource" group user listing' do
page.within '.project-members-groups' do
expect(page).to have_content('OpenSource')
- expect(find('select').value).to eq('40')
+ expect(first('.group_member')).to have_content('Master')
end
end
end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 67109ceeef9..cec2702e44d 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -64,6 +64,7 @@ module API
mount ::API::Session
mount ::API::Settings
mount ::API::SidekiqMetrics
+ mount ::API::Snippets
mount ::API::Subscriptions
mount ::API::SystemHooks
mount ::API::Tags
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 73aed624ea7..0950c3d2e88 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -23,9 +23,9 @@ module API
success Entities::RepoBranch
end
params do
- requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch'
+ requires :branch, type: String, desc: 'The name of the branch'
end
- get ':id/repository/branches/:branch' do
+ get ':id/repository/branches/:branch', requirements: { branch: /.+/ } do
branch = user_project.repository.find_branch(params[:branch])
not_found!("Branch") unless branch
@@ -39,11 +39,11 @@ module API
success Entities::RepoBranch
end
params do
- requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch'
+ requires :branch, type: String, desc: 'The name of the branch'
optional :developers_can_push, type: Boolean, desc: 'Flag if developers can push to that branch'
optional :developers_can_merge, type: Boolean, desc: 'Flag if developers can merge to that branch'
end
- put ':id/repository/branches/:branch/protect' do
+ put ':id/repository/branches/:branch/protect', requirements: { branch: /.+/ } do
authorize_admin_project
branch = user_project.repository.find_branch(params[:branch])
@@ -76,9 +76,9 @@ module API
success Entities::RepoBranch
end
params do
- requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch'
+ requires :branch, type: String, desc: 'The name of the branch'
end
- put ':id/repository/branches/:branch/unprotect' do
+ put ':id/repository/branches/:branch/unprotect', requirements: { branch: /.+/ } do
authorize_admin_project
branch = user_project.repository.find_branch(params[:branch])
@@ -112,9 +112,9 @@ module API
desc 'Delete a branch'
params do
- requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch'
+ requires :branch, type: String, desc: 'The name of the branch'
end
- delete ":id/repository/branches/:branch" do
+ delete ":id/repository/branches/:branch", requirements: { branch: /.+/ } do
authorize_push_project
result = DeleteBranchService.new(user_project, current_user).
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 006d5f9f44e..01c0f5072ba 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -201,6 +201,19 @@ module API
end
end
+ class PersonalSnippet < Grape::Entity
+ expose :id, :title, :file_name
+ expose :author, using: Entities::UserBasic
+ expose :updated_at, :created_at
+
+ expose :web_url do |snippet|
+ Gitlab::UrlBuilder.build(snippet)
+ end
+ expose :raw_url do |snippet|
+ Gitlab::UrlBuilder.build(snippet) + "/raw"
+ end
+ end
+
class ProjectEntity < Grape::Entity
expose :id, :iid
expose(:project_id) { |entity| entity.project.id }
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index fbf7513302b..105d3ee342e 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -1,7 +1,7 @@
module API
class Groups < Grape::API
include PaginationParams
-
+
before { authenticate! }
helpers do
@@ -117,11 +117,20 @@ module API
success Entities::Project
end
params do
+ optional :archived, type: Boolean, default: false, desc: 'Limit by archived status'
+ optional :visibility, type: String, values: %w[public internal private],
+ desc: 'Limit by visibility'
+ optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria'
+ optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at],
+ default: 'created_at', desc: 'Return projects ordered by field'
+ optional :sort, type: String, values: %w[asc desc], default: 'desc',
+ desc: 'Return projects sorted in ascending and descending order'
use :pagination
end
get ":id/projects" do
group = find_group!(params[:id])
projects = GroupProjectsFinder.new(group).execute(current_user)
+ projects = filter_projects(projects)
present paginate(projects), with: Entities::Project, user: current_user
end
diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb
new file mode 100644
index 00000000000..e096e636806
--- /dev/null
+++ b/lib/api/snippets.rb
@@ -0,0 +1,137 @@
+module API
+ # Snippets API
+ class Snippets < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ resource :snippets do
+ helpers do
+ def snippets_for_current_user
+ SnippetsFinder.new.execute(current_user, filter: :by_user, user: current_user)
+ end
+
+ def public_snippets
+ SnippetsFinder.new.execute(current_user, filter: :public)
+ end
+ end
+
+ desc 'Get a snippets list for authenticated user' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success Entities::PersonalSnippet
+ end
+ params do
+ use :pagination
+ end
+ get do
+ present paginate(snippets_for_current_user), with: Entities::PersonalSnippet
+ end
+
+ desc 'List all public snippets current_user has access to' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success Entities::PersonalSnippet
+ end
+ params do
+ use :pagination
+ end
+ get 'public' do
+ present paginate(public_snippets), with: Entities::PersonalSnippet
+ end
+
+ desc 'Get a single snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success Entities::PersonalSnippet
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of a snippet'
+ end
+ get ':id' do
+ snippet = snippets_for_current_user.find(params[:id])
+ present snippet, with: Entities::PersonalSnippet
+ end
+
+ desc 'Create new snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success Entities::PersonalSnippet
+ end
+ params do
+ requires :title, type: String, desc: 'The title of a snippet'
+ requires :file_name, type: String, desc: 'The name of a snippet file'
+ requires :content, type: String, desc: 'The content of a snippet'
+ optional :visibility_level, type: Integer,
+ values: Gitlab::VisibilityLevel.values,
+ default: Gitlab::VisibilityLevel::INTERNAL,
+ desc: 'The visibility level of the snippet'
+ end
+ post do
+ attrs = declared_params(include_missing: false)
+ snippet = CreateSnippetService.new(nil, current_user, attrs).execute
+
+ if snippet.persisted?
+ present snippet, with: Entities::PersonalSnippet
+ else
+ render_validation_error!(snippet)
+ end
+ end
+
+ desc 'Update an existing snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success Entities::PersonalSnippet
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of a snippet'
+ optional :title, type: String, desc: 'The title of a snippet'
+ optional :file_name, type: String, desc: 'The name of a snippet file'
+ optional :content, type: String, desc: 'The content of a snippet'
+ optional :visibility_level, type: Integer,
+ values: Gitlab::VisibilityLevel.values,
+ desc: 'The visibility level of the snippet'
+ at_least_one_of :title, :file_name, :content, :visibility_level
+ end
+ put ':id' do
+ snippet = snippets_for_current_user.find_by(id: params.delete(:id))
+ return not_found!('Snippet') unless snippet
+ authorize! :update_personal_snippet, snippet
+
+ attrs = declared_params(include_missing: false)
+
+ UpdateSnippetService.new(nil, current_user, snippet, attrs).execute
+ if snippet.persisted?
+ present snippet, with: Entities::PersonalSnippet
+ else
+ render_validation_error!(snippet)
+ end
+ end
+
+ desc 'Remove snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success Entities::PersonalSnippet
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of a snippet'
+ end
+ delete ':id' do
+ snippet = snippets_for_current_user.find_by(id: params.delete(:id))
+ return not_found!('Snippet') unless snippet
+ authorize! :destroy_personal_snippet, snippet
+ snippet.destroy
+ no_content!
+ end
+
+ desc 'Get a raw snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of a snippet'
+ end
+ get ":id/raw" do
+ snippet = snippets_for_current_user.find_by(id: params.delete(:id))
+ return not_found!('Snippet') unless snippet
+
+ env['api.format'] = :txt
+ content_type 'text/plain'
+ present snippet.content
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb
index 99d0c28e749..ccb456bcc94 100644
--- a/lib/gitlab/url_builder.rb
+++ b/lib/gitlab/url_builder.rb
@@ -24,6 +24,8 @@ module Gitlab
wiki_page_url
when ProjectSnippet
project_snippet_url(object)
+ when Snippet
+ personal_snippet_url(object)
else
raise NotImplementedError.new("No URL builder defined for #{object.class}")
end
diff --git a/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb b/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb
index 33bf6d3752f..be60b0489c7 100644
--- a/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb
+++ b/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb
@@ -10,7 +10,7 @@ feature 'Groups > Members > Last owner cannot leave group', feature: true do
visit group_path(group)
end
- scenario 'user does not see a "Leave Group" link' do
- expect(page).not_to have_content 'Leave Group'
+ scenario 'user does not see a "Leave group" link' do
+ expect(page).not_to have_content 'Leave group'
end
end
diff --git a/spec/features/groups/members/member_leaves_group_spec.rb b/spec/features/groups/members/member_leaves_group_spec.rb
index 3185ff924b9..ac4d94658ae 100644
--- a/spec/features/groups/members/member_leaves_group_spec.rb
+++ b/spec/features/groups/members/member_leaves_group_spec.rb
@@ -13,7 +13,7 @@ feature 'Groups > Members > Member leaves group', feature: true do
end
scenario 'user leaves group' do
- click_link 'Leave Group'
+ click_link 'Leave group'
expect(current_path).to eq(dashboard_groups_path)
expect(group.users.exists?(user.id)).to be_falsey
diff --git a/spec/features/groups/members/user_requests_access_spec.rb b/spec/features/groups/members/user_requests_access_spec.rb
index d8c9c487996..e4b5ea91bd3 100644
--- a/spec/features/groups/members/user_requests_access_spec.rb
+++ b/spec/features/groups/members/user_requests_access_spec.rb
@@ -29,7 +29,7 @@ feature 'Groups > Members > User requests access', feature: true do
expect(page).to have_content 'Your request for access has been queued for review.'
expect(page).to have_content 'Withdraw Access Request'
- expect(page).not_to have_content 'Leave Group'
+ expect(page).not_to have_content 'Leave group'
end
scenario 'user does not see private projects' do
diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb
index 4319d6db0d2..40a1fced8d8 100644
--- a/spec/features/help_pages_spec.rb
+++ b/spec/features/help_pages_spec.rb
@@ -1,16 +1,6 @@
require 'spec_helper'
describe 'Help Pages', feature: true do
- describe 'Show SSH page' do
- before do
- login_as :user
- end
- it 'replaces the variable $your_email with the email of the user' do
- visit help_page_path('ssh/README')
- expect(page).to have_content("ssh-keygen -t rsa -C \"#{@user.email}\"")
- end
- end
-
describe 'Get the main help page' do
shared_examples_for 'help page' do |prefix: ''|
it 'prefixes links correctly' do
diff --git a/spec/features/projects/members/group_links_spec.rb b/spec/features/projects/members/group_links_spec.rb
index cc2f695211c..94995f7cf95 100644
--- a/spec/features/projects/members/group_links_spec.rb
+++ b/spec/features/projects/members/group_links_spec.rb
@@ -16,12 +16,17 @@ feature 'Projects > Members > Anonymous user sees members', feature: true, js: t
end
it 'updates group access level' do
- select 'Guest', from: "member_access_level_#{group.id}"
+ click_button @group_link.human_access
+
+ page.within '.dropdown-menu' do
+ click_link 'Guest'
+ end
+
wait_for_ajax
visit namespace_project_project_members_path(project.namespace, project)
- expect(page).to have_select("member_access_level_#{group.id}", selected: 'Guest')
+ expect(first('.group_member')).to have_content('Guest')
end
it 'updates expiry date' do
diff --git a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
index 728c0e16361..b483ba4c54c 100644
--- a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
+++ b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
@@ -12,6 +12,6 @@ feature 'Projects > Members > Group member cannot leave group project', feature:
end
scenario 'user does not see a "Leave project" link' do
- expect(page).not_to have_content 'Leave Project'
+ expect(page).not_to have_content 'Leave project'
end
end
diff --git a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
index 4973e0aee85..bdeeef57273 100644
--- a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
+++ b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Projects > Members > Group requester cannot request access to project', feature: true do
+feature 'Projects > Members > Group requester cannot request access to project', feature: true, js: true do
let(:user) { create(:user) }
let(:owner) { create(:user) }
let(:group) { create(:group, :public, :access_requestable) }
diff --git a/spec/features/projects/members/member_leaves_project_spec.rb b/spec/features/projects/members/member_leaves_project_spec.rb
index 79dec442818..5daa932e4e6 100644
--- a/spec/features/projects/members/member_leaves_project_spec.rb
+++ b/spec/features/projects/members/member_leaves_project_spec.rb
@@ -11,7 +11,7 @@ feature 'Projects > Members > Member leaves project', feature: true do
end
scenario 'user leaves project' do
- click_link 'Leave Project'
+ click_link 'Leave project'
expect(current_path).to eq(dashboard_projects_path)
expect(project.users.exists?(user.id)).to be_falsey
diff --git a/spec/features/projects/members/owner_cannot_leave_project_spec.rb b/spec/features/projects/members/owner_cannot_leave_project_spec.rb
index 6e948b7a616..b26d55c5d5d 100644
--- a/spec/features/projects/members/owner_cannot_leave_project_spec.rb
+++ b/spec/features/projects/members/owner_cannot_leave_project_spec.rb
@@ -8,7 +8,7 @@ feature 'Projects > Members > Owner cannot leave project', feature: true do
visit namespace_project_path(project.namespace, project)
end
- scenario 'user does not see a "Leave Project" link' do
- expect(page).not_to have_content 'Leave Project'
+ scenario 'user does not see a "Leave project" link' do
+ expect(page).not_to have_content 'Leave project'
end
end
diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb
index 28bdc18e840..4427443208a 100644
--- a/spec/finders/snippets_finder_spec.rb
+++ b/spec/finders/snippets_finder_spec.rb
@@ -9,65 +9,74 @@ describe SnippetsFinder do
let(:project2) { create(:empty_project, :private, group: group) }
context ':all filter' do
- before do
- @snippet1 = create(:personal_snippet, :private)
- @snippet2 = create(:personal_snippet, :internal)
- @snippet3 = create(:personal_snippet, :public)
- end
+ let!(:snippet1) { create(:personal_snippet, :private) }
+ let!(:snippet2) { create(:personal_snippet, :internal) }
+ let!(:snippet3) { create(:personal_snippet, :public) }
it "returns all private and internal snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :all)
- expect(snippets).to include(@snippet2, @snippet3)
- expect(snippets).not_to include(@snippet1)
+ expect(snippets).to include(snippet2, snippet3)
+ expect(snippets).not_to include(snippet1)
end
it "returns all public snippets" do
snippets = SnippetsFinder.new.execute(nil, filter: :all)
- expect(snippets).to include(@snippet3)
- expect(snippets).not_to include(@snippet1, @snippet2)
+ expect(snippets).to include(snippet3)
+ expect(snippets).not_to include(snippet1, snippet2)
end
end
- context ':by_user filter' do
- before do
- @snippet1 = create(:personal_snippet, :private, author: user)
- @snippet2 = create(:personal_snippet, :internal, author: user)
- @snippet3 = create(:personal_snippet, :public, author: user)
+ context ':public filter' do
+ let!(:snippet1) { create(:personal_snippet, :private) }
+ let!(:snippet2) { create(:personal_snippet, :internal) }
+ let!(:snippet3) { create(:personal_snippet, :public) }
+
+ it "returns public public snippets" do
+ snippets = SnippetsFinder.new.execute(nil, filter: :public)
+
+ expect(snippets).to include(snippet3)
+ expect(snippets).not_to include(snippet1, snippet2)
end
+ end
+
+ context ':by_user filter' do
+ let!(:snippet1) { create(:personal_snippet, :private, author: user) }
+ let!(:snippet2) { create(:personal_snippet, :internal, author: user) }
+ let!(:snippet3) { create(:personal_snippet, :public, author: user) }
it "returns all public and internal snippets" do
snippets = SnippetsFinder.new.execute(user1, filter: :by_user, user: user)
- expect(snippets).to include(@snippet2, @snippet3)
- expect(snippets).not_to include(@snippet1)
+ expect(snippets).to include(snippet2, snippet3)
+ expect(snippets).not_to include(snippet1)
end
it "returns internal snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_internal")
- expect(snippets).to include(@snippet2)
- expect(snippets).not_to include(@snippet1, @snippet3)
+ expect(snippets).to include(snippet2)
+ expect(snippets).not_to include(snippet1, snippet3)
end
it "returns private snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_private")
- expect(snippets).to include(@snippet1)
- expect(snippets).not_to include(@snippet2, @snippet3)
+ expect(snippets).to include(snippet1)
+ expect(snippets).not_to include(snippet2, snippet3)
end
it "returns public snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_public")
- expect(snippets).to include(@snippet3)
- expect(snippets).not_to include(@snippet1, @snippet2)
+ expect(snippets).to include(snippet3)
+ expect(snippets).not_to include(snippet1, snippet2)
end
it "returns all snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user)
- expect(snippets).to include(@snippet1, @snippet2, @snippet3)
+ expect(snippets).to include(snippet1, snippet2, snippet3)
end
it "returns only public snippets if unauthenticated user" do
snippets = SnippetsFinder.new.execute(nil, filter: :by_user, user: user)
- expect(snippets).to include(@snippet3)
- expect(snippets).not_to include(@snippet2, @snippet1)
+ expect(snippets).to include(snippet3)
+ expect(snippets).not_to include(snippet2, snippet1)
end
end
diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js
index 62890f1ca96..6f91529db00 100644
--- a/spec/javascripts/merge_request_widget_spec.js
+++ b/spec/javascripts/merge_request_widget_spec.js
@@ -106,6 +106,18 @@
});
});
+ describe('mergeInProgress', function() {
+ it('should display error with h4 tag', function() {
+ spyOn(this.class.$widgetBody, 'html').and.callFake(function(html) {
+ expect(html).toBe('<h4>Sorry, something went wrong.</h4>');
+ });
+ spyOn($, 'ajax').and.callFake(function(e) {
+ e.success({ merge_error: 'Sorry, something went wrong.' });
+ });
+ this.class.mergeInProgress(null);
+ });
+ });
+
return describe('getCIStatus', function() {
beforeEach(function() {
this.ciStatusData = {
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 55b8c8c0c69..2878e0cb59b 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -11,6 +11,7 @@ describe API::Branches, api: true do
let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
let!(:branch_name) { 'feature' }
let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
+ let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") }
describe "GET /projects/:id/repository/branches" do
it "returns an array of project branches" do
@@ -37,6 +38,13 @@ describe API::Branches, api: true do
expect(json_response['developers_can_merge']).to eq(false)
end
+ it "returns the branch information for a single branch with dots in the name" do
+ get api("/projects/#{project.id}/repository/branches/with.1.2.3", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq("with.1.2.3")
+ end
+
context 'on a merged branch' do
it "returns the branch information for a single branch" do
get api("/projects/#{project.id}/repository/branches/merge-test", user)
@@ -71,6 +79,14 @@ describe API::Branches, api: true do
expect(json_response['developers_can_merge']).to eq(false)
end
+ it "protects a single branch with dots in the name" do
+ put api("/projects/#{project.id}/repository/branches/with.1.2.3/protect", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq("with.1.2.3")
+ expect(json_response['protected']).to eq(true)
+ end
+
it 'protects a single branch and developers can push' do
put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
developers_can_push: true
@@ -220,6 +236,14 @@ describe API::Branches, api: true do
expect(json_response['protected']).to eq(false)
end
+ it "update branches with dots in branch name" do
+ put api("/projects/#{project.id}/repository/branches/with.1.2.3/unprotect", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq("with.1.2.3")
+ expect(json_response['protected']).to eq(false)
+ end
+
it "returns success when unprotect branch" do
put api("/projects/#{project.id}/repository/branches/unknown/unprotect", user)
expect(response).to have_http_status(404)
@@ -292,6 +316,13 @@ describe API::Branches, api: true do
expect(json_response['branch_name']).to eq(branch_name)
end
+ it "removes a branch with dots in the branch name" do
+ delete api("/projects/#{project.id}/repository/branches/with.1.2.3", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['branch_name']).to eq("with.1.2.3")
+ end
+
it 'returns 404 if branch not exists' do
delete api("/projects/#{project.id}/repository/branches/foobar", user)
expect(response).to have_http_status(404)
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 548ed8e1892..15647b262b6 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -245,6 +245,17 @@ describe API::Groups, api: true do
expect(project_names).to match_array([project1.name, project3.name])
end
+ it 'filters the groups projects' do
+ public_projet = create(:project, :public, path: 'test1', group: group1)
+
+ get api("/groups/#{group1.id}/projects", user1), visibility: 'public'
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an(Array)
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['name']).to eq(public_projet.name)
+ end
+
it "does not return a non existing group" do
get api("/groups/1328/projects", user1)
expect(response).to have_http_status(404)
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
new file mode 100644
index 00000000000..f6fb6ea5506
--- /dev/null
+++ b/spec/requests/api/snippets_spec.rb
@@ -0,0 +1,157 @@
+require 'rails_helper'
+
+describe API::Snippets, api: true do
+ include ApiHelpers
+ let!(:user) { create(:user) }
+
+ describe 'GET /snippets/' do
+ it 'returns snippets available' do
+ public_snippet = create(:personal_snippet, :public, author: user)
+ private_snippet = create(:personal_snippet, :private, author: user)
+ internal_snippet = create(:personal_snippet, :internal, author: user)
+
+ get api("/snippets/", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
+ public_snippet.id,
+ internal_snippet.id,
+ private_snippet.id)
+ expect(json_response.last).to have_key('web_url')
+ expect(json_response.last).to have_key('raw_url')
+ end
+
+ it 'hides private snippets from regular user' do
+ create(:personal_snippet, :private)
+
+ get api("/snippets/", user)
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to eq(0)
+ end
+ end
+
+ describe 'GET /snippets/public' do
+ let!(:other_user) { create(:user) }
+ let!(:public_snippet) { create(:personal_snippet, :public, author: user) }
+ let!(:private_snippet) { create(:personal_snippet, :private, author: user) }
+ let!(:internal_snippet) { create(:personal_snippet, :internal, author: user) }
+ let!(:public_snippet_other) { create(:personal_snippet, :public, author: other_user) }
+ let!(:private_snippet_other) { create(:personal_snippet, :private, author: other_user) }
+ let!(:internal_snippet_other) { create(:personal_snippet, :internal, author: other_user) }
+
+ it 'returns all snippets with public visibility from all users' do
+ get api("/snippets/public", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
+ public_snippet.id,
+ public_snippet_other.id)
+ expect(json_response.map{ |snippet| snippet['web_url']} ).to include(
+ "http://localhost/snippets/#{public_snippet.id}",
+ "http://localhost/snippets/#{public_snippet_other.id}")
+ expect(json_response.map{ |snippet| snippet['raw_url']} ).to include(
+ "http://localhost/snippets/#{public_snippet.id}/raw",
+ "http://localhost/snippets/#{public_snippet_other.id}/raw")
+ end
+ end
+
+ describe 'GET /snippets/:id/raw' do
+ let(:snippet) { create(:personal_snippet, author: user) }
+
+ it 'returns raw text' do
+ get api("/snippets/#{snippet.id}/raw", user)
+
+ expect(response).to have_http_status(200)
+ expect(response.content_type).to eq 'text/plain'
+ expect(response.body).to eq(snippet.content)
+ end
+
+ it 'returns 404 for invalid snippet id' do
+ delete api("/snippets/1234", user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Snippet Not Found')
+ end
+ end
+
+ describe 'POST /snippets/' do
+ let(:params) do
+ {
+ title: 'Test Title',
+ file_name: 'test.rb',
+ content: 'puts "hello world"',
+ visibility_level: Gitlab::VisibilityLevel::PUBLIC
+ }
+ end
+
+ it 'creates a new snippet' do
+ expect do
+ post api("/snippets/", user), params
+ end.to change { PersonalSnippet.count }.by(1)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(params[:title])
+ expect(json_response['file_name']).to eq(params[:file_name])
+ end
+
+ it 'returns 400 for missing parameters' do
+ params.delete(:title)
+
+ post api("/snippets/", user), params
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ describe 'PUT /snippets/:id' do
+ let(:other_user) { create(:user) }
+ let(:public_snippet) { create(:personal_snippet, :public, author: user) }
+ it 'updates snippet' do
+ new_content = 'New content'
+
+ put api("/snippets/#{public_snippet.id}", user), content: new_content
+
+ expect(response).to have_http_status(200)
+ public_snippet.reload
+ expect(public_snippet.content).to eq(new_content)
+ end
+
+ it 'returns 404 for invalid snippet id' do
+ put api("/snippets/1234", user), title: 'foo'
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Snippet Not Found')
+ end
+
+ it "returns 404 for another user's snippet" do
+ put api("/snippets/#{public_snippet.id}", other_user), title: 'fubar'
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Snippet Not Found')
+ end
+
+ it 'returns 400 for missing parameters' do
+ put api("/snippets/1234", user)
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ describe 'DELETE /snippets/:id' do
+ let!(:public_snippet) { create(:personal_snippet, :public, author: user) }
+ it 'deletes snippet' do
+ expect do
+ delete api("/snippets/#{public_snippet.id}", user)
+
+ expect(response).to have_http_status(204)
+ end.to change { PersonalSnippet.count }.by(-1)
+ end
+
+ it 'returns 404 for invalid snippet id' do
+ delete api("/snippets/1234", user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Snippet Not Found')
+ end
+ end
+end
diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb
index c0b3e83244d..ad1eed5b369 100644
--- a/spec/support/login_helpers.rb
+++ b/spec/support/login_helpers.rb
@@ -75,7 +75,8 @@ module LoginHelpers
def logout
find(".header-user-dropdown-toggle").click
click_link "Sign out"
- expect(page).to have_content('Signed out successfully')
+ # check the sign_in button
+ expect(page).to have_button('Sign in')
end
# Logout without JavaScript driver