Commit 7611ca15 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Merge branch '238453-convert-group-members-list-view-from-haml-to-vue-ldap-controls' into 'master'

Add controls for LDAP group sync [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!45430
parents 24490f47 fb025ab5
......@@ -6,7 +6,13 @@ import { s__, sprintf } from '~/locale';
export default {
name: 'UserActionButtons',
components: { ActionButtonGroup, RemoveMemberButton, LeaveButton },
components: {
ActionButtonGroup,
RemoveMemberButton,
LeaveButton,
LdapOverrideButton: () =>
import('ee_component/vue_shared/components/members/ldap/ldap_override_button.vue'),
},
props: {
member: {
type: Object,
......@@ -57,5 +63,8 @@ export default {
:title="s__('Member|Remove member')"
/>
</div>
<div v-else-if="permissions.canOverride && !member.isOverridden" class="gl-px-1">
<ldap-override-button :member="member" />
</div>
</action-button-group>
</template>
......@@ -32,6 +32,13 @@ export default {
return getDateInFuture(beginningOfToday, 1);
},
disabled() {
return (
this.busy ||
!this.permissions.canUpdate ||
(this.permissions.canOverride && !this.member.isOverridden)
);
},
},
mounted() {
if (this.member.expiresAt) {
......@@ -85,7 +92,7 @@ export default {
:container="null"
:min-date="minDate"
:placeholder="__('Expiration date')"
:disabled="!permissions.canUpdate || busy"
:disabled="disabled"
@input="handleInput"
@clear="handleClear"
/>
......
<script>
import { mapState } from 'vuex';
import { GlTable, GlBadge } from '@gitlab/ui';
import MembersTableCell from 'ee_else_ce/vue_shared/components/members/table/members_table_cell.vue';
import { FIELDS } from '../constants';
import initUserPopovers from '~/user_popovers';
import MemberAvatar from './member_avatar.vue';
......@@ -8,7 +9,6 @@ import MemberSource from './member_source.vue';
import CreatedAt from './created_at.vue';
import ExpiresAt from './expires_at.vue';
import MemberActionButtons from './member_action_buttons.vue';
import MembersTableCell from './members_table_cell.vue';
import RoleDropdown from './role_dropdown.vue';
import RemoveGroupLinkModal from '../modals/remove_group_link_modal.vue';
import ExpirationDatepicker from './expiration_datepicker.vue';
......@@ -91,7 +91,7 @@ export default {
<template #cell(maxRole)="{ item: member }">
<members-table-cell #default="{ permissions }" :member="member">
<role-dropdown v-if="permissions.canUpdate" :member="member" />
<role-dropdown v-if="permissions.canUpdate" :permissions="permissions" :member="member" />
<gl-badge v-else>{{ member.accessLevel.stringValue }}</gl-badge>
</members-table-cell>
</template>
......
......@@ -9,12 +9,18 @@ export default {
components: {
GlDropdown,
GlDropdownItem,
LdapDropdownItem: () =>
import('ee_component/vue_shared/components/members/ldap/ldap_dropdown_item.vue'),
},
props: {
member: {
type: Object,
required: true,
},
permissions: {
type: Object,
required: true,
},
},
data() {
return {
......@@ -22,6 +28,11 @@ export default {
busy: false,
};
},
computed: {
disabled() {
return this.busy || (this.permissions.canOverride && !this.member.isOverridden);
},
},
mounted() {
this.isDesktop = bp.isDesktop();
},
......@@ -55,7 +66,7 @@ export default {
:right="!isDesktop"
:text="member.accessLevel.stringValue"
:header-text="__('Change permissions')"
:disabled="busy"
:disabled="disabled"
>
<gl-dropdown-item
v-for="(value, name) in member.validRoles"
......@@ -66,5 +77,9 @@ export default {
>
{{ name }}
</gl-dropdown-item>
<ldap-dropdown-item
v-if="permissions.canOverride && member.isOverridden"
:member-id="member.id"
/>
</gl-dropdown>
</template>
......@@ -225,7 +225,7 @@
}
.col-actions {
width: px-to-rem(50px);
width: px-to-rem(65px);
}
}
......
<script>
import { GlDropdownItem, GlDropdownDivider } from '@gitlab/ui';
import { mapActions } from 'vuex';
import { s__ } from '~/locale';
export default {
name: 'LdapDropdownItem',
components: { GlDropdownItem, GlDropdownDivider },
props: {
memberId: {
type: Number,
required: true,
},
},
methods: {
...mapActions(['updateLdapOverride']),
handleClick() {
this.updateLdapOverride({ memberId: this.memberId, override: false })
.then(() => {
this.$toast.show(s__('Members|Reverted to LDAP group sync settings.'));
})
.catch(() => {
// Do nothing, error handled in `updateLdapOverride` Vuex action
});
},
},
};
</script>
<template>
<span>
<gl-dropdown-divider />
<gl-dropdown-item is-check-item @click="handleClick">
{{ s__('Members|Revert to LDAP group sync settings') }}
</gl-dropdown-item>
</span>
</template>
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { mapActions } from 'vuex';
import { s__ } from '~/locale';
export default {
name: 'LdapOverrideButton',
i18n: {
title: s__('Members|Edit permissions'),
},
components: { GlButton },
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
member: {
type: Object,
required: true,
},
},
methods: {
...mapActions(['showLdapOverrideConfirmationModal']),
},
};
</script>
<template>
<gl-button
v-gl-tooltip.hover
:title="$options.i18n.title"
:aria-label="$options.i18n.title"
icon="pencil"
@click="showLdapOverrideConfirmationModal(member)"
/>
</template>
<script>
import CEMembersTableCell from '~/vue_shared/components/members/table/members_table_cell.vue';
export default {
name: 'MembersTableCell',
props: {
member: {
type: Object,
required: true,
},
},
computed: {
canOverride() {
return this.member.canOverride;
},
},
render(createElement) {
return createElement(CEMembersTableCell, {
props: { member: this.member },
scopedSlots: {
default: props => {
return this.$scopedSlots.default({
...props,
permissions: {
...props.permissions,
canOverride: this.canOverride,
},
});
},
},
});
},
};
</script>
import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import { member } from 'jest/vue_shared/components/members/mock_data';
import LdapOverrideButton from 'ee/vue_shared/components/members/ldap/ldap_override_button.vue';
import UserActionButtons from '~/vue_shared/components/members/action_buttons/user_action_buttons.vue';
describe('UserActionButtons', () => {
let wrapper;
const createComponent = (propsData = {}) => {
wrapper = shallowMount(UserActionButtons, {
propsData: {
member,
isCurrentUser: false,
...propsData,
},
});
return waitForPromises();
};
const findLdapOverrideButton = () => wrapper.find(LdapOverrideButton);
afterEach(() => {
wrapper.destroy();
});
describe('when member has `canOverride` permissions', () => {
describe('when member is not overridden', () => {
it('renders LDAP override button', async () => {
await createComponent({
permissions: { canOverride: true },
member: {
...member,
isOverridden: false,
},
});
expect(findLdapOverrideButton().exists()).toBe(true);
});
});
describe('when member is overridden', () => {
it('does not render the LDAP override button', async () => {
await createComponent({
permissions: { canOverride: true },
member: {
...member,
isOverridden: true,
},
});
expect(findLdapOverrideButton().exists()).toBe(false);
});
});
});
describe('when member does not have `canOverride` permissions', () => {
it('does not render the LDAP override button', async () => {
await createComponent({
permissions: { canOverride: false },
});
expect(findLdapOverrideButton().exists()).toBe(false);
});
});
});
import { mount, createLocalVue } from '@vue/test-utils';
import { GlDropdownItem } from '@gitlab/ui';
import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
import LdapDropdownItem from 'ee/vue_shared/components/members/ldap/ldap_dropdown_item.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('LdapDropdownItem', () => {
let wrapper;
let actions;
const $toast = {
show: jest.fn(),
};
const createStore = () => {
actions = {
updateLdapOverride: jest.fn(() => Promise.resolve()),
};
return new Vuex.Store({ actions });
};
const createComponent = (propsData = {}) => {
wrapper = mount(LdapDropdownItem, {
propsData: {
memberId: 1,
...propsData,
},
localVue,
store: createStore(),
mocks: {
$toast,
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe('when dropdown item is clicked', () => {
beforeEach(() => {
createComponent();
wrapper
.find(GlDropdownItem)
.find('[role="menuitem"]')
.trigger('click');
});
it('calls `updateLdapOverride` action', () => {
expect(actions.updateLdapOverride).toHaveBeenCalledWith(expect.any(Object), {
memberId: 1,
override: false,
});
});
it('displays toast when `updateLdapOverride` is successful', async () => {
await waitForPromises();
expect($toast.show).toHaveBeenCalledWith('Reverted to LDAP group sync settings.');
});
});
});
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { GlButton } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { member } from 'jest/vue_shared/components/members/mock_data';
import LdapOverrideButton from 'ee/vue_shared/components/members/ldap/ldap_override_button.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('LdapOverrideButton', () => {
let wrapper;
let actions;
const createStore = () => {
actions = {
showLdapOverrideConfirmationModal: jest.fn(),
};
return new Vuex.Store({ actions });
};
const createComponent = (propsData = {}) => {
wrapper = mount(LdapOverrideButton, {
localVue,
propsData: {
member,
...propsData,
},
store: createStore(),
directives: {
GlTooltip: createMockDirective(),
},
});
};
const findButton = () => wrapper.find(GlButton);
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('displays a tooltip', () => {
const button = findButton();
expect(getBinding(button.element, 'gl-tooltip')).not.toBeUndefined();
expect(button.attributes('title')).toBe('Edit permissions');
});
it('sets `aria-label` attribute', () => {
expect(findButton().attributes('aria-label')).toBe('Edit permissions');
});
it('calls Vuex action to open LDAP override confirmation modal when clicked', () => {
findButton().trigger('click');
expect(actions.showLdapOverrideConfirmationModal).toHaveBeenCalledWith(
expect.any(Object),
member,
);
});
});
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MemberTableCell exposes CE scoped slot props 1`] = `
Object {
"isCurrentUser": false,
"isDirectMember": false,
"memberType": "user",
"permissions": Object {
"canOverride": false,
"canRemove": false,
"canResend": false,
"canUpdate": false,
},
}
`;
import { mount } from '@vue/test-utils';
import { GlDatepicker } from '@gitlab/ui';
import { member } from 'jest/vue_shared/components/members/mock_data';
import ExpirationDatepicker from '~/vue_shared/components/members/table/expiration_datepicker.vue';
describe('ExpirationDatepicker', () => {
let wrapper;
const createComponent = (propsData = {}) => {
wrapper = mount(ExpirationDatepicker, {
propsData,
});
};
const findDatepicker = () => wrapper.find(GlDatepicker);
afterEach(() => {
wrapper.destroy();
});
it.each`
canOverride | isOverridden | expected
${true} | ${true} | ${false}
${true} | ${false} | ${true}
${false} | ${false} | ${false}
`(
'sets `disabled` prop to $expected when `canOverride` is $canOverride and `member.isOverridden` is $isOverridden',
({ canOverride, isOverridden, expected }) => {
createComponent({
permissions: {
canUpdate: true,
canOverride,
},
member: { ...member, isOverridden },
});
expect(findDatepicker().props('disabled')).toBe(expected);
},
);
});
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { member as memberMock } from 'jest/vue_shared/components/members/mock_data';
import MembersTableCell from 'ee/vue_shared/components/members/table/members_table_cell.vue';
describe('MemberTableCell', () => {
const WrappedComponent = {
props: {
memberType: {
type: String,
required: true,
},
isDirectMember: {
type: Boolean,
required: true,
},
isCurrentUser: {
type: Boolean,
required: true,
},
permissions: {
type: Object,
required: true,
},
},
render(createElement) {
return createElement('div', this.memberType);
},
};
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.component('wrapped-component', WrappedComponent);
const createStore = (state = {}) => {
return new Vuex.Store({
state: {
sourceId: 1,
currentUserId: 1,
...state,
},
});
};
let wrapper;
const createComponent = (propsData, state = {}) => {
wrapper = mount(MembersTableCell, {
localVue,
propsData,
store: createStore(state),
scopedSlots: {
default: `
<wrapped-component
:member-type="props.memberType"
:is-direct-member="props.isDirectMember"
:is-current-user="props.isCurrentUser"
:permissions="props.permissions"
/>
`,
},
});
};
const findWrappedComponent = () => wrapper.find(WrappedComponent);
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
// Implementation of props are tested in `spec/frontend/vue_shared/components/members/table/members_table_spec.js`
it('exposes CE scoped slot props', () => {
createComponent({ member: memberMock });
expect(findWrappedComponent().props()).toMatchSnapshot();
});
describe('permissions', () => {
describe('canOverride', () => {
it('returns `true` when `canOverride` is `true`', () => {
createComponent({
member: { memberMock, canOverride: true },
});
expect(findWrappedComponent().props('permissions').canOverride).toBe(true);
});
it('returns `false` when `canOverride` is `false`', () => {
createComponent({
member: { memberMock, canOverride: false },
});
expect(findWrappedComponent().props('permissions').canOverride).toBe(false);
});
});
});
});
import { shallowMount } from '@vue/test-utils';
import { GlDropdown } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import { member } from 'jest/vue_shared/components/members/mock_data';
import LdapDropdownItem from 'ee/vue_shared/components/members/ldap/ldap_dropdown_item.vue';
import RoleDropdown from '~/vue_shared/components/members/table/role_dropdown.vue';
describe('RoleDropdown', () => {
let wrapper;
const createComponent = (propsData = {}) => {
wrapper = shallowMount(RoleDropdown, {
propsData: {
member,
permissions: {},
...propsData,
},
});
return waitForPromises();
};
const findDropdown = () => wrapper.find(GlDropdown);
afterEach(() => {
wrapper.destroy();
});
describe('when member has `canOverride` permissions', () => {
describe('when member is overridden', () => {
it('renders LDAP dropdown item', async () => {
await createComponent({
permissions: {
canOverride: true,
},
member: { ...member, isOverridden: true },
});
expect(wrapper.find(LdapDropdownItem).exists()).toBe(true);
});
});
describe('when member is not overridden', () => {
it('disables dropdown', async () => {
await createComponent({
permissions: {
canOverride: true,
},
member: { ...member, isOverridden: false },
});
expect(findDropdown().attributes('disabled')).toBe('true');
});
});
});
describe('when member does not have `canOverride` permissions', () => {
it('does not render LDAP dropdown item', async () => {
await createComponent({
permissions: {
canOverride: false,
},
});
expect(wrapper.find(LdapDropdownItem).exists()).toBe(false);
});
});
});
......@@ -16414,6 +16414,12 @@ msgstr ""
msgid "Members|Remove group"
msgstr ""
msgid "Members|Revert to LDAP group sync settings"
msgstr ""
msgid "Members|Reverted to LDAP group sync settings."
msgstr ""
msgid "Members|Role updated successfully."
msgstr ""
......
......@@ -30,6 +30,7 @@ describe('RoleDropdown', () => {
wrapper = mount(RoleDropdown, {
propsData: {
member,
permissions: {},
...propsData,
},
localVue,
......
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