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 { ...@@ -190,6 +190,7 @@ export default {
'setProjectId', 'setProjectId',
'rotateInstanceId', 'rotateInstanceId',
'toggleFeatureFlag', 'toggleFeatureFlag',
'deleteUserList',
]), ]),
onChangeTab(scope) { onChangeTab(scope) {
this.scope = scope; this.scope = scope;
...@@ -330,6 +331,7 @@ export default { ...@@ -330,6 +331,7 @@ export default {
<user-lists-table <user-lists-table
v-else-if="shouldRenderTable($options.scopes.userLists)" v-else-if="shouldRenderTable($options.scopes.userLists)"
:user-lists="userLists" :user-lists="userLists"
@delete="deleteUserList"
/> />
<table-pagination <table-pagination
......
<script> <script>
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import { GlTooltipDirective } from '@gitlab/ui'; import { GlButton, GlModal, GlSprintf, GlTooltipDirective, GlModalDirective } from '@gitlab/ui';
export default { export default {
directives: { GlTooltip: GlTooltipDirective }, components: { GlButton, GlModal, GlSprintf },
directives: { GlTooltip: GlTooltipDirective, GlModal: GlModalDirective },
mixins: [timeagoMixin], mixins: [timeagoMixin],
props: { props: {
userLists: { userLists: {
...@@ -13,7 +14,31 @@ export default { ...@@ -13,7 +14,31 @@ export default {
}, },
}, },
translations: { 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: { methods: {
createdTimeago(list) { createdTimeago(list) {
...@@ -24,6 +49,12 @@ export default { ...@@ -24,6 +49,12 @@ export default {
displayList(list) { displayList(list) {
return list.user_xids.replace(/,/g, ', '); return list.user_xids.replace(/,/g, ', ');
}, },
onDelete() {
this.$emit('delete', this.deleteUserList);
},
confirmDeleteList(list) {
this.deleteUserList = list;
},
}, },
}; };
</script> </script>
...@@ -36,9 +67,9 @@ export default { ...@@ -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" 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"> <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">{{ <span data-testid="ffUserListName" class="gl-font-weight-bold gl-mb-2">
list.name {{ list.name }}
}}</span> </span>
<span <span
v-gl-tooltip v-gl-tooltip
:title="tooltipTitle(list.created_at)" :title="tooltipTitle(list.created_at)"
...@@ -49,6 +80,28 @@ export default { ...@@ -49,6 +80,28 @@ export default {
</span> </span>
<span data-testid="ffUserListIds" class="gl-str-truncated">{{ displayList(list) }}</span> <span data-testid="ffUserListIds" class="gl-str-truncated">{{ displayList(list) }}</span>
</div> </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> </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> </div>
</template> </template>
...@@ -67,6 +67,20 @@ export const receiveUpdateFeatureFlagSuccess = ({ commit }, data) => ...@@ -67,6 +67,20 @@ export const receiveUpdateFeatureFlagSuccess = ({ commit }, data) =>
export const receiveUpdateFeatureFlagError = ({ commit }, id) => export const receiveUpdateFeatureFlagError = ({ commit }, id) =>
commit(types.RECEIVE_UPDATE_FEATURE_FLAG_ERROR, 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 }) => { export const rotateInstanceId = ({ state, dispatch }) => {
dispatch('requestRotateInstanceId'); dispatch('requestRotateInstanceId');
......
...@@ -11,6 +11,10 @@ export const RECEIVE_FEATURE_FLAGS_ERROR = 'RECEIVE_FEATURE_FLAGS_ERROR'; ...@@ -11,6 +11,10 @@ export const RECEIVE_FEATURE_FLAGS_ERROR = 'RECEIVE_FEATURE_FLAGS_ERROR';
export const REQUEST_USER_LISTS = 'REQUEST_USER_LISTS'; export const REQUEST_USER_LISTS = 'REQUEST_USER_LISTS';
export const RECEIVE_USER_LISTS_SUCCESS = 'RECEIVE_USER_LISTS_SUCCESS'; export const RECEIVE_USER_LISTS_SUCCESS = 'RECEIVE_USER_LISTS_SUCCESS';
export const RECEIVE_USER_LISTS_ERROR = 'RECEIVE_USER_LISTS_ERROR'; 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 UPDATE_FEATURE_FLAG = 'UPDATE_FEATURE_FLAG';
export const RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS = 'RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS'; export const RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS = 'RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS';
export const RECEIVE_UPDATE_FEATURE_FLAG_ERROR = 'RECEIVE_UPDATE_FEATURE_FLAG_ERROR'; export const RECEIVE_UPDATE_FEATURE_FLAG_ERROR = 'RECEIVE_UPDATE_FEATURE_FLAG_ERROR';
......
...@@ -110,4 +110,12 @@ export default { ...@@ -110,4 +110,12 @@ export default {
const flag = state[FEATURE_FLAG_SCOPE].find(({ id }) => i === id); const flag = state[FEATURE_FLAG_SCOPE].find(({ id }) => i === id);
updateFlag(state, { ...flag, active: !flag.active }); 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 { mount } from '@vue/test-utils';
import * as timeago from 'timeago.js'; import * as timeago from 'timeago.js';
import { GlModal } from '@gitlab/ui';
import UserListsTable from 'ee/feature_flags/components/user_lists_table.vue'; import UserListsTable from 'ee/feature_flags/components/user_lists_table.vue';
import { userList } from '../mock_data'; import { userList } from '../mock_data';
...@@ -47,4 +48,45 @@ describe('User Lists Table', () => { ...@@ -47,4 +48,45 @@ describe('User Lists Table', () => {
expect(list.contains('[data-testid="ffUserListTimestamp"]')).toBe(true); 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 { ...@@ -20,6 +20,8 @@ import {
receiveUserListsSuccess, receiveUserListsSuccess,
receiveUserListsError, receiveUserListsError,
fetchUserLists, fetchUserLists,
deleteUserList,
receiveDeleteUserListError,
} from 'ee/feature_flags/store/modules/index/actions'; } from 'ee/feature_flags/store/modules/index/actions';
import { mapToScopesViewModel } from 'ee/feature_flags/store/modules/helpers'; import { mapToScopesViewModel } from 'ee/feature_flags/store/modules/helpers';
import state from 'ee/feature_flags/store/modules/index/state'; import state from 'ee/feature_flags/store/modules/index/state';
...@@ -521,4 +523,58 @@ describe('Feature flags actions', () => { ...@@ -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', () => { ...@@ -115,7 +115,7 @@ describe('Feature flags store Mutations', () => {
}); });
}); });
describe('RECIEVE_USER_LISTS_SUCCESS', () => { describe('RECEIVE_USER_LISTS_SUCCESS', () => {
const headers = { const headers = {
'x-next-page': '2', 'x-next-page': '2',
'x-page': '1', 'x-page': '1',
...@@ -281,4 +281,28 @@ describe('Feature flags store Mutations', () => { ...@@ -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 "" ...@@ -7310,6 +7310,9 @@ msgstr ""
msgid "Delete this attachment" msgid "Delete this attachment"
msgstr "" msgstr ""
msgid "Delete user list"
msgstr ""
msgid "Delete variable" msgid "Delete variable"
msgstr "" msgstr ""
...@@ -24761,6 +24764,9 @@ msgstr "" ...@@ -24761,6 +24764,9 @@ msgstr ""
msgid "User key was successfully removed." msgid "User key was successfully removed."
msgstr "" msgstr ""
msgid "User list %{name} will be removed. Are you sure?"
msgstr ""
msgid "User map" msgid "User map"
msgstr "" msgstr ""
...@@ -24782,6 +24788,12 @@ msgstr "" ...@@ -24782,6 +24788,12 @@ msgstr ""
msgid "User was successfully updated." msgid "User was successfully updated."
msgstr "" msgstr ""
msgid "UserList|Delete %{name}?"
msgstr ""
msgid "UserList|created %{timeago}"
msgstr ""
msgid "UserOnboardingTour|%{activeTour}/%{totalTours}" msgid "UserOnboardingTour|%{activeTour}/%{totalTours}"
msgstr "" msgstr ""
...@@ -26944,9 +26956,6 @@ msgstr "" ...@@ -26944,9 +26956,6 @@ msgstr ""
msgid "created %{timeAgo}" msgid "created %{timeAgo}"
msgstr "" msgstr ""
msgid "created %{timeago}"
msgstr ""
msgid "customize" msgid "customize"
msgstr "" 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