Commit 389e19ba authored by Phil Hughes's avatar Phil Hughes

Allow users to re-request a new review from a reviewer

This gives users a button that allows them to re-request
a review from a assigned
reviewer.

Closes https://gitlab.com/gitlab-org/gitlab/-/issues/293933
parent 57a1150a
......@@ -76,8 +76,8 @@ export default {
class="d-inline-block"
>
<!-- use d-flex so that slot can be appropriately styled -->
<span class="d-flex">
<reviewer-avatar :user="user" :img-size="32" :issuable-type="issuableType" />
<span class="gl-display-flex gl-align-items-center">
<reviewer-avatar :user="user" :img-size="24" :issuable-type="issuableType" />
<slot :user="user"></slot>
</span>
</gl-link>
......
......@@ -46,6 +46,9 @@ export default {
assignSelf() {
this.$emit('assign-self');
},
requestReview(data) {
this.$emit('request-review', data);
},
},
};
</script>
......@@ -66,6 +69,7 @@ export default {
:users="sortedReviewers"
:root-path="rootPath"
:issuable-type="issuableType"
@request-review="requestReview"
/>
</div>
</div>
......
......@@ -83,6 +83,9 @@ export default {
return new Flash(__('Error occurred when saving reviewers'));
});
},
requestReview(data) {
this.mediator.requestReview(data);
},
},
};
</script>
......@@ -101,6 +104,7 @@ export default {
:editable="store.editable"
:issuable-type="issuableType"
class="value"
@request-review="requestReview"
/>
</div>
</template>
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import ReviewerAvatarLink from './reviewer_avatar_link.vue';
......@@ -8,8 +9,13 @@ const DEFAULT_RENDER_COUNT = 5;
export default {
components: {
GlButton,
GlIcon,
ReviewerAvatarLink,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
users: {
type: Array,
......@@ -28,6 +34,8 @@ export default {
data() {
return {
showLess: true,
loading: false,
requestedReviewSuccess: false,
};
},
computed: {
......@@ -61,43 +69,53 @@ export default {
toggleShowLess() {
this.showLess = !this.showLess;
},
reRequestReview(userId) {
this.loading = true;
this.$emit('request-review', { userId, callback: this.requestReviewComplete });
},
requestReviewComplete(success) {
if (success) {
this.requestedReviewSuccess = true;
setTimeout(() => {
this.requestedReviewSuccess = false;
}, 1500);
}
this.loading = false;
},
},
};
</script>
<template>
<reviewer-avatar-link
v-if="hasOneUser"
#default="{ user }"
tooltip-placement="left"
:tooltip-has-name="false"
:user="firstUser"
:root-path="rootPath"
:issuable-type="issuableType"
>
<div class="gl-ml-3 gl-line-height-normal">
<div class="author">{{ user.name }}</div>
<div class="username">{{ username }}</div>
</div>
</reviewer-avatar-link>
<div v-else>
<div class="user-list">
<div v-for="user in uncollapsedUsers" :key="user.id" class="user-item">
<reviewer-avatar-link :user="user" :root-path="rootPath" :issuable-type="issuableType" />
</div>
</div>
<div v-if="renderShowMoreSection" class="user-list-more">
<button
type="button"
class="btn-link"
data-qa-selector="more_reviewers_link"
@click="toggleShowLess"
>
<template v-if="showLess">
{{ hiddenReviewersLabel }}
</template>
<template v-else>{{ __('- show less') }}</template>
</button>
<div>
<div
v-for="(user, index) in users"
:key="user.id"
:class="{ 'gl-mb-3': index !== users.length - 1 }"
data-testid="reviewer"
>
<reviewer-avatar-link :user="user" :root-path="rootPath" :issuable-type="issuableType">
<div class="gl-ml-3">@{{ user.username }}</div>
</reviewer-avatar-link>
<gl-icon
v-if="requestedReviewSuccess"
:size="24"
name="check"
class="float-right gl-text-green-500"
/>
<gl-button
v-else-if="user.can_update_merge_request && user.reviewed"
v-gl-tooltip.left
:title="__('Re-request review')"
:loading="loading"
class="float-right gl-text-gray-500!"
size="small"
icon="redo"
variant="link"
@click="reRequestReview(user.id)"
/>
</div>
</div>
</template>
mutation mergeRequestRequestRereview($projectPath: ID!, $iid: String!, $userId: ID!) {
mergeRequestReviewerRereview(input: { projectPath: $projectPath, iid: $iid, userId: $userId }) {
errors
}
}
import sidebarDetailsQuery from 'ee_else_ce/sidebar/queries/sidebarDetails.query.graphql';
import axios from '~/lib/utils/axios_utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import reviewerRereviewMutation from '../queries/reviewer_rereview.mutation.graphql';
export const gqClient = createGqClient(
{},
......@@ -70,4 +72,15 @@ export default class SidebarService {
move_to_project_id: moveToProjectId,
});
}
requestReview(userId) {
return gqClient.mutate({
mutation: reviewerRereviewMutation,
variables: {
userId: convertToGraphQLId('User', `${userId}`), // eslint-disable-line @gitlab/require-i18n-strings
projectPath: this.fullPath,
iid: this.iid.toString(),
},
});
}
}
import Store from 'ee_else_ce/sidebar/stores/sidebar_store';
import toast from '~/vue_shared/plugins/global_toast';
import { __ } from '~/locale';
import { visitUrl } from '../lib/utils/url_utility';
import { deprecatedCreateFlash as Flash } from '../flash';
......@@ -51,6 +52,17 @@ export default class SidebarMediator {
return this.service.update(field, data);
}
requestReview({ userId, callback }) {
return this.service
.requestReview(userId)
.then(() => {
this.store.updateReviewer(userId);
toast(__('Requested review'));
callback(true);
})
.catch(() => callback(false));
}
setMoveToProjectId(projectId) {
this.store.setMoveToProjectId(projectId);
}
......
......@@ -96,6 +96,14 @@ export default class SidebarStore {
}
}
updateReviewer(id) {
const reviewer = this.findReviewer({ id });
if (reviewer) {
reviewer.reviewed = false;
}
}
findAssignee(findAssignee) {
return this.assignees.find(({ id }) => id === findAssignee.id);
}
......
---
title: Allow users to re-request a review from a reviewer
merge_request: 50068
author:
type: added
......@@ -23858,6 +23858,9 @@ msgstr ""
msgid "Re-authentication required"
msgstr ""
msgid "Re-request review"
msgstr ""
msgid "Re-verification interval"
msgstr ""
......@@ -24677,6 +24680,9 @@ msgstr ""
msgid "Requested design version does not exist."
msgstr ""
msgid "Requested review"
msgstr ""
msgid "Requested states are invalid"
msgstr ""
......
......@@ -114,8 +114,7 @@ describe('Reviewer component', () => {
editable: true,
});
expect(wrapper.findAll('.user-item').length).toBe(users.length);
expect(wrapper.find('.user-list-more').exists()).toBe(false);
expect(wrapper.findAll('[data-testid="reviewer"]').length).toBe(users.length);
});
it('shows sorted reviewer where "can merge" users are sorted first', () => {
......@@ -144,10 +143,10 @@ describe('Reviewer component', () => {
users,
});
const userItems = wrapper.findAll('.user-list .user-item a');
const userItems = wrapper.findAll('[data-testid="reviewer"]');
expect(userItems.length).toBe(3);
expect(userItems.at(0).attributes('title')).toBe(users[2].name);
expect(userItems.at(0).find('a').attributes('title')).toBe(users[2].name);
});
it('passes the sorted reviewers to the collapsed-reviewer-list', () => {
......
......@@ -48,7 +48,7 @@ RSpec.shared_examples 'an editable merge request' do
end
page.within '.reviewer' do
expect(page).to have_content user.name
expect(page).to have_content user.username
end
page.within '.milestone' do
......
......@@ -40,7 +40,7 @@ RSpec.shared_examples 'multiple reviewers merge request' do |action, save_button
# Closing dropdown to persist
click_link 'Edit'
expect(page).to have_content user2.name
expect(page).to have_content user2.username
end
end
end
......
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