Commit 1ff4c2af authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch 'delete-user-list-error-ui' into 'master'

Show Error Message When Delete Feature Flag User List Fails

See merge request gitlab-org/gitlab!36487
parents 655a14fc 6646dfd8
...@@ -112,6 +112,7 @@ export default { ...@@ -112,6 +112,7 @@ export default {
...mapState([ ...mapState([
FEATURE_FLAG_SCOPE, FEATURE_FLAG_SCOPE,
USER_LIST_SCOPE, USER_LIST_SCOPE,
'alerts',
'count', 'count',
'pageInfo', 'pageInfo',
'isLoading', 'isLoading',
...@@ -191,6 +192,7 @@ export default { ...@@ -191,6 +192,7 @@ export default {
'rotateInstanceId', 'rotateInstanceId',
'toggleFeatureFlag', 'toggleFeatureFlag',
'deleteUserList', 'deleteUserList',
'clearAlert',
]), ]),
onChangeTab(scope) { onChangeTab(scope) {
this.scope = scope; this.scope = scope;
...@@ -285,6 +287,16 @@ export default { ...@@ -285,6 +287,16 @@ export default {
</gl-sprintf> </gl-sprintf>
</gl-alert> </gl-alert>
<gl-alert
v-for="(message, index) in alerts"
:key="index"
data-testid="serverErrors"
variant="danger"
@dismiss="clearAlert(index)"
>
{{ message }}
</gl-alert>
<div v-if="shouldRenderTabs" class="top-area scrolling-tabs-container inner-page-scroll-tabs"> <div v-if="shouldRenderTabs" class="top-area scrolling-tabs-container inner-page-scroll-tabs">
<navigation-tabs :tabs="tabs" scope="featureflags" @onChangeTab="onChangeTab" /> <navigation-tabs :tabs="tabs" scope="featureflags" @onChangeTab="onChangeTab" />
</div> </div>
......
...@@ -72,14 +72,20 @@ export const deleteUserList = ({ state, dispatch }, list) => { ...@@ -72,14 +72,20 @@ export const deleteUserList = ({ state, dispatch }, list) => {
return Api.deleteFeatureFlagUserList(state.projectId, list.iid) return Api.deleteFeatureFlagUserList(state.projectId, list.iid)
.then(() => dispatch('fetchUserLists')) .then(() => dispatch('fetchUserLists'))
.catch(() => dispatch('receiveDeleteUserListError', list)); .catch(error =>
dispatch('receiveDeleteUserListError', {
list,
error: error?.response?.data ?? error,
}),
);
}; };
export const requestDeleteUserList = ({ commit }, list) => export const requestDeleteUserList = ({ commit }, list) =>
commit(types.REQUEST_DELETE_USER_LIST, list); commit(types.REQUEST_DELETE_USER_LIST, list);
export const receiveDeleteUserListError = ({ commit }, list) => export const receiveDeleteUserListError = ({ commit }, { error, list }) => {
commit(types.RECEIVE_DELETE_USER_LIST_ERROR, list); commit(types.RECEIVE_DELETE_USER_LIST_ERROR, { error, list });
};
export const rotateInstanceId = ({ state, dispatch }) => { export const rotateInstanceId = ({ state, dispatch }) => {
dispatch('requestRotateInstanceId'); dispatch('requestRotateInstanceId');
...@@ -96,5 +102,9 @@ export const receiveRotateInstanceIdSuccess = ({ commit }, response) => ...@@ -96,5 +102,9 @@ export const receiveRotateInstanceIdSuccess = ({ commit }, response) =>
export const receiveRotateInstanceIdError = ({ commit }) => export const receiveRotateInstanceIdError = ({ commit }) =>
commit(types.RECEIVE_ROTATE_INSTANCE_ID_ERROR); commit(types.RECEIVE_ROTATE_INSTANCE_ID_ERROR);
export const clearAlert = ({ commit }, index) => {
commit(types.RECEIVE_CLEAR_ALERT, index);
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -22,3 +22,5 @@ export const RECEIVE_UPDATE_FEATURE_FLAG_ERROR = 'RECEIVE_UPDATE_FEATURE_FLAG_ER ...@@ -22,3 +22,5 @@ export const RECEIVE_UPDATE_FEATURE_FLAG_ERROR = 'RECEIVE_UPDATE_FEATURE_FLAG_ER
export const REQUEST_ROTATE_INSTANCE_ID = 'REQUEST_ROTATE_INSTANCE_ID'; export const REQUEST_ROTATE_INSTANCE_ID = 'REQUEST_ROTATE_INSTANCE_ID';
export const RECEIVE_ROTATE_INSTANCE_ID_SUCCESS = 'RECEIVE_ROTATE_INSTANCE_ID_SUCCESS'; export const RECEIVE_ROTATE_INSTANCE_ID_SUCCESS = 'RECEIVE_ROTATE_INSTANCE_ID_SUCCESS';
export const RECEIVE_ROTATE_INSTANCE_ID_ERROR = 'RECEIVE_ROTATE_INSTANCE_ID_ERROR'; export const RECEIVE_ROTATE_INSTANCE_ID_ERROR = 'RECEIVE_ROTATE_INSTANCE_ID_ERROR';
export const RECEIVE_CLEAR_ALERT = 'RECEIVE_CLEAR_ALERT';
...@@ -113,9 +113,13 @@ export default { ...@@ -113,9 +113,13 @@ export default {
[types.REQUEST_DELETE_USER_LIST](state, list) { [types.REQUEST_DELETE_USER_LIST](state, list) {
state.userLists = state.userLists.filter(l => l !== list); state.userLists = state.userLists.filter(l => l !== list);
}, },
[types.RECEIVE_DELETE_USER_LIST_ERROR](state, list) { [types.RECEIVE_DELETE_USER_LIST_ERROR](state, { error, list }) {
state.isLoading = false; state.isLoading = false;
state.hasError = true; state.hasError = false;
state.alerts = [].concat(error.message);
state.userLists = state.userLists.concat(list).sort((l1, l2) => l1.iid - l2.iid); state.userLists = state.userLists.concat(list).sort((l1, l2) => l1.iid - l2.iid);
}, },
[types.RECEIVE_CLEAR_ALERT](state, index) {
state.alerts.splice(index, 1);
},
}; };
...@@ -3,6 +3,7 @@ import { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from '../../../constants'; ...@@ -3,6 +3,7 @@ import { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from '../../../constants';
export default () => ({ export default () => ({
[FEATURE_FLAG_SCOPE]: [], [FEATURE_FLAG_SCOPE]: [],
[USER_LIST_SCOPE]: [], [USER_LIST_SCOPE]: [],
alerts: [],
count: {}, count: {},
pageInfo: { [FEATURE_FLAG_SCOPE]: {}, [USER_LIST_SCOPE]: {} }, pageInfo: { [FEATURE_FLAG_SCOPE]: {}, [USER_LIST_SCOPE]: {} },
isLoading: true, isLoading: true,
......
---
title: Show error when attempting to delete feature flag user list that is in use
merge_request: 36487
author:
type: fixed
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'User deletes feature flag user list', :js do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
before do
project.add_developer(developer)
stub_licensed_features(feature_flags: true)
sign_in(developer)
end
context 'with a list' do
before do
create(:operations_feature_flag_user_list, project: project, name: 'My List')
end
it 'deletes the list' do
visit(project_feature_flags_path(project, scope: 'userLists'))
delete_user_list_button.click
delete_user_list_modal_confirmation_button.click
expect(page).to have_text('Lists 0')
end
end
context 'with a list that is in use' do
before do
list = create(:operations_feature_flag_user_list, project: project, name: 'My List')
feature_flag = create(:operations_feature_flag, :new_version_flag, project: project)
create(:operations_strategy, feature_flag: feature_flag, name: 'gitlabUserList', user_list: list)
end
it 'does not delete the list' do
visit(project_feature_flags_path(project, scope: 'userLists'))
delete_user_list_button.click
delete_user_list_modal_confirmation_button.click
expect(page).to have_text('User list is associated with a strategy')
expect(page).to have_text('Lists 1')
expect(page).to have_text('My List')
alert_dismiss_button.click
expect(page).not_to have_text('User list is associated with a strategy')
end
end
def delete_user_list_button
find("button[data-testid='delete-user-list']")
end
def delete_user_list_modal_confirmation_button
find("button[data-testid='modal-confirm']")
end
def alert_dismiss_button
find("div[data-testid='serverErrors'] button")
end
end
...@@ -22,6 +22,7 @@ import { ...@@ -22,6 +22,7 @@ import {
fetchUserLists, fetchUserLists,
deleteUserList, deleteUserList,
receiveDeleteUserListError, receiveDeleteUserListError,
clearAlert,
} 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';
...@@ -547,7 +548,7 @@ describe('Feature flags actions', () => { ...@@ -547,7 +548,7 @@ describe('Feature flags actions', () => {
describe('error', () => { describe('error', () => {
beforeEach(() => { beforeEach(() => {
Api.deleteFeatureFlagUserList.mockRejectedValue(); Api.deleteFeatureFlagUserList.mockRejectedValue({ response: { data: 'some error' } });
}); });
it('should dispatch receiveDeleteUserListError', done => { it('should dispatch receiveDeleteUserListError', done => {
...@@ -558,20 +559,44 @@ describe('Feature flags actions', () => { ...@@ -558,20 +559,44 @@ describe('Feature flags actions', () => {
[], [],
[ [
{ type: 'requestDeleteUserList', payload: userList }, { type: 'requestDeleteUserList', payload: userList },
{ type: 'receiveDeleteUserListError', payload: userList }, {
type: 'receiveDeleteUserListError',
payload: { list: userList, error: 'some error' },
},
], ],
done, done,
); );
}); });
}); });
}); });
describe('receiveDeleteUserListError', () => { describe('receiveDeleteUserListError', () => {
it('should commit RECEIVE_DELETE_USER_LIST_ERROR with the given list', done => { it('should commit RECEIVE_DELETE_USER_LIST_ERROR with the given list', done => {
testAction( testAction(
receiveDeleteUserListError, receiveDeleteUserListError,
userList, { list: userList, error: 'mock error' },
mockedState,
[
{
type: 'RECEIVE_DELETE_USER_LIST_ERROR',
payload: { list: userList, error: 'mock error' },
},
],
[],
done,
);
});
});
describe('clearAlert', () => {
it('should commit RECEIVE_CLEAR_ALERT', done => {
const alertIndex = 3;
testAction(
clearAlert,
alertIndex,
mockedState, mockedState,
[{ type: 'RECEIVE_DELETE_USER_LIST_ERROR', payload: userList }], [{ type: 'RECEIVE_CLEAR_ALERT', payload: alertIndex }],
[], [],
done, done,
); );
......
...@@ -281,6 +281,7 @@ describe('Feature flags store Mutations', () => { ...@@ -281,6 +281,7 @@ describe('Feature flags store Mutations', () => {
]); ]);
}); });
}); });
describe('REQUEST_DELETE_USER_LIST', () => { describe('REQUEST_DELETE_USER_LIST', () => {
beforeEach(() => { beforeEach(() => {
stateCopy.userLists = [userList]; stateCopy.userLists = [userList];
...@@ -291,18 +292,41 @@ describe('Feature flags store Mutations', () => { ...@@ -291,18 +292,41 @@ describe('Feature flags store Mutations', () => {
expect(stateCopy.userLists).not.toContain(userList); expect(stateCopy.userLists).not.toContain(userList);
}); });
}); });
describe('RECEIVE_DELETE_USER_LIST_ERROR', () => { describe('RECEIVE_DELETE_USER_LIST_ERROR', () => {
beforeEach(() => { beforeEach(() => {
stateCopy.userLists = []; stateCopy.userLists = [];
mutations[types.RECEIVE_DELETE_USER_LIST_ERROR](stateCopy, userList); mutations[types.RECEIVE_DELETE_USER_LIST_ERROR](stateCopy, {
list: userList,
error: 'some error',
});
}); });
it('should set isLoading to false and hasError to true', () => { it('should set isLoading to false and hasError to false', () => {
expect(stateCopy.isLoading).toBe(false); expect(stateCopy.isLoading).toBe(false);
expect(stateCopy.hasError).toBe(true); expect(stateCopy.hasError).toBe(false);
}); });
it('should add the user list back to the list of user lists', () => { it('should add the user list back to the list of user lists', () => {
expect(stateCopy.userLists).toContain(userList); expect(stateCopy.userLists).toContain(userList);
}); });
}); });
describe('RECEIVE_CLEAR_ALERT', () => {
it('clears the alert', () => {
stateCopy.alerts = ['a server error'];
mutations[types.RECEIVE_CLEAR_ALERT](stateCopy, 0);
expect(stateCopy.alerts).toEqual([]);
});
it('clears the alert at the specified index', () => {
stateCopy.alerts = ['a server error', 'another error', 'final error'];
mutations[types.RECEIVE_CLEAR_ALERT](stateCopy, 1);
expect(stateCopy.alerts).toEqual(['a server error', 'final error']);
});
});
}); });
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