Commit 84f4c0fd authored by Samantha Ming's avatar Samantha Ming Committed by Martin Wortschack

Setup support for Reviewers in sidebar widget

Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/237921

- This is part 1 of 2.
- In this MR we will be setting up support of adding
Reviewers in the sidbar widget.
- Part 2 will be adding the corresponding Vue components.
parent 47c0f61b
...@@ -6,6 +6,7 @@ import UsersSelect from './users_select'; ...@@ -6,6 +6,7 @@ import UsersSelect from './users_select';
export default class IssuableContext { export default class IssuableContext {
constructor(currentUser) { constructor(currentUser) {
this.userSelect = new UsersSelect(currentUser); this.userSelect = new UsersSelect(currentUser);
this.reviewersSelect = new UsersSelect(currentUser, '.js-reviewer-search');
import(/* webpackChunkName: 'select2' */ 'select2/select2') import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(() => { .then(() => {
......
...@@ -40,6 +40,17 @@ export default class SidebarMediator { ...@@ -40,6 +40,17 @@ export default class SidebarMediator {
return this.service.update(field, data); 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) { setMoveToProjectId(projectId) {
this.store.setMoveToProjectId(projectId); this.store.setMoveToProjectId(projectId);
} }
...@@ -55,6 +66,7 @@ export default class SidebarMediator { ...@@ -55,6 +66,7 @@ export default class SidebarMediator {
processFetchedData(data) { processFetchedData(data) {
this.store.setAssigneeData(data); this.store.setAssigneeData(data);
this.store.setReviewerData(data);
this.store.setTimeTrackingData(data); this.store.setTimeTrackingData(data);
this.store.setParticipantsData(data); this.store.setParticipantsData(data);
this.store.setSubscriptionsData(data); this.store.setSubscriptionsData(data);
......
...@@ -18,8 +18,10 @@ export default class SidebarStore { ...@@ -18,8 +18,10 @@ export default class SidebarStore {
this.humanTimeSpent = ''; this.humanTimeSpent = '';
this.timeTrackingLimitToHours = timeTrackingLimitToHours; this.timeTrackingLimitToHours = timeTrackingLimitToHours;
this.assignees = []; this.assignees = [];
this.reviewers = [];
this.isFetching = { this.isFetching = {
assignees: true, assignees: true,
reviewers: true,
participants: true, participants: true,
subscriptions: true, subscriptions: true,
}; };
...@@ -42,6 +44,13 @@ export default class SidebarStore { ...@@ -42,6 +44,13 @@ export default class SidebarStore {
} }
} }
setReviewerData(data) {
this.isFetching.reviewers = false;
if (data.reviewers) {
this.reviewers = data.reviewers;
}
}
setTimeTrackingData(data) { setTimeTrackingData(data) {
this.timeEstimate = data.time_estimate; this.timeEstimate = data.time_estimate;
this.totalTimeSpent = data.total_time_spent; this.totalTimeSpent = data.total_time_spent;
...@@ -75,20 +84,40 @@ export default class SidebarStore { ...@@ -75,20 +84,40 @@ export default class SidebarStore {
} }
} }
addReviewer(reviewer) {
if (!this.findReviewer(reviewer)) {
this.reviewers.push(reviewer);
}
}
findAssignee(findAssignee) { findAssignee(findAssignee) {
return this.assignees.find(assignee => assignee.id === findAssignee.id); return this.assignees.find(assignee => assignee.id === findAssignee.id);
} }
findReviewer(findReviewer) {
return this.reviewers.find(reviewer => reviewer.id === findReviewer.id);
}
removeAssignee(removeAssignee) { removeAssignee(removeAssignee) {
if (removeAssignee) { if (removeAssignee) {
this.assignees = this.assignees.filter(assignee => assignee.id !== removeAssignee.id); 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() { removeAllAssignees() {
this.assignees = []; this.assignees = [];
} }
removeAllReviewers() {
this.reviewers = [];
}
setAssigneesFromRealtime(data) { setAssigneesFromRealtime(data) {
this.assignees = data; this.assignees = data;
} }
......
...@@ -19,6 +19,7 @@ import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; ...@@ -19,6 +19,7 @@ import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
window.emitSidebarEvent = window.emitSidebarEvent || $.noop; window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
function UsersSelect(currentUser, els, options = {}) { function UsersSelect(currentUser, els, options = {}) {
const elsClassName = els?.toString().match('.(.+$)')[1];
const $els = $(els || '.js-user-search'); const $els = $(els || '.js-user-search');
this.users = this.users.bind(this); this.users = this.users.bind(this);
this.user = this.user.bind(this); this.user = this.user.bind(this);
...@@ -127,9 +128,16 @@ function UsersSelect(currentUser, els, options = {}) { ...@@ -127,9 +128,16 @@ function UsersSelect(currentUser, els, options = {}) {
.find(`input[name='${$dropdown.data('fieldName')}'][value=${firstSelectedId}]`); .find(`input[name='${$dropdown.data('fieldName')}'][value=${firstSelectedId}]`);
firstSelected.remove(); firstSelected.remove();
emitSidebarEvent('sidebar.removeAssignee', {
id: firstSelectedId, if ($dropdown.hasClass(elsClassName)) {
}); emitSidebarEvent('sidebar.removeReviewer', {
id: firstSelectedId,
});
} else {
emitSidebarEvent('sidebar.removeAssignee', {
id: firstSelectedId,
});
}
} }
} }
}; };
...@@ -392,7 +400,11 @@ function UsersSelect(currentUser, els, options = {}) { ...@@ -392,7 +400,11 @@ function UsersSelect(currentUser, els, options = {}) {
defaultLabel, defaultLabel,
hidden() { hidden() {
if ($dropdown.hasClass('js-multiselect')) { if ($dropdown.hasClass('js-multiselect')) {
emitSidebarEvent('sidebar.saveAssignees'); if ($dropdown.hasClass(elsClassName)) {
emitSidebarEvent('sidebar.saveReviewers');
} else {
emitSidebarEvent('sidebar.saveAssignees');
}
} }
if (!$dropdown.data('alwaysShowSelectbox')) { if (!$dropdown.data('alwaysShowSelectbox')) {
...@@ -428,10 +440,18 @@ function UsersSelect(currentUser, els, options = {}) { ...@@ -428,10 +440,18 @@ function UsersSelect(currentUser, els, options = {}) {
previouslySelected.each((index, element) => { previouslySelected.each((index, element) => {
element.remove(); element.remove();
}); });
emitSidebarEvent('sidebar.removeAllAssignees'); if ($dropdown.hasClass(elsClassName)) {
emitSidebarEvent('sidebar.removeAllReviewers');
} else {
emitSidebarEvent('sidebar.removeAllAssignees');
}
} else if (isActive) { } else if (isActive) {
// user selected // user selected
emitSidebarEvent('sidebar.addAssignee', user); if ($dropdown.hasClass(elsClassName)) {
emitSidebarEvent('sidebar.addReviewer', user);
} else {
emitSidebarEvent('sidebar.addAssignee', user);
}
// Remove unassigned selection (if it was previously selected) // Remove unassigned selection (if it was previously selected)
const unassignedSelected = $dropdown const unassignedSelected = $dropdown
...@@ -448,7 +468,11 @@ function UsersSelect(currentUser, els, options = {}) { ...@@ -448,7 +468,11 @@ function UsersSelect(currentUser, els, options = {}) {
} }
// User unselected // User unselected
emitSidebarEvent('sidebar.removeAssignee', user); if ($dropdown.hasClass(elsClassName)) {
emitSidebarEvent('sidebar.removeReviewer', user);
} else {
emitSidebarEvent('sidebar.removeAssignee', user);
}
} }
if (getSelected().find(u => u === gon.current_user_id)) { if (getSelected().find(u => u === gon.current_user_id)) {
......
...@@ -386,6 +386,12 @@ module IssuablesHelper ...@@ -386,6 +386,12 @@ module IssuablesHelper
end end
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) def issuable_squash_option?(issuable, project)
if issuable.persisted? if issuable.persisted?
issuable.squash issuable.squash
......
...@@ -92,7 +92,7 @@ ...@@ -92,7 +92,7 @@
.loading.hide .loading.hide
.spinner.spinner-md .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) - if @merge_request.can_be_reverted?(current_user)
= render "projects/commit/change", type: 'revert', commit: @merge_request.merge_commit, title: @merge_request.title = render "projects/commit/change", type: 'revert', commit: @merge_request.merge_commit, title: @merge_request.title
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
- signed_in = !!issuable_sidebar.dig(:current_user, :id) - signed_in = !!issuable_sidebar.dig(:current_user, :id)
- can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit) - can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit)
- add_page_startup_api_call "#{issuable_sidebar[:issuable_json_path]}?serializer=sidebar_extras" - 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) - if Feature.enabled?(:vue_issuable_sidebar, @project.group)
%aside#js-vue-issuable-sidebar{ data: { signed_in: signed_in, %aside#js-vue-issuable-sidebar{ data: { signed_in: signed_in,
...@@ -28,6 +29,10 @@ ...@@ -28,6 +29,10 @@
.block.assignee.qa-assignee-block .block.assignee.qa-assignee-block
= render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees = 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 = render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar
- if issuable_sidebar[:supports_milestone] - 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)
...@@ -21456,6 +21456,9 @@ msgstr "" ...@@ -21456,6 +21456,9 @@ msgstr ""
msgid "Request parameter %{param} is missing." msgid "Request parameter %{param} is missing."
msgstr "" msgstr ""
msgid "Request review from"
msgstr ""
msgid "Request to link SAML account must be authorized" msgid "Request to link SAML account must be authorized"
msgstr "" msgstr ""
...@@ -21726,6 +21729,9 @@ msgstr "" ...@@ -21726,6 +21729,9 @@ msgstr ""
msgid "ReviewApp|Enable Review App" msgid "ReviewApp|Enable Review App"
msgstr "" msgstr ""
msgid "Reviewer"
msgstr ""
msgid "Reviewing" msgid "Reviewing"
msgstr "" msgstr ""
......
...@@ -306,6 +306,38 @@ RSpec.describe IssuablesHelper do ...@@ -306,6 +306,38 @@ RSpec.describe IssuablesHelper do
end end
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 describe '#issuable_squash_option?' do
using RSpec::Parameterized::TableSyntax 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