Commit 56b32391 authored by Constance Okoghenun's avatar Constance Okoghenun

Merge branch 'master' of https://gitlab.com/gitlab-org/gitlab-ce into...

Merge branch 'master' of https://gitlab.com/gitlab-org/gitlab-ce into protected-tags-bundle-refactor
parents 8c122d65 d4867c51
......@@ -11,8 +11,8 @@ engines:
exclude_paths:
- "lib/api/v3/*"
eslint:
# eslint-plugin-vue is locked to version 2 in codeclimate, we need version 4
enabled: false
enabled: true
channel: "eslint-4"
rubocop:
enabled: true
channel: "gitlab-rubocop-0-52-1"
......
......@@ -397,9 +397,9 @@ For issues related to the open source stewardship of GitLab,
there is the ~"stewardship" label.
This label is to be used for issues in which the stewardship of GitLab
is a topic of discussion. For instance if GitLab Inc. is planning to remove
features from GitLab CE to make exclusive in GitLab EE, related issues
would be labelled with ~"stewardship".
is a topic of discussion. For instance if GitLab Inc. is planning to add
features from GitLab EE to GitLab CE, related issues would be labelled with
~"stewardship".
A recent example of this was the issue for
[bringing the time tracking API to GitLab CE][time-tracking-issue].
......
<script>
import Sortable from 'vendor/Sortable';
import boardNewIssue from './board_new_issue';
import boardNewIssue from './board_new_issue.vue';
import boardCard from './board_card.vue';
import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
......
/* global ListIssue */
<script>
import eventHub from '../eventhub';
import ListIssue from '../models/issue';
const Store = gl.issueBoards.BoardsStore;
......@@ -17,6 +18,9 @@ export default {
error: false,
};
},
mounted() {
this.$refs.input.focus();
},
methods: {
submit(e) {
e.preventDefault();
......@@ -59,42 +63,51 @@ export default {
eventHub.$emit(`hide-issue-form-${this.list.id}`);
},
},
mounted() {
this.$refs.input.focus();
},
template: `
<div class="card board-new-issue-form">
<form @submit="submit($event)">
<div class="flash-container"
v-if="error">
<div class="flash-alert">
An error occurred. Please try again.
</div>
</div>
<label class="label-light"
:for="list.id + '-title'">
Title
</label>
<input class="form-control"
type="text"
v-model="title"
ref="input"
autocomplete="off"
:id="list.id + '-title'" />
<div class="clearfix prepend-top-10">
<button class="btn btn-success pull-left"
type="submit"
:disabled="title === ''"
ref="submit-button">
Submit issue
</button>
<button class="btn btn-default pull-right"
type="button"
@click="cancel">
Cancel
</button>
</div>
</form>
</div>
`,
};
</script>
<template>
<div class="card board-new-issue-form">
<form @submit="submit($event)">
<div
class="flash-container"
v-if="error"
>
<div class="flash-alert">
An error occurred. Please try again.
</div>
</div>
<label
class="label-light"
:for="list.id + '-title'"
>
Title
</label>
<input
class="form-control"
type="text"
v-model="title"
ref="input"
autocomplete="off"
:id="list.id + '-title'"
/>
<div class="clearfix prepend-top-10">
<button
class="btn btn-success pull-left"
type="submit"
:disabled="title === ''"
ref="submit-button"
>
Submit issue
</button>
<button
class="btn btn-default pull-right"
type="button"
@click="cancel"
>
Cancel
</button>
</div>
</form>
</div>
</template>
......@@ -2,11 +2,13 @@
import _ from 'underscore';
import Vue from 'vue';
import Flash from '../flash';
import { __ } from '../locale';
import Flash from '~/flash';
import { __ } from '~/locale';
import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub';
import sidebarEventHub from '../sidebar/event_hub';
import sidebarEventHub from '~/sidebar/event_hub'; // eslint-disable-line import/first
import './models/issue';
import './models/label';
import './models/list';
......@@ -22,7 +24,7 @@ import './components/board';
import './components/board_sidebar';
import './components/new_list_dropdown';
import './components/modal/index';
import '../vue_shared/vue_resource_interceptor';
import '~/vue_shared/vue_resource_interceptor'; // eslint-disable-line import/first
export default () => {
const $boardApp = document.getElementById('board-app');
......
......@@ -110,3 +110,5 @@ class ListIssue {
}
window.ListIssue = ListIssue;
export default ListIssue;
......@@ -2,7 +2,7 @@
/* global List */
import _ from 'underscore';
import Cookies from 'js-cookie';
import { getUrlParamsArray } from '../../lib/utils/common_utils';
import { getUrlParamsArray } from '~/lib/utils/common_utils';
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
......
......@@ -15,7 +15,7 @@ const CommitPipelinesTable = Vue.extend(commitPipelinesTable);
window.gl = window.gl || {};
window.gl.CommitPipelinesTable = CommitPipelinesTable;
document.addEventListener('DOMContentLoaded', () => {
export default () => {
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
if (pipelineTableViewEl) {
......@@ -43,4 +43,4 @@ document.addEventListener('DOMContentLoaded', () => {
pipelineTableViewEl.appendChild(table.$el);
}
}
});
};
......@@ -6,177 +6,80 @@ import GlFieldErrors from './gl_field_errors';
import Shortcuts from './shortcuts';
import SearchAutocomplete from './search_autocomplete';
var Dispatcher;
(function() {
Dispatcher = (function() {
function Dispatcher() {
this.initSearch();
this.initFieldErrors();
this.initPageScripts();
}
Dispatcher.prototype.initPageScripts = function() {
var path, shortcut_handler;
const page = $('body').attr('data-page');
if (!page) {
return false;
}
const fail = () => Flash('Error loading dynamic module');
const callDefault = m => m.default();
path = page.split(':');
shortcut_handler = null;
$('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => {
const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete);
gfm.setup($(el), {
emojis: true,
members: enableGFM,
issues: enableGFM,
milestones: enableGFM,
mergeRequests: enableGFM,
labels: enableGFM,
});
});
const shortcutHandlerPages = [
'projects:activity',
'projects:artifacts:browse',
'projects:artifacts:file',
'projects:blame:show',
'projects:blob:show',
'projects:commit:show',
'projects:commits:show',
'projects:find_file:show',
'projects:issues:edit',
'projects:issues:index',
'projects:issues:new',
'projects:issues:show',
'projects:merge_requests:creations:diffs',
'projects:merge_requests:creations:new',
'projects:merge_requests:edit',
'projects:merge_requests:index',
'projects:merge_requests:show',
'projects:network:show',
'projects:show',
'projects:tree:show',
'groups:show',
];
function initSearch() {
// Only when search form is present
if ($('.search').length) {
return new SearchAutocomplete();
}
}
if (shortcutHandlerPages.indexOf(page) !== -1) {
shortcut_handler = true;
}
function initFieldErrors() {
$('.gl-show-field-errors').each((i, form) => {
new GlFieldErrors(form);
});
}
switch (path[0]) {
case 'admin':
switch (path[1]) {
case 'broadcast_messages':
import('./pages/admin/broadcast_messages')
.then(callDefault)
.catch(fail);
break;
case 'cohorts':
import('./pages/admin/cohorts')
.then(callDefault)
.catch(fail);
break;
case 'groups':
switch (path[2]) {
case 'show':
import('./pages/admin/groups/show')
.then(callDefault)
.catch(fail);
break;
}
break;
case 'projects':
import('./pages/admin/projects')
.then(callDefault)
.catch(fail);
break;
case 'labels':
switch (path[2]) {
case 'new':
import('./pages/admin/labels/new')
.then(callDefault)
.catch(fail);
break;
case 'edit':
import('./pages/admin/labels/edit')
.then(callDefault)
.catch(fail);
break;
}
case 'abuse_reports':
import('./pages/admin/abuse_reports')
.then(callDefault)
.catch(fail);
break;
}
break;
case 'profiles':
import('./pages/profiles/index')
.then(callDefault)
.catch(fail);
break;
case 'projects':
import('./pages/projects')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
switch (path[1]) {
case 'compare':
import('./pages/projects/compare')
.then(callDefault)
.catch(fail);
break;
case 'create':
case 'new':
import('./pages/projects/new')
.then(callDefault)
.catch(fail);
break;
case 'wikis':
import('./pages/projects/wikis')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
}
break;
}
// If we haven't installed a custom shortcut handler, install the default one
if (!shortcut_handler) {
new Shortcuts();
}
function initPageShortcuts(page) {
const pagesWithCustomShortcuts = [
'projects:activity',
'projects:artifacts:browse',
'projects:artifacts:file',
'projects:blame:show',
'projects:blob:show',
'projects:commit:show',
'projects:commits:show',
'projects:find_file:show',
'projects:issues:edit',
'projects:issues:index',
'projects:issues:new',
'projects:issues:show',
'projects:merge_requests:creations:diffs',
'projects:merge_requests:creations:new',
'projects:merge_requests:edit',
'projects:merge_requests:index',
'projects:merge_requests:show',
'projects:network:show',
'projects:show',
'projects:tree:show',
'groups:show',
];
if (document.querySelector('#peek')) {
import('./performance_bar')
.then(m => new m.default({ container: '#peek' })) // eslint-disable-line new-cap
.catch(fail);
}
};
if (pagesWithCustomShortcuts.indexOf(page) === -1) {
new Shortcuts();
}
}
Dispatcher.prototype.initSearch = function() {
// Only when search form is present
if ($('.search').length) {
return new SearchAutocomplete();
}
};
function initGFMInput() {
$('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => {
const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete);
gfm.setup($(el), {
emojis: true,
members: enableGFM,
issues: enableGFM,
milestones: enableGFM,
mergeRequests: enableGFM,
labels: enableGFM,
});
});
}
Dispatcher.prototype.initFieldErrors = function() {
$('.gl-show-field-errors').each((i, form) => {
new GlFieldErrors(form);
});
};
function initPerformanceBar() {
if (document.querySelector('#peek')) {
import('./performance_bar')
.then(m => new m.default({ container: '#peek' })) // eslint-disable-line new-cap
.catch(() => Flash('Error loading performance bar module'));
}
}
return Dispatcher;
})();
})();
export default () => {
initSearch();
initFieldErrors();
export default function initDispatcher() {
return new Dispatcher();
}
const page = $('body').attr('data-page');
if (page) {
initPageShortcuts(page);
initGFMInput();
initPerformanceBar();
}
};
......@@ -2,8 +2,8 @@
/**
* Render environments table.
*/
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import environmentItem from './environment_item.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
components: {
......
......@@ -5,7 +5,7 @@ import Translate from '../../vue_shared/translate';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => new Vue({
export default () => new Vue({
el: '#environments-folder-list-view',
components: {
environmentsFolderApp,
......@@ -32,4 +32,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
},
});
},
}));
});
<script>
import { mapState } from 'vuex';
import timeAgoMixin from '../../vue_shared/mixins/timeago';
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import fileIcon from '~/vue_shared/components/file_icon.vue';
import newDropdown from './new_dropdown/index.vue';
import fileIcon from '../../vue_shared/components/file_icon.vue';
export default {
components: {
......
<script>
import { mapActions } from 'vuex';
import fileIcon from '../../vue_shared/components/file_icon.vue';
import fileIcon from '~/vue_shared/components/file_icon.vue';
export default {
components: {
......
......@@ -242,10 +242,16 @@ export default class LabelsSelect {
filterable: true,
selected: $dropdown.data('selected') || [],
toggleLabel: function(selected, el) {
var $dropdownParent = $dropdown.parent();
var $dropdownInputField = $dropdownParent.find('.dropdown-input-field');
var isSelected = el !== null ? el.hasClass('is-active') : false;
var title = selected.title;
var selectedLabels = this.selected;
if ($dropdownInputField.length && $dropdownInputField.val().length) {
$dropdownParent.find('.dropdown-input-clear').trigger('click');
}
if (selected.id === 0) {
this.selected = [];
return 'No Label';
......
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, max-len */
import ShortcutsNetwork from '../shortcuts_network';
import Network from './network';
$(function() {
if (!$(".network-graph").length) return;
var network_graph;
network_graph = new Network({
url: $(".network-graph").attr('data-url'),
commit_url: $(".network-graph").attr('data-commit-url'),
ref: $(".network-graph").attr('data-ref'),
commit_id: $(".network-graph").attr('data-commit-id')
});
return new ShortcutsNetwork(network_graph.branch_graph);
});
import AbuseReports from './abuse_reports';
export default () => new AbuseReports();
document.addEventListener('DOMContentLoaded', () => new AbuseReports());
......@@ -3,7 +3,7 @@ import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import { __ } from '~/locale';
export default function initBroadcastMessagesForm() {
export default () => {
$('input#broadcast_message_color').on('input', function onMessageColorInput() {
const previewColor = $(this).val();
$('div.broadcast-message-preview').css('background-color', previewColor);
......@@ -32,4 +32,4 @@ export default function initBroadcastMessagesForm() {
.catch(() => flash(__('An error occurred while rendering preview broadcast message')));
}
}, 250));
}
};
import initBroadcastMessagesForm from './broadcast_message';
export default () => initBroadcastMessagesForm();
document.addEventListener('DOMContentLoaded', initBroadcastMessagesForm);
import initUsagePing from './usage_ping';
export default () => initUsagePing();
document.addEventListener('DOMContentLoaded', initUsagePing);
import UsersSelect from '../../../../users_select';
export default () => new UsersSelect();
document.addEventListener('DOMContentLoaded', () => new UsersSelect());
import Labels from '../../../../labels';
export default () => new Labels();
document.addEventListener('DOMContentLoaded', () => new Labels());
import Labels from '../../../../labels';
export default () => new Labels();
document.addEventListener('DOMContentLoaded', () => new Labels());
import ProjectsList from '../../../projects_list';
import NamespaceSelect from '../../../namespace_select';
export default () => {
document.addEventListener('DOMContentLoaded', () => {
new ProjectsList(); // eslint-disable-line no-new
document.querySelectorAll('.js-namespace-select')
.forEach(dropdown => new NamespaceSelect({ dropdown }));
};
});
import NotificationsForm from '../../../notifications_form';
import notificationsDropdown from '../../../notifications_dropdown';
export default () => {
document.addEventListener('DOMContentLoaded', () => {
new NotificationsForm(); // eslint-disable-line no-new
notificationsDropdown();
};
});
import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
document.addEventListener('DOMContentLoaded', () => {
new MiniPipelineGraph({
container: '.js-commit-pipeline-graph',
}).bindEvents();
$('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
initPipelines();
});
......@@ -5,6 +5,7 @@ import ShortcutsNavigation from '~/shortcuts_navigation';
import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
import initNotes from '~/init_notes';
import initChangesDropdown from '~/init_changes_dropdown';
import initDiffNotes from '~/diff_notes/diff_notes_bundle';
import { fetchCommitMergeRequests } from '~/commit_merge_requests';
document.addEventListener('DOMContentLoaded', () => {
......@@ -19,4 +20,5 @@ document.addEventListener('DOMContentLoaded', () => {
initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - stickyBarPaddingTop);
$('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
fetchCommitMergeRequests();
initDiffNotes();
});
import initCompareAutocomplete from '~/compare_autocomplete';
export default () => {
initCompareAutocomplete();
};
document.addEventListener('DOMContentLoaded', initCompareAutocomplete);
import initEnvironmentsFolderBundle from '~/environments/folder/environments_folder_bundle';
document.addEventListener('DOMContentLoaded', initEnvironmentsFolderBundle);
import Project from './project';
import ShortcutsNavigation from '../../shortcuts_navigation';
export default () => {
document.addEventListener('DOMContentLoaded', () => {
new Project(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
};
});
import initIssuableSidebar from '~/init_issuable_sidebar';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
import Issue from '~/issue';
import ShortcutsIssuable from '~/shortcuts_issuable';
import ZenMode from '~/zen_mode';
......@@ -10,4 +11,5 @@ document.addEventListener('DOMContentLoaded', () => {
new ShortcutsIssuable(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
initIssuableSidebar();
initSidebarBundle();
});
import initSidebarBundle from '~/sidebar/sidebar_bundle';
document.addEventListener('DOMContentLoaded', initSidebarBundle);
import initSidebarBundle from '~/sidebar/sidebar_bundle';
document.addEventListener('DOMContentLoaded', initSidebarBundle);
import Compare from '~/compare';
import MergeRequest from '~/merge_request';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
document.addEventListener('DOMContentLoaded', () => {
const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare');
......@@ -14,5 +15,6 @@ document.addEventListener('DOMContentLoaded', () => {
new MergeRequest({ // eslint-disable-line no-new
action: mrNewSubmitNode.dataset.mrSubmitAction,
});
initPipelines();
}
});
......@@ -3,18 +3,23 @@ import ZenMode from '~/zen_mode';
import initNotes from '~/init_notes';
import initIssuableSidebar from '~/init_issuable_sidebar';
import initDiffNotes from '~/diff_notes/diff_notes_bundle';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
import ShortcutsIssuable from '~/shortcuts_issuable';
import Diff from '~/diff';
import { handleLocationHash } from '~/lib/utils/common_utils';
import howToMerge from '~/how_to_merge';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initWidget from '../../../../vue_merge_request_widget';
document.addEventListener('DOMContentLoaded', () => {
new Diff(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
initIssuableSidebar();
initSidebarBundle();
initNotes();
initDiffNotes();
initPipelines();
const mrShowNode = document.querySelector('.merge-request');
......@@ -25,4 +30,5 @@ document.addEventListener('DOMContentLoaded', () => {
new ShortcutsIssuable(true); // eslint-disable-line no-new
handleLocationHash();
howToMerge();
initWidget();
});
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, max-len */
import BranchGraph from './branch_graph';
import BranchGraph from '../../../network/branch_graph';
export default (function() {
function Network(opts) {
......
import ShortcutsNetwork from '../../../../shortcuts_network';
import Network from '../network';
document.addEventListener('DOMContentLoaded', () => {
if (!$('.network-graph').length) return;
const networkGraph = new Network({
url: $('.network-graph').attr('data-url'),
commit_url: $('.network-graph').attr('data-commit-url'),
ref: $('.network-graph').attr('data-ref'),
commit_id: $('.network-graph').attr('data-commit-id'),
});
// eslint-disable-next-line no-new
new ShortcutsNetwork(networkGraph.branch_graph);
});
......@@ -2,8 +2,8 @@ import ProjectNew from '../shared/project_new';
import initProjectVisibilitySelector from '../../../project_visibility';
import initProjectNew from '../../../projects/project_new';
export default () => {
document.addEventListener('DOMContentLoaded', () => {
new ProjectNew(); // eslint-disable-line no-new
initProjectVisibilitySelector();
initProjectNew.bindEvents();
};
});
import Vue from 'vue';
import PipelinesStore from './stores/pipelines_store';
import pipelinesComponent from './components/pipelines.vue';
import Translate from '../vue_shared/translate';
import PipelinesStore from '../../../../pipelines/stores/pipelines_store';
import pipelinesComponent from '../../../../pipelines/components/pipelines.vue';
import Translate from '../../../../vue_shared/translate';
Vue.use(Translate);
......
......@@ -3,9 +3,9 @@ import ShortcutsWiki from '../../../shortcuts_wiki';
import ZenMode from '../../../zen_mode';
import GLForm from '../../../gl_form';
export default () => {
document.addEventListener('DOMContentLoaded', () => {
new Wikis(); // eslint-disable-line no-new
new ShortcutsWiki(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
new GLForm($('.wiki-form'), true); // eslint-disable-line no-new
};
});
/* eslint-disable class-methods-use-this */
import Vue from 'vue';
import VueResource from 'vue-resource';
import '../../vue_shared/vue_resource_interceptor';
Vue.use(VueResource);
......
import Mediator from './sidebar_mediator';
import { mountSidebar, getSidebarOptions } from './mount_sidebar';
function domContentLoaded() {
export default () => {
const mediator = new Mediator(getSidebarOptions());
mediator.fetch();
mountSidebar(mediator);
}
document.addEventListener('DOMContentLoaded', domContentLoaded);
export default domContentLoaded;
};
<script>
/* eslint-disable vue/require-default-prop */
import pipelineStage from '../../pipelines/components/stage.vue';
import ciIcon from '../../vue_shared/components/ci_icon.vue';
import icon from '../../vue_shared/components/icon.vue';
import pipelineStage from '~/pipelines/components/stage.vue';
import ciIcon from '~/vue_shared/components/ci_icon.vue';
import icon from '~/vue_shared/components/icon.vue';
export default {
name: 'MRWidgetPipeline',
......
......@@ -6,7 +6,7 @@ import Translate from '../vue_shared/translate';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => {
export default () => {
gl.mrWidgetData.gitlabLogo = gon.gitlab_logo;
const vm = new Vue(mrWidgetOptions);
......@@ -14,4 +14,4 @@ document.addEventListener('DOMContentLoaded', () => {
window.gl.mrWidget = {
checkStatus: vm.checkStatus,
};
});
};
......@@ -196,17 +196,9 @@
@media (min-width: $screen-sm-min) {
font-size: 0;
div {
display: inline;
}
.fa-spinner {
font-size: 12px;
}
span {
font-size: 6px;
}
}
.ci-status-link {
......
......@@ -48,7 +48,7 @@ class Admin::GroupsController < Admin::ApplicationController
def members_update
member_params = params.permit(:user_ids, :access_level, :expires_at)
result = Members::CreateService.new(@group, current_user, member_params.merge(limit: -1)).execute
result = Members::CreateService.new(current_user, member_params.merge(limit: -1)).execute(@group)
if result[:status] == :success
redirect_to [:admin, @group], notice: 'Users were successfully added.'
......
......@@ -3,20 +3,31 @@ module MembershipActions
def create
create_params = params.permit(:user_ids, :access_level, :expires_at)
result = Members::CreateService.new(membershipable, current_user, create_params).execute
redirect_url = members_page_url
result = Members::CreateService.new(current_user, create_params).execute(membershipable)
if result[:status] == :success
redirect_to redirect_url, notice: 'Users were successfully added.'
redirect_to members_page_url, notice: 'Users were successfully added.'
else
redirect_to redirect_url, alert: result[:message]
redirect_to members_page_url, alert: result[:message]
end
end
def update
update_params = params.require(root_params_key).permit(:access_level, :expires_at)
member = membershipable.members_and_requesters.find(params[:id])
member = Members::UpdateService
.new(current_user, update_params)
.execute(member)
.present(current_user: current_user)
respond_to do |format|
format.js { render 'shared/members/update', locals: { member: member } }
end
end
def destroy
Members::DestroyService.new(membershipable, current_user, params)
.execute(:all)
member = membershipable.members_and_requesters.find(params[:id])
Members::DestroyService.new(current_user).execute(member)
respond_to do |format|
format.html do
......@@ -36,14 +47,17 @@ module MembershipActions
end
def approve_access_request
Members::ApproveAccessRequestService.new(membershipable, current_user, params).execute
access_requester = membershipable.requesters.find(params[:id])
Members::ApproveAccessRequestService
.new(current_user, params)
.execute(access_requester)
redirect_to members_page_url
end
def leave
member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id)
.execute(:all)
member = membershipable.members_and_requesters.find_by!(user_id: current_user.id)
Members::DestroyService.new(current_user).execute(member)
notice =
if member.request?
......@@ -62,17 +76,43 @@ module MembershipActions
end
end
def resend_invite
member = membershipable.members.find(params[:id])
if member.invite?
member.resend_invite
redirect_to members_page_url, notice: 'The invitation was successfully resent.'
else
redirect_to members_page_url, alert: 'The invitation has already been accepted.'
end
end
protected
def membershipable
raise NotImplementedError
end
def root_params_key
case membershipable
when Namespace
:group_member
when Project
:project_member
else
raise "Unknown membershipable type: #{membershipable}!"
end
end
def members_page_url
if membershipable.is_a?(Project)
case membershipable
when Namespace
polymorphic_url([membershipable, :members])
when Project
project_project_members_path(membershipable)
else
polymorphic_url([membershipable, :members])
raise "Unknown membershipable type: #{membershipable}!"
end
end
......
......@@ -18,10 +18,6 @@ class Groups::ApplicationController < ApplicationController
@projects ||= GroupProjectsFinder.new(group: group, current_user: current_user).execute
end
def group_merge_requests
@group_merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id).execute
end
def authorize_admin_group!
unless can?(current_user, :admin_group, group)
return render_404
......
......@@ -27,35 +27,6 @@ class Groups::GroupMembersController < Groups::ApplicationController
@group_member = @group.group_members.new
end
def update
@group_member = @group.members_and_requesters.find(params[:id])
.present(current_user: current_user)
return render_403 unless can?(current_user, :update_group_member, @group_member)
@group_member.update_attributes(member_params)
end
def resend_invite
redirect_path = group_group_members_path(@group)
@group_member = @group.group_members.find(params[:id])
if @group_member.invite?
@group_member.resend_invite
redirect_to redirect_path, notice: 'The invitation was successfully resent.'
else
redirect_to redirect_path, alert: 'The invitation has already been accepted.'
end
end
protected
def member_params
params.require(:group_member).permit(:access_level, :user_id, :expires_at)
end
# MembershipActions concern
alias_method :membershipable, :group
end
......@@ -14,7 +14,6 @@ class GroupsController < Groups::ApplicationController
before_action :authorize_create_group!, only: [:new]
before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests]
before_action :group_merge_requests, only: [:merge_requests]
before_action :event_filter, only: [:activity]
before_action :user_actions, only: [:show, :subgroups]
......
......@@ -26,29 +26,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@project_member = @project.project_members.new
end
def update
@project_member = @project.members_and_requesters.find(params[:id])
.present(current_user: current_user)
return render_403 unless can?(current_user, :update_project_member, @project_member)
@project_member.update_attributes(member_params)
end
def resend_invite
redirect_path = project_project_members_path(@project)
@project_member = @project.project_members.find(params[:id])
if @project_member.invite?
@project_member.resend_invite
redirect_to redirect_path, notice: 'The invitation was successfully resent.'
else
redirect_to redirect_path, alert: 'The invitation has already been accepted.'
end
end
def import
@projects = current_user.authorized_projects.order_id_desc
end
......@@ -67,12 +44,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
notice: notice)
end
protected
def member_params
params.require(:project_member).permit(:user_id, :access_level, :expires_at)
end
# MembershipActions concern
alias_method :membershipable, :project
end
......@@ -19,6 +19,20 @@ module GroupsHelper
can?(current_user, :change_share_with_group_lock, group)
end
def group_issues_count(state:)
IssuesFinder
.new(current_user, group_id: @group.id, state: state, non_archived: true, include_subgroups: true)
.execute
.count
end
def group_merge_requests_count(state:)
MergeRequestsFinder
.new(current_user, group_id: @group.id, state: state, non_archived: true, include_subgroups: true)
.execute
.count
end
def group_icon(group, options = {})
img_path = group_icon_url(group, options)
image_tag img_path, options
......@@ -77,10 +91,6 @@ module GroupsHelper
end
end
def group_issues(group)
IssuesFinder.new(current_user, group_id: group.id).execute
end
def remove_group_message(group)
_("You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?") %
{ group_name: group.name }
......
class ChatName < ActiveRecord::Base
LAST_USED_AT_INTERVAL = 1.hour
belongs_to :service
belongs_to :user
......@@ -9,4 +11,23 @@ class ChatName < ActiveRecord::Base
validates :user_id, uniqueness: { scope: [:service_id] }
validates :chat_id, uniqueness: { scope: [:service_id, :team_id] }
# Updates the "last_used_timestamp" but only if it wasn't already updated
# recently.
#
# The throttling this method uses is put in place to ensure that high chat
# traffic doesn't result in many UPDATE queries being performed.
def update_last_used_at
return unless update_last_used_at?
obtained = Gitlab::ExclusiveLease
.new("chat_name/last_used_at/#{id}", timeout: LAST_USED_AT_INTERVAL.to_i)
.try_obtain
touch(:last_used_at) if obtained
end
def update_last_used_at?
last_used_at.nil? || last_used_at > LAST_USED_AT_INTERVAL.ago
end
end
......@@ -6,7 +6,10 @@ module Ci
belongs_to :group
validates :key, uniqueness: { scope: :group_id }
validates :key, uniqueness: {
scope: :group_id,
message: "(%{value}) has already been taken"
}
scope :unprotected, -> { where(protected: false) }
end
......
......@@ -6,7 +6,10 @@ module Ci
belongs_to :project
validates :key, uniqueness: { scope: [:project_id, :environment_scope] }
validates :key, uniqueness: {
scope: [:project_id, :environment_scope],
message: "(%{value}) has already been taken"
}
scope :unprotected, -> { where(protected: false) }
end
......
......@@ -8,6 +8,6 @@ module AccessRequestable
extend ActiveSupport::Concern
def request_access(user)
Members::RequestAccessService.new(self, user).execute
Members::RequestAccessService.new(user).execute(self)
end
end
......@@ -128,7 +128,7 @@ class Member < ActiveRecord::Base
find_by(invite_token: invite_token)
end
def add_user(source, user, access_level, existing_members: nil, current_user: nil, expires_at: nil)
def add_user(source, user, access_level, existing_members: nil, current_user: nil, expires_at: nil, ldap: false)
# `user` can be either a User object, User ID or an email to be invited
member = retrieve_member(source, user, existing_members)
access_level = retrieve_access_level(access_level)
......@@ -143,11 +143,13 @@ class Member < ActiveRecord::Base
if member.request?
::Members::ApproveAccessRequestService.new(
source,
current_user,
id: member.id,
access_level: access_level
).execute
).execute(
member,
skip_authorization: ldap,
skip_log_audit_event: ldap
)
else
member.save
end
......
......@@ -30,10 +30,10 @@ class SlashCommandsService < Service
def trigger(params)
return unless valid_token?(params[:token])
user = find_chat_user(params)
chat_user = find_chat_user(params)
if user
Gitlab::SlashCommands::Command.new(project, user, params).execute
if chat_user&.user
Gitlab::SlashCommands::Command.new(project, chat_user, params).execute
else
url = authorize_chat_name_url(params)
Gitlab::SlashCommands::Presenters::Access.new(url).authorize
......
......@@ -431,7 +431,7 @@ class User < ActiveRecord::Base
end
def self.non_internal
where(Hash[internal_attributes.zip([[false, nil]] * internal_attributes.size)])
where(internal_attributes.map { |attr| "#{attr} IS NOT TRUE" }.join(" AND "))
end
#
......
......@@ -9,8 +9,8 @@ module ChatNames
chat_name = find_chat_name
return unless chat_name
chat_name.touch(:last_used_at)
chat_name.user
chat_name.update_last_used_at
chat_name
end
private
......
module Members
class ApproveAccessRequestService < BaseService
include MembersHelper
attr_accessor :source
# source - The source object that respond to `#requesters` (i.g. project or group)
# current_user - The user that performs the access request approval
# params - A hash of parameters
# :user_id - User ID used to retrieve the access requester
# :id - Member ID used to retrieve the access requester
# :access_level - Optional access level set when the request is accepted
def initialize(source, current_user, params = {})
@source = source
@current_user = current_user
@params = params.slice(:user_id, :id, :access_level)
end
# opts - A hash of options
# :force - Bypass permission check: current_user can be nil in that case
def execute(opts = {})
condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] }
access_requester = source.requesters.find_by!(condition)
raise Gitlab::Access::AccessDeniedError unless can_update_access_requester?(access_requester, opts)
class ApproveAccessRequestService < Members::BaseService
def execute(access_requester, skip_authorization: false, skip_log_audit_event: false)
raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_update_access_requester?(access_requester)
access_requester.access_level = params[:access_level] if params[:access_level]
access_requester.accept_request
after_execute(member: access_requester, skip_log_audit_event: skip_log_audit_event)
access_requester
end
private
def can_update_access_requester?(access_requester, opts = {})
access_requester && (
opts[:force] ||
can?(current_user, update_member_permission(access_requester), access_requester)
)
end
def update_member_permission(member)
case member
when GroupMember
:update_group_member
when ProjectMember
:update_project_member
end
def can_update_access_requester?(access_requester)
can?(current_user, update_member_permission(access_requester), access_requester)
end
end
end
module Members
class AuthorizedDestroyService < BaseService
attr_accessor :member, :user
def initialize(member, user = nil)
@member, @user = member, user
end
def execute
return false if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
Member.transaction do
unassign_issues_and_merge_requests(member) unless member.invite?
member.notification_setting&.destroy
member.destroy
end
if member.request? && member.user != user
notification_service.decline_access_request(member)
end
member
end
private
def unassign_issues_and_merge_requests(member)
if member.is_a?(GroupMember)
issues = Issue.unscoped.select(1)
.joins(:project)
.where('issues.id = issue_assignees.issue_id AND projects.namespace_id = ?', member.source_id)
# DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...)
IssueAssignee.unscoped
.where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues)
.delete_all
MergeRequestsFinder.new(user, group_id: member.source_id, assignee_id: member.user_id)
.execute
.update_all(assignee_id: nil)
else
project = member.source
# SELECT 1 FROM issues WHERE issues.id = issue_assignees.issue_id AND issues.project_id = X
issues = Issue.unscoped.select(1)
.where('issues.id = issue_assignees.issue_id')
.where(project_id: project.id)
# DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...)
IssueAssignee.unscoped
.where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues)
.delete_all
project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil)
end
member.user.invalidate_cache_counts
end
end
end
module Members
class BaseService < ::BaseService
# current_user - The user that performs the action
# params - A hash of parameters
def initialize(current_user = nil, params = {})
@current_user = current_user
@params = params
end
def after_execute(args)
# overriden in EE::Members modules
end
private
def update_member_permission(member)
case member
when GroupMember
:update_group_member
when ProjectMember
:update_project_member
else
raise "Unknown member type: #{member}!"
end
end
def override_member_permission(member)
case member
when GroupMember
:override_group_member
when ProjectMember
:override_project_member
else
raise "Unknown member type: #{member}!"
end
end
def action_member_permission(action, member)
case action
when :update
update_member_permission(member)
when :override
override_member_permission(member)
else
raise "Unknown action '#{action}' on #{member}!"
end
end
end
end
module Members
class CreateService < BaseService
class CreateService < Members::BaseService
DEFAULT_LIMIT = 100
def initialize(source, current_user, params = {})
@source = source
@current_user = current_user
@params = params
@error = nil
end
def execute
def execute(source)
return error('No users specified.') if params[:user_ids].blank?
user_ids = params[:user_ids].split(',').uniq
......@@ -17,13 +10,15 @@ module Members
return error("Too many users specified (limit is #{user_limit})") if
user_limit && user_ids.size > user_limit
@source.add_users(
members = source.add_users(
user_ids,
params[:access_level],
expires_at: params[:expires_at],
current_user: current_user
)
members.each { |member| after_execute(member: member) }
success
end
......
module Members
class DestroyService < BaseService
include MembersHelper
class DestroyService < Members::BaseService
def execute(member, skip_authorization: false)
raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_destroy_member?(member)
attr_accessor :source
return member if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
ALLOWED_SCOPES = %i[members requesters all].freeze
Member.transaction do
unassign_issues_and_merge_requests(member) unless member.invite?
member.notification_setting&.destroy
def initialize(source, current_user, params = {})
@source = source
@current_user = current_user
@params = params
end
def execute(scope = :members)
raise "scope :#{scope} is not allowed!" unless ALLOWED_SCOPES.include?(scope)
member.destroy
end
member = find_member!(scope)
if member.request? && member.user != current_user
notification_service.decline_access_request(member)
end
raise Gitlab::Access::AccessDeniedError unless can_destroy_member?(member)
after_execute(member: member)
AuthorizedDestroyService.new(member, current_user).execute
member
end
private
def find_member!(scope)
condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] }
case scope
when :all
source.members.find_by(condition) ||
source.requesters.find_by!(condition)
else
source.public_send(scope).find_by!(condition) # rubocop:disable GitlabSecurity/PublicSend
end
end
def can_destroy_member?(member)
member && can?(current_user, destroy_member_permission(member), member)
can?(current_user, destroy_member_permission(member), member)
end
def destroy_member_permission(member)
......@@ -45,7 +33,42 @@ module Members
:destroy_group_member
when ProjectMember
:destroy_project_member
else
raise "Unknown member type: #{member}!"
end
end
def unassign_issues_and_merge_requests(member)
if member.is_a?(GroupMember)
issues = Issue.unscoped.select(1)
.joins(:project)
.where('issues.id = issue_assignees.issue_id AND projects.namespace_id = ?', member.source_id)
# DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...)
IssueAssignee.unscoped
.where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues)
.delete_all
MergeRequestsFinder.new(current_user, group_id: member.source_id, assignee_id: member.user_id)
.execute
.update_all(assignee_id: nil)
else
project = member.source
# SELECT 1 FROM issues WHERE issues.id = issue_assignees.issue_id AND issues.project_id = X
issues = Issue.unscoped.select(1)
.where('issues.id = issue_assignees.issue_id')
.where(project_id: project.id)
# DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...)
IssueAssignee.unscoped
.where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues)
.delete_all
project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil)
end
member.user.invalidate_cache_counts
end
end
end
module Members
class RequestAccessService < BaseService
attr_accessor :source
def initialize(source, current_user)
@source = source
@current_user = current_user
end
def execute
class RequestAccessService < Members::BaseService
def execute(source)
raise Gitlab::Access::AccessDeniedError unless can_request_access?(source)
source.members.create(
......@@ -19,7 +12,7 @@ module Members
private
def can_request_access?(source)
source && can?(current_user, :request_access, source)
can?(current_user, :request_access, source)
end
end
end
module Members
class UpdateService < Members::BaseService
# returns the updated member
def execute(member, permission: :update)
raise Gitlab::Access::AccessDeniedError unless can?(current_user, action_member_permission(permission, member), member)
old_access_level = member.human_access
if member.update_attributes(params)
after_execute(action: permission, old_access_level: old_access_level, member: member)
end
member
end
end
end
......@@ -5,6 +5,8 @@
# - Use `validates :xxx, uniqueness: { scope: :xxx_id }` in a child model
class VariableDuplicatesValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if record.errors.include?(:"#{attribute}.key")
if options[:scope]
scoped = value.group_by do |variable|
Array(options[:scope]).map { |attr| variable.send(attr) } # rubocop:disable GitlabSecurity/PublicSend
......
: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)}"));
- page_title "Issues"
- group_issues_exists = group_issues(@group).exists?
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@group.name} issues")
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
- if group_issues_exists
- if group_issues_count(state: 'all').zero?
= render 'shared/empty_states/issues', project_select_button: true
- else
.top-area
= render 'shared/issuable/nav', type: :issues
.nav-controls
......@@ -19,5 +20,3 @@
= render 'shared/issuable/search_bar', type: :issues
= render 'shared/issues'
- else
= render 'shared/empty_states/issues', project_select_button: true
......@@ -3,7 +3,7 @@
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
- if @group_merge_requests.empty?
- if group_merge_requests_count(state: 'all').zero?
= render 'shared/empty_states/merge_requests', project_select_button: true
- else
.top-area
......
- issues_count = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute.count
- merge_requests_count = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute.count
- issues_count = group_issues_count(state: 'opened')
- merge_requests_count = group_merge_requests_count(state: 'opened')
- issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index']
.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
......
......@@ -9,4 +9,3 @@
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue')
= webpack_bundle_tag('commit_pipelines')
......@@ -8,7 +8,6 @@
- page_description @commit.description
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue')
= webpack_bundle_tag('diff_notes')
.container-fluid{ class: [limited_container_width, container_class] }
= render "commit_box"
......
......@@ -20,7 +20,7 @@
.avatar-cell.hidden-xs
= author_avatar(commit, size: 36)
.commit-detail
.commit-detail.flex-list
.commit-content
= link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title")
%span.commit-row-message.visible-xs-inline
......
......@@ -3,7 +3,6 @@
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue')
= webpack_bundle_tag("environments_folder")
#environments-folder-list-view{ data: { endpoint: folder_project_environments_path(@project, @folder, format: :json),
"folder-name" => @folder,
......
......@@ -23,9 +23,6 @@
#js-vue-mr-widget.mr-widget
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'vue_merge_request_widget'
.content-block.content-block-small.emoji-list-container
= render 'award_emoji/awards_block', awardable: @merge_request, inline: true
......
- breadcrumb_title "Graph"
- page_title "Graph", @ref
- content_for :page_specific_javascripts do
= webpack_bundle_tag('network')
= render "head"
%div{ class: container_class }
.project-network
......
......@@ -12,6 +12,3 @@
"has-ci" => @repository.gitlab_ci_yml,
"ci-lint-path" => ci_lint_path,
"reset-cache-path" => reset_cache_project_settings_ci_cd_path(@project) } }
= webpack_bundle_tag('common_vue')
= webpack_bundle_tag('pipelines')
: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)}"));
%h4
= s_('PrometheusService|Auto configuration')
- if service.manual_configuration?
.well
= s_('PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below')
- else
.container-fluid
.row
- if service.prometheus_installed?
.col-sm-2
.svg-container
= image_tag 'illustrations/monitoring/getting_started.svg'
.col-sm-10
%p.text-success.prepend-top-default
= s_('PrometheusService|Prometheus is being automatically managed on your clusters')
= link_to s_('PrometheusService|Manage clusters'), project_clusters_path(project), class: 'btn'
- else
.col-sm-2
= image_tag 'illustrations/monitoring/loading.svg'
.col-sm-10
%p.prepend-top-default
= s_('PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your project’s environments')
= link_to s_('PrometheusService|Install Prometheus on clusters'), project_clusters_path(project), class: 'btn btn-success'
%hr
%h4
= s_('PrometheusService|Auto configuration')
- if @service.manual_configuration?
.well
= s_('PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below')
- else
.container-fluid
.row
- if @service.prometheus_installed?
.col-sm-2
.svg-container
= image_tag 'illustrations/monitoring/getting_started.svg'
.col-sm-10
%p.text-success.prepend-top-default
= s_('PrometheusService|Prometheus is being automatically managed on your clusters')
= link_to s_('PrometheusService|Manage clusters'), project_clusters_path(@project), class: 'btn'
- else
.col-sm-2
= image_tag 'illustrations/monitoring/loading.svg'
.col-sm-10
%p.prepend-top-default
= s_('PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your project’s environments')
= link_to s_('PrometheusService|Install Prometheus on clusters'), project_clusters_path(@project), class: 'btn btn-success'
%hr
- if @project
= render 'projects/services/prometheus/configuration_banner', project: @project, service: @service
%h4.append-bottom-default
= s_('PrometheusService|Manual configuration')
......
- todo = issuable_todo(issuable)
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue')
= webpack_bundle_tag('sidebar')
%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: current_user.present? } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar{ data: { endpoint: "#{issuable_json_path(issuable)}" } }
......
- member = local_assigns.fetch(:member)
:plain
var $listItem = $('#{escape_javascript(render('shared/members/member', member: member))}');
$("##{dom_id(member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(member)}"));
......@@ -5,7 +5,7 @@ class RemoveExpiredMembersWorker
def perform
Member.expired.find_each do |member|
begin
Members::AuthorizedDestroyService.new(member).execute
Members::DestroyService.new.execute(member, skip_authorization: true)
rescue => ex
logger.error("Expired Member ID=#{member.id} cannot be removed - #{ex}")
end
......
---
title: Clear the Labels dropdown search filter after a selection is made
merge_request: 17393
author: Andrew Torres
type: changed
---
title: Keep link when redacting unauthorized object links
merge_request:
author:
type: fixed
---
title: Enables eslint in codeclimate job
merge_request: 17392
author:
type: other
---
title: Allow Prometheus application to be installed from Cluster applications
merge_request: 17372
author:
type: fixed
---
title: Remove duplicated error message on duplicate variable validation
merge_request: 17135
author:
type: fixed
---
title: Fixes gpg popover layout
merge_request: 17323
author:
type: fixed
---
title: Ensure group issues and merge requests pages show results from subgroups when
there are no results from the current group
merge_request: 17312
author:
type: fixed
---
title: Fixes Prometheus admin configuration page
merge_request: 17377
author:
type: fixed
---
title: Add catch-up background migration to migrate pipeline stages
merge_request: 15741
author:
type: performance
---
title: Move BoardNewIssue vue component
merge_request: 16947
author: George Tsiolis
type: performance
......@@ -21,63 +21,49 @@ var DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false';
var WEBPACK_REPORT = process.env.WEBPACK_REPORT;
var NO_COMPRESSION = process.env.NO_COMPRESSION;
// generate automatic entry points
var autoEntries = {};
var pageEntries = glob.sync('pages/**/index.js', { cwd: path.join(ROOT_PATH, 'app/assets/javascripts') });
// filter out entries currently imported dynamically in dispatcher.js
var dispatcher = fs.readFileSync(path.join(ROOT_PATH, 'app/assets/javascripts/dispatcher.js')).toString();
var dispatcherChunks = dispatcher.match(/(?!import\(')\.\/pages\/[^']+/g);
function generateAutoEntries(path, prefix = '.') {
const chunkPath = path.replace(/\/index\.js$/, '');
if (!dispatcherChunks.includes(`${prefix}/${chunkPath}`)) {
var autoEntriesCount = 0;
var watchAutoEntries = [];
function generateEntries() {
// generate automatic entry points
var autoEntries = {};
var pageEntries = glob.sync('pages/**/index.js', { cwd: path.join(ROOT_PATH, 'app/assets/javascripts') });
watchAutoEntries = [
path.join(ROOT_PATH, 'app/assets/javascripts/pages/'),
];
function generateAutoEntries(path, prefix = '.') {
const chunkPath = path.replace(/\/index\.js$/, '');
const chunkName = chunkPath.replace(/\//g, '.');
autoEntries[chunkName] = `${prefix}/${path}`;
}
}
pageEntries.forEach(( path ) => generateAutoEntries(path));
pageEntries.forEach(( path ) => generateAutoEntries(path));
// report our auto-generated bundle count
var autoEntriesCount = Object.keys(autoEntries).length;
console.log(`${autoEntriesCount} entries from '/pages' automatically added to webpack output.`);
autoEntriesCount = Object.keys(autoEntries).length;
var config = {
// because sqljs requires fs.
node: {
fs: "empty"
},
context: path.join(ROOT_PATH, 'app/assets/javascripts'),
entry: {
const manualEntries = {
balsamiq_viewer: './blob/balsamiq_viewer.js',
common: './commons/index.js',
common_vue: './vue_shared/vue_resource_interceptor.js',
cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js',
commit_pipelines: './commit/pipelines/pipelines_bundle.js',
diff_notes: './diff_notes/diff_notes_bundle.js',
environments: './environments/environments_bundle.js',
environments_folder: './environments/folder/environments_folder_bundle.js',
filtered_search: './filtered_search/filtered_search_bundle.js',
help: './help/help.js',
merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js',
monitoring: './monitoring/monitoring_bundle.js',
network: './network/network_bundle.js',
notebook_viewer: './blob/notebook_viewer.js',
pdf_viewer: './blob/pdf_viewer.js',
pipelines: './pipelines/pipelines_bundle.js',
pipelines_details: './pipelines/pipeline_details_bundle.js',
profile: './profile/profile_bundle.js',
project_import_gl: './projects/project_import_gitlab_project.js',
protected_branches: './protected_branches',
registry_list: './registry/index.js',
sidebar: './sidebar/sidebar_bundle.js',
snippet: './snippet/snippet_bundle.js',
sketch_viewer: './blob/sketch_viewer.js',
stl_viewer: './blob/stl_viewer.js',
terminal: './terminal/terminal_bundle.js',
ui_development_kit: './ui_development_kit.js',
vue_merge_request_widget: './vue_merge_request_widget/index.js',
two_factor_auth: './two_factor_auth.js',
......@@ -90,7 +76,15 @@ var config = {
test: './test.js',
u2f: ['vendor/u2f'],
webpack_runtime: './webpack.js',
},
};
return Object.assign(manualEntries, autoEntries);
}
var config = {
context: path.join(ROOT_PATH, 'app/assets/javascripts'),
entry: generateEntries,
output: {
path: path.join(ROOT_PATH, 'public/assets/webpack'),
......@@ -243,12 +237,9 @@ var config = {
name: 'common_vue',
chunks: [
'boards',
'commit_pipelines',
'cycle_analytics',
'deploy_keys',
'diff_notes',
'environments',
'environments_folder',
'filtered_search',
'groups',
'merge_conflicts',
......@@ -308,11 +299,15 @@ var config = {
'images': path.join(ROOT_PATH, 'app/assets/images'),
'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'),
'vue$': 'vue/dist/vue.esm.js',
'spec': path.join(ROOT_PATH, 'spec/javascripts'),
}
}
}
},
config.entry = Object.assign({}, autoEntries, config.entry);
// sqljs requires fs
node: {
fs: 'empty',
},
};
if (IS_PRODUCTION) {
config.devtool = 'source-map';
......@@ -349,7 +344,24 @@ if (IS_DEV_SERVER) {
};
config.plugins.push(
// watch node_modules for changes if we encounter a missing module compile error
new WatchMissingNodeModulesPlugin(path.join(ROOT_PATH, 'node_modules'))
new WatchMissingNodeModulesPlugin(path.join(ROOT_PATH, 'node_modules')),
// watch for changes to our automatic entry point modules
{
apply(compiler) {
compiler.plugin('emit', (compilation, callback) => {
compilation.contextDependencies = [
...compilation.contextDependencies,
...watchAutoEntries,
];
// report our auto-generated bundle count
console.log(`${autoEntriesCount} entries from '/pages' automatically added to webpack output.`);
callback();
})
},
},
);
if (DEV_SERVER_LIVERELOAD) {
config.plugins.push(new webpack.HotModuleReplacementPlugin());
......
class AddTmpPartialNullIndexToBuilds < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
add_concurrent_index(:ci_builds, :id, where: 'stage_id IS NULL',
name: 'tmp_id_partial_null_index')
end
def down
remove_concurrent_index_by_name(:ci_builds, 'tmp_id_partial_null_index')
end
end
class ScheduleBuildStageMigration < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
MIGRATION = 'MigrateBuildStage'.freeze
BATCH_SIZE = 500
disable_ddl_transaction!
class Build < ActiveRecord::Base
include EachBatch
self.table_name = 'ci_builds'
end
def up
disable_statement_timeout
Build.where('stage_id IS NULL').tap do |relation|
queue_background_migration_jobs_by_range_at_intervals(relation,
MIGRATION,
5.minutes,
batch_size: BATCH_SIZE)
end
end
def down
# noop
end
end
class RemoveTmpPartialNullIndexFromBuilds < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
remove_concurrent_index_by_name(:ci_builds, 'tmp_id_partial_null_index')
end
def down
add_concurrent_index(:ci_builds, :id, where: 'stage_id IS NULL',
name: 'tmp_id_partial_null_index')
end
end
......@@ -53,7 +53,10 @@ module API
put ':id/access_requests/:user_id/approve' do
source = find_source(source_type, params[:id])
member = ::Members::ApproveAccessRequestService.new(source, current_user, declared_params).execute
access_requester = source.requesters.find_by!(user_id: params[:user_id])
member = ::Members::ApproveAccessRequestService
.new(current_user, declared_params)
.execute(access_requester)
status :created
present member, with: Entities::Member
......@@ -70,8 +73,7 @@ module API
member = source.requesters.find_by!(user_id: params[:user_id])
destroy_conditionally!(member) do
::Members::DestroyService.new(source, current_user, params)
.execute(:requesters)
::Members::DestroyService.new(current_user).execute(member)
end
end
end
......
......@@ -81,12 +81,16 @@ module API
source = find_source(source_type, params.delete(:id))
authorize_admin_source!(source_type, source)
member = source.members.find_by!(user_id: params.delete(:user_id))
member = source.members.find_by!(user_id: params[:user_id])
updated_member =
::Members::UpdateService
.new(current_user, declared_params(include_missing: false))
.execute(member)
if member.update_attributes(declared_params(include_missing: false))
present member, with: Entities::Member
if updated_member.valid?
present updated_member, with: Entities::Member
else
render_validation_error!(member)
render_validation_error!(updated_member)
end
end
......@@ -99,7 +103,7 @@ module API
member = source.members.find_by!(user_id: params[:user_id])
destroy_conditionally!(member) do
::Members::DestroyService.new(source, current_user, declared_params).execute
::Members::DestroyService.new(current_user).execute(member)
end
end
end
......
# frozen_string_literal: true
module API
class Services < Grape::API
chat_notification_settings = [
CHAT_NOTIFICATION_SETTINGS = [
{
required: true,
name: :webhook,
......@@ -19,9 +20,9 @@ module API
type: String,
desc: 'The default chat channel'
}
]
].freeze
chat_notification_flags = [
CHAT_NOTIFICATION_FLAGS = [
{
required: false,
name: :notify_only_broken_pipelines,
......@@ -34,9 +35,9 @@ module API
type: Boolean,
desc: 'Send notifications only for the default branch'
}
]
].freeze
chat_notification_channels = [
CHAT_NOTIFICATION_CHANNELS = [
{
required: false,
name: :push_channel,
......@@ -85,9 +86,9 @@ module API
type: String,
desc: 'The name of the channel to receive wiki_page_events notifications'
}
]
].freeze
chat_notification_events = [
CHAT_NOTIFICATION_EVENTS = [
{
required: false,
name: :push_events,
......@@ -136,7 +137,7 @@ module API
type: Boolean,
desc: 'Enable notifications for wiki_page_events'
}
]
].freeze
services = {
'asana' => [
......@@ -627,10 +628,10 @@ module API
}
],
'slack' => [
chat_notification_settings,
chat_notification_flags,
chat_notification_channels,
chat_notification_events
CHAT_NOTIFICATION_SETTINGS,
CHAT_NOTIFICATION_FLAGS,
CHAT_NOTIFICATION_CHANNELS,
CHAT_NOTIFICATION_EVENTS
].flatten,
'microsoft-teams' => [
{
......@@ -641,10 +642,10 @@ module API
}
],
'mattermost' => [
chat_notification_settings,
chat_notification_flags,
chat_notification_channels,
chat_notification_events
CHAT_NOTIFICATION_SETTINGS,
CHAT_NOTIFICATION_FLAGS,
CHAT_NOTIFICATION_CHANNELS,
CHAT_NOTIFICATION_EVENTS
].flatten,
'teamcity' => [
{
......@@ -724,7 +725,22 @@ module API
]
end
trigger_services = {
SERVICES = services.freeze
SERVICE_CLASSES = service_classes.freeze
SERVICE_CLASSES.each do |service|
event_names = service.try(:event_names) || next
event_names.each do |event_name|
SERVICES[service.to_param.tr("_", "-")] << {
required: false,
name: event_name.to_sym,
type: String,
desc: ServicesHelper.service_event_description(event_name)
}
end
end
TRIGGER_SERVICES = {
'mattermost-slash-commands' => [
{
name: :token,
......@@ -756,22 +772,9 @@ module API
end
end
services.each do |service_slug, settings|
SERVICES.each do |service_slug, settings|
desc "Set #{service_slug} service for project"
params do
service_classes.each do |service|
event_names = service.try(:event_names) || next
event_names.each do |event_name|
services[service.to_param.tr("_", "-")] << {
required: false,
name: event_name.to_sym,
type: String,
desc: ServicesHelper.service_event_description(event_name)
}
end
end
services.freeze
settings.each do |setting|
if setting[:required]
requires setting[:name], type: setting[:type], desc: setting[:desc]
......@@ -794,7 +797,7 @@ module API
desc "Delete a service for project"
params do
requires :service_slug, type: String, values: services.keys, desc: 'The name of the service'
requires :service_slug, type: String, values: SERVICES.keys, desc: 'The name of the service'
end
delete ":id/services/:service_slug" do
service = user_project.find_or_initialize_service(params[:service_slug].underscore)
......@@ -814,7 +817,7 @@ module API
success Entities::ProjectService
end
params do
requires :service_slug, type: String, values: services.keys, desc: 'The name of the service'
requires :service_slug, type: String, values: SERVICES.keys, desc: 'The name of the service'
end
get ":id/services/:service_slug" do
service = user_project.find_or_initialize_service(params[:service_slug].underscore)
......@@ -822,7 +825,7 @@ module API
end
end
trigger_services.each do |service_slug, settings|
TRIGGER_SERVICES.each do |service_slug, settings|
helpers do
def slash_command_service(project, service_slug, params)
project.services.active.where(template: false).find do |service|
......
......@@ -124,7 +124,7 @@ module API
status(200 )
{ message: "Access revoked", id: params[:user_id].to_i }
else
::Members::DestroyService.new(source, current_user, declared_params).execute
::Members::DestroyService.new(current_user).execute(member)
present member, with: ::API::Entities::Member
end
......
......@@ -174,7 +174,9 @@ module Banzai
title = object_link_title(object)
klass = reference_class(object_sym)
data = data_attributes_for(link_content || match, parent, object, link: !!link_content)
data = data_attributes_for(link_content || match, parent, object,
link_content: !!link_content,
link_reference: link_reference)
url =
if matches.names.include?("url") && matches[:url]
......@@ -194,12 +196,13 @@ module Banzai
end
end
def data_attributes_for(text, project, object, link: false)
def data_attributes_for(text, project, object, link_content: false, link_reference: false)
data_attribute(
original: text,
link: link,
project: project.id,
object_sym => object.id
original: text,
link: link_content,
link_reference: link_reference,
project: project.id,
object_sym => object.id
)
end
......
......@@ -42,16 +42,33 @@ module Banzai
next if visible.include?(node)
doc_data[:visible_reference_count] -= 1
# The reference should be replaced by the original link's content,
# which is not always the same as the rendered one.
content = node.attr('data-original') || node.inner_html
node.replace(content)
redacted_content = redacted_node_content(node)
node.replace(redacted_content)
end
end
metadata
end
# Return redacted content of given node as either the original link (<a> tag),
# the original content (text), or the inner HTML of the node.
#
def redacted_node_content(node)
original_content = node.attr('data-original')
link_reference = node.attr('data-link-reference')
# Build the raw <a> tag just with a link as href and content if
# it's originally a link pattern. We shouldn't return a plain text href.
original_link =
if link_reference == 'true' && href = original_content
%(<a href="#{href}">#{href}</a>)
end
# The reference should be replaced by the original link's content,
# which is not always the same as the rendered one.
original_link || original_content || node.inner_html
end
def redact_cross_project_references(documents)
extractor = Banzai::IssuableExtractor.new(project, user)
issuables = extractor.extract(documents)
......
# frozen_string_literal: true
# rubocop:disable Metrics/AbcSize
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
class MigrateBuildStage
module Migratable
class Stage < ActiveRecord::Base
self.table_name = 'ci_stages'
end
class Build < ActiveRecord::Base
self.table_name = 'ci_builds'
def ensure_stage!(attempts: 2)
find_stage || create_stage!
rescue ActiveRecord::RecordNotUnique
retry if (attempts -= 1) > 0
raise
end
def find_stage
Stage.find_by(name: self.stage || 'test',
pipeline_id: self.commit_id,
project_id: self.project_id)
end
def create_stage!
Stage.create!(name: self.stage || 'test',
pipeline_id: self.commit_id,
project_id: self.project_id)
end
end
end
def perform(start_id, stop_id)
stages = Migratable::Build.where('stage_id IS NULL')
.where('id BETWEEN ? AND ?', start_id, stop_id)
.map { |build| build.ensure_stage! }
.compact.map(&:id)
MigrateBuildStageIdReference.new.perform(start_id, stop_id)
MigrateStageStatus.new.perform(stages.min, stages.max)
end
end
end
end
......@@ -31,10 +31,11 @@ module Gitlab
raise NotImplementedError
end
attr_accessor :project, :current_user, :params
attr_accessor :project, :current_user, :params, :chat_name
def initialize(project, user, params = {})
@project, @current_user, @params = project, user, params.dup
def initialize(project, chat_name, params = {})
@project, @current_user, @params = project, chat_name.user, params.dup
@chat_name = chat_name
end
private
......
......@@ -13,12 +13,13 @@ module Gitlab
if command
if command.allowed?(project, current_user)
command.new(project, current_user, params).execute(match)
command.new(project, chat_name, params).execute(match)
else
Gitlab::SlashCommands::Presenters::Access.new.access_denied
end
else
Gitlab::SlashCommands::Help.new(project, current_user, params).execute(available_commands, params[:text])
Gitlab::SlashCommands::Help.new(project, chat_name, params)
.execute(available_commands, params[:text])
end
end
......
{
"private": true,
"scripts": {
"dev-server": "nodemon -w 'config/webpack.config.js' -w 'app/assets/javascripts/dispatcher.js' -w 'app/assets/javascripts/pages/**/index.js' --exec 'webpack-dev-server --config config/webpack.config.js'",
"dev-server": "nodemon -w 'config/webpack.config.js' --exec 'webpack-dev-server --config config/webpack.config.js'",
"eslint": "eslint --max-warnings 0 --ext .js,.vue .",
"eslint-fix": "eslint --max-warnings 0 --ext .js,.vue --fix .",
"eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html .",
......@@ -116,6 +116,6 @@
"karma-webpack": "2.0.7",
"nodemon": "^1.15.1",
"prettier": "1.9.2",
"webpack-dev-server": "^2.11.1"
"webpack-dev-server": "^2.11.2"
}
}
require 'spec_helper'
describe 'Admin activates Prometheus' do
let(:admin) { create(:user, :admin) }
before do
sign_in(admin)
visit(admin_application_settings_services_path)
click_link('Prometheus')
end
it 'activates service' do
check('Active')
fill_in('API URL', with: 'http://prometheus.example.com')
click_button('Save')
expect(page).to have_content('Application settings saved successfully')
end
end
require 'spec_helper'
feature 'Groups Merge Requests Empty States' do
feature 'Group empty states' do
let(:group) { create(:group) }
let(:user) { create(:group_member, :developer, user: create(:user), group: group ).user }
......@@ -8,62 +8,100 @@ feature 'Groups Merge Requests Empty States' do
sign_in(user)
end
context 'group has a project' do
let(:project) { create(:project, namespace: group) }
[:issue, :merge_request].each do |issuable|
issuable_name = issuable.to_s.humanize.downcase
project_relation = issuable == :issue ? :project : :source_project
before do
project.add_master(user)
end
context "for #{issuable_name}s" do
let(:path) { public_send(:"#{issuable}s_group_path", group) }
context 'the project has a merge request' do
before do
create(:merge_request, source_project: project)
context 'group has a project' do
let(:project) { create(:project, namespace: group) }
visit merge_requests_group_path(group)
end
before do
project.add_master(user)
end
it 'should not display an empty state' do
expect(page).not_to have_selector('.empty-state')
end
end
context "the project has #{issuable_name}s" do
before do
create(issuable, project_relation => project)
context 'the project has no merge requests', :js do
before do
visit merge_requests_group_path(group)
end
visit path
end
it 'should display an empty state' do
expect(page).to have_selector('.empty-state')
end
it 'does not display an empty state' do
expect(page).not_to have_selector('.empty-state')
end
end
context "the project has no #{issuable_name}s", :js do
before do
visit path
end
it 'displays an empty state' do
expect(page).to have_selector('.empty-state')
end
it "shows a new #{issuable_name} button" do
within '.empty-state' do
expect(page).to have_content("create #{issuable_name}")
end
end
it "the new #{issuable_name} button opens a project dropdown" do
within '.empty-state' do
find('.new-project-item-select-button').click
end
it 'should show a new merge request button' do
within '.empty-state' do
expect(page).to have_content('create merge request')
expect(page).to have_selector('.ajax-project-dropdown')
end
end
end
it 'the new merge request button opens a project dropdown' do
within '.empty-state' do
find('.new-project-item-select-button').click
end
context 'group without a project' do
context 'group has a subgroup', :nested_groups do
let(:subgroup) { create(:group, parent: group) }
let(:subgroup_project) { create(:project, namespace: subgroup) }
expect(page).to have_selector('.ajax-project-dropdown')
end
end
end
context "the project has #{issuable_name}s" do
before do
create(issuable, project_relation => subgroup_project)
context 'group without a project' do
before do
visit merge_requests_group_path(group)
end
visit path
end
it 'should display an empty state' do
expect(page).to have_selector('.empty-state')
end
it 'does not display an empty state' do
expect(page).not_to have_selector('.empty-state')
end
end
it 'should not show a new merge request button' do
within '.empty-state' do
expect(page).not_to have_link('create merge request')
context "the project has no #{issuable_name}s" do
before do
visit path
end
it 'displays an empty state' do
expect(page).to have_selector('.empty-state')
end
end
end
context 'group has no subgroups' do
before do
visit path
end
it 'displays an empty state' do
expect(page).to have_selector('.empty-state')
end
it "shows a new #{issuable_name} button" do
within '.empty-state' do
expect(page).not_to have_link("create #{issuable_name}")
end
end
end
end
end
end
......
......@@ -189,6 +189,18 @@ describe 'New/edit issue', :js do
expect(find('.js-label-select')).to have_content('Labels')
end
it 'clears label search input field when a label is selected' do
click_button 'Labels'
page.within '.dropdown-menu-labels' do
search_field = find('input[type="search"]')
search_field.set(label2.title)
click_link label2.title
expect(search_field.value).to eq ''
end
end
it 'correctly updates the selected user when changing assignee' do
click_button 'Unassigned'
......
require 'spec_helper'
describe 'User activates Prometheus' do
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
project.add_master(user)
sign_in(user)
visit(project_settings_integrations_path(project))
click_link('Prometheus')
end
it 'activates service' do
check('Active')
fill_in('API URL', with: 'http://prometheus.example.com')
click_button('Save changes')
expect(page).to have_content('Prometheus activated.')
end
end
......@@ -4,7 +4,7 @@
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import boardNewIssue from '~/boards/components/board_new_issue';
import boardNewIssue from '~/boards/components/board_new_issue.vue';
import '~/boards/models/list';
import { listObj, boardsMockInterceptor, mockBoardService } from './mock_data';
......
import VariableList from '~/ci_variable_list/ci_variable_list';
import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper';
import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
const HIDE_CLASS = 'hide';
......
......@@ -7,7 +7,7 @@ import {
REQUEST_SUCCESS,
REQUEST_FAILURE,
} from '~/clusters/constants';
import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper';
import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
describe('Clusters', () => {
let cluster;
......
......@@ -12,7 +12,7 @@ import {
REQUEST_FAILURE,
} from '~/clusters/constants';
import applicationRow from '~/clusters/components/application_row.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { DEFAULT_APPLICATION_STATE } from '../services/mock_data';
describe('Application Row', () => {
......
import Vue from 'vue';
import applications from '~/clusters/components/applications.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Applications', () => {
let vm;
......
......@@ -2,7 +2,7 @@ import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import mountComponent from '../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Commit pipeline status component', () => {
let vm;
......
import Vue from 'vue';
import banner from '~/cycle_analytics/components/banner.vue';
import mountComponent from '../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Cycle analytics banner', () => {
let vm;
......
import Vue from 'vue';
import component from '~/cycle_analytics/components/total_time_component.vue';
import mountComponent from '../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Total time component', () => {
let vm;
......
import Vue from 'vue';
import emptyState from '~/environments/components/empty_state.vue';
import mountComponent from '../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('environments empty state', () => {
let vm;
......
import Vue from 'vue';
import environmentTableComp from '~/environments/components/environments_table.vue';
import mountComponent from '../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Environment table', () => {
let Component;
......
import _ from 'underscore';
import Vue from 'vue';
import environmentsComponent from '~/environments/components/environments_app.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { headersInterceptor } from 'spec/helpers/vue_resource_helper';
import { environment, folder } from './mock_data';
import { headersInterceptor } from '../helpers/vue_resource_helper';
import mountComponent from '../helpers/vue_mount_component_helper';
describe('Environment', () => {
const mockData = {
......
import _ from 'underscore';
import Vue from 'vue';
import environmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue';
import { headersInterceptor } from 'spec/helpers/vue_resource_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { environmentsList } from '../mock_data';
import { headersInterceptor } from '../../helpers/vue_resource_helper';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Environments Folder View', () => {
let Component;
......
......@@ -8,7 +8,7 @@ import {
mouseenter,
inserted,
} from '~/feature_highlight/feature_highlight_helper';
import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper';
import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
describe('feature highlight helper', () => {
describe('getSelector', () => {
......
......@@ -3,10 +3,9 @@ import * as urlUtils from '~/lib/utils/url_utility';
import groupItemComponent from '~/groups/components/group_item.vue';
import groupFolderComponent from '~/groups/components/group_folder.vue';
import eventHub from '~/groups/event_hub';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockParentGroupItem, mockChildren } from '../mock_data';
import mountComponent from '../../helpers/vue_mount_component_helper';
const createComponent = (group = mockParentGroupItem, parentGroup = mockChildren[0]) => {
const Component = Vue.extend(groupItemComponent);
......
......@@ -4,10 +4,9 @@ import groupsComponent from '~/groups/components/groups.vue';
import groupFolderComponent from '~/groups/components/group_folder.vue';
import groupItemComponent from '~/groups/components/group_item.vue';
import eventHub from '~/groups/event_hub';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockGroups, mockPageInfo } from '../mock_data';
import mountComponent from '../../helpers/vue_mount_component_helper';
const createComponent = (searchEmpty = false) => {
const Component = Vue.extend(groupsComponent);
......
......@@ -2,10 +2,9 @@ import Vue from 'vue';
import itemActionsComponent from '~/groups/components/item_actions.vue';
import eventHub from '~/groups/event_hub';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockParentGroupItem, mockChildren } from '../mock_data';
import mountComponent from '../../helpers/vue_mount_component_helper';
const createComponent = (group = mockParentGroupItem, parentGroup = mockChildren[0]) => {
const Component = Vue.extend(itemActionsComponent);
......
......@@ -2,7 +2,7 @@ import Vue from 'vue';
import itemCaretComponent from '~/groups/components/item_caret.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
const createComponent = (isGroupOpen = false) => {
const Component = Vue.extend(itemCaretComponent);
......
import Vue from 'vue';
import itemStatsComponent from '~/groups/components/item_stats.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import {
mockParentGroupItem,
ITEM_TYPE,
......@@ -9,8 +10,6 @@ import {
PROJECT_VISIBILITY_TYPE,
} from '../mock_data';
import mountComponent from '../../helpers/vue_mount_component_helper';
const createComponent = (item = mockParentGroupItem) => {
const Component = Vue.extend(itemStatsComponent);
......
......@@ -2,7 +2,7 @@ import Vue from 'vue';
import itemStatsValueComponent from '~/groups/components/item_stats_value.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
const createComponent = ({ title, cssClass, iconName, tooltipPlacement, value }) => {
const Component = Vue.extend(itemStatsValueComponent);
......
import Vue from 'vue';
import itemTypeIconComponent from '~/groups/components/item_type_icon.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { ITEM_TYPE } from '../mock_data';
import mountComponent from '../../helpers/vue_mount_component_helper';
const createComponent = (itemType = ITEM_TYPE.GROUP, isGroupOpen = false) => {
const Component = Vue.extend(itemTypeIconComponent);
......
......@@ -6,8 +6,8 @@ import '~/render_gfm';
import * as urlUtils from '~/lib/utils/url_utility';
import issuableApp from '~/issue_show/components/app.vue';
import eventHub from '~/issue_show/event_hub';
import setTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
import issueShowData from '../mock_data';
import setTimeoutPromise from '../../helpers/set_timeout_promise_helper';
function formatText(text) {
return text.trim().replace(/\s\s+/g, ' ');
......
import Vue from 'vue';
import descriptionComponent from '~/issue_show/components/description.vue';
import * as taskList from '~/task_list';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Description component', () => {
let vm;
......
import Vue from 'vue';
import headerComponent from '~/jobs/components/header.vue';
import mountComponent from '../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Job details header', () => {
let HeaderComponent;
......
......@@ -4,7 +4,7 @@ import axios from '~/lib/utils/axios_utils';
import stopJobsModal from '~/pages/admin/jobs/index/components/stop_jobs_modal.vue';
import * as urlUtility from '~/lib/utils/url_utility';
import mountComponent from '../../../../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('stop_jobs_modal.vue', () => {
const props = {
......
......@@ -5,7 +5,7 @@ import deleteMilestoneModal from '~/pages/milestones/shared/components/delete_mi
import eventHub from '~/pages/milestones/shared/event_hub';
import * as urlUtility from '~/lib/utils/url_utility';
import mountComponent from '../../../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('delete_milestone_modal.vue', () => {
const Component = Vue.extend(deleteMilestoneModal);
......
import Vue from 'vue';
import jobComponent from '~/pipelines/components/graph/job_component.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('pipeline graph job component', () => {
let JobComponent;
......
......@@ -2,7 +2,7 @@ import _ from 'underscore';
import Vue from 'vue';
import pipelinesComp from '~/pipelines/components/pipelines.vue';
import Store from '~/pipelines/stores/pipelines_store';
import mountComponent from '../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Pipelines', () => {
const jsonFixtureName = 'pipelines/pipelines.json';
......
......@@ -2,7 +2,7 @@ import Vue from 'vue';
import deleteAccountModal from '~/profile/account/components/delete_account_modal.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('DeleteAccountModal component', () => {
const actionUrl = `${gl.TEST_HOST}/delete/user`;
......
......@@ -6,7 +6,7 @@ import eventHub from '~/projects_dropdown/event_hub';
import ProjectsStore from '~/projects_dropdown/store/projects_store';
import ProjectsService from '~/projects_dropdown/service/projects_service';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { currentSession, mockProject, mockRawProject } from '../mock_data';
const createComponent = () => {
......
......@@ -2,7 +2,7 @@ import Vue from 'vue';
import projectsListFrequentComponent from '~/projects_dropdown/components/projects_list_frequent.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockFrequents } from '../mock_data';
const createComponent = () => {
......
......@@ -2,7 +2,7 @@ import Vue from 'vue';
import projectsListItemComponent from '~/projects_dropdown/components/projects_list_item.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockProject } from '../mock_data';
const createComponent = () => {
......
......@@ -2,7 +2,7 @@ import Vue from 'vue';
import projectsListSearchComponent from '~/projects_dropdown/components/projects_list_search.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockProject } from '../mock_data';
const createComponent = () => {
......
......@@ -3,7 +3,7 @@ import Vue from 'vue';
import searchComponent from '~/projects_dropdown/components/search.vue';
import eventHub from '~/projects_dropdown/event_hub';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
const createComponent = () => {
const Component = Vue.extend(searchComponent);
......
import _ from 'underscore';
import Vue from 'vue';
import registry from '~/registry/components/app.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { reposServerResponse } from '../mock_data';
describe('Registry List', () => {
......
import Vue from 'vue';
import store from '~/ide/stores';
import listCollapsed from '~/ide/components/commit_sidebar/list_collapsed.vue';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { file } from '../../helpers';
describe('Multi-file editor commit sidebar list collapsed', () => {
......
import Vue from 'vue';
import listItem from '~/ide/components/commit_sidebar/list_item.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { file } from '../../helpers';
describe('Multi-file editor commit sidebar list item', () => {
......
import Vue from 'vue';
import store from '~/ide/stores';
import commitSidebarList from '~/ide/components/commit_sidebar/list.vue';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { file } from '../../helpers';
describe('Multi-file editor commit sidebar list', () => {
......
import Vue from 'vue';
import store from '~/ide/stores';
import ideContextBar from '~/ide/components/ide_context_bar.vue';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
describe('Multi-file editor right context bar', () => {
let vm;
......
import Vue from 'vue';
import store from '~/ide/stores';
import ideSidebar from '~/ide/components/ide_side_bar.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../helpers';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
describe('IdeSidebar', () => {
let vm;
......
import Vue from 'vue';
import store from '~/ide/stores';
import ide from '~/ide/components/ide.vue';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { file, resetStore } from '../helpers';
describe('ide component', () => {
......
import Vue from 'vue';
import store from '~/ide/stores';
import newBranchForm from '~/ide/components/new_branch_form.vue';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../helpers';
describe('Multi-file editor new branch form', () => {
......
import Vue from 'vue';
import store from '~/ide/stores';
import newDropdown from '~/ide/components/new_dropdown/index.vue';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../../helpers';
describe('new dropdown component', () => {
......
......@@ -2,7 +2,7 @@ import Vue from 'vue';
import store from '~/ide/stores';
import service from '~/ide/services';
import modal from '~/ide/components/new_dropdown/modal.vue';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { file, resetStore } from '../../helpers';
describe('new file modal component', () => {
......
......@@ -2,7 +2,7 @@ import Vue from 'vue';
import upload from '~/ide/components/new_dropdown/upload.vue';
import store from '~/ide/stores';
import service from '~/ide/services';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../../helpers';
describe('new dropdown upload', () => {
......
......@@ -3,7 +3,7 @@ import * as urlUtils from '~/lib/utils/url_utility';
import store from '~/ide/stores';
import service from '~/ide/services';
import repoCommitSection from '~/ide/components/repo_commit_section.vue';
import getSetTimeoutPromise from '../../helpers/set_timeout_promise_helper';
import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
import { file, resetStore } from '../helpers';
describe('RepoCommitSection', () => {
......
import Vue from 'vue';
import editFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('EditFormButtons', () => {
let vm1;
......
import Vue from 'vue';
import participants from '~/sidebar/components/participants/participants.vue';
import mountComponent from '../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
const PARTICIPANT = {
id: 1,
......
......@@ -4,8 +4,8 @@ import SidebarAssignees from '~/sidebar/components/assignees/sidebar_assignees';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarStore from '~/sidebar/stores/sidebar_store';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import Mock from './mock_data';
import mountComponent from '../helpers/vue_mount_component_helper';
describe('sidebar assignees', () => {
let vm;
......
......@@ -4,7 +4,7 @@ import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarStore from '~/sidebar/stores/sidebar_store';
import eventHub from '~/sidebar/event_hub';
import mountComponent from '../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import Mock from './mock_data';
describe('Sidebar Subscriptions', function () {
......
import Vue from 'vue';
import subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
import mountComponent from '../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Subscriptions', function () {
let vm;
......
import Vue from 'vue';
import authorComponent from '~/vue_merge_request_widget/components/mr_widget_author.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetAuthor', () => {
let vm;
......
import Vue from 'vue';
import authorTimeComponent from '~/vue_merge_request_widget/components/mr_widget_author_time.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetAuthorTime', () => {
let vm;
......
import Vue from 'vue';
import headerComponent from '~/vue_merge_request_widget/components/mr_widget_header.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetHeader', () => {
let vm;
......
import Vue from 'vue';
import mergeHelpComponent from '~/vue_merge_request_widget/components/mr_widget_merge_help.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetMergeHelp', () => {
let vm;
......
import Vue from 'vue';
import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mockData from '../mock_data';
describe('MRWidgetPipeline', () => {
......
import Vue from 'vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
import component from '~/vue_merge_request_widget/components/states/mr_widget_rebase.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Merge request widget rebase component', () => {
let Component;
......
import Vue from 'vue';
import relatedLinksComponent from '~/vue_merge_request_widget/components/mr_widget_related_links.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetRelatedLinks', () => {
let vm;
......
import Vue from 'vue';
import mrStatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MR widget status icon component', () => {
let vm;
......
import Vue from 'vue';
import archivedComponent from '~/vue_merge_request_widget/components/states/mr_widget_archived.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetArchived', () => {
let vm;
......
import Vue from 'vue';
import autoMergeFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
import mountComponent from '../../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetAutoMergeFailed', () => {
let vm;
......
import Vue from 'vue';
import checkingComponent from '~/vue_merge_request_widget/components/states/mr_widget_checking.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetChecking', () => {
let Component;
......
import Vue from 'vue';
import closedComponent from '~/vue_merge_request_widget/components/states/mr_widget_closed.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetClosed', () => {
let vm;
......
import Vue from 'vue';
import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetConflicts', () => {
let Component;
......
import Vue from 'vue';
import failedToMergeComponent from '~/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
import mountComponent from '../../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetFailedToMerge', () => {
let Component;
......
import Vue from 'vue';
import mwpsComponent from '~/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
import mountComponent from '../../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetMergeWhenPipelineSucceeds', () => {
let vm;
......
import Vue from 'vue';
import mergedComponent from '~/vue_merge_request_widget/components/states/mr_widget_merged.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
import mountComponent from '../../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetMerged', () => {
let vm;
......
import Vue from 'vue';
import mergingComponent from '~/vue_merge_request_widget/components/states/mr_widget_merging.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetMerging', () => {
let vm;
......
import Vue from 'vue';
import missingBranchComponent from '~/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetMissingBranch', () => {
let vm;
......
import Vue from 'vue';
import notAllowedComponent from '~/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetNotAllowed', () => {
let vm;
......
import Vue from 'vue';
import pipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetPipelineBlocked', () => {
let vm;
......
......@@ -3,8 +3,8 @@ import mrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options';
import eventHub from '~/vue_merge_request_widget/event_hub';
import notify from '~/lib/utils/notify';
import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mockData from './mock_data';
import mountComponent from '../helpers/vue_mount_component_helper';
const returnPromise = data => new Promise((resolve) => {
resolve({
......
import Vue from 'vue';
import ciBadge from '~/vue_shared/components/ci_badge_link.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('CI Badge Link Component', () => {
let CIBadge;
......
import Vue from 'vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('clipboard button', () => {
let vm;
......
import Vue from 'vue';
import expandButton from '~/vue_shared/components/expand_button.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('expand button', () => {
let vm;
......
import Vue from 'vue';
import fileIcon from '~/vue_shared/components/file_icon.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('File Icon component', () => {
let vm;
......
import Vue from 'vue';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
const modalComponent = Vue.extend(GlModal);
......
import Vue from 'vue';
import headerCi from '~/vue_shared/components/header_ci_component.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Header CI Component', () => {
let HeaderCi;
......
import Vue from 'vue';
import Icon from '~/vue_shared/components/icon.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Sprite Icon Component', function () {
describe('Initialization', function () {
......
import Vue from 'vue';
import issueWarning from '~/vue_shared/components/issue/issue_warning.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
const IssueWarning = Vue.extend(issueWarning);
......
import Vue from 'vue';
import loadingButton from '~/vue_shared/components/loading_button.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
const LABEL = 'Hello';
......
import Vue from 'vue';
import modal from '~/vue_shared/components/modal.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
const modalComponent = Vue.extend(modal);
......
import Vue from 'vue';
import navigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('navigation tabs component', () => {
let vm;
......
import Vue from 'vue';
import placeholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('placeholder system note component', () => {
let PlaceholderSystemNote;
......
import Vue from 'vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Panel Resizer component', () => {
let vm;
......
import Vue from 'vue';
import datePicker from '~/vue_shared/components/pikaday.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('datePicker', () => {
let vm;
......
import Vue from 'vue';
import collapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('collapsedCalendarIcon', () => {
let vm;
......
import Vue from 'vue';
import collapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('collapsedGroupedDatePicker', () => {
let vm;
......
import Vue from 'vue';
import sidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('sidebarDatePicker', () => {
let vm;
......
import Vue from 'vue';
import toggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('toggleSidebar', () => {
let vm;
......
import Vue from 'vue';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Skeleton loading container', () => {
let vm;
......
......@@ -2,7 +2,7 @@ import Vue from 'vue';
import stackedProgressBarComponent from '~/vue_shared/components/stacked_progress_bar.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
const createComponent = (config) => {
const Component = Vue.extend(stackedProgressBarComponent);
......
import Vue from 'vue';
import toggleButton from '~/vue_shared/components/toggle_button.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Toggle Button', () => {
let vm;
......
import Vue from 'vue';
import { placeholderImage } from '~/lazy_loader';
import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
const DEFAULT_PROPS = {
size: 99,
......
......@@ -40,6 +40,16 @@ describe Banzai::Redactor do
expect(doc.to_html).to eq(original_content)
end
end
it 'returns <a> tag with original href if it is originally a link reference' do
href = 'http://localhost:3000'
doc = Nokogiri::HTML
.fragment("<a class='gfm' data-reference-type='issue' data-original=#{href} data-link-reference='true'>#{href}</a>")
redactor.redact([doc])
expect(doc.to_html).to eq('<a href="http://localhost:3000">http://localhost:3000</a>')
end
end
context 'when project is in pending delete' do
......
require 'spec_helper'
describe Gitlab::BackgroundMigration::MigrateBuildStage, :migration, schema: 20180212101928 do
let(:projects) { table(:projects) }
let(:pipelines) { table(:ci_pipelines) }
let(:stages) { table(:ci_stages) }
let(:jobs) { table(:ci_builds) }
STATUSES = { created: 0, pending: 1, running: 2, success: 3,
failed: 4, canceled: 5, skipped: 6, manual: 7 }.freeze
before do
projects.create!(id: 123, name: 'gitlab', path: 'gitlab-ce')
pipelines.create!(id: 1, project_id: 123, ref: 'master', sha: 'adf43c3a')
jobs.create!(id: 1, commit_id: 1, project_id: 123,
stage_idx: 2, stage: 'build', status: :success)
jobs.create!(id: 2, commit_id: 1, project_id: 123,
stage_idx: 2, stage: 'build', status: :success)
jobs.create!(id: 3, commit_id: 1, project_id: 123,
stage_idx: 1, stage: 'test', status: :failed)
jobs.create!(id: 4, commit_id: 1, project_id: 123,
stage_idx: 1, stage: 'test', status: :success)
jobs.create!(id: 5, commit_id: 1, project_id: 123,
stage_idx: 3, stage: 'deploy', status: :pending)
jobs.create!(id: 6, commit_id: 1, project_id: 123,
stage_idx: 3, stage: nil, status: :pending)
end
it 'correctly migrates builds stages' do
expect(stages.count).to be_zero
described_class.new.perform(1, 6)
expect(stages.count).to eq 3
expect(stages.all.pluck(:name)).to match_array %w[test build deploy]
expect(jobs.where(stage_id: nil)).to be_one
expect(jobs.find_by(stage_id: nil).id).to eq 6
expect(stages.all.pluck(:status)).to match_array [STATUSES[:success],
STATUSES[:failed],
STATUSES[:pending]]
end
it 'recovers from unique constraint violation only twice' do
allow(described_class::Migratable::Stage)
.to receive(:find_by).and_return(nil)
expect(described_class::Migratable::Stage)
.to receive(:find_by).exactly(3).times
expect { described_class.new.perform(1, 6) }
.to raise_error ActiveRecord::RecordNotUnique
end
end
......@@ -3,10 +3,11 @@ require 'spec_helper'
describe Gitlab::SlashCommands::Command do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:chat_name) { double(:chat_name, user: user) }
describe '#execute' do
subject do
described_class.new(project, user, params).execute
described_class.new(project, chat_name, params).execute
end
context 'when no command is available' do
......@@ -88,7 +89,7 @@ describe Gitlab::SlashCommands::Command do
end
describe '#match_command' do
subject { described_class.new(project, user, params).match_command.first }
subject { described_class.new(project, chat_name, params).match_command.first }
context 'IssueShow is triggered' do
let(:params) { { text: 'issue show 123' } }
......
......@@ -4,6 +4,7 @@ describe Gitlab::SlashCommands::Deploy do
describe '#execute' do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:chat_name) { double(:chat_name, user: user) }
let(:regex_match) { described_class.match('deploy staging to production') }
before do
......@@ -16,7 +17,7 @@ describe Gitlab::SlashCommands::Deploy do
end
subject do
described_class.new(project, user).execute(regex_match)
described_class.new(project, chat_name).execute(regex_match)
end
context 'if no environment is defined' do
......
......@@ -4,6 +4,7 @@ describe Gitlab::SlashCommands::IssueNew do
describe '#execute' do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:chat_name) { double(:chat_name, user: user) }
let(:regex_match) { described_class.match("issue create bird is the word") }
before do
......@@ -11,7 +12,7 @@ describe Gitlab::SlashCommands::IssueNew do
end
subject do
described_class.new(project, user).execute(regex_match)
described_class.new(project, chat_name).execute(regex_match)
end
context 'without description' do
......
......@@ -6,10 +6,11 @@ describe Gitlab::SlashCommands::IssueSearch do
let!(:confidential) { create(:issue, :confidential, project: project, title: 'mepmep find') }
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:chat_name) { double(:chat_name, user: user) }
let(:regex_match) { described_class.match("issue search find") }
subject do
described_class.new(project, user).execute(regex_match)
described_class.new(project, chat_name).execute(regex_match)
end
context 'when the user has no access' do
......
......@@ -5,6 +5,7 @@ describe Gitlab::SlashCommands::IssueShow do
let(:issue) { create(:issue, project: project) }
let(:project) { create(:project) }
let(:user) { issue.author }
let(:chat_name) { double(:chat_name, user: user) }
let(:regex_match) { described_class.match("issue show #{issue.iid}") }
before do
......@@ -12,7 +13,7 @@ describe Gitlab::SlashCommands::IssueShow do
end
subject do
described_class.new(project, user).execute(regex_match)
described_class.new(project, chat_name).execute(regex_match)
end
context 'the issue exists' do
......
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180212101928_schedule_build_stage_migration')
describe ScheduleBuildStageMigration, :migration do
let(:projects) { table(:projects) }
let(:pipelines) { table(:ci_pipelines) }
let(:stages) { table(:ci_stages) }
let(:jobs) { table(:ci_builds) }
before do
stub_const("#{described_class}::BATCH_SIZE", 1)
projects.create!(id: 123, name: 'gitlab', path: 'gitlab-ce')
pipelines.create!(id: 1, project_id: 123, ref: 'master', sha: 'adf43c3a')
stages.create!(id: 1, project_id: 123, pipeline_id: 1, name: 'test')
jobs.create!(id: 11, commit_id: 1, project_id: 123, stage_id: nil)
jobs.create!(id: 206, commit_id: 1, project_id: 123, stage_id: nil)
jobs.create!(id: 3413, commit_id: 1, project_id: 123, stage_id: nil)
jobs.create!(id: 4109, commit_id: 1, project_id: 123, stage_id: 1)
end
it 'schedules delayed background migrations in batches in bulk' do
Sidekiq::Testing.fake! do
Timecop.freeze do
migrate!
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, 11, 11)
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, 206, 206)
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(15.minutes, 3413, 3413)
expect(BackgroundMigrationWorker.jobs.size).to eq 3
end
end
end
end
......@@ -14,4 +14,24 @@ describe ChatName do
it { is_expected.to validate_uniqueness_of(:user_id).scoped_to(:service_id) }
it { is_expected.to validate_uniqueness_of(:chat_id).scoped_to(:service_id, :team_id) }
describe '#update_last_used_at', :clean_gitlab_redis_shared_state do
it 'updates the last_used_at timestamp' do
expect(subject.last_used_at).to be_nil
subject.update_last_used_at
expect(subject.last_used_at).to be_present
end
it 'does not update last_used_at if it was recently updated' do
subject.update_last_used_at
time = subject.last_used_at
subject.update_last_used_at
expect(subject.last_used_at).to eq(time)
end
end
end
......@@ -5,7 +5,7 @@ describe Ci::GroupVariable do
it { is_expected.to include_module(HasVariable) }
it { is_expected.to include_module(Presentable) }
it { is_expected.to validate_uniqueness_of(:key).scoped_to(:group_id) }
it { is_expected.to validate_uniqueness_of(:key).scoped_to(:group_id).with_message(/\(\w+\) has already been taken/) }
describe '.unprotected' do
subject { described_class.unprotected }
......
......@@ -6,7 +6,7 @@ describe Ci::Variable do
describe 'validations' do
it { is_expected.to include_module(HasVariable) }
it { is_expected.to include_module(Presentable) }
it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id, :environment_scope) }
it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id, :environment_scope).with_message(/\(\w+\) has already been taken/) }
end
describe '.unprotected' do
......
......@@ -1417,7 +1417,7 @@ describe API::Issues do
context 'when source project does not exist' do
it 'returns 404 when trying to move an issue' do
post api("/projects/12345/issues/#{issue.iid}/move", user),
post api("/projects/0/issues/#{issue.iid}/move", user),
to_project_id: target_project.id
expect(response).to have_gitlab_http_status(404)
......@@ -1428,7 +1428,7 @@ describe API::Issues do
context 'when target project does not exist' do
it 'returns 404 when trying to move an issue' do
post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
to_project_id: 12345
to_project_id: 0
expect(response).to have_gitlab_http_status(404)
end
......
......@@ -1218,7 +1218,7 @@ describe API::V3::Issues do
context 'when source project does not exist' do
it 'returns 404 when trying to move an issue' do
post v3_api("/projects/123/issues/#{issue.id}/move", user),
post v3_api("/projects/0/issues/#{issue.id}/move", user),
to_project_id: target_project.id
expect(response).to have_gitlab_http_status(404)
......@@ -1229,7 +1229,7 @@ describe API::V3::Issues do
context 'when target project does not exist' do
it 'returns 404 when trying to move an issue' do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
to_project_id: 123
to_project_id: 0
expect(response).to have_gitlab_http_status(404)
end
......
require 'spec_helper'
describe ChatNames::FindUserService do
describe ChatNames::FindUserService, :clean_gitlab_redis_shared_state do
describe '#execute' do
let(:service) { create(:service) }
......@@ -13,21 +13,30 @@ describe ChatNames::FindUserService do
context 'when existing user is requested' do
let(:params) { { team_id: chat_name.team_id, user_id: chat_name.chat_id } }
it 'returns the existing user' do
is_expected.to eq(user)
it 'returns the existing chat_name' do
is_expected.to eq(chat_name)
end
it 'updates when last time chat name was used' do
it 'updates the last used timestamp if one is not already set' do
expect(chat_name.last_used_at).to be_nil
subject
initial_last_used = chat_name.reload.last_used_at
expect(initial_last_used).to be_present
expect(chat_name.reload.last_used_at).to be_present
end
it 'only updates an existing timestamp once within a certain time frame' do
service = described_class.new(service, params)
expect(chat_name.last_used_at).to be_nil
service.execute
time = chat_name.reload.last_used_at
Timecop.travel(2.days.from_now) { described_class.new(service, params).execute }
service.execute
expect(chat_name.reload.last_used_at).to be > initial_last_used
expect(chat_name.reload.last_used_at).to eq(time)
end
end
......
require 'spec_helper'
describe Members::ApproveAccessRequestService do
let(:user) { create(:user) }
let(:access_requester) { create(:user) }
let(:project) { create(:project, :public, :access_requestable) }
let(:group) { create(:group, :public, :access_requestable) }
let(:current_user) { create(:user) }
let(:access_requester_user) { create(:user) }
let(:access_requester) { source.requesters.find_by!(user_id: access_requester_user.id) }
let(:opts) { {} }
shared_examples 'a service raising ActiveRecord::RecordNotFound' do
it 'raises ActiveRecord::RecordNotFound' do
expect { described_class.new(source, user, params).execute(opts) }.to raise_error(ActiveRecord::RecordNotFound)
expect { described_class.new(current_user).execute(access_requester, opts) }.to raise_error(ActiveRecord::RecordNotFound)
end
end
shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
it 'raises Gitlab::Access::AccessDeniedError' do
expect { described_class.new(source, user, params).execute(opts) }.to raise_error(Gitlab::Access::AccessDeniedError)
expect { described_class.new(current_user).execute(access_requester, opts) }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
shared_examples 'a service approving an access request' do
it 'succeeds' do
expect { described_class.new(source, user, params).execute(opts) }.to change { source.requesters.count }.by(-1)
expect { described_class.new(current_user).execute(access_requester, opts) }.to change { source.requesters.count }.by(-1)
end
it 'returns a <Source>Member' do
member = described_class.new(source, user, params).execute(opts)
member = described_class.new(current_user).execute(access_requester, opts)
expect(member).to be_a "#{source.class}Member".constantize
expect(member.requested_at).to be_nil
end
context 'with a custom access level' do
let(:params2) { params.merge(user_id: access_requester.id, access_level: Gitlab::Access::MASTER) }
it 'returns a ProjectMember with the custom access level' do
member = described_class.new(source, user, params2).execute(opts)
member = described_class.new(current_user, access_level: Gitlab::Access::MASTER).execute(access_requester, opts)
expect(member.access_level).to eq Gitlab::Access::MASTER
expect(member.access_level).to eq(Gitlab::Access::MASTER)
end
end
end
context 'when no access requester are found' do
let(:params) { { user_id: 42 } }
it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do
let(:source) { project }
end
it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do
let(:source) { group }
end
end
context 'when an access requester is found' do
before do
project.request_access(access_requester)
group.request_access(access_requester)
project.request_access(access_requester_user)
group.request_access(access_requester_user)
end
let(:params) { { user_id: access_requester.id } }
context 'when current user is nil' do
let(:user) { nil }
context 'and :force option is not given' do
context 'and :ldap option is not given' do
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:source) { project }
end
......@@ -74,8 +60,8 @@ describe Members::ApproveAccessRequestService do
end
end
context 'and :force option is false' do
let(:opts) { { force: false } }
context 'and :skip_authorization option is false' do
let(:opts) { { skip_authorization: false } }
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:source) { project }
......@@ -86,8 +72,8 @@ describe Members::ApproveAccessRequestService do
end
end
context 'and :force option is true' do
let(:opts) { { force: true } }
context 'and :skip_authorization option is true' do
let(:opts) { { skip_authorization: true } }
it_behaves_like 'a service approving an access request' do
let(:source) { project }
......@@ -97,18 +83,6 @@ describe Members::ApproveAccessRequestService do
let(:source) { group }
end
end
context 'and :force param is true' do
let(:params) { { user_id: access_requester.id, force: true } }
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:source) { project }
end
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:source) { group }
end
end
end
context 'when current user cannot approve access request to the project' do
......@@ -123,8 +97,8 @@ describe Members::ApproveAccessRequestService do
context 'when current user can approve access request to the project' do
before do
project.add_master(user)
group.add_owner(user)
project.add_master(current_user)
group.add_owner(current_user)
end
it_behaves_like 'a service approving an access request' do
......@@ -134,14 +108,6 @@ describe Members::ApproveAccessRequestService do
it_behaves_like 'a service approving an access request' do
let(:source) { group }
end
context 'when given a :id' do
let(:params) { { id: project.requesters.find_by!(user_id: access_requester.id).id } }
it_behaves_like 'a service approving an access request' do
let(:source) { project }
end
end
end
end
end
require 'spec_helper'
describe Members::AuthorizedDestroyService do
let(:member_user) { create(:user) }
let(:project) { create(:project, :public) }
let(:group) { create(:group, :public) }
let(:group_project) { create(:project, :public, group: group) }
def number_of_assigned_issuables(user)
Issue.assigned_to(user).count + MergeRequest.assigned_to(user).count
end
context 'Invited users' do
# Regression spec for issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/32504
it 'destroys invited project member' do
project.add_developer(member_user)
member = create :project_member, :invited, project: project
expect { described_class.new(member, member_user).execute }
.to change { Member.count }.from(3).to(2)
end
it "doesn't destroy invited project member notification_settings" do
project.add_developer(member_user)
member = create :project_member, :invited, project: project
expect { described_class.new(member, member_user).execute }
.not_to change { NotificationSetting.count }
end
it 'destroys invited group member' do
group.add_developer(member_user)
member = create :group_member, :invited, group: group
expect { described_class.new(member, member_user).execute }
.to change { Member.count }.from(2).to(1)
end
it "doesn't destroy invited group member notification_settings" do
group.add_developer(member_user)
member = create :group_member, :invited, group: group
expect { described_class.new(member, member_user).execute }
.not_to change { NotificationSetting.count }
end
end
context 'Requested user' do
it "doesn't destroy member notification_settings" do
member = create(:project_member, user: member_user, requested_at: Time.now)
expect { described_class.new(member, member_user).execute }
.not_to change { NotificationSetting.count }
end
end
context 'Group member' do
let(:member) { group.members.find_by(user_id: member_user.id) }
before do
group.add_developer(member_user)
end
it "unassigns issues and merge requests" do
issue = create :issue, project: group_project, assignees: [member_user]
create :issue, assignees: [member_user]
merge_request = create :merge_request, target_project: group_project, source_project: group_project, assignee: member_user
create :merge_request, target_project: project, source_project: project, assignee: member_user
expect { described_class.new(member, member_user).execute }
.to change { number_of_assigned_issuables(member_user) }.from(4).to(2)
expect(issue.reload.assignee_ids).to be_empty
expect(merge_request.reload.assignee_id).to be_nil
end
it 'destroys member notification_settings' do
group.add_developer(member_user)
member = group.members.find_by(user_id: member_user.id)
expect { described_class.new(member, member_user).execute }
.to change { member_user.notification_settings.count }.by(-1)
end
end
context 'Project member' do
let(:member) { project.members.find_by(user_id: member_user.id) }
before do
project.add_developer(member_user)
end
it "unassigns issues and merge requests" do
create :issue, project: project, assignees: [member_user]
create :merge_request, target_project: project, source_project: project, assignee: member_user
expect { described_class.new(member, member_user).execute }
.to change { number_of_assigned_issuables(member_user) }.from(2).to(0)
end
it 'destroys member notification_settings' do
expect { described_class.new(member, member_user).execute }
.to change { member_user.notification_settings.count }.by(-1)
end
end
end
......@@ -11,7 +11,7 @@ describe Members::CreateService do
it 'adds user to members' do
params = { user_ids: project_user.id.to_s, access_level: Gitlab::Access::GUEST }
result = described_class.new(project, user, params).execute
result = described_class.new(user, params).execute(project)
expect(result[:status]).to eq(:success)
expect(project.users).to include project_user
......@@ -19,7 +19,7 @@ describe Members::CreateService do
it 'adds no user to members' do
params = { user_ids: '', access_level: Gitlab::Access::GUEST }
result = described_class.new(project, user, params).execute
result = described_class.new(user, params).execute(project)
expect(result[:status]).to eq(:error)
expect(result[:message]).to be_present
......@@ -30,7 +30,7 @@ describe Members::CreateService do
user_ids = 1.upto(101).to_a.join(',')
params = { user_ids: user_ids, access_level: Gitlab::Access::GUEST }
result = described_class.new(project, user, params).execute
result = described_class.new(user, params).execute(project)
expect(result[:status]).to eq(:error)
expect(result[:message]).to be_present
......
require 'spec_helper'
describe Members::DestroyService do
let(:user) { create(:user) }
let(:current_user) { create(:user) }
let(:member_user) { create(:user) }
let(:project) { create(:project, :public) }
let(:group) { create(:group, :public) }
let(:group_project) { create(:project, :public, group: group) }
let(:opts) { {} }
shared_examples 'a service raising ActiveRecord::RecordNotFound' do
it 'raises ActiveRecord::RecordNotFound' do
expect { described_class.new(source, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound)
expect { described_class.new(current_user).execute(member) }.to raise_error(ActiveRecord::RecordNotFound)
end
end
shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
it 'raises Gitlab::Access::AccessDeniedError' do
expect { described_class.new(source, user, params).execute }.to raise_error(Gitlab::Access::AccessDeniedError)
expect { described_class.new(current_user).execute(member) }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
def number_of_assigned_issuables(user)
Issue.assigned_to(user).count + MergeRequest.assigned_to(user).count
end
shared_examples 'a service destroying a member' do
it 'destroys the member' do
expect { described_class.new(source, user, params).execute }.to change { source.members.count }.by(-1)
expect { described_class.new(current_user).execute(member, opts) }.to change { member.source.members_and_requesters.count }.by(-1)
end
context 'when the given member is an access requester' do
before do
source.members.find_by(user_id: member_user).destroy
source.update_attributes(request_access_enabled: true)
source.request_access(member_user)
it 'unassigns issues and merge requests' do
if member.invite?
expect { described_class.new(current_user).execute(member, opts) }
.not_to change { number_of_assigned_issuables(member_user) }
else
create :issue, assignees: [member_user]
issue = create :issue, project: group_project, assignees: [member_user]
merge_request = create :merge_request, target_project: group_project, source_project: group_project, assignee: member_user
expect { described_class.new(current_user).execute(member, opts) }
.to change { number_of_assigned_issuables(member_user) }.from(3).to(1)
expect(issue.reload.assignee_ids).to be_empty
expect(merge_request.reload.assignee_id).to be_nil
end
let(:access_requester) { source.requesters.find_by(user_id: member_user) }
end
it_behaves_like 'a service raising ActiveRecord::RecordNotFound'
it 'destroys member notification_settings' do
if member_user.notification_settings.any?
expect { described_class.new(current_user).execute(member, opts) }
.to change { member_user.notification_settings.count }.by(-1)
else
expect { described_class.new(current_user).execute(member, opts) }
.not_to change { member_user.notification_settings.count }
end
end
end
%i[requesters all].each do |scope|
context "and #{scope} scope is passed" do
it 'destroys the access requester' do
expect { described_class.new(source, user, params).execute(scope) }.to change { source.requesters.count }.by(-1)
end
shared_examples 'a service destroying an access requester' do
it_behaves_like 'a service destroying a member'
it 'calls Member#after_decline_request' do
expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(access_requester)
it 'calls Member#after_decline_request' do
expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(member)
described_class.new(source, user, params).execute(scope)
end
described_class.new(current_user).execute(member)
end
context 'when current user is the member' do
it 'does not call Member#after_decline_request' do
expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(access_requester)
context 'when current user is the member' do
it 'does not call Member#after_decline_request' do
expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member)
described_class.new(source, member_user, params).execute(scope)
end
end
end
described_class.new(member_user).execute(member)
end
end
end
context 'when no member are found' do
let(:params) { { user_id: 42 } }
context 'with a member' do
before do
group_project.add_developer(member_user)
group.add_developer(member_user)
end
context 'when current user cannot destroy the given member' do
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:member) { group_project.members.find_by(user_id: member_user.id) }
end
it_behaves_like 'a service destroying a member' do
let(:opts) { { skip_authorization: true } }
let(:member) { group_project.members.find_by(user_id: member_user.id) }
end
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:member) { group.members.find_by(user_id: member_user.id) }
end
it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do
let(:source) { project }
it_behaves_like 'a service destroying a member' do
let(:opts) { { skip_authorization: true } }
let(:member) { group.members.find_by(user_id: member_user.id) }
end
end
it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do
let(:source) { group }
context 'when current user can destroy the given member' do
before do
group_project.add_master(current_user)
group.add_owner(current_user)
end
it_behaves_like 'a service destroying a member' do
let(:member) { group_project.members.find_by(user_id: member_user.id) }
end
it_behaves_like 'a service destroying a member' do
let(:member) { group.members.find_by(user_id: member_user.id) }
end
end
end
context 'when a member is found' do
context 'with an access requester' do
before do
project.add_developer(member_user)
group.add_developer(member_user)
group_project.update_attributes(request_access_enabled: true)
group.update_attributes(request_access_enabled: true)
group_project.request_access(member_user)
group.request_access(member_user)
end
let(:params) { { user_id: member_user.id } }
context 'when current user cannot destroy the given member' do
context 'when current user cannot destroy the given access requester' do
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:source) { project }
let(:member) { group_project.requesters.find_by(user_id: member_user.id) }
end
it_behaves_like 'a service destroying a member' do
let(:opts) { { skip_authorization: true } }
let(:member) { group_project.requesters.find_by(user_id: member_user.id) }
end
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:source) { group }
let(:member) { group.requesters.find_by(user_id: member_user.id) }
end
it_behaves_like 'a service destroying a member' do
let(:opts) { { skip_authorization: true } }
let(:member) { group.requesters.find_by(user_id: member_user.id) }
end
end
context 'when current user can destroy the given member' do
context 'when current user can destroy the given access requester' do
before do
project.add_master(user)
group.add_owner(user)
group_project.add_master(current_user)
group.add_owner(current_user)
end
it_behaves_like 'a service destroying an access requester' do
let(:member) { group_project.requesters.find_by(user_id: member_user.id) }
end
it_behaves_like 'a service destroying an access requester' do
let(:member) { group.requesters.find_by(user_id: member_user.id) }
end
end
end
context 'with an invited user' do
let(:project_invited_member) { create(:project_member, :invited, project: group_project) }
let(:group_invited_member) { create(:group_member, :invited, group: group) }
context 'when current user cannot destroy the given invited user' do
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:member) { project_invited_member }
end
it_behaves_like 'a service destroying a member' do
let(:source) { project }
let(:opts) { { skip_authorization: true } }
let(:member) { project_invited_member }
end
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:member) { group_invited_member }
end
it_behaves_like 'a service destroying a member' do
let(:source) { group }
let(:opts) { { skip_authorization: true } }
let(:member) { group_invited_member }
end
end
context 'when given a :id' do
let(:params) { { id: project.members.find_by!(user_id: user.id).id } }
context 'when current user can destroy the given invited user' do
before do
group_project.add_master(current_user)
group.add_owner(current_user)
end
it 'destroys the member' do
expect { described_class.new(project, user, params).execute }
.to change { project.members.count }.by(-1)
end
# Regression spec for issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/32504
it_behaves_like 'a service destroying a member' do
let(:member) { project_invited_member }
end
it_behaves_like 'a service destroying a member' do
let(:member) { group_invited_member }
end
end
end
......
......@@ -5,17 +5,17 @@ describe Members::RequestAccessService do
shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
it 'raises Gitlab::Access::AccessDeniedError' do
expect { described_class.new(source, user).execute }.to raise_error(Gitlab::Access::AccessDeniedError)
expect { described_class.new(user).execute(source) }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
shared_examples 'a service creating a access request' do
it 'succeeds' do
expect { described_class.new(source, user).execute }.to change { source.requesters.count }.by(1)
expect { described_class.new(user).execute(source) }.to change { source.requesters.count }.by(1)
end
it 'returns a <Source>Member' do
member = described_class.new(source, user).execute
member = described_class.new(user).execute(source)
expect(member).to be_a "#{source.class}Member".constantize
expect(member.requested_at).to be_present
......
require 'spec_helper'
describe Members::UpdateService do
let(:project) { create(:project, :public) }
let(:group) { create(:group, :public) }
let(:current_user) { create(:user) }
let(:member_user) { create(:user) }
let(:permission) { :update }
let(:member) { source.members_and_requesters.find_by!(user_id: member_user.id) }
let(:params) do
{ access_level: Gitlab::Access::MASTER }
end
shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
it 'raises Gitlab::Access::AccessDeniedError' do
expect { described_class.new(current_user, params).execute(member, permission: permission) }
.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
shared_examples 'a service updating a member' do
it 'updates the member' do
updated_member = described_class.new(current_user, params).execute(member, permission: permission)
expect(updated_member).to be_valid
expect(updated_member.access_level).to eq(Gitlab::Access::MASTER)
end
end
before do
project.add_developer(member_user)
group.add_developer(member_user)
end
context 'when current user cannot update the given member' do
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:source) { project }
end
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:source) { group }
end
end
context 'when current user can update the given member' do
before do
project.add_master(current_user)
group.add_owner(current_user)
end
it_behaves_like 'a service updating a member' do
let(:source) { project }
end
it_behaves_like 'a service updating a member' do
let(:source) { group }
end
end
end
......@@ -261,6 +261,8 @@ shared_examples 'variable list' do
click_button('Save variables')
wait_for_requests
expect(all('.js-ci-variable-list-section .js-ci-variable-error-box ul li').count).to eq(1)
# We check the first row because it re-sorts to alphabetical order on refresh
page.within('.js-ci-variable-list-section') do
expect(find('.js-ci-variable-error-box')).to have_content(/Validation failed Variables have duplicate values \(.+\)/)
......
......@@ -10,6 +10,9 @@ nodeExporter:
pushgateway:
enabled: false
rbac:
create: false
server:
image:
tag: v2.1.0
......
......@@ -8732,9 +8732,9 @@ webpack-dev-middleware@1.12.2, webpack-dev-middleware@^1.12.0:
range-parser "^1.0.3"
time-stamp "^2.0.0"
webpack-dev-server@^2.11.1:
version "2.11.1"
resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.11.1.tgz#6f9358a002db8403f016e336816f4485384e5ec0"
webpack-dev-server@^2.11.2:
version "2.11.2"
resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.11.2.tgz#1f4f4c78bf1895378f376815910812daf79a216f"
dependencies:
ansi-html "0.0.7"
array-includes "^3.0.3"
......
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