Commit b0c431c0 authored by Stan Hu's avatar Stan Hu

Merge branch 'ce-to-ee-2018-08-28' into 'master'

CE upstream - 2018-08-28 13:21 UTC

Closes gitlab-ce#40059

See merge request gitlab-org/gitlab-ee!7009
parents 9095be90 8d67b118
......@@ -6,6 +6,7 @@ app/controllers/projects/approvers_controller.rb
app/controllers/projects/protected_branches/merge_access_levels_controller.rb
app/controllers/projects/protected_branches/push_access_levels_controller.rb
app/controllers/projects/protected_tags/create_access_levels_controller.rb
app/helpers/system_note_helper.rb
app/policies/project_policy.rb
app/models/concerns/relative_positioning.rb
app/workers/stuck_merge_jobs_worker.rb
......
......@@ -146,13 +146,7 @@ export default {
staged: false,
prevPath: '',
moved: false,
lastCommit: Object.assign(state.entries[file.path].lastCommit, {
id: lastCommit.commit.id,
url: lastCommit.commit_path,
message: lastCommit.commit.message,
author: lastCommit.commit.author_name,
updatedAt: lastCommit.commit.authored_date,
}),
lastCommitSha: lastCommit.commit.id,
});
if (prevPath) {
......
<script>
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { sprintf, __ } from '../../locale';
export default {
components: {
CiIcon,
},
props: {
deploymentStatus: {
type: Object,
required: true,
},
},
computed: {
environment() {
let environmentText;
switch (this.deploymentStatus.status) {
case 'latest':
environmentText = sprintf(
__('This job is the most recent deployment to %{link}.'),
{ link: this.environmentLink },
false,
);
break;
case 'out_of_date':
if (this.hasLastDeployment) {
environmentText = sprintf(
__(
'This job is an out-of-date deployment to %{environmentLink}. View the most recent deployment %{deploymentLink}.',
),
{
environmentLink: this.environmentLink,
deploymentLink: this.deploymentLink,
},
false,
);
} else {
environmentText = sprintf(
__('This job is an out-of-date deployment to %{environmentLink}.'),
{ environmentLink: this.environmentLink },
false,
);
}
break;
case 'failed':
environmentText = sprintf(
__('The deployment of this job to %{environmentLink} did not succeed.'),
{ environmentLink: this.environmentLink },
false,
);
break;
case 'creating':
if (this.hasLastDeployment) {
environmentText = sprintf(
__(
'This job is creating a deployment to %{environmentLink} and will overwrite the last %{deploymentLink}.',
),
{
environmentLink: this.environmentLink,
deploymentLink: this.deploymentLink,
},
false,
);
} else {
environmentText = sprintf(
__('This job is creating a deployment to %{environmentLink}.'),
{ environmentLink: this.environmentLink },
false,
);
}
break;
default:
break;
}
return environmentText;
},
environmentLink() {
return sprintf(
'%{startLink}%{name}%{endLink}',
{
startLink: `<a href="${this.deploymentStatus.environment.path}">`,
name: _.escape(this.deploymentStatus.environment.name),
endLink: '</a>',
},
false,
);
},
deploymentLink() {
return sprintf(
'%{startLink}%{name}%{endLink}',
{
startLink: `<a href="${this.lastDeployment.path}">`,
name: _.escape(this.lastDeployment.name),
endLink: '</a>',
},
false,
);
},
hasLastDeployment() {
return this.deploymentStatus.environment.last_deployment;
},
lastDeployment() {
return this.deploymentStatus.environment.last_deployment;
},
},
};
</script>
<template>
<div class="prepend-top-default js-environment-container">
<div class="environment-information">
<ci-icon :status="deploymentStatus.icon" />
<p v-html="environment"></p>
</div>
</div>
</template>
import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show';
import initDeleteMilestoneModal from '~/pages/milestones/shared/delete_milestone_modal_init';
import Milestone from '~/milestone';
document.addEventListener('DOMContentLoaded', () => {
initMilestonesShow();
initDeleteMilestoneModal();
Milestone.initDeprecationMessage();
});
......@@ -40,8 +40,8 @@
if (this.issueCount === 0 && this.mergeRequestCount === 0) {
return sprintf(
s__(`Milestones|
You’re about to permanently delete the milestone %{milestoneTitle} from this project.
%{milestoneTitle} is not currently used in any issues or merge requests.`),
You’re about to permanently delete the milestone %{milestoneTitle}.
This milestone is not currently used in any issues or merge requests.`),
{
milestoneTitle,
},
......@@ -51,7 +51,7 @@ You’re about to permanently delete the milestone %{milestoneTitle} from this p
return sprintf(
s__(`Milestones|
You’re about to permanently delete the milestone %{milestoneTitle} from this project and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}.
You’re about to permanently delete the milestone %{milestoneTitle} and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}.
Once deleted, it cannot be undone or recovered.`),
{
milestoneTitle,
......
......@@ -3,15 +3,22 @@ import createFlash from '~/flash';
import GfmAutoComplete from '~/gfm_auto_complete';
import EmojiMenu from './emoji_menu';
const defaultStatusEmoji = 'speech_balloon';
document.addEventListener('DOMContentLoaded', () => {
const toggleEmojiMenuButtonSelector = '.js-toggle-emoji-menu';
const toggleEmojiMenuButton = document.querySelector(toggleEmojiMenuButtonSelector);
const statusEmojiField = document.getElementById('js-status-emoji-field');
const statusMessageField = document.getElementById('js-status-message-field');
const findNoEmojiPlaceholder = () => document.getElementById('js-no-emoji-placeholder');
const toggleNoEmojiPlaceholder = (isVisible) => {
const placeholderElement = document.getElementById('js-no-emoji-placeholder');
placeholderElement.classList.toggle('hidden', !isVisible);
};
const findStatusEmoji = () => toggleEmojiMenuButton.querySelector('gl-emoji');
const removeStatusEmoji = () => {
const statusEmoji = toggleEmojiMenuButton.querySelector('gl-emoji');
const statusEmoji = findStatusEmoji();
if (statusEmoji) {
statusEmoji.remove();
}
......@@ -19,7 +26,7 @@ document.addEventListener('DOMContentLoaded', () => {
const selectEmojiCallback = (emoji, emojiTag) => {
statusEmojiField.value = emoji;
findNoEmojiPlaceholder().classList.add('hidden');
toggleNoEmojiPlaceholder(false);
removeStatusEmoji();
toggleEmojiMenuButton.innerHTML += emojiTag;
};
......@@ -29,7 +36,7 @@ document.addEventListener('DOMContentLoaded', () => {
statusEmojiField.value = '';
statusMessageField.value = '';
removeStatusEmoji();
findNoEmojiPlaceholder().classList.remove('hidden');
toggleNoEmojiPlaceholder(true);
});
const emojiAutocomplete = new GfmAutoComplete();
......@@ -44,6 +51,23 @@ document.addEventListener('DOMContentLoaded', () => {
selectEmojiCallback,
);
emojiMenu.bindEvents();
const defaultEmojiTag = Emoji.glEmojiTag(defaultStatusEmoji);
statusMessageField.addEventListener('input', () => {
const hasStatusMessage = statusMessageField.value.trim() !== '';
const statusEmoji = findStatusEmoji();
if (hasStatusMessage && statusEmoji) {
return;
}
if (hasStatusMessage) {
toggleNoEmojiPlaceholder(false);
toggleEmojiMenuButton.innerHTML += defaultEmojiTag;
} else if (statusEmoji.dataset.name === defaultStatusEmoji) {
toggleNoEmojiPlaceholder(true);
removeStatusEmoji();
}
});
})
.catch(() => createFlash('Failed to load emoji list!'));
});
......@@ -4,8 +4,8 @@ class Groups::MilestonesController < Groups::ApplicationController
include MilestoneActions
before_action :group_projects
before_action :milestone, only: [:edit, :show, :update, :merge_requests, :participants, :labels]
before_action :authorize_admin_milestones!, only: [:edit, :new, :create, :update]
before_action :milestone, only: [:edit, :show, :update, :merge_requests, :participants, :labels, :destroy]
before_action :authorize_admin_milestones!, only: [:edit, :new, :create, :update, :destroy]
def index
respond_to do |format|
......@@ -58,10 +58,21 @@ class Groups::MilestonesController < Groups::ApplicationController
redirect_to milestone_path
end
def destroy
return render_404 if @milestone.legacy_group_milestone?
Milestones::DestroyService.new(group, current_user).execute(@milestone)
respond_to do |format|
format.html { redirect_to group_milestones_path(group), status: :see_other }
format.js { head :ok }
end
end
private
def authorize_admin_milestones!
return render_404 unless can?(current_user, :admin_milestones, group)
return render_404 unless can?(current_user, :admin_milestone, group)
end
def milestone_params
......
......@@ -89,8 +89,7 @@ class Project < ActiveRecord::Base
after_create :create_project_feature, unless: :project_feature
after_create -> { SiteStatistic.track(STATISTICS_ATTRIBUTE) }
before_destroy ->(project) { project.project_feature.untrack_statistics_for_deletion! }
after_destroy -> { SiteStatistic.untrack(STATISTICS_ATTRIBUTE) }
before_destroy :untrack_site_statistics
after_create :create_ci_cd_settings,
unless: :ci_cd_settings,
......@@ -2113,6 +2112,11 @@ class Project < ActiveRecord::Base
Gitlab::PagesTransfer.new.rename_project(path_before, self.path, namespace.full_path)
end
def untrack_site_statistics
SiteStatistic.untrack(STATISTICS_ATTRIBUTE)
self.project_feature.untrack_statistics_for_deletion!
end
def execute_rename_repository_hooks!(full_path_before)
# When we import a project overwriting the original project, there
# is a move operation. In that case we don't want to send the instructions.
......
......@@ -56,7 +56,7 @@ class GroupPolicy < BasePolicy
rule { has_access }.enable :read_namespace
rule { developer }.enable :admin_milestones
rule { developer }.enable :admin_milestone
rule { reporter }.policy do
enable :admin_label
......
......@@ -14,12 +14,15 @@ module Groups
def execute
group.prepare_for_destroy
group.projects.each do |project|
group.projects.includes(:project_feature).each do |project|
# Execute the destruction of the models immediately to ensure atomic cleanup.
success = ::Projects::DestroyService.new(project, current_user).execute
raise DestroyError, "Project #{project.id} can't be deleted" unless success
end
# reload the relation to prevent triggering destroy hooks on the projects again
group.projects.reload
group.children.each do |group|
# This needs to be synchronous since the namespace gets destroyed below
DestroyService.new(group, current_user).execute
......
......@@ -3,8 +3,6 @@
module Milestones
class DestroyService < Milestones::BaseService
def execute(milestone)
return unless milestone.project_milestone?
Milestone.transaction do
update_params = { milestone: nil }
......@@ -16,15 +14,21 @@ module Milestones
MergeRequests::UpdateService.new(parent, current_user, update_params).execute(merge_request)
end
event_service.destroy_milestone(milestone, current_user)
Event.for_milestone_id(milestone.id).each do |event|
event.target_id = nil
event.save
end
log_destroy_event_for(milestone)
milestone.destroy
end
end
def log_destroy_event_for(milestone)
return if milestone.group_milestone?
event_service.destroy_milestone(milestone, current_user)
Event.for_milestone_id(milestone.id).each do |event|
event.target_id = nil
event.save
end
end
end
end
......@@ -39,6 +39,10 @@
%strong= email.email
= link_to remove_email_admin_user_path(@user, email), data: { confirm: "Are you sure you want to remove #{email.email}?" }, method: :delete, class: "btn-sm btn btn-remove float-right", title: 'Remove secondary email', id: "remove_email_#{email.id}" do
%i.fa.fa-times
%li
%span.light ID:
%strong
= @user.id
%li.two-factor-status
%span.light Two-factor Authentication:
......
......@@ -5,7 +5,7 @@
.nav-controls
= render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestones, @group)
- if can?(current_user, :admin_milestone, @group)
= link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-new"
.milestones
......
#modal_merge_info.modal{ tabindex: '-1' }
.modal-dialog
.modal-dialog.modal-lg
.modal-content
.modal-header
%h3.modal-title Check out, review, and merge locally
......
......@@ -43,18 +43,7 @@
- else
= link_to 'Reopen milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped"
%button.js-delete-milestone-button.btn.btn-grouped.btn-danger{ data: { toggle: 'modal',
target: '#delete-milestone-modal',
milestone_id: @milestone.id,
milestone_title: markdown_field(@milestone, :title),
milestone_url: project_milestone_path(@project, @milestone),
milestone_issue_count: @milestone.issues.count,
milestone_merge_request_count: @milestone.merge_requests.count },
disabled: true }
= _('Delete')
= icon('spin spinner', class: 'js-loading-icon hidden' )
#delete-milestone-modal
= render 'shared/milestones/delete_button'
%a.btn.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left')
......
- milestone_url = @milestone.project_milestone? ? project_milestone_path(@project, @milestone) : group_milestone_path(@group, @milestone)
%button.js-delete-milestone-button.btn.btn-grouped.btn-danger{ data: { toggle: 'modal',
target: '#delete-milestone-modal',
milestone_id: @milestone.id,
milestone_title: markdown_field(@milestone, :title),
milestone_url: milestone_url,
milestone_issue_count: @milestone.issues.count,
milestone_merge_request_count: @milestone.merge_requests.count },
disabled: true }
= _('Delete')
= icon('spin spinner', class: 'js-loading-icon hidden' )
#delete-milestone-modal
......@@ -52,7 +52,7 @@
- unless milestone.active?
= link_to 'Reopen Milestone', project_milestone_path(@project, milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen"
- if @group
- if can?(current_user, :admin_milestones, @group)
- if can?(current_user, :admin_milestone, @group)
- if milestone.closed?
= link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen"
- else
......
......@@ -23,7 +23,7 @@
= milestone_date_range(milestone)
- if group
.float-right
- if can?(current_user, :admin_milestones, group)
- if can?(current_user, :admin_milestone, group)
- if milestone.group_milestone?
= link_to edit_group_milestone_path(group, milestone), class: "btn btn btn-grouped" do
Edit
......@@ -32,6 +32,9 @@
- else
= link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen"
- unless is_dynamic_milestone
= render 'shared/milestones/delete_button'
= render 'shared/milestones/deprecation_message' if is_dynamic_milestone
.detail-page-description.milestone-detail
......
---
title: Increase width of checkout branch modal box
merge_request:
author:
type: fixed
---
title: Creates vue component for environments information in job log view
merge_request:
author:
type: other
---
title: Removing a group no longer triggers hooks for project deletion twice
merge_request: 21366
author:
type: fixed
---
title: Fix Web IDE unable to commit to same file twice
merge_request: 21372
author:
type: fixed
---
title: Add Galician as an available language.
merge_request: 21202
author:
type: added
---
title: Expose user's id in /admin/users/ show page
merge_request:
author: Eva Kadlecova
type: changed
---
title: Allow to delete group milestones
merge_request:
author:
type: added
---
title: 'Rails5: fix can''t quote ActiveSupport::HashWithIndifferentAccess'
merge_request: 21397
author: Jasper Maes
type: other
---
title: Display default status emoji if only message is entered
merge_request: 21330
author:
type: changed
......@@ -38,7 +38,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
post :toggle_subscription, on: :member
end
resources :milestones, constraints: { id: %r{[^/]+} }, only: [:index, :show, :edit, :update, :new, :create] do
resources :milestones, constraints: { id: %r{[^/]+} } do
member do
get :merge_requests
get :participants
......
# frozen_string_literal: true
class RecalculateSiteStatistics < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
transaction do
execute('SET LOCAL statement_timeout TO 0') if Gitlab::Database.postgresql? # see https://gitlab.com/gitlab-org/gitlab-ce/issues/48967
execute("UPDATE site_statistics SET repositories_count = (SELECT COUNT(*) FROM projects)")
end
transaction do
execute('SET LOCAL statement_timeout TO 0') if Gitlab::Database.postgresql? # see https://gitlab.com/gitlab-org/gitlab-ce/issues/48967
execute("UPDATE site_statistics SET wikis_count = (SELECT COUNT(*) FROM project_features WHERE wiki_access_level != 0)")
end
end
def down
# No downside in keeping the counter up-to-date
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180823132905) do
ActiveRecord::Schema.define(version: 20180826111825) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......
......@@ -96,6 +96,19 @@ Parameters:
- `start_date` (optional) - The start date of the milestone
- `state_event` (optional) - The state event of the milestone (close|activate)
## Delete group milestone
Only for user with developer access to the group.
```
DELETE /groups/:id/milestones/:milestone_id
```
Parameters:
- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user
- `milestone_id` (required) - The ID of the group's milestone
## Get all issues assigned to a single milestone
Gets all issues assigned to a single group milestone.
......
......@@ -474,7 +474,7 @@ POST /projects/:id/issues
| `assignee_ids` | Array[integer] | no | The ID of a user to assign issue |
| `milestone_id` | integer | no | The global ID of a milestone to assign issue |
| `labels` | string | no | Comma-separated label names for an issue |
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project/group owner rights) |
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project/group owner rights) |
| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
| `merge_request_to_resolve_discussions_of` | integer | no | The IID of a merge request in which to resolve all issues. This will fill the issue with a default description and mark all discussions as resolved. When passing a description or title, these values will take precedence over the default values.|
| `discussion_to_resolve` | string | no | The ID of a discussion to resolve. This will fill in the issue with a default description and mark the discussion as resolved. Use in combination with `merge_request_to_resolve_discussions_of`. |
......
......@@ -41,7 +41,7 @@ module API
use :optional_params
end
post ":id/milestones" do
authorize! :admin_milestones, user_group
authorize! :admin_milestone, user_group
create_milestone_for(user_group)
end
......@@ -53,11 +53,21 @@ module API
use :update_params
end
put ":id/milestones/:milestone_id" do
authorize! :admin_milestones, user_group
authorize! :admin_milestone, user_group
update_milestone_for(user_group)
end
desc 'Remove a project milestone'
delete ":id/milestones/:milestone_id" do
authorize! :admin_milestone, user_group
milestone = user_group.milestones.find(params[:milestone_id])
Milestones::DestroyService.new(user_group, current_user).execute(milestone)
status(204)
end
desc 'Get all issues for a single group milestone' do
success Entities::IssueBasic
end
......
......@@ -64,7 +64,8 @@ module API
delete ":id/milestones/:milestone_id" do
authorize! :admin_milestone, user_project
user_project.milestones.find(params[:milestone_id]).destroy
milestone = user_project.milestones.find(params[:milestone_id])
Milestones::DestroyService.new(user_project, current_user).execute(milestone)
status(204)
end
......
......@@ -5,6 +5,7 @@ module Gitlab
AVAILABLE_LANGUAGES = {
'en' => 'English',
'es' => 'Español',
'gl_ES' => 'Galego',
'de' => 'Deutsch',
'fr' => 'Français',
'pt_BR' => 'Português (Brasil)',
......
......@@ -4677,10 +4677,10 @@ msgstr ""
msgid "Milestones"
msgstr ""
msgid "Milestones| You’re about to permanently delete the milestone %{milestoneTitle} from this project and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}. Once deleted, it cannot be undone or recovered."
msgid "Milestones| You’re about to permanently delete the milestone %{milestoneTitle} and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}. Once deleted, it cannot be undone or recovered."
msgstr ""
msgid "Milestones| You’re about to permanently delete the milestone %{milestoneTitle} from this project. %{milestoneTitle} is not currently used in any issues or merge requests."
msgid "Milestones| You’re about to permanently delete the milestone %{milestoneTitle}. This milestone is not currently used in any issues or merge requests."
msgstr ""
msgid "Milestones|<p>%{milestonePromotion}</p> %{finalWarning}"
......@@ -6974,6 +6974,9 @@ msgstr ""
msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
msgstr ""
msgid "The deployment of this job to %{environmentLink} did not succeed."
msgstr ""
msgid "The fork relationship has been removed."
msgstr ""
......@@ -7187,6 +7190,18 @@ msgstr ""
msgid "This job has not started yet"
msgstr ""
msgid "This job is an out-of-date deployment to %{environmentLink}."
msgstr ""
msgid "This job is an out-of-date deployment to %{environmentLink}. View the most recent deployment %{deploymentLink}."
msgstr ""
msgid "This job is creating a deployment to %{environmentLink} and will overwrite the last %{deploymentLink}."
msgstr ""
msgid "This job is creating a deployment to %{environmentLink}."
msgstr ""
msgid "This job is in pending state and is waiting to be picked by a runner"
msgstr ""
......@@ -7196,6 +7211,9 @@ msgstr ""
msgid "This job is stuck, because you don't have any active runners that can run this job."
msgstr ""
msgid "This job is the most recent deployment to %{link}."
msgstr ""
msgid "This job requires a manual action"
msgstr ""
......
......@@ -141,6 +141,17 @@ describe Groups::MilestonesController do
end
end
describe "#destroy" do
let(:milestone) { create(:milestone, group: group) }
it "removes milestone" do
delete :destroy, group_id: group.to_param, id: milestone.iid, format: :js
expect(response).to be_success
expect { Milestone.find(milestone.id) }.to raise_exception(ActiveRecord::RecordNotFound)
end
end
describe '#ensure_canonical_path' do
before do
sign_in(user)
......
......@@ -34,7 +34,7 @@ FactoryBot.define do
milestone.project_id = evaluator.project_id
elsif evaluator.parent
id = evaluator.parent.id
evaluator.parent.is_a?(Group) ? board.group_id = id : evaluator.project_id = id
evaluator.parent.is_a?(Group) ? evaluator.group_id = id : evaluator.project_id = id
else
milestone.project = create(:project)
end
......
......@@ -134,6 +134,7 @@ describe "Admin::Users" do
expect(page).to have_content(user.email)
expect(page).to have_content(user.name)
expect(page).to have_content(user.id)
expect(page).to have_link('Block user', href: block_admin_user_path(user))
expect(page).to have_button('Delete user')
expect(page).to have_button('Delete user and contributions')
......
require "rails_helper"
describe "User deletes milestone", :js do
set(:user) { create(:user) }
set(:project) { create(:project) }
set(:milestone) { create(:milestone, project: project) }
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
before do
project.add_developer(user)
sign_in(user)
end
context "when milestone belongs to project" do
let!(:milestone) { create(:milestone, parent: project, title: "project milestone") }
it "deletes milestone" do
project.add_developer(user)
visit(project_milestones_path(project))
click_link(milestone.title)
click_button("Delete")
click_button("Delete milestone")
expect(page).to have_content("No milestones to show")
visit(activity_project_path(project))
visit(project_milestones_path(project))
expect(page).to have_content("#{user.name} destroyed milestone")
end
end
it "deletes milestone" do
click_link(milestone.title)
click_button("Delete")
click_button("Delete milestone")
context "when milestone belongs to group" do
let!(:milestone_to_be_deleted) { create(:milestone, parent: group, title: "group milestone 1") }
let!(:milestone) { create(:milestone, parent: group, title: "group milestone 2") }
expect(page).to have_content("No milestones to show")
it "deletes milestone" do
group.add_developer(user)
visit(group_milestones_path(group))
visit(activity_project_path(project))
click_link(milestone_to_be_deleted.title)
click_button("Delete")
click_button("Delete milestone")
expect(page).to have_content("#{user.name} destroyed milestone")
expect(page).to have_content(milestone.title)
expect(page).not_to have_content(milestone_to_be_deleted)
end
end
end
......@@ -130,5 +130,15 @@ describe 'User edit profile' do
visit user_path(user)
expect(page).not_to have_selector '.cover-status'
end
it 'displays a default emoji if only message is entered' do
message = 'a status without emoji'
visit(profile_path)
fill_in 'js-status-message-field', with: message
within('.js-toggle-emoji-menu') do
expect(page).to have_emoji('speech_balloon')
end
end
end
end
......@@ -57,8 +57,11 @@ describe('Flash', () => {
hideFlash(el);
expect(
el.style.transition,
).toBe('opacity 0.3s');
el.style['transition-property'],
).toBe('opacity');
expect(
el.style['transition-duration'],
).toBe('0.3s');
});
it('sets opacity style', () => {
......
......@@ -184,7 +184,7 @@ describe('IDE commit module actions', () => {
branch,
})
.then(() => {
expect(f.lastCommit.message).toBe(data.message);
expect(f.lastCommitSha).toBe(data.id);
})
.then(done)
.catch(done.fail);
......@@ -266,19 +266,21 @@ describe('IDE commit module actions', () => {
});
describe('success', () => {
const COMMIT_RESPONSE = {
id: '123456',
short_id: '123',
message: 'test message',
committed_date: 'date',
stats: {
additions: '1',
deletions: '2',
},
};
beforeEach(() => {
spyOn(service, 'commit').and.returnValue(
Promise.resolve({
data: {
id: '123456',
short_id: '123',
message: 'test message',
committed_date: 'date',
stats: {
additions: '1',
deletions: '2',
},
},
data: COMMIT_RESPONSE,
}),
);
});
......@@ -352,8 +354,8 @@ describe('IDE commit module actions', () => {
store
.dispatch('commit/commitChanges')
.then(() => {
expect(store.state.entries[store.state.openFiles[0].path].lastCommit.message).toBe(
'test message',
expect(store.state.entries[store.state.openFiles[0].path].lastCommitSha).toBe(
COMMIT_RESPONSE.id,
);
done();
......
import Vue from 'vue';
import component from '~/jobs/components/environments_block.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Environments block', () => {
const Component = Vue.extend(component);
let vm;
const icon = {
group: 'success',
icon: 'status_success',
label: 'passed',
text: 'passed',
tooltip: 'passed',
};
const deployment = {
path: 'deployment',
name: 'deployment name',
};
const environment = {
path: '/environment',
name: 'environment',
};
afterEach(() => {
vm.$destroy();
});
describe('with latest deployment', () => {
it('renders info for most recent deployment', () => {
vm = mountComponent(Component, {
deploymentStatus: {
status: 'latest',
icon,
deployment,
environment,
},
});
expect(vm.$el.textContent.trim()).toEqual(
'This job is the most recent deployment to environment.',
);
});
});
describe('with out of date deployment', () => {
describe('with last deployment', () => {
it('renders info for out date and most recent', () => {
vm = mountComponent(Component, {
deploymentStatus: {
status: 'out_of_date',
icon,
deployment,
environment: Object.assign({}, environment, {
last_deployment: { name: 'deployment', path: 'last_deployment' },
}),
},
});
expect(vm.$el.textContent.trim()).toEqual(
'This job is an out-of-date deployment to environment. View the most recent deployment deployment.',
);
});
});
describe('without last deployment', () => {
it('renders info about out of date deployment', () => {
vm = mountComponent(Component, {
deploymentStatus: {
status: 'out_of_date',
icon,
deployment: null,
environment,
},
});
expect(vm.$el.textContent.trim()).toEqual(
'This job is an out-of-date deployment to environment.',
);
});
});
});
describe('with failed deployment', () => {
it('renders info about failed deployment', () => {
vm = mountComponent(Component, {
deploymentStatus: {
status: 'failed',
icon,
deployment: null,
environment,
},
});
expect(vm.$el.textContent.trim()).toEqual(
'The deployment of this job to environment did not succeed.',
);
});
});
describe('creating deployment', () => {
describe('with last deployment', () => {
it('renders info about creating deployment and overriding lastest deployment', () => {
vm = mountComponent(Component, {
deploymentStatus: {
status: 'creating',
icon,
deployment,
environment: Object.assign({}, environment, {
last_deployment: { name: 'deployment', path: 'last_deployment' },
}),
},
});
expect(vm.$el.textContent.trim()).toEqual(
'This job is creating a deployment to environment and will overwrite the last deployment.',
);
});
});
describe('without last deployment', () => {
it('renders info about failed deployment', () => {
vm = mountComponent(Component, {
deploymentStatus: {
status: 'creating',
icon,
deployment: null,
environment,
},
});
expect(vm.$el.textContent.trim()).toEqual(
'This job is creating a deployment to environment.',
);
});
});
});
});
......@@ -11,7 +11,7 @@ describe Gitlab::Import::MergeRequestCreator do
context 'merge request already exists' do
let(:merge_request) { create(:merge_request, target_project: project, source_project: project) }
let(:commits) { merge_request.merge_request_diffs.first.commits }
let(:attributes) { HashWithIndifferentAccess.new(merge_request.attributes) }
let(:attributes) { HashWithIndifferentAccess.new(merge_request.attributes.except("merge_params")) }
it 'updates the data' do
commits_count = commits.count
......@@ -28,7 +28,7 @@ describe Gitlab::Import::MergeRequestCreator do
context 'new merge request' do
let(:merge_request) { build(:merge_request, target_project: project, source_project: project) }
let(:attributes) { HashWithIndifferentAccess.new(merge_request.attributes) }
let(:attributes) { HashWithIndifferentAccess.new(merge_request.attributes.except("merge_params")) }
it 'creates a new merge request' do
attributes.delete(:id)
......
......@@ -18,7 +18,7 @@ describe GroupPolicy do
let(:reporter_permissions) { [:admin_label] }
let(:developer_permissions) { [:admin_milestones] }
let(:developer_permissions) { [:admin_milestone] }
let(:maintainer_permissions) do
[
......
......@@ -39,19 +39,6 @@ describe API::ProjectMilestones do
expect(response).to have_gitlab_http_status(404)
end
it "rejects a member with reporter access from deleting a milestone" do
delete api("/projects/#{project.id}/milestones/#{milestone.id}", reporter)
expect(response).to have_gitlab_http_status(403)
end
it 'deletes the milestone when the user has developer access to the project' do
delete api("/projects/#{project.id}/milestones/#{milestone.id}", user)
expect(project.milestones.find_by_id(milestone.id)).to be_nil
expect(response).to have_gitlab_http_status(204)
end
end
describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do
......
......@@ -35,6 +35,14 @@ describe Groups::DestroyService do
it { expect(NotificationSetting.unscoped.all).not_to include(notification_setting) }
end
context 'site statistics' do
it 'doesnt trigger project deletion hooks twice' do
expect_any_instance_of(Project).to receive(:untrack_site_statistics).once
destroy_group(group, user, async)
end
end
context 'mattermost team' do
let!(:chat_team) { create(:chat_team, namespace: group) }
......
......@@ -4,8 +4,6 @@ describe Milestones::DestroyService do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:milestone) { create(:milestone, title: 'Milestone v1.0', project: project) }
let!(:issue) { create(:issue, project: project, milestone: milestone) }
let!(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) }
before do
project.add_maintainer(user)
......@@ -23,12 +21,23 @@ describe Milestones::DestroyService do
end
it 'deletes milestone id from issuables' do
issue = create(:issue, project: project, milestone: milestone)
merge_request = create(:merge_request, source_project: project, milestone: milestone)
service.execute(milestone)
expect(issue.reload.milestone).to be_nil
expect(merge_request.reload.milestone).to be_nil
end
it 'logs destroy event' do
service.execute(milestone)
event = Event.where(project_id: milestone.project_id, target_type: 'Milestone')
expect(event.count).to eq(1)
end
context 'group milestones' do
let(:group) { create(:group) }
let(:group_milestone) { create(:milestone, group: group) }
......@@ -38,13 +47,20 @@ describe Milestones::DestroyService do
group.add_developer(user)
end
it { expect(service.execute(group_milestone)).to be_nil }
it { expect(service.execute(group_milestone)).to eq(group_milestone) }
it 'does not update milestone issuables' do
expect(MergeRequests::UpdateService).not_to receive(:new)
expect(Issues::UpdateService).not_to receive(:new)
it 'deletes milestone id from issuables' do
issue = create(:issue, project: project, milestone: group_milestone)
merge_request = create(:merge_request, source_project: project, milestone: group_milestone)
service.execute(group_milestone)
expect(issue.reload.milestone).to be_nil
expect(merge_request.reload.milestone).to be_nil
end
it 'does not log destroy event' do
expect { service.execute(group_milestone) }.not_to change { Event.count }
end
end
end
......
......@@ -196,6 +196,24 @@ shared_examples_for 'group and project milestones' do |route_definition|
end
end
describe "DELETE #{route_definition}/:milestone_id" do
it "rejects a member with reporter access from deleting a milestone" do
reporter = create(:user)
milestone.parent.add_reporter(reporter)
delete api(resource_route, reporter)
expect(response).to have_gitlab_http_status(403)
end
it 'deletes the milestone when the user has developer access to the project' do
delete api(resource_route, user)
expect(project.milestones.find_by_id(milestone.id)).to be_nil
expect(response).to have_gitlab_http_status(204)
end
end
describe "GET #{route_definition}/:milestone_id/issues" do
let(:issues_route) { "#{route}/#{milestone.id}/issues" }
......
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