Commit 0518e590 authored by Andrew Fontaine's avatar Andrew Fontaine Committed by Jose Ivan Vargas

Allow Users to Delete User Lists

Requests confirmation before the list is deleted.
parent c9d043c6
......@@ -190,6 +190,7 @@ export default {
'setProjectId',
'rotateInstanceId',
'toggleFeatureFlag',
'deleteUserList',
]),
onChangeTab(scope) {
this.scope = scope;
......@@ -330,6 +331,7 @@ export default {
<user-lists-table
v-else-if="shouldRenderTable($options.scopes.userLists)"
:user-lists="userLists"
@delete="deleteUserList"
/>
<table-pagination
......
<script>
import { s__, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { GlTooltipDirective } from '@gitlab/ui';
import { GlButton, GlModal, GlSprintf, GlTooltipDirective, GlModalDirective } from '@gitlab/ui';
export default {
directives: { GlTooltip: GlTooltipDirective },
components: { GlButton, GlModal, GlSprintf },
directives: { GlTooltip: GlTooltipDirective, GlModal: GlModalDirective },
mixins: [timeagoMixin],
props: {
userLists: {
......@@ -13,7 +14,31 @@ export default {
},
},
translations: {
createdTimeagoLabel: s__('created %{timeago}'),
createdTimeagoLabel: s__('UserList|created %{timeago}'),
deleteListTitle: s__('UserList|Delete %{name}?'),
deleteListMessage: s__('User list %{name} will be removed. Are you sure?'),
},
modal: {
id: 'deleteListModal',
actionPrimary: {
text: s__('Delete user list'),
attributes: { variant: 'danger', 'data-testid': 'modal-confirm' },
},
},
data() {
return {
deleteUserList: null,
};
},
computed: {
deleteListName() {
return this.deleteUserList?.name;
},
modalTitle() {
return sprintf(this.$options.translations.deleteListTitle, {
name: this.deleteListName,
});
},
},
methods: {
createdTimeago(list) {
......@@ -24,6 +49,12 @@ export default {
displayList(list) {
return list.user_xids.replace(/,/g, ', ');
},
onDelete() {
this.$emit('delete', this.deleteUserList);
},
confirmDeleteList(list) {
this.deleteUserList = list;
},
},
};
</script>
......@@ -36,9 +67,9 @@ export default {
class="gl-border-b-solid gl-border-gray-100 gl-border-b-1 gl-w-full gl-py-4 gl-display-flex gl-justify-content-space-between"
>
<div class="gl-display-flex gl-flex-direction-column gl-overflow-hidden gl-flex-grow-1">
<span data-testid="ffUserListName" class="gl-font-weight-bold gl-mb-2">{{
list.name
}}</span>
<span data-testid="ffUserListName" class="gl-font-weight-bold gl-mb-2">
{{ list.name }}
</span>
<span
v-gl-tooltip
:title="tooltipTitle(list.created_at)"
......@@ -49,6 +80,28 @@ export default {
</span>
<span data-testid="ffUserListIds" class="gl-str-truncated">{{ displayList(list) }}</span>
</div>
<gl-button
v-gl-modal="$options.modal.id"
class="gl-align-self-start gl-mt-2"
category="secondary"
variant="danger"
icon="remove"
data-testid="delete-user-list"
@click="confirmDeleteList(list)"
/>
</div>
<gl-modal
:title="modalTitle"
:modal-id="$options.modal.id"
:action-primary="$options.modal.actionPrimary"
static
@primary="onDelete"
>
<gl-sprintf :message="$options.translations.deleteListMessage">
<template #name>
<b>{{ deleteListName }}</b>
</template>
</gl-sprintf>
</gl-modal>
</div>
</template>
......@@ -67,6 +67,20 @@ export const receiveUpdateFeatureFlagSuccess = ({ commit }, data) =>
export const receiveUpdateFeatureFlagError = ({ commit }, id) =>
commit(types.RECEIVE_UPDATE_FEATURE_FLAG_ERROR, id);
export const deleteUserList = ({ state, dispatch }, list) => {
dispatch('requestDeleteUserList', list);
return Api.deleteFeatureFlagUserList(state.projectId, list.iid)
.then(() => dispatch('fetchUserLists'))
.catch(() => dispatch('receiveDeleteUserListError', list));
};
export const requestDeleteUserList = ({ commit }, list) =>
commit(types.REQUEST_DELETE_USER_LIST, list);
export const receiveDeleteUserListError = ({ commit }, list) =>
commit(types.RECEIVE_DELETE_USER_LIST_ERROR, list);
export const rotateInstanceId = ({ state, dispatch }) => {
dispatch('requestRotateInstanceId');
......
......@@ -11,6 +11,10 @@ export const RECEIVE_FEATURE_FLAGS_ERROR = 'RECEIVE_FEATURE_FLAGS_ERROR';
export const REQUEST_USER_LISTS = 'REQUEST_USER_LISTS';
export const RECEIVE_USER_LISTS_SUCCESS = 'RECEIVE_USER_LISTS_SUCCESS';
export const RECEIVE_USER_LISTS_ERROR = 'RECEIVE_USER_LISTS_ERROR';
export const REQUEST_DELETE_USER_LIST = 'REQUEST_DELETE_USER_LIST';
export const RECEIVE_DELETE_USER_LIST_ERROR = 'RECEIVE_DELETE_USER_LIST_ERROR';
export const UPDATE_FEATURE_FLAG = 'UPDATE_FEATURE_FLAG';
export const RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS = 'RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS';
export const RECEIVE_UPDATE_FEATURE_FLAG_ERROR = 'RECEIVE_UPDATE_FEATURE_FLAG_ERROR';
......
......@@ -110,4 +110,12 @@ export default {
const flag = state[FEATURE_FLAG_SCOPE].find(({ id }) => i === id);
updateFlag(state, { ...flag, active: !flag.active });
},
[types.REQUEST_DELETE_USER_LIST](state, list) {
state.userLists = state.userLists.filter(l => l !== list);
},
[types.RECEIVE_DELETE_USER_LIST_ERROR](state, list) {
state.isLoading = false;
state.hasError = true;
state.userLists = state.userLists.concat(list).sort((l1, l2) => l1.iid - l2.iid);
},
};
---
title: Allow Users to Delete User Lists
merge_request: 34425
author:
type: added
import { mount } from '@vue/test-utils';
import * as timeago from 'timeago.js';
import { GlModal } from '@gitlab/ui';
import UserListsTable from 'ee/feature_flags/components/user_lists_table.vue';
import { userList } from '../mock_data';
......@@ -47,4 +48,45 @@ describe('User Lists Table', () => {
expect(list.contains('[data-testid="ffUserListTimestamp"]')).toBe(true);
});
});
describe('delete button', () => {
it('should display the confirmation modal', () => {
const modal = wrapper.find(GlModal);
wrapper.find('[data-testid="delete-user-list"]').trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(modal.text()).toContain(`Delete ${userList.name}?`);
expect(modal.text()).toContain(`User list ${userList.name} will be removed.`);
});
});
});
describe('confirmation modal', () => {
let modal;
beforeEach(() => {
modal = wrapper.find(GlModal);
wrapper.find('button').trigger('click');
return wrapper.vm.$nextTick();
});
it('should emit delete with list on confirmation', () => {
modal.find('[data-testid="modal-confirm"]').trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('delete')).toEqual([[userLists[0]]]);
});
});
it('should not emit delete with list when not confirmed', () => {
modal.find('button').trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('delete')).toBeUndefined();
});
});
});
});
......@@ -20,6 +20,8 @@ import {
receiveUserListsSuccess,
receiveUserListsError,
fetchUserLists,
deleteUserList,
receiveDeleteUserListError,
} from 'ee/feature_flags/store/modules/index/actions';
import { mapToScopesViewModel } from 'ee/feature_flags/store/modules/helpers';
import state from 'ee/feature_flags/store/modules/index/state';
......@@ -521,4 +523,58 @@ describe('Feature flags actions', () => {
);
});
});
describe('deleteUserList', () => {
beforeEach(() => {
mockedState.userLists = [userList];
});
describe('success', () => {
beforeEach(() => {
Api.deleteFeatureFlagUserList.mockResolvedValue();
});
it('should refresh the user lists', done => {
testAction(
deleteUserList,
userList,
mockedState,
[],
[{ type: 'requestDeleteUserList', payload: userList }, { type: 'fetchUserLists' }],
done,
);
});
});
describe('error', () => {
beforeEach(() => {
Api.deleteFeatureFlagUserList.mockRejectedValue();
});
it('should dispatch receiveDeleteUserListError', done => {
testAction(
deleteUserList,
userList,
mockedState,
[],
[
{ type: 'requestDeleteUserList', payload: userList },
{ type: 'receiveDeleteUserListError', payload: userList },
],
done,
);
});
});
});
describe('receiveDeleteUserListError', () => {
it('should commit RECEIVE_DELETE_USER_LIST_ERROR with the given list', done => {
testAction(
receiveDeleteUserListError,
userList,
mockedState,
[{ type: 'RECEIVE_DELETE_USER_LIST_ERROR', payload: userList }],
[],
done,
);
});
});
});
......@@ -115,7 +115,7 @@ describe('Feature flags store Mutations', () => {
});
});
describe('RECIEVE_USER_LISTS_SUCCESS', () => {
describe('RECEIVE_USER_LISTS_SUCCESS', () => {
const headers = {
'x-next-page': '2',
'x-page': '1',
......@@ -281,4 +281,28 @@ describe('Feature flags store Mutations', () => {
]);
});
});
describe('REQUEST_DELETE_USER_LIST', () => {
beforeEach(() => {
stateCopy.userLists = [userList];
mutations[types.REQUEST_DELETE_USER_LIST](stateCopy, userList);
});
it('should remove the deleted list', () => {
expect(stateCopy.userLists).not.toContain(userList);
});
});
describe('RECEIVE_DELETE_USER_LIST_ERROR', () => {
beforeEach(() => {
stateCopy.userLists = [];
mutations[types.RECEIVE_DELETE_USER_LIST_ERROR](stateCopy, userList);
});
it('should set isLoading to false and hasError to true', () => {
expect(stateCopy.isLoading).toBe(false);
expect(stateCopy.hasError).toBe(true);
});
it('should add the user list back to the list of user lists', () => {
expect(stateCopy.userLists).toContain(userList);
});
});
});
......@@ -7310,6 +7310,9 @@ msgstr ""
msgid "Delete this attachment"
msgstr ""
msgid "Delete user list"
msgstr ""
msgid "Delete variable"
msgstr ""
......@@ -24761,6 +24764,9 @@ msgstr ""
msgid "User key was successfully removed."
msgstr ""
msgid "User list %{name} will be removed. Are you sure?"
msgstr ""
msgid "User map"
msgstr ""
......@@ -24782,6 +24788,12 @@ msgstr ""
msgid "User was successfully updated."
msgstr ""
msgid "UserList|Delete %{name}?"
msgstr ""
msgid "UserList|created %{timeago}"
msgstr ""
msgid "UserOnboardingTour|%{activeTour}/%{totalTours}"
msgstr ""
......@@ -26944,9 +26956,6 @@ msgstr ""
msgid "created %{timeAgo}"
msgstr ""
msgid "created %{timeago}"
msgstr ""
msgid "customize"
msgstr ""
......
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