Commit c7a6b1f4 authored by Peter Hegman's avatar Peter Hegman Committed by Nicolò Maria Mezzopera

Setup `updateMemberRole` action and related mutations

Part of a larger initiative to convert the group members view from
HAML to Vue
parent 593d8264
<script>
import { mapState, mapMutations } from 'vuex';
import { GlAlert } from '@gitlab/ui';
import MembersTable from '~/vue_shared/components/members/table/members_table.vue';
import { scrollToElement } from '~/lib/utils/common_utils';
import { HIDE_ERROR } from '~/vuex_shared/modules/members/mutation_types';
export default {
name: 'GroupMembersApp',
components: { MembersTable },
components: { MembersTable, GlAlert },
computed: {
...mapState(['showError', 'errorMessage']),
},
watch: {
showError(value) {
if (value) {
this.$nextTick(() => {
scrollToElement(this.$refs.errorAlert.$el);
});
}
},
},
methods: {
...mapMutations({
hideError: HIDE_ERROR,
}),
},
};
</script>
<template>
<members-table />
<div>
<gl-alert v-if="showError" ref="errorAlert" variant="danger" @dismiss="hideError">{{
errorMessage
}}</gl-alert>
<members-table />
</div>
</template>
export const GROUP_MEMBER_BASE_PROPERTY_NAME = 'group_member';
export const GROUP_MEMBER_ACCESS_LEVEL_PROPERTY_NAME = 'access_level';
export const GROUP_LINK_BASE_PROPERTY_NAME = 'group_link';
export const GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME = 'group_access';
......@@ -4,7 +4,7 @@ import { parseDataAttributes } from 'ee_else_ce/groups/members/utils';
import App from './components/app.vue';
import membersModule from '~/vuex_shared/modules/members';
export const initGroupMembersApp = (el, tableFields) => {
export const initGroupMembersApp = (el, tableFields, requestFormatter) => {
if (!el) {
return () => {};
}
......@@ -16,6 +16,7 @@ export const initGroupMembersApp = (el, tableFields) => {
...parseDataAttributes(el),
currentUserId: gon.current_user_id || null,
tableFields,
requestFormatter,
}),
});
......
import { isUndefined } from 'lodash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import {
GROUP_MEMBER_BASE_PROPERTY_NAME,
GROUP_MEMBER_ACCESS_LEVEL_PROPERTY_NAME,
GROUP_LINK_BASE_PROPERTY_NAME,
GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME,
} from './constants';
export const parseDataAttributes = el => {
const { members, groupId, memberPath } = el.dataset;
......@@ -9,3 +16,29 @@ export const parseDataAttributes = el => {
memberPath,
};
};
const baseRequestFormatter = (basePropertyName, accessLevelPropertyName) => ({
accessLevel,
...otherProperties
}) => {
const accessLevelProperty = !isUndefined(accessLevel)
? { [accessLevelPropertyName]: accessLevel }
: {};
return {
[basePropertyName]: {
...accessLevelProperty,
...otherProperties,
},
};
};
export const memberRequestFormatter = baseRequestFormatter(
GROUP_MEMBER_BASE_PROPERTY_NAME,
GROUP_MEMBER_ACCESS_LEVEL_PROPERTY_NAME,
);
export const groupLinkRequestFormatter = baseRequestFormatter(
GROUP_LINK_BASE_PROPERTY_NAME,
GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME,
);
......@@ -5,6 +5,7 @@ import UsersSelect from '~/users_select';
import groupsSelect from '~/groups_select';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
import { initGroupMembersApp } from '~/groups/members';
import { memberRequestFormatter, groupLinkRequestFormatter } from '~/groups/members/utils';
function mountRemoveMemberModal() {
const el = document.querySelector('.js-remove-member-modal');
......@@ -31,18 +32,22 @@ document.addEventListener('DOMContentLoaded', () => {
initGroupMembersApp(
document.querySelector('.js-group-members-list'),
SHARED_FIELDS.concat(['source', 'granted']),
memberRequestFormatter,
);
initGroupMembersApp(
document.querySelector('.js-group-linked-list'),
SHARED_FIELDS.concat('granted'),
groupLinkRequestFormatter,
);
initGroupMembersApp(
document.querySelector('.js-group-invited-members-list'),
SHARED_FIELDS.concat('invited'),
memberRequestFormatter,
);
initGroupMembersApp(
document.querySelector('.js-group-access-requests-list'),
SHARED_FIELDS.concat('requested'),
memberRequestFormatter,
);
new Members(); // eslint-disable-line no-new
......
import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
export const updateMemberRole = async ({ state, commit }, { memberId, accessLevel }) => {
try {
await axios.put(
state.memberPath.replace(/:id$/, memberId),
state.requestFormatter({ accessLevel: accessLevel.integerValue }),
);
commit(types.RECEIVE_MEMBER_ROLE_SUCCESS, { memberId, accessLevel });
} catch (error) {
commit(types.RECEIVE_MEMBER_ROLE_ERROR);
throw error;
}
};
import createState from 'ee_else_ce/vuex_shared/modules/members/state';
import * as actions from './actions';
import mutations from './mutations';
export default initialState => ({
namespaced: true,
state: createState(initialState),
actions,
mutations,
});
export const RECEIVE_MEMBER_ROLE_SUCCESS = 'RECEIVE_MEMBER_ROLE_SUCCESS';
export const RECEIVE_MEMBER_ROLE_ERROR = 'RECEIVE_MEMBER_ROLE_ERROR';
export const HIDE_ERROR = 'HIDE_ERROR';
import Vue from 'vue';
import { s__ } from '~/locale';
import * as types from './mutation_types';
import { findMember } from './utils';
export default {
[types.RECEIVE_MEMBER_ROLE_SUCCESS](state, { memberId, accessLevel }) {
const member = findMember(state, memberId);
if (!member) {
return;
}
Vue.set(member, 'accessLevel', accessLevel);
},
[types.RECEIVE_MEMBER_ROLE_ERROR](state) {
state.errorMessage = s__(
"Members|An error occurred while updating the member's role, please try again.",
);
state.showError = true;
},
[types.HIDE_ERROR](state) {
state.showError = false;
state.errorMessage = '';
},
};
export default ({ members, sourceId, currentUserId, tableFields, memberPath }) => ({
export default ({
members,
sourceId,
currentUserId,
tableFields,
memberPath,
requestFormatter,
}) => ({
members,
sourceId,
currentUserId,
tableFields,
memberPath,
requestFormatter,
showError: false,
errorMessage: '',
});
export const findMember = (state, memberId) => state.members.find(member => member.id === memberId);
......@@ -16053,6 +16053,9 @@ msgstr ""
msgid "Members|%{time} by %{user}"
msgstr ""
msgid "Members|An error occurred while updating the member's role, please try again."
msgstr ""
msgid "Members|Are you sure you want to deny %{usersName}'s request to join \"%{source}\""
msgstr ""
......
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import Vuex from 'vuex';
import { GlAlert } from '@gitlab/ui';
import App from '~/groups/members/components/app.vue';
import * as commonUtils from '~/lib/utils/common_utils';
import {
RECEIVE_MEMBER_ROLE_ERROR,
HIDE_ERROR,
} from '~/vuex_shared/modules/members/mutation_types';
import mutations from '~/vuex_shared/modules/members/mutations';
describe('GroupMembersApp', () => {
const localVue = createLocalVue();
localVue.use(Vuex);
let wrapper;
let store;
const createComponent = (state = {}) => {
store = new Vuex.Store({
state: {
showError: true,
errorMessage: 'Something went wrong, please try again.',
...state,
},
mutations,
});
wrapper = shallowMount(App, {
localVue,
store,
});
};
const findAlert = () => wrapper.find(GlAlert);
beforeEach(() => {
commonUtils.scrollToElement = jest.fn();
});
afterEach(() => {
wrapper.destroy();
store = null;
});
describe('when `showError` is changed to `true`', () => {
it('renders and scrolls to error alert', async () => {
createComponent({ showError: false, errorMessage: '' });
store.commit(RECEIVE_MEMBER_ROLE_ERROR);
await nextTick();
const alert = findAlert();
expect(alert.exists()).toBe(true);
expect(alert.text()).toBe(
"An error occurred while updating the member's role, please try again.",
);
expect(commonUtils.scrollToElement).toHaveBeenCalledWith(alert.element);
});
});
describe('when `showError` is changed to `false`', () => {
it('does not render and scroll to error alert', async () => {
createComponent();
store.commit(HIDE_ERROR);
await nextTick();
expect(findAlert().exists()).toBe(false);
expect(commonUtils.scrollToElement).not.toHaveBeenCalled();
});
});
describe('when alert is dismissed', () => {
it('hides alert', async () => {
createComponent();
findAlert().vm.$emit('dismiss');
await nextTick();
expect(findAlert().exists()).toBe(false);
});
});
});
......@@ -9,7 +9,7 @@ describe('initGroupMembersApp', () => {
let wrapper;
const setup = () => {
vm = initGroupMembersApp(el, ['account']);
vm = initGroupMembersApp(el, ['account'], () => ({}));
wrapper = createWrapper(vm);
};
......@@ -68,6 +68,12 @@ describe('initGroupMembersApp', () => {
expect(vm.$store.state.tableFields).toEqual(['account']);
});
it('sets `requestFormatter` in Vuex store', () => {
setup();
expect(vm.$store.state.requestFormatter()).toEqual({});
});
it('sets `memberPath` in Vuex store', () => {
setup();
......
import { membersJsonString, membersParsed } from './mock_data';
import { parseDataAttributes } from '~/groups/members/utils';
import {
parseDataAttributes,
memberRequestFormatter,
groupLinkRequestFormatter,
} from '~/groups/members/utils';
describe('group member utils', () => {
describe('parseDataAttributes', () => {
......@@ -22,4 +26,26 @@ describe('group member utils', () => {
});
});
});
describe('memberRequestFormatter', () => {
it('returns expected format', () => {
expect(
memberRequestFormatter({
accessLevel: 50,
expires_at: '2020-10-16',
}),
).toEqual({ group_member: { access_level: 50, expires_at: '2020-10-16' } });
});
});
describe('groupLinkRequestFormatter', () => {
it('returns expected format', () => {
expect(
groupLinkRequestFormatter({
accessLevel: 50,
expires_at: '2020-10-16',
}),
).toEqual({ group_link: { group_access: 50, expires_at: '2020-10-16' } });
});
});
});
import { noop } from 'lodash';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { members } from 'jest/vue_shared/components/members/mock_data';
import testAction from 'helpers/vuex_action_helper';
import httpStatusCodes from '~/lib/utils/http_status';
import * as types from '~/vuex_shared/modules/members/mutation_types';
import { updateMemberRole } from '~/vuex_shared/modules/members/actions';
describe('Vuex members actions', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('updateMemberRole', () => {
const memberId = members[0].id;
const accessLevel = { integerValue: 30, stringValue: 'Developer' };
const payload = {
memberId,
accessLevel,
};
const state = {
members,
memberPath: '/groups/foo-bar/-/group_members/:id',
requestFormatter: noop,
};
describe('successful request', () => {
it(`commits ${types.RECEIVE_MEMBER_ROLE_SUCCESS} mutation`, async () => {
let requestPath;
mock.onPut().replyOnce(config => {
requestPath = config.url;
return [httpStatusCodes.OK, {}];
});
await testAction(updateMemberRole, payload, state, [
{
type: types.RECEIVE_MEMBER_ROLE_SUCCESS,
payload,
},
]);
expect(requestPath).toBe('/groups/foo-bar/-/group_members/238');
});
});
describe('unsuccessful request', () => {
beforeEach(() => {
mock.onPut().replyOnce(httpStatusCodes.BAD_REQUEST, { message: 'Bad request' });
});
it(`commits ${types.RECEIVE_MEMBER_ROLE_ERROR} mutation`, async () => {
try {
await testAction(updateMemberRole, payload, state, [
{
type: types.RECEIVE_MEMBER_ROLE_SUCCESS,
},
]);
} catch {
// Do nothing
}
});
it('throws error', async () => {
await expect(testAction(updateMemberRole, payload, state)).rejects.toThrowError();
});
});
});
});
import { members } from 'jest/vue_shared/components/members/mock_data';
import mutations from '~/vuex_shared/modules/members/mutations';
import * as types from '~/vuex_shared/modules/members/mutation_types';
describe('Vuex members mutations', () => {
describe(types.RECEIVE_MEMBER_ROLE_SUCCESS, () => {
it('updates member', () => {
const state = {
members,
};
const accessLevel = { integerValue: 30, stringValue: 'Developer' };
mutations[types.RECEIVE_MEMBER_ROLE_SUCCESS](state, {
memberId: members[0].id,
accessLevel,
});
expect(state.members[0].accessLevel).toEqual(accessLevel);
});
});
describe(types.RECEIVE_MEMBER_ROLE_ERROR, () => {
it('shows error message', () => {
const state = {
showError: false,
errorMessage: '',
};
mutations[types.RECEIVE_MEMBER_ROLE_ERROR](state);
expect(state.showError).toBe(true);
expect(state.errorMessage).toBe(
"An error occurred while updating the member's role, please try again.",
);
});
});
describe(types.HIDE_ERROR, () => {
it('sets `showError` to `false`', () => {
const state = {
showError: true,
errorMessage: 'foo bar',
};
mutations[types.HIDE_ERROR](state);
expect(state.showError).toBe(false);
});
it('sets `errorMessage` to an empty string', () => {
const state = {
showError: true,
errorMessage: 'foo bar',
};
mutations[types.HIDE_ERROR](state);
expect(state.errorMessage).toBe('');
});
});
});
import { members } from 'jest/vue_shared/components/members/mock_data';
import { findMember } from '~/vuex_shared/modules/members/utils';
describe('Members Vuex utils', () => {
describe('findMember', () => {
it('finds member by ID', () => {
const state = {
members,
};
expect(findMember(state, members[0].id)).toEqual(members[0]);
});
});
});
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