Commit d81fd9e9 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '237921-side-widget-add-reviewers' into 'master'

Part 1: Add reviewers to side widget (Setup HAML + JS)

Closes #237921

See merge request gitlab-org/gitlab!40424
parents f1c813d4 84f4c0fd
......@@ -6,6 +6,7 @@ import UsersSelect from './users_select';
export default class IssuableContext {
constructor(currentUser) {
this.userSelect = new UsersSelect(currentUser);
this.reviewersSelect = new UsersSelect(currentUser, '.js-reviewer-search');
import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(() => {
......
......@@ -40,6 +40,17 @@ export default class SidebarMediator {
return this.service.update(field, data);
}
saveReviewers(field) {
const selected = this.store.reviewers.map(u => u.id);
// If there are no ids, that means we have to unassign (which is id = 0)
// And it only accepts an array, hence [0]
const reviewers = selected.length === 0 ? [0] : selected;
const data = { reviewer_ids: reviewers };
return this.service.update(field, data);
}
setMoveToProjectId(projectId) {
this.store.setMoveToProjectId(projectId);
}
......@@ -55,6 +66,7 @@ export default class SidebarMediator {
processFetchedData(data) {
this.store.setAssigneeData(data);
this.store.setReviewerData(data);
this.store.setTimeTrackingData(data);
this.store.setParticipantsData(data);
this.store.setSubscriptionsData(data);
......
......@@ -18,8 +18,10 @@ export default class SidebarStore {
this.humanTimeSpent = '';
this.timeTrackingLimitToHours = timeTrackingLimitToHours;
this.assignees = [];
this.reviewers = [];
this.isFetching = {
assignees: true,
reviewers: true,
participants: true,
subscriptions: true,
};
......@@ -42,6 +44,13 @@ export default class SidebarStore {
}
}
setReviewerData(data) {
this.isFetching.reviewers = false;
if (data.reviewers) {
this.reviewers = data.reviewers;
}
}
setTimeTrackingData(data) {
this.timeEstimate = data.time_estimate;
this.totalTimeSpent = data.total_time_spent;
......@@ -75,20 +84,40 @@ export default class SidebarStore {
}
}
addReviewer(reviewer) {
if (!this.findReviewer(reviewer)) {
this.reviewers.push(reviewer);
}
}
findAssignee(findAssignee) {
return this.assignees.find(assignee => assignee.id === findAssignee.id);
}
findReviewer(findReviewer) {
return this.reviewers.find(reviewer => reviewer.id === findReviewer.id);
}
removeAssignee(removeAssignee) {
if (removeAssignee) {
this.assignees = this.assignees.filter(assignee => assignee.id !== removeAssignee.id);
}
}
removeReviewer(removeReviewer) {
if (removeReviewer) {
this.reviewers = this.reviewers.filter(reviewer => reviewer.id !== removeReviewer.id);
}
}
removeAllAssignees() {
this.assignees = [];
}
removeAllReviewers() {
this.reviewers = [];
}
setAssigneesFromRealtime(data) {
this.assignees = data;
}
......
......@@ -19,6 +19,7 @@ import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
function UsersSelect(currentUser, els, options = {}) {
const elsClassName = els?.toString().match('.(.+$)')[1];
const $els = $(els || '.js-user-search');
this.users = this.users.bind(this);
this.user = this.user.bind(this);
......@@ -127,11 +128,18 @@ function UsersSelect(currentUser, els, options = {}) {
.find(`input[name='${$dropdown.data('fieldName')}'][value=${firstSelectedId}]`);
firstSelected.remove();
if ($dropdown.hasClass(elsClassName)) {
emitSidebarEvent('sidebar.removeReviewer', {
id: firstSelectedId,
});
} else {
emitSidebarEvent('sidebar.removeAssignee', {
id: firstSelectedId,
});
}
}
}
};
const getMultiSelectDropdownTitle = function(selectedUser, isSelected) {
......@@ -392,8 +400,12 @@ function UsersSelect(currentUser, els, options = {}) {
defaultLabel,
hidden() {
if ($dropdown.hasClass('js-multiselect')) {
if ($dropdown.hasClass(elsClassName)) {
emitSidebarEvent('sidebar.saveReviewers');
} else {
emitSidebarEvent('sidebar.saveAssignees');
}
}
if (!$dropdown.data('alwaysShowSelectbox')) {
$selectbox.hide();
......@@ -428,10 +440,18 @@ function UsersSelect(currentUser, els, options = {}) {
previouslySelected.each((index, element) => {
element.remove();
});
if ($dropdown.hasClass(elsClassName)) {
emitSidebarEvent('sidebar.removeAllReviewers');
} else {
emitSidebarEvent('sidebar.removeAllAssignees');
}
} else if (isActive) {
// user selected
if ($dropdown.hasClass(elsClassName)) {
emitSidebarEvent('sidebar.addReviewer', user);
} else {
emitSidebarEvent('sidebar.addAssignee', user);
}
// Remove unassigned selection (if it was previously selected)
const unassignedSelected = $dropdown
......@@ -448,8 +468,12 @@ function UsersSelect(currentUser, els, options = {}) {
}
// User unselected
if ($dropdown.hasClass(elsClassName)) {
emitSidebarEvent('sidebar.removeReviewer', user);
} else {
emitSidebarEvent('sidebar.removeAssignee', user);
}
}
if (getSelected().find(u => u === gon.current_user_id)) {
$assignToMeLink.hide();
......
......@@ -386,6 +386,12 @@ module IssuablesHelper
end
end
def reviewer_sidebar_data(reviewer, merge_request: nil)
{ avatar_url: reviewer.avatar_url, name: reviewer.name, username: reviewer.username }.tap do |data|
data[:can_merge] = merge_request.can_be_merged_by?(reviewer) if merge_request
end
end
def issuable_squash_option?(issuable, project)
if issuable.persisted?
issuable.squash
......
......@@ -92,7 +92,7 @@
.loading.hide
.spinner.spinner-md
= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees, source_branch: @merge_request.source_branch
= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees, reviewers: @merge_request.reviewers, source_branch: @merge_request.source_branch
- if @merge_request.can_be_reverted?(current_user)
= render "projects/commit/change", type: 'revert', commit: @merge_request.merge_commit, title: @merge_request.title
......
......@@ -5,6 +5,7 @@
- signed_in = !!issuable_sidebar.dig(:current_user, :id)
- can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit)
- add_page_startup_api_call "#{issuable_sidebar[:issuable_json_path]}?serializer=sidebar_extras"
- reviewers = local_assigns.fetch(:reviewers, nil)
- if Feature.enabled?(:vue_issuable_sidebar, @project.group)
%aside#js-vue-issuable-sidebar{ data: { signed_in: signed_in,
......@@ -28,6 +29,10 @@
.block.assignee.qa-assignee-block
= render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees
- if reviewers
.block.reviewer.qa-reviewer-block
= render "shared/issuable/sidebar_reviewers", issuable_sidebar: issuable_sidebar, reviewers: reviewers
= render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar
- if issuable_sidebar[:supports_milestone]
......
- issuable_type = issuable_sidebar[:type]
- signed_in = !!issuable_sidebar.dig(:current_user, :id)
#js-vue-sidebar-reviewers{ data: { field: issuable_type, signed_in: signed_in } }
.title.hide-collapsed
= _('Reviewer')
= loading_icon(css_class: 'gl-vertical-align-text-bottom')
.selectbox.hide-collapsed
- if reviewers.none?
= hidden_field_tag "#{issuable_type}[reviewer_ids][]", 0, id: nil
- else
- reviewers.each do |reviewer|
= hidden_field_tag "#{issuable_type}[reviewer_ids][]", reviewer.id, id: nil, data: reviewer_sidebar_data(reviewer, merge_request: @merge_request)
- options = { toggle_class: 'js-reviewer-search js-author-search',
title: _('Request review from'),
filter: true,
dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author',
placeholder: _('Search users'),
data: { first_user: issuable_sidebar.dig(:current_user, :username),
current_user: true,
iid: issuable_sidebar[:iid],
issuable_type: issuable_type,
project_id: issuable_sidebar[:project_id],
author_id: issuable_sidebar[:author_id],
field_name: "#{issuable_type}[reviewer_ids][]",
issue_update: issuable_sidebar[:issuable_json_path],
ability_name: issuable_type,
null_user: true,
display: 'static' } }
- dropdown_options = reviewers_dropdown_options(issuable_type)
- title = dropdown_options[:title]
- options[:toggle_class] += ' js-multiselect js-save-user-data'
- data = { field_name: "#{issuable_type}[reviewer_ids][]" }
- data[:multi_select] = true
- data['dropdown-title'] = title
- data['dropdown-header'] = dropdown_options[:data][:'dropdown-header']
- data['max-select'] = dropdown_options[:data][:'max-select'] if dropdown_options[:data][:'max-select']
- options[:data].merge!(data)
- if experiment_enabled?(:invite_members_version_a) && can_import_members?
- options[:dropdown_class] += ' dropdown-extended-height'
- options[:footer_content] = true
- options[:wrapper_class] = 'js-sidebar-reviewer-dropdown'
= dropdown_tag(title, options: options) do
%ul.dropdown-footer-list
%li
= link_to _('Invite Members'),
project_project_members_path(@project),
title: _('Invite Members'),
data: { 'is-link': true, 'track-event': 'click_invite_members', 'track-label': 'edit_reviewer' }
- else
= dropdown_tag(title, options: options)
......@@ -21465,6 +21465,9 @@ msgstr ""
msgid "Request parameter %{param} is missing."
msgstr ""
msgid "Request review from"
msgstr ""
msgid "Request to link SAML account must be authorized"
msgstr ""
......@@ -21735,6 +21738,9 @@ msgstr ""
msgid "ReviewApp|Enable Review App"
msgstr ""
msgid "Reviewer"
msgstr ""
msgid "Reviewing"
msgstr ""
......
......@@ -306,6 +306,38 @@ RSpec.describe IssuablesHelper do
end
end
describe '#reviewer_sidebar_data' do
let(:user) { create(:user) }
subject { helper.reviewer_sidebar_data(user, merge_request: merge_request) }
context 'without merge_request' do
let(:merge_request) { nil }
it 'returns hash of reviewer data' do
is_expected.to eql({
avatar_url: user.avatar_url,
name: user.name,
username: user.username
})
end
end
context 'with merge_request' do
let(:merge_request) { build(:merge_request) }
where(can_merge: [true, false])
with_them do
before do
allow(merge_request).to receive(:can_be_merged_by?).and_return(can_merge)
end
it { is_expected.to include({ can_merge: can_merge })}
end
end
end
describe '#issuable_squash_option?' do
using RSpec::Parameterized::TableSyntax
......
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