Commit 0c6901ab authored by Martin Wortschack's avatar Martin Wortschack

Add approvers column

- Adds the list of approvers
to the MR table as well as tooltips
for author and approvers.
parent 254a7ef3
<script>
import { GlAvatarLink, GlAvatar, GlAvatarsInline, GlTooltipDirective } from '@gitlab/ui';
export const MAX_VISIBLE_AVATARS_DEFAULT = 3;
export const MAX_VISIBLE_AVATARS_COLLAPSED = 2;
export default {
name: 'ApproversColumn',
components: {
GlAvatarLink,
GlAvatar,
GlAvatarsInline,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
approvers: {
type: Array,
required: false,
default: () => [],
},
},
computed: {
maxVisible() {
return this.approvers.length > MAX_VISIBLE_AVATARS_DEFAULT
? MAX_VISIBLE_AVATARS_COLLAPSED
: MAX_VISIBLE_AVATARS_DEFAULT;
},
hasMultipleApprovers() {
return this.approvers.length > 1;
},
hasSingleApprover() {
return this.approvers.length === 1;
},
firstApprover() {
return this.approvers[0];
},
},
avatarSize: 24,
};
</script>
<template>
<div>
<gl-avatars-inline
v-if="hasMultipleApprovers"
collapsed
:avatars="approvers"
:max-visible="maxVisible"
:avatar-size="$options.avatarSize"
>
<template #avatar="{ avatar }">
<gl-avatar-link v-gl-tooltip target="_blank" :href="avatar.web_url" :title="avatar.name">
<gl-avatar :src="avatar.avatar_url" :size="$options.avatarSize" />
</gl-avatar-link>
</template>
</gl-avatars-inline>
<gl-avatar-link
v-else-if="hasSingleApprover"
v-gl-tooltip
target="_blank"
:href="firstApprover.web_url"
:title="firstApprover.name"
>
<gl-avatar :src="firstApprover.avatar_url" :size="$options.avatarSize" />
</gl-avatar-link>
<template v-else>
&ndash;
</template>
</div>
</template>
...@@ -3,7 +3,8 @@ import { escape } from 'underscore'; ...@@ -3,7 +3,8 @@ import { escape } from 'underscore';
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import { __, sprintf, n__ } from '~/locale'; import { __, sprintf, n__ } from '~/locale';
import { getTimeago } from '~/lib/utils/datetime_utility'; import { getTimeago } from '~/lib/utils/datetime_utility';
import { GlTable, GlLink, GlIcon, GlAvatarLink, GlAvatar } from '@gitlab/ui'; import { GlTable, GlLink, GlIcon, GlAvatarLink, GlAvatar, GlTooltipDirective } from '@gitlab/ui';
import ApproversColumn from './approvers_column.vue';
export default { export default {
name: 'MergeRequestTable', name: 'MergeRequestTable',
...@@ -13,6 +14,10 @@ export default { ...@@ -13,6 +14,10 @@ export default {
GlIcon, GlIcon,
GlAvatarLink, GlAvatarLink,
GlAvatar, GlAvatar,
ApproversColumn,
},
directives: {
GlTooltip: GlTooltipDirective,
}, },
computed: { computed: {
...mapState(['mergeRequests']), ...mapState(['mergeRequests']),
...@@ -52,6 +57,11 @@ export default { ...@@ -52,6 +57,11 @@ export default {
label: __('Author'), label: __('Author'),
tdClass: 'table-col d-flex align-items-center d-sm-table-cell', tdClass: 'table-col d-flex align-items-center d-sm-table-cell',
}, },
{
key: 'approved_by',
label: __('Approvers'),
tdClass: 'table-col d-flex align-items-center d-sm-table-cell',
},
{ {
key: 'notes_count', key: 'notes_count',
label: __('Comments'), label: __('Comments'),
...@@ -112,11 +122,15 @@ export default { ...@@ -112,11 +122,15 @@ export default {
</template> </template>
<template #cell(author)="{ value }"> <template #cell(author)="{ value }">
<gl-avatar-link target="blank" :href="value.web_url"> <gl-avatar-link v-gl-tooltip target="blank" :href="value.web_url" :title="value.name">
<gl-avatar :size="24" :src="value.avatar_url" :entity-name="value.name" /> <gl-avatar :size="24" :src="value.avatar_url" :entity-name="value.name" />
</gl-avatar-link> </gl-avatar-link>
</template> </template>
<template #cell(approved_by)="{ value }">
<approvers-column :approvers="value && value.length ? value : []" />
</template>
<template #cell(diff_stats)="{ value }"> <template #cell(diff_stats)="{ value }">
<span>{{ value.commits_count }}</span> <span>{{ value.commits_count }}</span>
</template> </template>
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ApproversColumn component when a list with more than three approvers is passed matches the snapshot 1`] = `
<div>
<gl-avatars-inline-stub
avatars="[object Object],[object Object],[object Object],[object Object]"
avatarsize="24"
collapsed="true"
maxvisible="2"
/>
</div>
`;
exports[`ApproversColumn component when a list with one approver is passed matches the snapshot 1`] = `
<div>
<gl-avatar-link-stub
href="http://127.0.0.1:3000/root"
target="_blank"
title="Administrator"
>
<gl-avatar-stub
alt="avatar"
entityid="0"
entityname=""
shape="circle"
size="24"
src="https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon"
/>
</gl-avatar-link-stub>
</div>
`;
exports[`ApproversColumn component when a list with three approvers is passed matches the snapshot 1`] = `
<div>
<gl-avatars-inline-stub
avatars="[object Object],[object Object],[object Object]"
avatarsize="24"
collapsed="true"
maxvisible="3"
/>
</div>
`;
exports[`ApproversColumn component when a list with two approvers is passed matches the snapshot 1`] = `
<div>
<gl-avatars-inline-stub
avatars="[object Object],[object Object]"
avatarsize="24"
collapsed="true"
maxvisible="3"
/>
</div>
`;
exports[`ApproversColumn component when an empty list approvers is passed matches the snapshot 1`] = `
<div>
</div>
`;
...@@ -3,10 +3,10 @@ ...@@ -3,10 +3,10 @@
exports[`MergeRequestTable component template matches the snapshot 1`] = ` exports[`MergeRequestTable component template matches the snapshot 1`] = `
<table <table
aria-busy="false" aria-busy="false"
aria-colcount="6" aria-colcount="7"
aria-describedby="__BVID__31__caption_" aria-describedby="__BVID__33__caption_"
class="table b-table gl-table my-3 b-table-stacked-sm" class="table b-table gl-table my-3 b-table-stacked-sm"
id="__BVID__31" id="__BVID__33"
role="table" role="table"
> >
<!----> <!---->
...@@ -46,6 +46,14 @@ exports[`MergeRequestTable component template matches the snapshot 1`] = ` ...@@ -46,6 +46,14 @@ exports[`MergeRequestTable component template matches the snapshot 1`] = `
</th> </th>
<th <th
aria-colindex="4" aria-colindex="4"
class=""
role="columnheader"
scope="col"
>
Approvers
</th>
<th
aria-colindex="5"
class="text-right" class="text-right"
role="columnheader" role="columnheader"
scope="col" scope="col"
...@@ -53,7 +61,7 @@ exports[`MergeRequestTable component template matches the snapshot 1`] = ` ...@@ -53,7 +61,7 @@ exports[`MergeRequestTable component template matches the snapshot 1`] = `
Comments Comments
</th> </th>
<th <th
aria-colindex="5" aria-colindex="6"
class="text-right" class="text-right"
role="columnheader" role="columnheader"
scope="col" scope="col"
...@@ -61,7 +69,7 @@ exports[`MergeRequestTable component template matches the snapshot 1`] = ` ...@@ -61,7 +69,7 @@ exports[`MergeRequestTable component template matches the snapshot 1`] = `
Commits Commits
</th> </th>
<th <th
aria-colindex="6" aria-colindex="7"
class="text-right" class="text-right"
role="columnheader" role="columnheader"
scope="col" scope="col"
......
import { shallowMount } from '@vue/test-utils';
import { GlAvatarLink, GlAvatarsInline } from '@gitlab/ui';
import ApproversColumn from 'ee/analytics/code_review_analytics/components/approvers_column.vue';
describe('ApproversColumn component', () => {
let wrapper;
const approvers = [
{
avatar_url:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
web_url: 'http://127.0.0.1:3000/root',
name: 'Administrator',
username: 'root',
},
{
avatar_url:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
web_url: 'http://127.0.0.1:3000/desiree',
name: 'Sharla Beier',
username: 'desiree',
},
{
avatar_url:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
web_url: 'http://127.0.0.1:3000/nina',
name: 'Cory Eichmann',
username: 'nina',
},
{
avatar_url:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
web_url: 'http://127.0.0.1:3000/shamika',
name: 'Melaine Gibson',
username: 'shamika',
},
];
const createComponent = (props = {}) => {
wrapper = shallowMount(ApproversColumn, {
propsData: {
...props,
},
});
};
afterEach(() => {
if (wrapper) wrapper.destroy();
});
const findAvatar = () => wrapper.find(GlAvatarLink);
const findInlineAvatars = () => wrapper.find(GlAvatarsInline);
describe('when an empty list approvers is passed', () => {
beforeEach(() => {
createComponent({ approvers: [] });
});
it('renders a dash', () => {
expect(wrapper.text()).toContain('');
});
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
describe('when a list with one approver is passed', () => {
beforeEach(() => {
createComponent({ approvers: [approvers[0]] });
});
it('renders the GlAvatarLink component', () => {
expect(findAvatar().exists()).toBe(true);
});
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
describe.each`
totalApprovers | data | maxVisible
${'two'} | ${approvers.slice(0, 2)} | ${3}
${'three'} | ${approvers.slice(0, 3)} | ${3}
${'more than three'} | ${approvers} | ${2}
`('when a list with $totalApprovers approvers is passed', ({ data, maxVisible }) => {
beforeEach(() => {
createComponent({ approvers: data });
});
it('renders a GlAvatarsInline component', () => {
expect(findInlineAvatars().exists()).toBe(true);
});
it(`sets collapsed to true`, () => {
expect(findInlineAvatars().props('collapsed')).toBe(true);
});
it(`returns maxVisible to be ${maxVisible}`, () => {
expect(wrapper.vm.maxVisible).toBe(maxVisible);
});
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
});
...@@ -60,6 +60,7 @@ describe('MergeRequestTable component', () => { ...@@ -60,6 +60,7 @@ describe('MergeRequestTable component', () => {
'Merge Request', 'Merge Request',
'Review time', 'Review time',
'Author', 'Author',
'Approvers',
'Comments', 'Comments',
'Commits', 'Commits',
'Line changes', 'Line changes',
......
...@@ -2110,6 +2110,9 @@ msgstr "" ...@@ -2110,6 +2110,9 @@ msgstr ""
msgid "Approver" msgid "Approver"
msgstr "" msgstr ""
msgid "Approvers"
msgstr ""
msgid "Apr" msgid "Apr"
msgstr "" msgstr ""
......
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