Commit 50580e04 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '238459-convert-group-members-view-source-component' into 'master'

Group members Vue conversion - source field

See merge request gitlab-org/gitlab!43457
parents 6fbf8eaf e82aa7b4
<script>
import { GlTooltipDirective } from '@gitlab/ui';
export default {
name: 'MemberSource',
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
memberSource: {
type: Object,
required: true,
},
isDirectMember: {
type: Boolean,
required: true,
},
},
};
</script>
<template>
<span v-if="isDirectMember">{{ __('Direct member') }}</span>
<a v-else v-gl-tooltip.hover :title="__('Inherited')" :href="memberSource.webUrl">{{
memberSource.name
}}</a>
</template>
...@@ -4,6 +4,7 @@ import { GlTable } from '@gitlab/ui'; ...@@ -4,6 +4,7 @@ import { GlTable } from '@gitlab/ui';
import { FIELDS } from '../constants'; import { FIELDS } from '../constants';
import initUserPopovers from '~/user_popovers'; import initUserPopovers from '~/user_popovers';
import MemberAvatar from './member_avatar.vue'; import MemberAvatar from './member_avatar.vue';
import MemberSource from './member_source.vue';
import MembersTableCell from './members_table_cell.vue'; import MembersTableCell from './members_table_cell.vue';
export default { export default {
...@@ -12,6 +13,7 @@ export default { ...@@ -12,6 +13,7 @@ export default {
GlTable, GlTable,
MemberAvatar, MemberAvatar,
MembersTableCell, MembersTableCell,
MemberSource,
}, },
computed: { computed: {
...mapState(['members', 'tableFields']), ...mapState(['members', 'tableFields']),
...@@ -43,8 +45,10 @@ export default { ...@@ -43,8 +45,10 @@ export default {
</members-table-cell> </members-table-cell>
</template> </template>
<template #cell(source)> <template #cell(source)="{ item: member }">
<!-- Temporarily empty --> <members-table-cell #default="{ isDirectMember }" :member="member">
<member-source :is-direct-member="isDirectMember" :member-source="member.source" />
</members-table-cell>
</template> </template>
<template #head(actions)="{ label }"> <template #head(actions)="{ label }">
......
<script> <script>
import { mapState } from 'vuex';
import { MEMBER_TYPES } from '../constants'; import { MEMBER_TYPES } from '../constants';
export default { export default {
...@@ -10,6 +11,7 @@ export default { ...@@ -10,6 +11,7 @@ export default {
}, },
}, },
computed: { computed: {
...mapState(['sourceId']),
isGroup() { isGroup() {
return Boolean(this.member.sharedWithGroup); return Boolean(this.member.sharedWithGroup);
}, },
...@@ -30,10 +32,14 @@ export default { ...@@ -30,10 +32,14 @@ export default {
return MEMBER_TYPES.user; return MEMBER_TYPES.user;
}, },
isDirectMember() {
return this.member.source?.id === this.sourceId;
},
}, },
render() { render() {
return this.$scopedSlots.default({ return this.$scopedSlots.default({
memberType: this.memberType, memberType: this.memberType,
isDirectMember: this.isDirectMember,
}); });
}, },
}; };
......
...@@ -8934,6 +8934,9 @@ msgstr "" ...@@ -8934,6 +8934,9 @@ msgstr ""
msgid "Diffs|Something went wrong while fetching diff lines." msgid "Diffs|Something went wrong while fetching diff lines."
msgstr "" msgstr ""
msgid "Direct member"
msgstr ""
msgid "Direction" msgid "Direction"
msgstr "" msgstr ""
...@@ -13614,6 +13617,9 @@ msgstr "" ...@@ -13614,6 +13617,9 @@ msgstr ""
msgid "Information about additional Pages templates and how to install them can be found in our %{pages_getting_started_guide}." msgid "Information about additional Pages templates and how to install them can be found in our %{pages_getting_started_guide}."
msgstr "" msgstr ""
msgid "Inherited"
msgstr ""
msgid "Inherited:" msgid "Inherited:"
msgstr "" msgstr ""
......
import { mount, createWrapper } from '@vue/test-utils';
import { getByText as getByTextHelper } from '@testing-library/dom';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import MemberSource from '~/vue_shared/components/members/table/member_source.vue';
describe('MemberSource', () => {
let wrapper;
const createComponent = propsData => {
wrapper = mount(MemberSource, {
propsData: {
memberSource: {
id: 102,
name: 'Foo bar',
webUrl: 'https://gitlab.com/groups/foo-bar',
},
...propsData,
},
directives: {
GlTooltip: createMockDirective(),
},
});
};
const getByText = (text, options) =>
createWrapper(getByTextHelper(wrapper.element, text, options));
const getTooltipDirective = elementWrapper => getBinding(elementWrapper.element, 'gl-tooltip');
afterEach(() => {
wrapper.destroy();
});
describe('direct member', () => {
it('displays "Direct member"', () => {
createComponent({
isDirectMember: true,
});
expect(getByText('Direct member').exists()).toBe(true);
});
});
describe('inherited member', () => {
let sourceGroupLink;
beforeEach(() => {
createComponent({
isDirectMember: false,
});
sourceGroupLink = getByText('Foo bar');
});
it('displays a link to source group', () => {
createComponent({
isDirectMember: false,
});
expect(sourceGroupLink.exists()).toBe(true);
expect(sourceGroupLink.attributes('href')).toBe('https://gitlab.com/groups/foo-bar');
});
it('displays tooltip with "Inherited"', () => {
const tooltipDirective = getTooltipDirective(sourceGroupLink);
expect(tooltipDirective).not.toBeUndefined();
expect(sourceGroupLink.attributes('title')).toBe('Inherited');
});
});
});
import { mount, createLocalVue } from '@vue/test-utils'; import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { MEMBER_TYPES } from '~/vue_shared/components/members/constants'; import { MEMBER_TYPES } from '~/vue_shared/components/members/constants';
import { member as memberMock, group, invite, accessRequest } from '../mock_data'; import { member as memberMock, group, invite, accessRequest } from '../mock_data';
import MembersTableCell from '~/vue_shared/components/members/table/members_table_cell.vue'; import MembersTableCell from '~/vue_shared/components/members/table/members_table_cell.vue';
...@@ -10,6 +11,10 @@ describe('MemberList', () => { ...@@ -10,6 +11,10 @@ describe('MemberList', () => {
type: String, type: String,
required: true, required: true,
}, },
isDirectMember: {
type: Boolean,
required: true,
},
}, },
render(createElement) { render(createElement) {
return createElement('div', this.memberType); return createElement('div', this.memberType);
...@@ -17,20 +22,34 @@ describe('MemberList', () => { ...@@ -17,20 +22,34 @@ describe('MemberList', () => {
}; };
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex);
localVue.component('wrapped-component', WrappedComponent); localVue.component('wrapped-component', WrappedComponent);
const createStore = (state = {}) => {
return new Vuex.Store({
state: {
sourceId: 1,
...state,
},
});
};
let wrapper; let wrapper;
const createComponent = propsData => { const createComponent = (propsData, state = {}) => {
wrapper = mount(MembersTableCell, { wrapper = mount(MembersTableCell, {
localVue, localVue,
propsData, propsData,
store: createStore(state),
scopedSlots: { scopedSlots: {
default: '<wrapped-component :member-type="props.memberType" />', default:
'<wrapped-component :member-type="props.memberType" :is-direct-member="props.isDirectMember" />',
}, },
}); });
}; };
const findWrappedComponent = () => wrapper.find(WrappedComponent);
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
...@@ -47,7 +66,31 @@ describe('MemberList', () => { ...@@ -47,7 +66,31 @@ describe('MemberList', () => {
({ member, expectedMemberType }) => { ({ member, expectedMemberType }) => {
createComponent({ member }); createComponent({ member });
expect(wrapper.find(WrappedComponent).props('memberType')).toBe(expectedMemberType); expect(findWrappedComponent().props('memberType')).toBe(expectedMemberType);
}, },
); );
describe('isDirectMember', () => {
it('returns `true` when member source has same ID as `sourceId`', () => {
createComponent({
member: {
...memberMock,
source: {
...memberMock.source,
id: 1,
},
},
});
expect(findWrappedComponent().props('isDirectMember')).toBe(true);
});
it('returns `false` when member is inherited', () => {
createComponent({
member: memberMock,
});
expect(findWrappedComponent().props('isDirectMember')).toBe(false);
});
});
}); });
...@@ -5,7 +5,10 @@ import { ...@@ -5,7 +5,10 @@ import {
getByTestId as getByTestIdHelper, getByTestId as getByTestIdHelper,
} from '@testing-library/dom'; } from '@testing-library/dom';
import MembersTable from '~/vue_shared/components/members/table/members_table.vue'; import MembersTable from '~/vue_shared/components/members/table/members_table.vue';
import MemberAvatar from '~/vue_shared/components/members/table/member_avatar.vue';
import MemberSource from '~/vue_shared/components/members/table/member_source.vue';
import * as initUserPopovers from '~/user_popovers'; import * as initUserPopovers from '~/user_popovers';
import { member as memberMock, invite, accessRequest } from '../mock_data';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
...@@ -44,20 +47,31 @@ describe('MemberList', () => { ...@@ -44,20 +47,31 @@ describe('MemberList', () => {
describe('fields', () => { describe('fields', () => {
it.each` it.each`
field | label field | label | member | expectedComponent
${'source'} | ${'Source'} ${'account'} | ${'Account'} | ${memberMock} | ${MemberAvatar}
${'granted'} | ${'Access granted'} ${'source'} | ${'Source'} | ${memberMock} | ${MemberSource}
${'invited'} | ${'Invited'} ${'granted'} | ${'Access granted'} | ${memberMock} | ${null}
${'requested'} | ${'Requested'} ${'invited'} | ${'Invited'} | ${invite} | ${null}
${'expires'} | ${'Access expires'} ${'requested'} | ${'Requested'} | ${accessRequest} | ${null}
${'maxRole'} | ${'Max role'} ${'expires'} | ${'Access expires'} | ${memberMock} | ${null}
${'expiration'} | ${'Expiration'} ${'maxRole'} | ${'Max role'} | ${memberMock} | ${null}
`('renders the $label field', ({ field, label }) => { ${'expiration'} | ${'Expiration'} | ${memberMock} | ${null}
`('renders the $label field', ({ field, label, member, expectedComponent }) => {
createComponent({ createComponent({
members: [member],
tableFields: [field], tableFields: [field],
}); });
expect(getByText(label, { selector: '[role="columnheader"]' }).exists()).toBe(true); expect(getByText(label, { selector: '[role="columnheader"]' }).exists()).toBe(true);
if (expectedComponent) {
expect(
wrapper
.find(`[data-label="${label}"][role="cell"]`)
.find(expectedComponent)
.exists(),
).toBe(true);
}
}); });
it('renders "Actions" field for screen readers', () => { it('renders "Actions" field for screen readers', () => {
......
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