Commit 2b043b03 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 23be6b41 3399b4c5
...@@ -28,6 +28,7 @@ export default { ...@@ -28,6 +28,7 @@ export default {
modal-type="delete" modal-type="delete"
:username="username" :username="username"
:paths="paths" :paths="paths"
:delete-path="paths.delete"
:oncall-schedules="oncallSchedules" :oncall-schedules="oncallSchedules"
> >
<slot></slot> <slot></slot>
......
...@@ -28,6 +28,7 @@ export default { ...@@ -28,6 +28,7 @@ export default {
modal-type="delete-with-contributions" modal-type="delete-with-contributions"
:username="username" :username="username"
:paths="paths" :paths="paths"
:delete-path="paths.deleteWithContributions"
:oncall-schedules="oncallSchedules" :oncall-schedules="oncallSchedules"
> >
<slot></slot> <slot></slot>
......
...@@ -14,6 +14,10 @@ export default { ...@@ -14,6 +14,10 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
deletePath: {
type: String,
required: true,
},
modalType: { modalType: {
type: String, type: String,
required: true, required: true,
...@@ -27,7 +31,7 @@ export default { ...@@ -27,7 +31,7 @@ export default {
modalAttributes() { modalAttributes() {
return { return {
'data-block-user-url': this.paths.block, 'data-block-user-url': this.paths.block,
'data-delete-user-url': this.paths.delete, 'data-delete-user-url': this.deletePath,
'data-gl-modal-action': this.modalType, 'data-gl-modal-action': this.modalType,
'data-username': this.username, 'data-username': this.username,
'data-oncall-schedules': JSON.stringify(this.oncallSchedules), 'data-oncall-schedules': JSON.stringify(this.oncallSchedules),
......
...@@ -36,10 +36,13 @@ import { ...@@ -36,10 +36,13 @@ import {
filterVariables, filterVariables,
} from '../boards_util'; } from '../boards_util';
import boardLabelsQuery from '../graphql/board_labels.query.graphql'; import boardLabelsQuery from '../graphql/board_labels.query.graphql';
import groupBoardMilestonesQuery from '../graphql/group_board_milestones.query.graphql';
import groupProjectsQuery from '../graphql/group_projects.query.graphql'; import groupProjectsQuery from '../graphql/group_projects.query.graphql';
import issueCreateMutation from '../graphql/issue_create.mutation.graphql'; import issueCreateMutation from '../graphql/issue_create.mutation.graphql';
import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql'; import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
import listsIssuesQuery from '../graphql/lists_issues.query.graphql'; import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
import projectBoardMilestonesQuery from '../graphql/project_board_milestones.query.graphql';
import * as types from './mutation_types'; import * as types from './mutation_types';
export const gqlClient = createGqClient( export const gqlClient = createGqClient(
...@@ -216,6 +219,52 @@ export default { ...@@ -216,6 +219,52 @@ export default {
}); });
}, },
fetchMilestones({ state, commit }, searchTerm) {
commit(types.RECEIVE_MILESTONES_REQUEST);
const { fullPath, boardType } = state;
const variables = {
fullPath,
searchTerm,
};
let query;
if (boardType === BoardType.project) {
query = projectBoardMilestonesQuery;
}
if (boardType === BoardType.group) {
query = groupBoardMilestonesQuery;
}
if (!query) {
// eslint-disable-next-line @gitlab/require-i18n-strings
throw new Error('Unknown board type');
}
return gqlClient
.query({
query,
variables,
})
.then(({ data }) => {
const errors = data[boardType]?.errors;
const milestones = data[boardType]?.milestones.nodes;
if (errors?.[0]) {
throw new Error(errors[0]);
}
commit(types.RECEIVE_MILESTONES_SUCCESS, milestones);
return milestones;
})
.catch((e) => {
commit(types.RECEIVE_MILESTONES_FAILURE);
throw e;
});
},
moveList: ( moveList: (
{ state: { boardLists }, commit, dispatch }, { state: { boardLists }, commit, dispatch },
{ {
......
...@@ -18,6 +18,9 @@ export const RESET_ITEMS_FOR_LIST = 'RESET_ITEMS_FOR_LIST'; ...@@ -18,6 +18,9 @@ export const RESET_ITEMS_FOR_LIST = 'RESET_ITEMS_FOR_LIST';
export const REQUEST_ITEMS_FOR_LIST = 'REQUEST_ITEMS_FOR_LIST'; export const REQUEST_ITEMS_FOR_LIST = 'REQUEST_ITEMS_FOR_LIST';
export const RECEIVE_ITEMS_FOR_LIST_FAILURE = 'RECEIVE_ITEMS_FOR_LIST_FAILURE'; export const RECEIVE_ITEMS_FOR_LIST_FAILURE = 'RECEIVE_ITEMS_FOR_LIST_FAILURE';
export const RECEIVE_ITEMS_FOR_LIST_SUCCESS = 'RECEIVE_ITEMS_FOR_LIST_SUCCESS'; export const RECEIVE_ITEMS_FOR_LIST_SUCCESS = 'RECEIVE_ITEMS_FOR_LIST_SUCCESS';
export const RECEIVE_MILESTONES_REQUEST = 'RECEIVE_MILESTONES_REQUEST';
export const RECEIVE_MILESTONES_SUCCESS = 'RECEIVE_MILESTONES_SUCCESS';
export const RECEIVE_MILESTONES_FAILURE = 'RECEIVE_MILESTONES_FAILURE';
export const UPDATE_BOARD_ITEM = 'UPDATE_BOARD_ITEM'; export const UPDATE_BOARD_ITEM = 'UPDATE_BOARD_ITEM';
export const REMOVE_BOARD_ITEM = 'REMOVE_BOARD_ITEM'; export const REMOVE_BOARD_ITEM = 'REMOVE_BOARD_ITEM';
export const MUTATE_ISSUE_SUCCESS = 'MUTATE_ISSUE_SUCCESS'; export const MUTATE_ISSUE_SUCCESS = 'MUTATE_ISSUE_SUCCESS';
......
import { cloneDeep, pull, union } from 'lodash'; import { cloneDeep, pull, union } from 'lodash';
import Vue from 'vue'; import Vue from 'vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { s__ } from '~/locale'; import { s__, __ } from '~/locale';
import { formatIssue } from '../boards_util'; import { formatIssue } from '../boards_util';
import { issuableTypes } from '../constants'; import { issuableTypes } from '../constants';
import * as mutationTypes from './mutation_types'; import * as mutationTypes from './mutation_types';
...@@ -133,6 +133,20 @@ export default { ...@@ -133,6 +133,20 @@ export default {
Vue.set(state.listsFlags, listId, { [fetchNext ? 'isLoadingMore' : 'isLoading']: true }); Vue.set(state.listsFlags, listId, { [fetchNext ? 'isLoadingMore' : 'isLoading']: true });
}, },
[mutationTypes.RECEIVE_MILESTONES_SUCCESS](state, milestones) {
state.milestones = milestones;
state.milestonesLoading = false;
},
[mutationTypes.RECEIVE_MILESTONES_REQUEST](state) {
state.milestonesLoading = true;
},
[mutationTypes.RECEIVE_MILESTONES_FAILURE](state) {
state.milestonesLoading = false;
state.error = __('Failed to load milestones.');
},
[mutationTypes.RECEIVE_ITEMS_FOR_LIST_SUCCESS]: (state, { listItems, listPageInfo, listId }) => { [mutationTypes.RECEIVE_ITEMS_FOR_LIST_SUCCESS]: (state, { listItems, listPageInfo, listId }) => {
const { listData, boardItems } = listItems; const { listData, boardItems } = listItems;
Vue.set(state, 'boardItems', { ...state.boardItems, ...boardItems }); Vue.set(state, 'boardItems', { ...state.boardItems, ...boardItems });
......
...@@ -19,6 +19,8 @@ export default () => ({ ...@@ -19,6 +19,8 @@ export default () => ({
boardConfig: {}, boardConfig: {},
labelsLoading: false, labelsLoading: false,
labels: [], labels: [],
milestones: [],
milestonesLoading: false,
highlightedLists: [], highlightedLists: [],
selectedBoardItems: [], selectedBoardItems: [],
groupProjects: [], groupProjects: [],
......
...@@ -75,6 +75,7 @@ export default { ...@@ -75,6 +75,7 @@ export default {
:key="note.id" :key="note.id"
:img-src="note.author.avatar_url" :img-src="note.author.avatar_url"
:tooltip-text="getTooltipText(note)" :tooltip-text="getTooltipText(note)"
lazy
class="diff-comment-avatar js-diff-comment-avatar" class="diff-comment-avatar js-diff-comment-avatar"
@click.native="$emit('toggleLineDiscussions')" @click.native="$emit('toggleLineDiscussions')"
/> />
......
...@@ -392,6 +392,7 @@ export default { ...@@ -392,6 +392,7 @@ export default {
:img-src="author.avatar_url" :img-src="author.avatar_url"
:img-alt="author.name" :img-alt="author.name"
:img-size="40" :img-size="40"
lazy
> >
<template #avatar-badge> <template #avatar-badge>
<slot name="avatar-badge"></slot> <slot name="avatar-badge"></slot>
......
...@@ -30,6 +30,11 @@ export default { ...@@ -30,6 +30,11 @@ export default {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
props: { props: {
lazy: {
type: Boolean,
required: false,
default: false,
},
linkHref: { linkHref: {
type: String, type: String,
required: false, required: false,
...@@ -91,6 +96,7 @@ export default { ...@@ -91,6 +96,7 @@ export default {
:size="imgSize" :size="imgSize"
:tooltip-text="avatarTooltipText" :tooltip-text="avatarTooltipText"
:tooltip-placement="tooltipPlacement" :tooltip-placement="tooltipPlacement"
:lazy="lazy"
> >
<slot></slot> </user-avatar-image <slot></slot> </user-avatar-image
><span ><span
......
...@@ -48,9 +48,9 @@ module Admin ...@@ -48,9 +48,9 @@ module Admin
end end
def delete_actions def delete_actions
return unless can?(current_user, :destroy_user, @user) && !@user.blocked_pending_approval? && @user.can_be_removed? return unless can?(current_user, :destroy_user, @user) && !@user.blocked_pending_approval?
@actions << 'delete' @actions << 'delete' if @user.can_be_removed?
@actions << 'delete_with_contributions' @actions << 'delete_with_contributions'
end end
......
...@@ -184,7 +184,7 @@ module UsersHelper ...@@ -184,7 +184,7 @@ module UsersHelper
activate: activate_admin_user_path(:id), activate: activate_admin_user_path(:id),
unlock: unlock_admin_user_path(:id), unlock: unlock_admin_user_path(:id),
delete: admin_user_path(:id), delete: admin_user_path(:id),
delete_with_contributions: admin_user_path(:id), delete_with_contributions: admin_user_path(:id, hard_delete: true),
admin_user: admin_user_path(:id), admin_user: admin_user_path(:id),
ban: ban_admin_user_path(:id), ban: ban_admin_user_path(:id),
unban: unban_admin_user_path(:id) unban: unban_admin_user_path(:id)
......
...@@ -119,7 +119,7 @@ script on the GitLab task runner pod. For more details, see ...@@ -119,7 +119,7 @@ script on the GitLab task runner pod. For more details, see
[backing up a GitLab installation](https://gitlab.com/gitlab-org/charts/gitlab/blob/master/doc/backup-restore/backup.md#backing-up-a-gitlab-installation). [backing up a GitLab installation](https://gitlab.com/gitlab-org/charts/gitlab/blob/master/doc/backup-restore/backup.md#backing-up-a-gitlab-installation).
```shell ```shell
kubectl exec -it <gitlab task-runner pod> backup-utility kubectl exec -it <gitlab task-runner pod> -- backup-utility
``` ```
Similar to the Kubernetes case, if you have scaled out your GitLab cluster to Similar to the Kubernetes case, if you have scaled out your GitLab cluster to
......
...@@ -103,7 +103,7 @@ being modified after the database dump is created. ...@@ -103,7 +103,7 @@ being modified after the database dump is created.
1. Connect to the pod with: 1. Connect to the pod with:
```shell ```shell
kubectl exec -it production-postgres-5db86568d7-qxlxv --namespace "$APP_NAMESPACE" bash kubectl exec -it production-postgres-5db86568d7-qxlxv --namespace "$APP_NAMESPACE" -- bash
``` ```
1. Once, connected, create a dump file with the following command. 1. Once, connected, create a dump file with the following command.
...@@ -221,7 +221,7 @@ higher*. This is the ...@@ -221,7 +221,7 @@ higher*. This is the
1. Connect to the pod: 1. Connect to the pod:
```shell ```shell
kubectl exec -it production-postgresql-0 --namespace "$APP_NAMESPACE" bash kubectl exec -it production-postgresql-0 --namespace "$APP_NAMESPACE" -- bash
``` ```
1. Once connected to the pod, run the following command to restore the database. 1. Once connected to the pod, run the following command to restore the database.
......
...@@ -34,11 +34,9 @@ import epicCreateMutation from '../graphql/epic_create.mutation.graphql'; ...@@ -34,11 +34,9 @@ import epicCreateMutation from '../graphql/epic_create.mutation.graphql';
import epicMoveListMutation from '../graphql/epic_move_list.mutation.graphql'; import epicMoveListMutation from '../graphql/epic_move_list.mutation.graphql';
import epicsSwimlanesQuery from '../graphql/epics_swimlanes.query.graphql'; import epicsSwimlanesQuery from '../graphql/epics_swimlanes.query.graphql';
import groupBoardIterationsQuery from '../graphql/group_board_iterations.query.graphql'; import groupBoardIterationsQuery from '../graphql/group_board_iterations.query.graphql';
import groupBoardMilestonesQuery from '../graphql/group_board_milestones.query.graphql';
import listUpdateLimitMetricsMutation from '../graphql/list_update_limit_metrics.mutation.graphql'; import listUpdateLimitMetricsMutation from '../graphql/list_update_limit_metrics.mutation.graphql';
import listsEpicsQuery from '../graphql/lists_epics.query.graphql'; import listsEpicsQuery from '../graphql/lists_epics.query.graphql';
import projectBoardIterationsQuery from '../graphql/project_board_iterations.query.graphql'; import projectBoardIterationsQuery from '../graphql/project_board_iterations.query.graphql';
import projectBoardMilestonesQuery from '../graphql/project_board_milestones.query.graphql';
import updateBoardEpicUserPreferencesMutation from '../graphql/update_board_epic_user_preferences.mutation.graphql'; import updateBoardEpicUserPreferencesMutation from '../graphql/update_board_epic_user_preferences.mutation.graphql';
import updateEpicLabelsMutation from '../graphql/update_epic_labels.mutation.graphql'; import updateEpicLabelsMutation from '../graphql/update_epic_labels.mutation.graphql';
...@@ -424,50 +422,6 @@ export default { ...@@ -424,50 +422,6 @@ export default {
); );
}, },
fetchMilestones({ state, commit }, searchTerm) {
commit(types.RECEIVE_MILESTONES_REQUEST);
const { fullPath, boardType } = state;
const variables = {
fullPath,
searchTerm,
};
let query;
if (boardType === BoardType.project) {
query = projectBoardMilestonesQuery;
}
if (boardType === BoardType.group) {
query = groupBoardMilestonesQuery;
}
if (!query) {
// eslint-disable-next-line @gitlab/require-i18n-strings
throw new Error('Unknown board type');
}
return gqlClient
.query({
query,
variables,
})
.then(({ data }) => {
const errors = data[boardType]?.errors;
const milestones = data[boardType]?.milestones.nodes;
if (errors?.[0]) {
throw new Error(errors[0]);
}
commit(types.RECEIVE_MILESTONES_SUCCESS, milestones);
})
.catch((e) => {
commit(types.RECEIVE_MILESTONES_FAILURE);
throw e;
});
},
fetchIterations({ state, commit }, title) { fetchIterations({ state, commit }, title) {
commit(types.RECEIVE_ITERATIONS_REQUEST); commit(types.RECEIVE_ITERATIONS_REQUEST);
......
...@@ -15,9 +15,6 @@ export const SET_SHOW_LABELS = 'SET_SHOW_LABELS'; ...@@ -15,9 +15,6 @@ export const SET_SHOW_LABELS = 'SET_SHOW_LABELS';
export const MOVE_EPIC = 'MOVE_EPIC'; export const MOVE_EPIC = 'MOVE_EPIC';
export const MOVE_EPIC_FAILURE = 'MOVE_EPIC_FAILURE'; export const MOVE_EPIC_FAILURE = 'MOVE_EPIC_FAILURE';
export const SET_BOARD_EPIC_USER_PREFERENCES = 'SET_BOARD_EPIC_USER_PREFERENCES'; export const SET_BOARD_EPIC_USER_PREFERENCES = 'SET_BOARD_EPIC_USER_PREFERENCES';
export const RECEIVE_MILESTONES_REQUEST = 'RECEIVE_MILESTONES_REQUEST';
export const RECEIVE_MILESTONES_SUCCESS = 'RECEIVE_MILESTONES_SUCCESS';
export const RECEIVE_MILESTONES_FAILURE = 'RECEIVE_MILESTONES_FAILURE';
export const RECEIVE_ITERATIONS_REQUEST = 'RECEIVE_ITERATIONS_REQUEST'; export const RECEIVE_ITERATIONS_REQUEST = 'RECEIVE_ITERATIONS_REQUEST';
export const RECEIVE_ITERATIONS_SUCCESS = 'RECEIVE_ITERATIONS_SUCCESS'; export const RECEIVE_ITERATIONS_SUCCESS = 'RECEIVE_ITERATIONS_SUCCESS';
export const RECEIVE_ITERATIONS_FAILURE = 'RECEIVE_ITERATIONS_FAILURE'; export const RECEIVE_ITERATIONS_FAILURE = 'RECEIVE_ITERATIONS_FAILURE';
......
...@@ -171,20 +171,6 @@ export default { ...@@ -171,20 +171,6 @@ export default {
} }
}, },
[mutationTypes.RECEIVE_MILESTONES_REQUEST](state) {
state.milestonesLoading = true;
},
[mutationTypes.RECEIVE_MILESTONES_SUCCESS](state, milestones) {
state.milestones = milestones;
state.milestonesLoading = false;
},
[mutationTypes.RECEIVE_MILESTONES_FAILURE](state) {
state.milestonesLoading = false;
state.error = __('Failed to load milestones.');
},
[mutationTypes.RECEIVE_ITERATIONS_REQUEST](state) { [mutationTypes.RECEIVE_ITERATIONS_REQUEST](state) {
state.iterationsLoading = true; state.iterationsLoading = true;
}, },
......
...@@ -15,8 +15,6 @@ export default () => ({ ...@@ -15,8 +15,6 @@ export default () => ({
epicsEndCursor: null, epicsEndCursor: null,
epics: [], epics: [],
epicsFlags: {}, epicsFlags: {},
milestones: [],
milestonesLoading: false,
iterations: [], iterations: [],
iterationsLoading: false, iterationsLoading: false,
assignees: [], assignees: [],
......
...@@ -1120,87 +1120,6 @@ describe('addListNewEpic', () => { ...@@ -1120,87 +1120,6 @@ describe('addListNewEpic', () => {
}); });
}); });
describe('fetchMilestones', () => {
const queryResponse = {
data: {
project: {
milestones: {
nodes: mockMilestones,
},
},
},
};
const queryErrors = {
data: {
project: {
errors: ['You cannot view these milestones'],
milestones: {},
},
},
};
function createStore({
state = {
boardType: 'project',
fullPath: 'gitlab-org/gitlab',
milestones: [],
milestonesLoading: false,
},
} = {}) {
return new Vuex.Store({
state,
mutations,
});
}
it('throws error if state.boardType is not group or project', () => {
const store = createStore({
state: {
boardType: 'invalid',
},
});
expect(() => actions.fetchMilestones(store)).toThrow(new Error('Unknown board type'));
});
it('sets milestonesLoading to true', async () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
const store = createStore();
actions.fetchMilestones(store);
expect(store.state.milestonesLoading).toBe(true);
});
describe('success', () => {
it('sets state.milestones from query result', async () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
const store = createStore();
await actions.fetchMilestones(store);
expect(store.state.milestonesLoading).toBe(false);
expect(store.state.milestones).toBe(mockMilestones);
});
});
describe('failure', () => {
it('sets state.milestones from query result', async () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryErrors);
const store = createStore();
await expect(actions.fetchMilestones(store)).rejects.toThrow();
expect(store.state.milestonesLoading).toBe(false);
expect(store.state.error).toBe('Failed to load milestones.');
});
});
});
describe('fetchIterations', () => { describe('fetchIterations', () => {
const queryResponse = { const queryResponse = {
data: { data: {
......
...@@ -90,6 +90,39 @@ RSpec.describe 'Admin::Users::User' do ...@@ -90,6 +90,39 @@ RSpec.describe 'Admin::Users::User' do
end end
end end
context 'when user is the sole owner of a group' do
let_it_be(:group) { create(:group) }
let_it_be(:user_sole_owner_of_group) { create(:user) }
before do
group.add_owner(user_sole_owner_of_group)
end
it 'shows `Delete user and contributions` action but not `Delete user` action', :js do
visit admin_user_path(user_sole_owner_of_group)
click_user_dropdown_toggle(user_sole_owner_of_group.id)
expect(page).to have_button('Delete user and contributions')
expect(page).not_to have_button('Delete user', exact: true)
end
it 'allows user to be deleted by using the `Delete user and contributions` action', :js do
visit admin_user_path(user_sole_owner_of_group)
click_action_in_user_dropdown(user_sole_owner_of_group.id, 'Delete user and contributions')
page.within('[role="dialog"]') do
fill_in('username', with: user_sole_owner_of_group.name)
click_button('Delete user and contributions')
end
wait_for_requests
expect(page).to have_content('The user is being deleted.')
end
end
describe 'Impersonation' do describe 'Impersonation' do
let_it_be(:another_user) { create(:user) } let_it_be(:another_user) { create(:user) }
......
...@@ -5,8 +5,8 @@ import { nextTick } from 'vue'; ...@@ -5,8 +5,8 @@ import { nextTick } from 'vue';
import Actions from '~/admin/users/components/actions'; import Actions from '~/admin/users/components/actions';
import SharedDeleteAction from '~/admin/users/components/actions/shared/shared_delete_action.vue'; import SharedDeleteAction from '~/admin/users/components/actions/shared/shared_delete_action.vue';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { CONFIRMATION_ACTIONS, DELETE_ACTIONS } from '../../constants'; import { CONFIRMATION_ACTIONS, DELETE_ACTIONS } from '../../constants';
import { paths } from '../../mock_data';
describe('Action components', () => { describe('Action components', () => {
let wrapper; let wrapper;
...@@ -47,32 +47,33 @@ describe('Action components', () => { ...@@ -47,32 +47,33 @@ describe('Action components', () => {
describe('DELETE_ACTION_COMPONENTS', () => { describe('DELETE_ACTION_COMPONENTS', () => {
const oncallSchedules = [{ name: 'schedule1' }, { name: 'schedule2' }]; const oncallSchedules = [{ name: 'schedule1' }, { name: 'schedule2' }];
it.each(DELETE_ACTIONS)('renders a dropdown item for "%s"', async (action) => {
initComponent({ it.each(DELETE_ACTIONS.map((action) => [action, paths[action]]))(
component: Actions[capitalizeFirstCharacter(action)], 'renders a dropdown item for "%s"',
props: { async (action, expectedPath) => {
username: 'John Doe', initComponent({
paths: { component: Actions[capitalizeFirstCharacter(action)],
delete: '/delete', props: {
block: '/block', username: 'John Doe',
paths,
oncallSchedules,
}, },
oncallSchedules, stubs: { SharedDeleteAction },
}, });
stubs: { SharedDeleteAction },
});
await nextTick(); await nextTick();
const sharedAction = wrapper.find(SharedDeleteAction); const sharedAction = wrapper.find(SharedDeleteAction);
expect(sharedAction.attributes('data-block-user-url')).toBe('/block'); expect(sharedAction.attributes('data-block-user-url')).toBe(paths.block);
expect(sharedAction.attributes('data-delete-user-url')).toBe('/delete'); expect(sharedAction.attributes('data-delete-user-url')).toBe(expectedPath);
expect(sharedAction.attributes('data-gl-modal-action')).toBe(kebabCase(action)); expect(sharedAction.attributes('data-gl-modal-action')).toBe(kebabCase(action));
expect(sharedAction.attributes('data-username')).toBe('John Doe'); expect(sharedAction.attributes('data-username')).toBe('John Doe');
expect(sharedAction.attributes('data-oncall-schedules')).toBe( expect(sharedAction.attributes('data-oncall-schedules')).toBe(
JSON.stringify(oncallSchedules), JSON.stringify(oncallSchedules),
); );
expect(findDropdownItem().exists()).toBe(true); expect(findDropdownItem().exists()).toBe(true);
}); },
);
}); });
}); });
...@@ -30,7 +30,7 @@ export const paths = { ...@@ -30,7 +30,7 @@ export const paths = {
activate: '/admin/users/id/activate', activate: '/admin/users/id/activate',
unlock: '/admin/users/id/unlock', unlock: '/admin/users/id/unlock',
delete: '/admin/users/id', delete: '/admin/users/id',
deleteWithContributions: '/admin/users/id', deleteWithContributions: '/admin/users/id?hard_delete=true',
adminUser: '/admin/users/id', adminUser: '/admin/users/id',
ban: '/admin/users/id/ban', ban: '/admin/users/id/ban',
unban: '/admin/users/id/unban', unban: '/admin/users/id/unban',
......
...@@ -101,6 +101,17 @@ export const mockMilestone = { ...@@ -101,6 +101,17 @@ export const mockMilestone = {
due_date: '2019-12-31', due_date: '2019-12-31',
}; };
export const mockMilestones = [
{
id: 'gid://gitlab/Milestone/1',
title: 'Milestone 1',
},
{
id: 'gid://gitlab/Milestone/2',
title: 'Milestone 2',
},
];
export const assignees = [ export const assignees = [
{ {
id: 'gid://gitlab/User/2', id: 'gid://gitlab/User/2',
......
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import Vue from 'vue';
import Vuex from 'vuex';
import { import {
inactiveId, inactiveId,
ISSUABLE, ISSUABLE,
...@@ -22,6 +24,7 @@ import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutati ...@@ -22,6 +24,7 @@ import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutati
import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql'; import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql';
import actions, { gqlClient } from '~/boards/stores/actions'; import actions, { gqlClient } from '~/boards/stores/actions';
import * as types from '~/boards/stores/mutation_types'; import * as types from '~/boards/stores/mutation_types';
import mutations from '~/boards/stores/mutations';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { import {
...@@ -38,6 +41,7 @@ import { ...@@ -38,6 +41,7 @@ import {
mockMoveState, mockMoveState,
mockMoveData, mockMoveData,
mockList, mockList,
mockMilestones,
} from '../mock_data'; } from '../mock_data';
jest.mock('~/flash'); jest.mock('~/flash');
...@@ -46,6 +50,8 @@ jest.mock('~/flash'); ...@@ -46,6 +50,8 @@ jest.mock('~/flash');
// subgroups when the movIssue action is called. // subgroups when the movIssue action is called.
const getProjectPath = (path) => path.split('#')[0]; const getProjectPath = (path) => path.split('#')[0];
Vue.use(Vuex);
beforeEach(() => { beforeEach(() => {
window.gon = { features: {} }; window.gon = { features: {} };
}); });
...@@ -261,6 +267,87 @@ describe('fetchLists', () => { ...@@ -261,6 +267,87 @@ describe('fetchLists', () => {
); );
}); });
describe('fetchMilestones', () => {
const queryResponse = {
data: {
project: {
milestones: {
nodes: mockMilestones,
},
},
},
};
const queryErrors = {
data: {
project: {
errors: ['You cannot view these milestones'],
milestones: {},
},
},
};
function createStore({
state = {
boardType: 'project',
fullPath: 'gitlab-org/gitlab',
milestones: [],
milestonesLoading: false,
},
} = {}) {
return new Vuex.Store({
state,
mutations,
});
}
it('throws error if state.boardType is not group or project', () => {
const store = createStore({
state: {
boardType: 'invalid',
},
});
expect(() => actions.fetchMilestones(store)).toThrow(new Error('Unknown board type'));
});
it('sets milestonesLoading to true', async () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
const store = createStore();
actions.fetchMilestones(store);
expect(store.state.milestonesLoading).toBe(true);
});
describe('success', () => {
it('sets state.milestones from query result', async () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
const store = createStore();
await actions.fetchMilestones(store);
expect(store.state.milestonesLoading).toBe(false);
expect(store.state.milestones).toBe(mockMilestones);
});
});
describe('failure', () => {
it('sets state.milestones from query result', async () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryErrors);
const store = createStore();
await expect(actions.fetchMilestones(store)).rejects.toThrow();
expect(store.state.milestonesLoading).toBe(false);
expect(store.state.error).toBe('Failed to load milestones.');
});
});
});
describe('createList', () => { describe('createList', () => {
it('should dispatch createIssueList action', () => { it('should dispatch createIssueList action', () => {
testAction({ testAction({
......
...@@ -104,4 +104,15 @@ describe('User Avatar Link Component', () => { ...@@ -104,4 +104,15 @@ describe('User Avatar Link Component', () => {
); );
}); });
}); });
describe('lazy', () => {
it('passes lazy prop to avatar image', () => {
createWrapper({
username: '',
lazy: true,
});
expect(wrapper.find(UserAvatarImage).props('lazy')).toBe(true);
});
});
}); });
...@@ -106,7 +106,7 @@ RSpec.describe Admin::UserActionsHelper do ...@@ -106,7 +106,7 @@ RSpec.describe Admin::UserActionsHelper do
group.add_owner(user) group.add_owner(user)
end end
it { is_expected.to contain_exactly("edit", "block", "ban", "deactivate") } it { is_expected.to contain_exactly("edit", "block", "ban", "deactivate", "delete_with_contributions") }
end end
context 'the user is a bot' do context 'the user is a bot' do
......
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