diff --git a/app/assets/javascripts/invite_members/init_invite_members_trigger.js b/app/assets/javascripts/invite_members/init_invite_members_trigger.js index a7b95960995c397017d0293239d9bb07dcf2593d..935edb353495aa6d94596562d395fff765a6d905 100644 --- a/app/assets/javascripts/invite_members/init_invite_members_trigger.js +++ b/app/assets/javascripts/invite_members/init_invite_members_trigger.js @@ -2,19 +2,21 @@ import Vue from 'vue'; import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue'; export default function initInviteMembersTrigger() { - const el = document.querySelector('.js-invite-members-trigger'); + const triggers = document.querySelectorAll('.js-invite-members-trigger'); - if (!el) { + if (!triggers) { return false; } - return new Vue({ - el, - render: (createElement) => - createElement(InviteMembersTrigger, { - props: { - ...el.dataset, - }, - }), + return triggers.forEach((el) => { + return new Vue({ + el, + render: (createElement) => + createElement(InviteMembersTrigger, { + props: { + ...el.dataset, + }, + }), + }); }); } diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js index 3143ff5adacbfe7a6d75ad88b967dba3b13312ae..3cea61262ea98781485d9f7c11c0ef2247ef159d 100644 --- a/app/assets/javascripts/pages/projects/issues/show.js +++ b/app/assets/javascripts/pages/projects/issues/show.js @@ -1,8 +1,6 @@ import loadAwardsHandler from '~/awards_handler'; import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; import initIssuableSidebar from '~/init_issuable_sidebar'; -import initInviteMembersModal from '~/invite_members/init_invite_members_modal'; -import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger'; import { IssuableType } from '~/issuable_show/constants'; import Issue from '~/issue'; import '~/notes/index'; @@ -34,8 +32,6 @@ export default function initShowIssue() { initIssueHeaderActions(store); initSentryErrorStackTraceApp(); initRelatedMergeRequestsApp(); - initInviteMembersModal(); - initInviteMembersTrigger(); import(/* webpackChunkName: 'design_management' */ '~/design_management') .then((module) => module.default()) diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js index 6cd3202815bbcc71dbca9d7ae6e09df39398f743..d6b6c9fe06a38f4d02d8fee6f16f3b0c875451a4 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js +++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js @@ -4,8 +4,6 @@ import loadAwardsHandler from '~/awards_handler'; import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; import initPipelines from '~/commit/pipelines/pipelines_bundle'; import initIssuableSidebar from '~/init_issuable_sidebar'; -import initInviteMembersModal from '~/invite_members/init_invite_members_modal'; -import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger'; import StatusBox from '~/issuable/components/status_box.vue'; import createDefaultClient from '~/lib/graphql'; import { handleLocationHash } from '~/lib/utils/common_utils'; @@ -29,8 +27,6 @@ export default function initMergeRequestShow() { } else { loadAwardsHandler(); } - initInviteMembersModal(); - initInviteMembersTrigger(); const el = document.querySelector('.js-mr-status-box'); const apolloProvider = new VueApollo({ defaultClient: createDefaultClient() }); diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index 2eec6b7f3fc92acb97e256507c26266cb6588d27..d28450e431fd8b9d45fed1c04fd38affd98e01b9 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -2,6 +2,8 @@ import $ from 'jquery'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createFlash from '~/flash'; +import initInviteMembersModal from '~/invite_members/init_invite_members_modal'; +import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger'; import { IssuableType } from '~/issue_show/constants'; import { isInIssuePage, @@ -17,6 +19,7 @@ import SidebarDueDateWidget from '~/sidebar/components/date/sidebar_date_widget. import SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_widget.vue'; import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue'; import { apolloProvider } from '~/sidebar/graphql'; +import trackShowInviteMemberLink from '~/sidebar/track_invite_members'; import Translate from '../vue_shared/translate'; import SidebarAssignees from './components/assignees/sidebar_assignees.vue'; import CopyEmailToClipboard from './components/copy_email_to_clipboard.vue'; @@ -123,6 +126,12 @@ function mountAssigneesComponent() { }, }), }); + + const assigneeDropdown = document.querySelector('.js-sidebar-assignee-dropdown'); + + if (assigneeDropdown) { + trackShowInviteMemberLink(assigneeDropdown); + } } function mountReviewersComponent(mediator) { @@ -149,6 +158,12 @@ function mountReviewersComponent(mediator) { }, }), }); + + const reviewerDropdown = document.querySelector('.js-sidebar-reviewer-dropdown'); + + if (reviewerDropdown) { + trackShowInviteMemberLink(reviewerDropdown); + } } export function mountSidebarLabels() { @@ -438,6 +453,9 @@ const isAssigneesWidgetShown = (isInIssuePage() || isInDesignPage()) && gon.features.issueAssigneesWidget; export function mountSidebar(mediator) { + initInviteMembersModal(); + initInviteMembersTrigger(); + if (isAssigneesWidgetShown) { mountAssigneesComponent(); } else { diff --git a/app/assets/javascripts/sidebar/track_invite_members.js b/app/assets/javascripts/sidebar/track_invite_members.js new file mode 100644 index 0000000000000000000000000000000000000000..eab15578f0fed6f9a1f1ed5ddd38dd2e65be3312 --- /dev/null +++ b/app/assets/javascripts/sidebar/track_invite_members.js @@ -0,0 +1,12 @@ +import $ from 'jquery'; +import Tracking from '~/tracking'; + +export default function initTrackInviteMembers(userDropdown) { + const { trackEvent, trackLabel } = userDropdown.querySelector('.js-invite-members-track').dataset; + + $(userDropdown).on('shown.bs.dropdown', () => { + Tracking.event(undefined, trackEvent, { + label: trackLabel, + }); + }); +} diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml index 86369b32e98cfad97a3262f7d34d21bed6c2f3de..8216d48fcec5354802dbfbb2cd60dae6a34faf86 100644 --- a/app/views/shared/issuable/_sidebar_assignees.html.haml +++ b/app/views/shared/issuable/_sidebar_assignees.html.haml @@ -42,22 +42,7 @@ - data['max-select'] = dropdown_options[:data][:'max-select'] if dropdown_options[:data][:'max-select'] - options[:data].merge!(data) - - if directly_invite_members? - - options[:dropdown_class] += ' dropdown-extended-height' - - options[:footer_content] = true - - options[:wrapper_class] = 'js-sidebar-assignee-dropdown' - - options[:toggle_class] += ' js-invite-members-track' - - data['track-event'] = 'show_invite_members' - - options[:data].merge!(data) - - invite_text = _('Invite Members') - - track_label = 'edit_assignee' - - = dropdown_tag(title, options: options) do - %ul.dropdown-footer-list - %li - .js-invite-members-trigger{ data: { trigger_element: 'anchor', - display_text: invite_text, - event: 'click_invite_members', - label: track_label } } - - else - = dropdown_tag(title, options: options) + = render 'shared/issuable/sidebar_user_dropdown', + options: options, + wrapper_class: 'js-sidebar-assignee-dropdown', + track_label: 'edit_assignee' diff --git a/app/views/shared/issuable/_sidebar_reviewers.html.haml b/app/views/shared/issuable/_sidebar_reviewers.html.haml index 1a8f1a2639f05dfd12a03889131a2a7cf5c21501..95502b9fa607512290f42c01598c73e17eed658d 100644 --- a/app/views/shared/issuable/_sidebar_reviewers.html.haml +++ b/app/views/shared/issuable/_sidebar_reviewers.html.haml @@ -39,4 +39,7 @@ - data['max-select'] = dropdown_options[:data][:'max-select'] if dropdown_options[:data][:'max-select'] - options[:data].merge!(data) - = dropdown_tag(title, options: options) + = render 'shared/issuable/sidebar_user_dropdown', + options: options, + wrapper_class: 'js-sidebar-reviewer-dropdown', + track_label: 'edit_reviewer' diff --git a/app/views/shared/issuable/_sidebar_user_dropdown.html.haml b/app/views/shared/issuable/_sidebar_user_dropdown.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..c8b9108d046ddc1850b5ec7c6d6bb872706faf66 --- /dev/null +++ b/app/views/shared/issuable/_sidebar_user_dropdown.html.haml @@ -0,0 +1,20 @@ +- options = local_assigns.fetch(:options) +- data = options[:data] + +- if directly_invite_members? + - options[:dropdown_class] += ' dropdown-extended-height' + - options[:footer_content] = true + - options[:wrapper_class] = local_assigns.fetch(:wrapper_class) + - options[:toggle_class] += ' js-invite-members-track' + - data['track-event'] = 'show_invite_members' + - data['track-label'] = local_assigns.fetch(:track_label) + + = dropdown_tag(data['dropdown-title'], options: options) do + %ul.dropdown-footer-list + %li + .js-invite-members-trigger{ data: { trigger_element: 'anchor', + display_text: _('Invite Members'), + event: 'click_invite_members', + label: data['track-label'] } } +- else + = dropdown_tag(data['dropdown-title'], options: options) diff --git a/ee/app/assets/javascripts/pages/projects/issues/show/index.js b/ee/app/assets/javascripts/pages/projects/issues/show/index.js index d27d99c822f61df3db3913b3c3855a9e8e6a0266..79992f0d7e3b4e4f62a58778fcbdf97f783b846a 100644 --- a/ee/app/assets/javascripts/pages/projects/issues/show/index.js +++ b/ee/app/assets/javascripts/pages/projects/issues/show/index.js @@ -1,4 +1,3 @@ -import trackShowInviteMemberLink from 'ee/projects/track_invite_members'; import initSidebarBundle from 'ee/sidebar/sidebar_bundle'; import initShow from '~/pages/projects/issues/show'; @@ -13,7 +12,3 @@ initRelatedIssues(); new UserCallout({ className: 'js-epics-sidebar-callout' }); // eslint-disable-next-line no-new new UserCallout({ className: 'js-weight-sidebar-callout' }); - -const assigneeDropdown = document.querySelector('.js-sidebar-assignee-dropdown'); - -if (assigneeDropdown) trackShowInviteMemberLink(assigneeDropdown); diff --git a/ee/app/assets/javascripts/pages/projects/merge_requests/show/index.js b/ee/app/assets/javascripts/pages/projects/merge_requests/show/index.js index a10946feebc8cd63c3ccf7737e49513204408db4..9b2235bd4e25d0a0377c5cf11ca279ba13e8b7ac 100644 --- a/ee/app/assets/javascripts/pages/projects/merge_requests/show/index.js +++ b/ee/app/assets/javascripts/pages/projects/merge_requests/show/index.js @@ -1,4 +1,3 @@ -import trackShowInviteMemberLink from 'ee/projects/track_invite_members'; import initSidebarBundle from 'ee/sidebar/sidebar_bundle'; import { initReviewBar } from '~/batch_comments'; import initMrNotes from '~/mr_notes'; @@ -11,7 +10,3 @@ initSidebarBundle(); initMrNotes(); initReviewBar(); initIssuableHeaderWarning(store); - -const assigneeDropdown = document.querySelector('.js-sidebar-assignee-dropdown'); - -if (assigneeDropdown) trackShowInviteMemberLink(assigneeDropdown); diff --git a/ee/app/assets/javascripts/projects/track_invite_members.js b/ee/app/assets/javascripts/projects/track_invite_members.js deleted file mode 100644 index 26766c6343cbf16355609bdd0198f12ca0ada248..0000000000000000000000000000000000000000 --- a/ee/app/assets/javascripts/projects/track_invite_members.js +++ /dev/null @@ -1,13 +0,0 @@ -import $ from 'jquery'; -import Tracking from '~/tracking'; - -export default function initTrackInviteMembers(assigneeDropdown) { - const trackLabel = 'edit_assignee'; - const { trackEvent } = assigneeDropdown.querySelector('.js-invite-members-track').dataset; - - $(assigneeDropdown).on('shown.bs.dropdown', () => { - Tracking.event(undefined, trackEvent, { - label: trackLabel, - }); - }); -} diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index d147476f1ab6b7393d54d6eb9fae15c9e49fd59f..1512504b93c0a06571aa9deb57257c6264c1d6b1 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -32,7 +32,7 @@ stub_feature_flags(issue_assignees_widget: false) end - include_examples 'issuable invite members experiments' do + include_examples 'issuable invite members' do let(:issuable_path) { project_issue_path(project, issue2) } end diff --git a/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb b/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb index 7d55a72c2b16c85c26956c6507830716d3a4e06b..1087be3d8c6283616ff87c94c8842a8f41ee3f21 100644 --- a/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb +++ b/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb @@ -68,14 +68,14 @@ end end - context 'with invite members experiment considerations' do + context 'with invite members considerations' do let_it_be(:user) { create(:user) } before do sign_in(user) end - include_examples 'issuable invite members experiments' do + include_examples 'issuable invite members' do let(:issuable_path) { project_merge_request_path(project, merge_request) } end end diff --git a/spec/features/merge_request/user_edits_reviewers_sidebar_spec.rb b/spec/features/merge_request/user_edits_reviewers_sidebar_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..45ee914de9d356b5d217110b25d46fe1b39bb599 --- /dev/null +++ b/spec/features/merge_request/user_edits_reviewers_sidebar_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Merge request > User edits reviewers sidebar', :js do + context 'with invite members considerations' do + let_it_be(:merge_request) { create(:merge_request) } + let_it_be(:project) { merge_request.project } + let_it_be(:user) { create(:user) } + + before do + sign_in(user) + end + + context 'when a privileged user can invite in reviewer dropdown' do + before do + project.add_maintainer(user) + end + + it 'shows a link for inviting members and launches invite modal' do + visit project_merge_request_path(project, merge_request) + + reviewer_edit_link.click + + wait_for_requests + + page.within '.dropdown-menu-user' do + expect(page).to have_link('Invite Members') + expect(page).to have_selector('[data-track-event="click_invite_members"]') + expect(page).to have_selector('[data-track-label="edit_reviewer"]') + end + + click_link 'Invite Members' + + expect(page).to have_content("You're inviting members to the") + end + end + + context 'when user cannot invite members in reviewer dropdown' do + before do + project.add_developer(user) + end + + it 'shows author in assignee dropdown and no invite link' do + visit project_merge_request_path(project, merge_request) + + reviewer_edit_link.click + + wait_for_requests + + page.within '.dropdown-menu-user' do + expect(page).not_to have_link('Invite Members') + end + end + end + + def reviewer_edit_link + find('.block.reviewer .edit-link') + end + end +end diff --git a/ee/spec/frontend/projects/track_invite_members_spec.js b/spec/frontend/sidebar/track_invite_members_spec.js similarity index 86% rename from ee/spec/frontend/projects/track_invite_members_spec.js rename to spec/frontend/sidebar/track_invite_members_spec.js index 06d6aa8dba053f69b51cfd19fb8645e4991e32d5..6c96e4cfc76e07b4b5d17a80e74cfe973118eab1 100644 --- a/ee/spec/frontend/projects/track_invite_members_spec.js +++ b/spec/frontend/sidebar/track_invite_members_spec.js @@ -1,6 +1,6 @@ import $ from 'jquery'; -import trackShowInviteMemberLink from 'ee/projects/track_invite_members'; import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; +import trackShowInviteMemberLink from '~/sidebar/track_invite_members'; describe('Track user dropdown open', () => { let trackingSpy; @@ -10,7 +10,7 @@ describe('Track user dropdown open', () => { document.body.innerHTML = ` <div id="dummy-wrapper-element"> <div class="js-sidebar-assignee-dropdown"> - <div class="js-invite-members-track" data-track-event="_track_event_"> + <div class="js-invite-members-track" data-track-event="_track_event_" data-track-label="_track_label_"> </div> </div> </div> @@ -31,7 +31,7 @@ describe('Track user dropdown open', () => { $(dropdownElement).trigger('shown.bs.dropdown'); expect(trackingSpy).toHaveBeenCalledWith(undefined, '_track_event_', { - label: 'edit_assignee', + label: '_track_label_', }); }); }); diff --git a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb index 736c353c2aac0dd2cd1e4d4eb14d2981f0f8803f..c0cfc27ceaf0b5ace044aaf76ef40c71f64c867e 100644 --- a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb +++ b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb @@ -1,9 +1,12 @@ # frozen_string_literal: true -RSpec.shared_examples 'issuable invite members experiments' do +RSpec.shared_examples 'issuable invite members' do context 'when a privileged user can invite' do - it 'shows a link for inviting members and launches invite modal' do + before do project.add_maintainer(user) + end + + it 'shows a link for inviting members and launches invite modal' do visit issuable_path find('.block.assignee .edit-link').click @@ -23,8 +26,11 @@ end context 'when user cannot invite members in assignee dropdown' do - it 'shows author in assignee dropdown and no invite link' do + before do project.add_developer(user) + end + + it 'shows author in assignee dropdown and no invite link' do visit issuable_path find('.block.assignee .edit-link').click