Commit 3e333282 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch...

Merge branch '326251-experiment-cleanup-invite_members_version_b-in-assignee-dropdown' into 'master'

Remove invite_members_version_b experiment [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!60426
parents 9d4f4b20 2c660711
<script>
import { GlModal, GlLink } from '@gitlab/ui';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { s__, __ } from '~/locale';
import { OPEN_MODAL, MODAL_ID } from '../constants';
import eventHub from '../event_hub';
export default {
cancelProps: {
text: __('Got it'),
attributes: [
{
variant: 'info',
},
],
},
modalId: MODAL_ID,
components: {
GlLink,
GlModal,
},
props: {
membersPath: {
type: String,
required: false,
default: '',
},
},
i18n: {
modalTitle: s__("InviteMember|Oops, this feature isn't ready yet"),
bodyTopMessage: s__(
"InviteMember|We're working to allow everyone to invite new members, making it easier for teams to get started with GitLab",
),
bodyMiddleMessage: s__(
'InviteMember|Until then, ask an owner to invite new project members for you',
),
linkText: s__('InviteMember|See who can invite members for you'),
},
mounted() {
eventHub.$on(OPEN_MODAL, this.openModal);
},
methods: {
openModal() {
this.$root.$emit(BV_SHOW_MODAL, MODAL_ID);
},
},
};
</script>
<template>
<gl-modal :modal-id="$options.modalId" size="sm" :action-cancel="$options.cancelProps">
<template #modal-title>
{{ $options.i18n.modalTitle }}
<gl-emoji
class="gl-vertical-align-baseline font-size-inherit gl-mr-1"
data-name="sweat_smile"
/>
</template>
<p>{{ $options.i18n.bodyTopMessage }}</p>
<p>{{ $options.i18n.bodyMiddleMessage }}</p>
<gl-link
:href="membersPath"
data-track-event="click_who_can_invite_link"
data-track-label="invite_members_message"
>{{ $options.i18n.linkText }}</gl-link
>
</gl-modal>
</template>
<script>
import { GlLink } from '@gitlab/ui';
import { OPEN_MODAL } from '../constants';
import eventHub from '../event_hub';
export default {
components: {
GlLink,
},
props: {
displayText: {
type: String,
required: false,
default: '',
},
event: {
type: String,
required: false,
default: '',
},
label: {
type: String,
required: false,
default: '',
},
},
methods: {
openModal() {
eventHub.$emit(OPEN_MODAL);
},
},
};
</script>
<template>
<gl-link
data-is-link="true"
:data-track-event="event"
:data-track-label="label"
@click="openModal"
>{{ displayText }}
</gl-link>
</template>
export const OPEN_MODAL = 'openModal';
export const MODAL_ID = 'invite-member-modal';
import createEventHub from '~/helpers/event_hub_factory';
export default createEventHub();
import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import { isInIssuePage, isInDesignPage } from '~/lib/utils/common_utils';
import InviteMemberModal from './components/invite_member_modal.vue';
Vue.use(GlToast);
const isAssigneesWidgetShown =
(isInIssuePage() || isInDesignPage()) && gon.features.issueAssigneesWidget;
export default function initInviteMembersModal() {
const el = document.querySelector('.js-invite-member-modal');
if (!el || isAssigneesWidgetShown) {
return false;
}
const { membersPath } = el.dataset;
return new Vue({
el,
render: (createElement) =>
createElement(InviteMemberModal, {
props: { membersPath },
}),
});
}
import Vue from 'vue';
import InviteMemberTrigger from './components/invite_member_trigger.vue';
export default function initInviteMembersTrigger() {
const el = document.querySelector('.js-invite-member-trigger');
if (!el) {
return false;
}
return new Vue({
el,
render: (createElement) =>
createElement(InviteMemberTrigger, {
props: { ...el.dataset },
}),
});
}
import loadAwardsHandler from '~/awards_handler';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import initIssuableSidebar from '~/init_issuable_sidebar';
import initInviteMemberModal from '~/invite_member/init_invite_member_modal';
import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { IssuableType } from '~/issuable_show/constants';
......@@ -58,7 +56,5 @@ export default function initShowIssue() {
} else {
loadAwardsHandler();
}
initInviteMemberModal();
initInviteMemberTrigger();
}
}
......@@ -3,8 +3,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 initInviteMemberModal from '~/invite_member/init_invite_member_modal';
import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { handleLocationHash } from '~/lib/utils/common_utils';
......@@ -28,8 +26,6 @@ export default function initMergeRequestShow() {
} else {
loadAwardsHandler();
}
initInviteMemberModal();
initInviteMemberTrigger();
initInviteMembersModal();
initInviteMembersTrigger();
......
......@@ -47,9 +47,6 @@ export default {
directlyInviteMembers: {
default: false,
},
indirectlyInviteMembers: {
default: false,
},
},
props: {
iid: {
......@@ -444,7 +441,7 @@ export default {
</template>
<template #footer>
<gl-dropdown-item>
<sidebar-invite-members v-if="directlyInviteMembers || indirectlyInviteMembers" />
<sidebar-invite-members v-if="directlyInviteMembers" />
</gl-dropdown-item>
</template>
</multi-select-dropdown>
......
<script>
import InviteMemberModal from '~/invite_member/components/invite_member_modal.vue';
import InviteMemberTrigger from '~/invite_member/components/invite_member_trigger.vue';
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import { __ } from '~/locale';
export default {
displayText: __('Invite members'),
dataTrackLabel: 'edit_assignee',
dataTrackEvent: 'click_invite_members',
components: {
InviteMemberTrigger,
InviteMemberModal,
InviteMembersTrigger,
},
inject: {
projectMembersPath: {
default: '',
},
directlyInviteMembers: {
default: false,
},
},
computed: {
trackEvent() {
return this.directlyInviteMembers ? 'click_invite_members' : 'click_invite_members_version_b';
},
},
};
</script>
<template>
<div>
<invite-members-trigger
v-if="directlyInviteMembers"
trigger-element="anchor"
:display-text="$options.displayText"
:event="trackEvent"
:label="$options.dataTrackLabel"
classes="gl-display-block gl-pl-6 gl-hover-text-decoration-none gl-hover-text-blue-800!"
/>
<template v-else>
<invite-member-trigger
:display-text="$options.displayText"
:event="trackEvent"
:label="$options.dataTrackLabel"
class="gl-display-block gl-pl-6 gl-hover-text-decoration-none gl-hover-text-blue-800!"
/>
<invite-member-modal :members-path="projectMembersPath" />
</template>
</div>
<invite-members-trigger
trigger-element="anchor"
:display-text="$options.displayText"
:event="$options.dataTrackEvent"
:label="$options.dataTrackLabel"
classes="gl-display-block gl-pl-6 gl-hover-text-decoration-none gl-hover-text-blue-800!"
/>
</template>
......@@ -86,7 +86,7 @@ function mountAssigneesComponent() {
if (!el) return;
const { id, iid, fullPath, editable, projectMembersPath } = getSidebarOptions();
const { id, iid, fullPath, editable } = getSidebarOptions();
// eslint-disable-next-line no-new
new Vue({
el,
......@@ -96,9 +96,7 @@ function mountAssigneesComponent() {
},
provide: {
canUpdate: editable,
projectMembersPath,
directlyInviteMembers: el.hasAttribute('data-directly-invite-members'),
indirectlyInviteMembers: el.hasAttribute('data-indirectly-invite-members'),
},
render: (createElement) =>
createElement('sidebar-assignees-widget', {
......
......@@ -56,8 +56,6 @@ class Projects::IssuesController < Projects::ApplicationController
push_frontend_feature_flag(:confidential_notes, @project, default_enabled: :yaml)
push_frontend_feature_flag(:issue_assignees_widget, @project, default_enabled: :yaml)
record_experiment_user(:invite_members_version_b)
experiment(:invite_members_in_comment, namespace: @project.root_ancestor) do |experiment_instance|
experiment_instance.exclude! unless helpers.can_import_members?
......
......@@ -47,8 +47,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:users_expanding_widgets_usage_data, @project, default_enabled: :yaml)
push_frontend_feature_flag(:diff_settings_usage_data, default_enabled: :yaml)
record_experiment_user(:invite_members_version_b)
experiment(:invite_members_in_comment, namespace: @project.root_ancestor) do |experiment_instance|
experiment_instance.exclude! unless helpers.can_import_members?
......
......@@ -17,20 +17,6 @@ module InviteMembersHelper
end
end
def indirectly_invite_members?
strong_memoize(:indirectly_invite_members) do
experiment_enabled?(:invite_members_version_b) && !can_import_members?
end
end
def show_invite_members_track_event
if directly_invite_members?
'show_invite_members'
elsif indirectly_invite_members?
'show_invite_members_version_b'
end
end
def invite_group_members?(group)
experiment_enabled?(:invite_members_empty_group_version_a) && Ability.allowed?(current_user, :admin_group_member, group)
end
......
......@@ -390,8 +390,7 @@ module IssuablesHelper
severity: issuable[:severity],
timeTrackingLimitToHours: Gitlab::CurrentSettings.time_tracking_limit_to_hours,
createNoteEmail: issuable[:create_note_email],
issuableType: issuable[:type],
projectMembersPath: project_project_members_path(@project, sort: :access_level_desc)
issuableType: issuable[:type]
}
end
......
- issuable_type = issuable_sidebar[:type]
- dropdown_options = assignees_dropdown_options(issuable_type)
#js-vue-sidebar-assignees{ data: { field: issuable_type, signed_in: signed_in, max_assignees: dropdown_options[:data][:"max-select"], directly_invite_members: directly_invite_members?, indirectly_invite_members: indirectly_invite_members? } }
#js-vue-sidebar-assignees{ data: { field: issuable_type,
signed_in: signed_in,
max_assignees: dropdown_options[:data][:"max-select"],
directly_invite_members: directly_invite_members? } }
.title.hide-collapsed
= _('Assignee')
= loading_icon(css_class: 'gl-vertical-align-text-bottom')
......@@ -39,12 +42,12 @@
- data['max-select'] = dropdown_options[:data][:'max-select'] if dropdown_options[:data][:'max-select']
- options[:data].merge!(data)
- if directly_invite_members? || indirectly_invite_members?
- 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_track_event
- data['track-event'] = 'show_invite_members'
- options[:data].merge!(data)
- invite_text = _('Invite Members')
- track_label = 'edit_assignee'
......@@ -52,15 +55,9 @@
= dropdown_tag(title, options: options) do
%ul.dropdown-footer-list
%li
- if directly_invite_members?
.js-invite-members-trigger{ data: { trigger_element: 'anchor',
display_text: invite_text,
event: 'click_invite_members',
label: track_label } }
- else
.js-invite-member-trigger{ data: { display_text: invite_text, event: 'click_invite_members_version_b', label: track_label } }
.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)
- if indirectly_invite_members?
.js-invite-member-modal{ data: { members_path: project_project_members_path(@project, sort: :access_level_desc) } }
---
title: Remove invite_members_version_b experiment
merge_request: 60426
author:
type: other
---
name: invite_members_version_b_experiment_percentage
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43900
rollout_issue_url: https://gitlab.com/gitlab-org/growth/team-tasks/-/issues/214
milestone: '13.5'
type: experiment
group: group::expansion
default_enabled: false
......@@ -34,10 +34,6 @@
module Gitlab
module Experimentation
EXPERIMENTS = {
invite_members_version_b: {
tracking_category: 'Growth::Expansion::Experiment::InviteMembersVersionB',
use_backwards_compatible_subject_index: true
},
invite_members_empty_group_version_a: {
tracking_category: 'Growth::Expansion::Experiment::InviteMembersEmptyGroupVersionA',
use_backwards_compatible_subject_index: true
......
......@@ -17865,24 +17865,12 @@ msgstr ""
msgid "InviteMember|Invited users will be added with developer level permissions. %{linkStart}View the documentation%{linkEnd} to see how to change this later."
msgstr ""
msgid "InviteMember|Oops, this feature isn't ready yet"
msgstr ""
msgid "InviteMember|See who can invite members for you"
msgstr ""
msgid "InviteMember|Send invitations"
msgstr ""
msgid "InviteMember|Skip this for now"
msgstr ""
msgid "InviteMember|Until then, ask an owner to invite new project members for you"
msgstr ""
msgid "InviteMember|We're working to allow everyone to invite new members, making it easier for teams to get started with GitLab"
msgstr ""
msgid "InviteReminderEmail|%{inviter} is still waiting for you to join GitLab"
msgstr ""
......
......@@ -130,30 +130,7 @@ RSpec.describe 'Issue Sidebar' do
end
end
context 'when invite_members_version_b experiment is enabled' do
before do
stub_experiment_for_subject(invite_members_version_b: true)
end
it 'shows a link for inviting members and follows through to modal' do
project.add_developer(user)
visit_issue(project, issue2)
open_assignees_dropdown
page.within '.dropdown-menu-user' do
expect(page).to have_link('Invite members', href: '#')
expect(page).to have_selector('[data-track-event="click_invite_members_version_b"]')
expect(page).to have_selector('[data-track-label="edit_assignee"]')
end
click_link 'Invite members'
expect(page).to have_content("Oops, this feature isn't ready yet")
end
end
context 'when invite_members_version_b experiment is disabled' do
context 'when user cannot invite members in assignee dropdown' do
it 'shows author in assignee dropdown and no invite link' do
project.add_developer(user)
visit_issue(project, issue2)
......
import { GlLink, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper';
import InviteMemberModal from '~/invite_member/components/invite_member_modal.vue';
const memberPath = 'member_path';
const GlEmoji = { template: '<img />' };
const createComponent = () => {
return shallowMount(InviteMemberModal, {
propsData: {
membersPath: memberPath,
},
stubs: {
GlEmoji,
GlModal: stubComponent(GlModal, {
template: '<div><slot name="modal-title"></slot><slot></slot></div>',
}),
},
});
};
describe('InviteMemberModal', () => {
let wrapper;
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findLink = () => wrapper.find(GlLink);
describe('rendering the modal', () => {
it('renders the modal with the correct title', () => {
expect(wrapper.text()).toContain("Oops, this feature isn't ready yet");
});
describe('rendering the see who link', () => {
it('renders the correct link', () => {
expect(findLink().attributes('href')).toBe(memberPath);
});
});
});
describe('tracking', () => {
let trackingSpy;
afterEach(() => {
unmockTracking();
});
it('send an event when go to pipelines is clicked', () => {
trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
triggerEvent(findLink().element);
expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_who_can_invite_link', {
label: 'invite_members_message',
});
});
});
});
const triggerProvides = {
displayText: 'Invite member',
event: 'click_invite_members_version_b',
label: 'edit_assignee',
};
export default triggerProvides;
import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper';
import InviteMemberTrigger from '~/invite_member/components/invite_member_trigger.vue';
import triggerProvides from './invite_member_trigger_mock_data';
const createComponent = () => {
return shallowMount(InviteMemberTrigger, { propsData: triggerProvides });
};
describe('InviteMemberTrigger', () => {
let wrapper;
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findLink = () => wrapper.find(GlLink);
describe('displayText', () => {
it('includes the correct displayText for the link', () => {
expect(findLink().text()).toBe(triggerProvides.displayText);
});
});
describe('tracking', () => {
let trackingSpy;
afterEach(() => {
unmockTracking();
});
it('send an event when go to pipelines is clicked', () => {
trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
triggerEvent(findLink().element);
expect(trackingSpy).toHaveBeenCalledWith('_category_', triggerProvides.event, {
label: triggerProvides.label,
});
});
});
});
......@@ -533,7 +533,7 @@ describe('Sidebar assignees widget', () => {
expect(findInviteMembersLink().exists()).toBe(false);
});
it('does not render invite members link if `directlyInviteMembers` and `indirectlyInviteMembers` were not passed', async () => {
it('does not render invite members link if `directlyInviteMembers` was not passed', async () => {
createComponent();
await waitForPromises();
expect(findInviteMembersLink().exists()).toBe(false);
......@@ -548,14 +548,4 @@ describe('Sidebar assignees widget', () => {
await waitForPromises();
expect(findInviteMembersLink().exists()).toBe(true);
});
it('renders invite members link if `indirectlyInviteMembers` is true', async () => {
createComponent({
provide: {
indirectlyInviteMembers: true,
},
});
await waitForPromises();
expect(findInviteMembersLink().exists()).toBe(true);
});
});
import { shallowMount } from '@vue/test-utils';
import InviteMemberModal from '~/invite_member/components/invite_member_modal.vue';
import InviteMemberTrigger from '~/invite_member/components/invite_member_trigger.vue';
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import SidebarInviteMembers from '~/sidebar/components/assignees/sidebar_invite_members.vue';
const testProjectMembersPath = 'test-path';
describe('Sidebar invite members component', () => {
let wrapper;
const findDirectInviteLink = () => wrapper.findComponent(InviteMembersTrigger);
const findIndirectInviteLink = () => wrapper.findComponent(InviteMemberTrigger);
const findInviteModal = () => wrapper.findComponent(InviteMemberModal);
const createComponent = ({ directlyInviteMembers = false } = {}) => {
wrapper = shallowMount(SidebarInviteMembers, {
provide: {
directlyInviteMembers,
projectMembersPath: testProjectMembersPath,
},
});
const createComponent = () => {
wrapper = shallowMount(SidebarInviteMembers);
};
afterEach(() => {
......@@ -28,32 +17,11 @@ describe('Sidebar invite members component', () => {
describe('when directly inviting members', () => {
beforeEach(() => {
createComponent({ directlyInviteMembers: true });
createComponent();
});
it('renders a direct link to project members path', () => {
expect(findDirectInviteLink().exists()).toBe(true);
});
it('does not render invite members trigger and modal components', () => {
expect(findIndirectInviteLink().exists()).toBe(false);
expect(findInviteModal().exists()).toBe(false);
});
});
describe('when indirectly inviting members', () => {
beforeEach(() => {
createComponent();
});
it('does not render a direct link to project members path', () => {
expect(findDirectInviteLink().exists()).toBe(false);
});
it('does not render invite members trigger and modal components', () => {
expect(findIndirectInviteLink().exists()).toBe(true);
expect(findInviteModal().exists()).toBe(true);
expect(findInviteModal().props('membersPath')).toBe(testProjectMembersPath);
});
});
});
......@@ -12,21 +12,6 @@ RSpec.describe InviteMembersHelper do
helper.extend(Gitlab::Experimentation::ControllerConcern)
end
describe '#show_invite_members_track_event' do
it 'shows values when can directly invite members' do
allow(helper).to receive(:directly_invite_members?).and_return(true)
expect(helper.show_invite_members_track_event).to eq 'show_invite_members'
end
it 'shows values when can indirectly invite members' do
allow(helper).to receive(:directly_invite_members?).and_return(false)
allow(helper).to receive(:indirectly_invite_members?).and_return(true)
expect(helper.show_invite_members_track_event).to eq 'show_invite_members_version_b'
end
end
context 'with project' do
before do
assign(:project, project)
......@@ -87,38 +72,6 @@ RSpec.describe InviteMembersHelper do
end
end
end
describe "#indirectly_invite_members?" do
context 'when a user is a developer' do
before do
allow(helper).to receive(:current_user) { developer }
end
it 'returns false' do
allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_b) { false }
expect(helper.indirectly_invite_members?).to eq false
end
it 'returns true' do
allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_b) { true }
expect(helper.indirectly_invite_members?).to eq true
end
end
context 'when a user is an owner' do
before do
allow(helper).to receive(:current_user) { owner }
end
it 'returns false' do
allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_b) { true }
expect(helper.indirectly_invite_members?).to eq false
end
end
end
end
context 'with group' do
......
......@@ -7,7 +7,6 @@ require 'spec_helper'
RSpec.describe Gitlab::Experimentation::EXPERIMENTS do
it 'temporarily ensures we know what experiments exist for backwards compatibility' do
expected_experiment_keys = [
:invite_members_version_b,
:invite_members_empty_group_version_a,
:contact_sales_btn_in_app
]
......
......@@ -22,32 +22,7 @@ RSpec.shared_examples 'issuable invite members experiments' do
end
end
context 'when invite_members_version_b experiment is enabled' do
before do
stub_experiment_for_subject(invite_members_version_b: true)
end
it 'shows a link for inviting members and follows through to modal' do
project.add_developer(user)
visit issuable_path
find('.block.assignee .edit-link').click
wait_for_requests
page.within '.dropdown-menu-user' do
expect(page).to have_link('Invite Members', href: '#')
expect(page).to have_selector('[data-track-event="click_invite_members_version_b"]')
expect(page).to have_selector('[data-track-label="edit_assignee"]')
end
click_link 'Invite Members'
expect(page).to have_content("Oops, this feature isn't ready yet")
end
end
context 'when invite_members_version_b experiment is disabled' do
context 'when user cannot invite members in assignee dropdown' do
it 'shows author in assignee dropdown and no invite link' do
project.add_developer(user)
visit issuable_path
......
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