Commit 6bed0939 authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera

Merge branch '235603-convert-group-members-list-view-from-haml-to-vue-role-dropdown' into 'master'

Setup `updateMemberRole` action and related mutations

See merge request gitlab-org/gitlab!44254
parents 365ad18a c7a6b1f4
<script> <script>
import { mapState, mapMutations } from 'vuex';
import { GlAlert } from '@gitlab/ui';
import MembersTable from '~/vue_shared/components/members/table/members_table.vue'; 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 { export default {
name: 'GroupMembersApp', 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> </script>
<template> <template>
<members-table /> <div>
<gl-alert v-if="showError" ref="errorAlert" variant="danger" @dismiss="hideError">{{
errorMessage
}}</gl-alert>
<members-table />
</div>
</template> </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'; ...@@ -4,7 +4,7 @@ import { parseDataAttributes } from 'ee_else_ce/groups/members/utils';
import App from './components/app.vue'; import App from './components/app.vue';
import membersModule from '~/vuex_shared/modules/members'; import membersModule from '~/vuex_shared/modules/members';
export const initGroupMembersApp = (el, tableFields) => { export const initGroupMembersApp = (el, tableFields, requestFormatter) => {
if (!el) { if (!el) {
return () => {}; return () => {};
} }
...@@ -16,6 +16,7 @@ export const initGroupMembersApp = (el, tableFields) => { ...@@ -16,6 +16,7 @@ export const initGroupMembersApp = (el, tableFields) => {
...parseDataAttributes(el), ...parseDataAttributes(el),
currentUserId: gon.current_user_id || null, currentUserId: gon.current_user_id || null,
tableFields, tableFields,
requestFormatter,
}), }),
}); });
......
import { isUndefined } from 'lodash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; 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 => { export const parseDataAttributes = el => {
const { members, groupId, memberPath } = el.dataset; const { members, groupId, memberPath } = el.dataset;
...@@ -9,3 +16,29 @@ export const parseDataAttributes = el => { ...@@ -9,3 +16,29 @@ export const parseDataAttributes = el => {
memberPath, 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'; ...@@ -5,6 +5,7 @@ import UsersSelect from '~/users_select';
import groupsSelect from '~/groups_select'; import groupsSelect from '~/groups_select';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue'; import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
import { initGroupMembersApp } from '~/groups/members'; import { initGroupMembersApp } from '~/groups/members';
import { memberRequestFormatter, groupLinkRequestFormatter } from '~/groups/members/utils';
function mountRemoveMemberModal() { function mountRemoveMemberModal() {
const el = document.querySelector('.js-remove-member-modal'); const el = document.querySelector('.js-remove-member-modal');
...@@ -31,18 +32,22 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -31,18 +32,22 @@ document.addEventListener('DOMContentLoaded', () => {
initGroupMembersApp( initGroupMembersApp(
document.querySelector('.js-group-members-list'), document.querySelector('.js-group-members-list'),
SHARED_FIELDS.concat(['source', 'granted']), SHARED_FIELDS.concat(['source', 'granted']),
memberRequestFormatter,
); );
initGroupMembersApp( initGroupMembersApp(
document.querySelector('.js-group-linked-list'), document.querySelector('.js-group-linked-list'),
SHARED_FIELDS.concat('granted'), SHARED_FIELDS.concat('granted'),
groupLinkRequestFormatter,
); );
initGroupMembersApp( initGroupMembersApp(
document.querySelector('.js-group-invited-members-list'), document.querySelector('.js-group-invited-members-list'),
SHARED_FIELDS.concat('invited'), SHARED_FIELDS.concat('invited'),
memberRequestFormatter,
); );
initGroupMembersApp( initGroupMembersApp(
document.querySelector('.js-group-access-requests-list'), document.querySelector('.js-group-access-requests-list'),
SHARED_FIELDS.concat('requested'), SHARED_FIELDS.concat('requested'),
memberRequestFormatter,
); );
new Members(); // eslint-disable-line no-new 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 createState from 'ee_else_ce/vuex_shared/modules/members/state';
import * as actions from './actions';
import mutations from './mutations';
export default initialState => ({ export default initialState => ({
namespaced: true, namespaced: true,
state: createState(initialState), 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, members,
sourceId, sourceId,
currentUserId, currentUserId,
tableFields, tableFields,
memberPath, memberPath,
requestFormatter,
}) => ({
members,
sourceId,
currentUserId,
tableFields,
memberPath,
requestFormatter,
showError: false,
errorMessage: '',
}); });
export const findMember = (state, memberId) => state.members.find(member => member.id === memberId);
...@@ -16080,6 +16080,9 @@ msgstr "" ...@@ -16080,6 +16080,9 @@ msgstr ""
msgid "Members|%{time} by %{user}" msgid "Members|%{time} by %{user}"
msgstr "" 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}\"" msgid "Members|Are you sure you want to deny %{usersName}'s request to join \"%{source}\""
msgstr "" 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', () => { ...@@ -9,7 +9,7 @@ describe('initGroupMembersApp', () => {
let wrapper; let wrapper;
const setup = () => { const setup = () => {
vm = initGroupMembersApp(el, ['account']); vm = initGroupMembersApp(el, ['account'], () => ({}));
wrapper = createWrapper(vm); wrapper = createWrapper(vm);
}; };
...@@ -68,6 +68,12 @@ describe('initGroupMembersApp', () => { ...@@ -68,6 +68,12 @@ describe('initGroupMembersApp', () => {
expect(vm.$store.state.tableFields).toEqual(['account']); 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', () => { it('sets `memberPath` in Vuex store', () => {
setup(); setup();
......
import { membersJsonString, membersParsed } from './mock_data'; 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('group member utils', () => {
describe('parseDataAttributes', () => { describe('parseDataAttributes', () => {
...@@ -22,4 +26,26 @@ describe('group member utils', () => { ...@@ -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