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';
import { mapState } from 'vuex';
import { __, sprintf, n__ } from '~/locale';
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 {
name: 'MergeRequestTable',
......@@ -13,6 +14,10 @@ export default {
GlIcon,
GlAvatarLink,
GlAvatar,
ApproversColumn,
},
directives: {
GlTooltip: GlTooltipDirective,
},
computed: {
...mapState(['mergeRequests']),
......@@ -52,6 +57,11 @@ export default {
label: __('Author'),
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',
label: __('Comments'),
......@@ -112,11 +122,15 @@ export default {
</template>
<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-link>
</template>
<template #cell(approved_by)="{ value }">
<approvers-column :approvers="value && value.length ? value : []" />
</template>
<template #cell(diff_stats)="{ value }">
<span>{{ value.commits_count }}</span>
</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 @@
exports[`MergeRequestTable component template matches the snapshot 1`] = `
<table
aria-busy="false"
aria-colcount="6"
aria-describedby="__BVID__31__caption_"
aria-colcount="7"
aria-describedby="__BVID__33__caption_"
class="table b-table gl-table my-3 b-table-stacked-sm"
id="__BVID__31"
id="__BVID__33"
role="table"
>
<!---->
......@@ -46,6 +46,14 @@ exports[`MergeRequestTable component template matches the snapshot 1`] = `
</th>
<th
aria-colindex="4"
class=""
role="columnheader"
scope="col"
>
Approvers
</th>
<th
aria-colindex="5"
class="text-right"
role="columnheader"
scope="col"
......@@ -53,7 +61,7 @@ exports[`MergeRequestTable component template matches the snapshot 1`] = `
Comments
</th>
<th
aria-colindex="5"
aria-colindex="6"
class="text-right"
role="columnheader"
scope="col"
......@@ -61,7 +69,7 @@ exports[`MergeRequestTable component template matches the snapshot 1`] = `
Commits
</th>
<th
aria-colindex="6"
aria-colindex="7"
class="text-right"
role="columnheader"
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', () => {
'Merge Request',
'Review time',
'Author',
'Approvers',
'Comments',
'Commits',
'Line changes',
......
......@@ -2110,6 +2110,9 @@ msgstr ""
msgid "Approver"
msgstr ""
msgid "Approvers"
msgstr ""
msgid "Apr"
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