Commit 56bace8b authored by Doug Stull's avatar Doug Stull Committed by Rémy Coutable

Add invite for group to registration

- experiment to see if we get more acceptance.
parent c012265e
...@@ -58,6 +58,7 @@ Rails.application.routes.draw do ...@@ -58,6 +58,7 @@ Rails.application.routes.draw do
Gitlab.ee do Gitlab.ee do
resources :groups, only: [:new, :create] resources :groups, only: [:new, :create]
resources :group_invites, only: [:new, :create]
resources :projects, only: [:new, :create] resources :projects, only: [:new, :create]
end end
end end
......
...@@ -19,10 +19,35 @@ export default { ...@@ -19,10 +19,35 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
initialEmailInputs: {
type: Number,
required: false,
default: 1,
},
emailPlaceholderPrefix: {
type: String,
required: false,
default: 'member',
},
addAnotherText: {
type: String,
required: false,
default: s__('InviteMember|Invite another member'),
},
inviteLabel: {
type: String,
required: false,
default: '',
},
inputName: {
type: String,
required: false,
default: 'group[emails][]',
},
}, },
data() { data() {
return { return {
numberOfInputs: Math.max(this.emails.length, 1), numberOfInputs: Math.max(this.emails.length, this.initialEmailInputs),
}; };
}, },
methods: { methods: {
...@@ -36,7 +61,9 @@ export default { ...@@ -36,7 +61,9 @@ export default {
return sprintf(this.$options.i18n.emailLabel, { number }); return sprintf(this.$options.i18n.emailLabel, { number });
}, },
emailPlaceholder(number) { emailPlaceholder(number) {
return sprintf(this.$options.i18n.emailPlaceholder, { number }); const emailPrefix = this.emailPlaceholderPrefix + number;
return sprintf(this.$options.i18n.emailPlaceholder, { emailPrefix });
}, },
emailID(number) { emailID(number) {
return `email-${number}`; return `email-${number}`;
...@@ -48,14 +75,14 @@ export default { ...@@ -48,14 +75,14 @@ export default {
'InviteMember|Invited users will be added with developer level permissions. %{linkStart}View the documentation%{linkEnd} to see how to change this later.', 'InviteMember|Invited users will be added with developer level permissions. %{linkStart}View the documentation%{linkEnd} to see how to change this later.',
), ),
emailLabel: __('Email %{number}'), emailLabel: __('Email %{number}'),
emailPlaceholder: __('member%{number}@company.com'), emailPlaceholder: __('%{emailPrefix}@company.com'),
inviteAnother: s__('InviteMember|Invite another member'), inviteAnother: s__('InviteMember|Invite another member'),
}, },
}; };
</script> </script>
<template> <template>
<div class="gl-mb-6"> <div class="gl-mb-6">
<gl-form-group :label="$options.i18n.inviteMembersLabel"> <gl-form-group :label="inviteLabel" data-testid="no-input-form-group">
<template #description> <template #description>
<gl-sprintf :message="$options.i18n.inviteMembersDescription"> <gl-sprintf :message="$options.i18n.inviteMembersDescription">
<template #link="{ content }"> <template #link="{ content }">
...@@ -75,11 +102,11 @@ export default { ...@@ -75,11 +102,11 @@ export default {
<gl-form-input <gl-form-input
:id="emailID(number)" :id="emailID(number)"
:ref="emailID(number)" :ref="emailID(number)"
name="group[emails][]" :name="inputName"
:placeholder="emailPlaceholder(number)" :placeholder="emailPlaceholder(number)"
:value="emails[index]" :value="emails[index]"
/> />
</gl-form-group> </gl-form-group>
<gl-button icon="plus" @click="addInput">{{ $options.i18n.inviteAnother }}</gl-button> <gl-button icon="plus" @click="addInput">{{ addAnotherText }}</gl-button>
</div> </div>
</template> </template>
import Vue from 'vue'; import Vue from 'vue';
import { s__ } from '~/locale';
import InviteMembers from './components/invite_members.vue'; import InviteMembers from './components/invite_members.vue';
export default () => { export default () => {
...@@ -9,6 +10,7 @@ export default () => { ...@@ -9,6 +10,7 @@ export default () => {
} }
const { emails, docsPath } = el.dataset; const { emails, docsPath } = el.dataset;
const inviteLabel = s__('InviteMember|Invite Members (optional)');
return new Vue({ return new Vue({
el, el,
...@@ -17,6 +19,7 @@ export default () => { ...@@ -17,6 +19,7 @@ export default () => {
props: { props: {
emails: JSON.parse(emails), emails: JSON.parse(emails),
docsPath, docsPath,
inviteLabel,
}, },
}); });
}, },
......
import initGroupInvites from 'ee/registrations/group_invites/new';
initGroupInvites();
<script>
import { GlForm, GlButton, GlCard } from '@gitlab/ui';
import InviteMembers from 'ee/groups/components/invite_members.vue';
import csrf from '~/lib/utils/csrf';
import { s__ } from '~/locale';
export default {
components: {
GlCard,
GlForm,
GlButton,
InviteMembers,
},
inject: {
endpoint: {
default: '',
},
},
props: {
docsPath: {
type: String,
required: true,
},
emails: {
type: Array,
required: true,
},
},
i18n: {
inviteAnother: s__('InviteMember|Invite another teammate'),
sendInvitations: s__('InviteMember|Send invitations'),
},
csrf,
};
</script>
<template>
<gl-card>
<gl-form ref="form" :action="endpoint" method="post">
<input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
<invite-members
:docs-path="docsPath"
:emails="emails"
:initial-email-inputs="3"
email-placeholder-prefix="teammate"
:add-another-text="$options.i18n.inviteAnother"
input-name="emails[]"
/>
<gl-button type="submit" variant="success" class="gl-w-full!">
{{ $options.i18n.sendInvitations }}
</gl-button>
</gl-form>
</gl-card>
</template>
import Vue from 'vue';
import inviteMembersForm from '../../components/invite_members_form.vue';
import ProgressBar from '../../components/progress_bar.vue';
import { STEPS, SIGNUP_ONBOARDING_FLOW_STEPS } from '../../constants';
function loadProgressBar() {
const el = document.getElementById('progress-bar');
if (!el) {
return null;
}
return new Vue({
el,
render(createElement) {
return createElement(ProgressBar, {
props: { steps: SIGNUP_ONBOARDING_FLOW_STEPS, currentStep: STEPS.yourGroup },
});
},
});
}
function loadInviteMembersForm() {
const el = document.querySelector('.js-invite-group-members');
if (!el) {
return null;
}
const { endpoint, emails, docsPath } = el.dataset;
return new Vue({
el,
provide: { endpoint },
render(createElement) {
return createElement(inviteMembersForm, {
props: {
emails: JSON.parse(emails),
docsPath,
},
});
},
});
}
export default () => {
loadProgressBar();
loadInviteMembersForm();
};
...@@ -168,14 +168,18 @@ $subscriptions-full-width-lg: 541px; ...@@ -168,14 +168,18 @@ $subscriptions-full-width-lg: 541px;
} }
} }
.edit-group, @mixin default-widths {
.edit-profile,
.new-project {
max-width: 460px; max-width: 460px;
.bar { .bar {
width: 100%; width: 100%;
} }
}
.edit-group,
.edit-profile,
.new-project {
@include default-widths;
.normal { .normal {
font-weight: $gl-font-weight-normal; font-weight: $gl-font-weight-normal;
...@@ -201,4 +205,8 @@ $subscriptions-full-width-lg: 541px; ...@@ -201,4 +205,8 @@ $subscriptions-full-width-lg: 541px;
} }
} }
} }
.group-invites {
@include default-widths;
}
} }
# frozen_string_literal: true
module Registrations
class GroupInvitesController < Groups::ApplicationController
layout 'checkout'
before_action :authorize_invite_to_group!
feature_category :navigation
def new
end
def create
result = Members::CreateService.new(current_user, invite_params).execute(group)
if result[:status] == :success
experiment(:registrations_group_invite, actor: :user)
.track(:invites_sent, property: group.id.to_s, value: group.members.invite.size)
end
redirect_to new_users_sign_up_project_path(namespace_id: group.id,
trial: helpers.in_trial_during_signup_flow?,
trial_onboarding_flow: helpers.in_trial_onboarding_flow?,
hide_trial_activation_banner: true)
end
private
def authorize_invite_to_group!
access_denied! unless can?(current_user, :admin_group_member, group)
end
def group
@group ||= Group.find(params[:group_id])
end
def invite_params
{
user_ids: emails_param[:emails]&.reject(&:blank?)&.join(','),
access_level: Gitlab::Access::DEVELOPER
}
end
def emails_param
params.permit(emails: [])
end
end
end
...@@ -63,7 +63,11 @@ module Registrations ...@@ -63,7 +63,11 @@ module Registrations
record_experiment_conversion_event(:trial_registration_with_social_signin) record_experiment_conversion_event(:trial_registration_with_social_signin)
record_experiment_conversion_event(:trial_onboarding_issues) record_experiment_conversion_event(:trial_onboarding_issues)
redirect_to new_users_sign_up_project_path(namespace_id: @group.id, trial: helpers.in_trial_during_signup_flow?, trial_onboarding_flow: true) experiment(:registrations_group_invite, actor: :user) do |experiment_instance|
experiment_instance.use { redirect_to new_users_sign_up_project_path(namespace_id: @group.id, trial: helpers.in_trial_during_signup_flow?, trial_onboarding_flow: true) } # control
experiment_instance.try(:invite_page) { redirect_to new_users_sign_up_group_invite_path(group_id: @group.id, trial: helpers.in_trial_during_signup_flow?, trial_onboarding_flow: true) } # with separate page
experiment_instance.track(:created, property: @group.id.to_s)
end
else else
render action: :new render action: :new
end end
...@@ -75,7 +79,11 @@ module Registrations ...@@ -75,7 +79,11 @@ module Registrations
if experiment_enabled?(:trial_during_signup) if experiment_enabled?(:trial_during_signup)
trial_during_signup_flow trial_during_signup_flow
else else
invite_on_create experiment(:registrations_group_invite, actor: :user) do |experiment_instance|
experiment_instance.use { invite_on_create } # control
experiment_instance.try(:invite_page) { redirect_to new_users_sign_up_group_invite_path(group_id: @group.id, trial: helpers.in_trial_during_signup_flow?) } # with separate page
experiment_instance.track(:created, property: @group.id.to_s)
end
end end
end end
...@@ -89,7 +97,11 @@ module Registrations ...@@ -89,7 +97,11 @@ module Registrations
if helpers.in_trial_during_signup_flow? if helpers.in_trial_during_signup_flow?
create_lead_and_apply_trial_flow create_lead_and_apply_trial_flow
else else
redirect_to new_users_sign_up_project_path(namespace_id: @group.id, trial: helpers.in_trial_during_signup_flow?) experiment(:registrations_group_invite, actor: :user) do |experiment_instance|
experiment_instance.use { redirect_to new_users_sign_up_project_path(namespace_id: @group.id, trial: helpers.in_trial_during_signup_flow?) } # control
experiment_instance.try(:invite_page) { redirect_to new_users_sign_up_group_invite_path(group_id: @group.id, trial: helpers.in_trial_during_signup_flow?) } # with separate page
experiment_instance.track(:created, property: @group.id.to_s)
end
end end
end end
...@@ -97,7 +109,11 @@ module Registrations ...@@ -97,7 +109,11 @@ module Registrations
if create_lead && apply_trial if create_lead && apply_trial
record_experiment_conversion_event(:trial_during_signup) record_experiment_conversion_event(:trial_during_signup)
redirect_to new_users_sign_up_project_path(namespace_id: @group.id, trial: helpers.in_trial_during_signup_flow?) experiment(:registrations_group_invite, actor: :user) do |experiment_instance|
experiment_instance.use { redirect_to new_users_sign_up_project_path(namespace_id: @group.id, trial: helpers.in_trial_during_signup_flow?) } # control
experiment_instance.try(:invite_page) { redirect_to new_users_sign_up_group_invite_path(group_id: @group.id, trial: helpers.in_trial_during_signup_flow?) } # with separate page
experiment_instance.track(:created, property: @group.id.to_s)
end
else else
render action: :new render action: :new
end end
......
...@@ -35,6 +35,9 @@ module Registrations ...@@ -35,6 +35,9 @@ module Registrations
record_experiment_user(:trial_onboarding_issues, trial_onboarding_context) record_experiment_user(:trial_onboarding_issues, trial_onboarding_context)
record_experiment_conversion_event(:trial_onboarding_issues) record_experiment_conversion_event(:trial_onboarding_issues)
experiment(:registrations_group_invite, actor: @project.group)
.track(:signup_successful, property: @project.namespace_id)
redirect_to trial_getting_started_users_sign_up_welcome_path(learn_gitlab_project_id: learn_gitlab_project.id) redirect_to trial_getting_started_users_sign_up_welcome_path(learn_gitlab_project_id: learn_gitlab_project.id)
else else
redirect_to users_sign_up_experience_level_path(namespace_path: @project.namespace) redirect_to users_sign_up_experience_level_path(namespace_path: @project.namespace)
......
...@@ -20,6 +20,10 @@ module EE ...@@ -20,6 +20,10 @@ module EE
params[:trial] == 'true' params[:trial] == 'true'
end end
def already_showed_trial_activation?
params[:hide_trial_activation_banner] == 'true'
end
def in_invitation_flow? def in_invitation_flow?
redirect_path.present? && redirect_path.starts_with?('/-/invites/') redirect_path.present? && redirect_path.starts_with?('/-/invites/')
end end
......
- page_title s_('InviteMember|Invite teammates to your GitLab group')
- if in_trial_during_signup_flow? || in_trial_onboarding_flow?
.row
.gl-display-flex.gl-flex-direction-column.gl-align-items-center.gl-w-full.gl-mt-3
= render 'registrations/trial_is_activated_banner'
.row.gl-flex-grow-1
.gl-display-flex.gl-flex-direction-column.gl-align-items-center.gl-w-full.p-3.gl-bg-gray-10
.group-invites.gl-display-flex.gl-flex-direction-column.gl-align-items-center.pt-5
#progress-bar
%h2.gl-text-center= s_('InviteMember|Invite your teammates')
%p.gl-text-center= s_('InviteMember|Invite users to your group %{group_name} so you can collaborate on your projects') % { group_name: @group.name }
.js-invite-group-members{ data: { emails: params.dig(:group, :emails) || [],
docs_path: help_page_path('user/permissions'),
endpoint: users_sign_up_group_invites_path(group_id: @group.id, trial: params[:trial], trial_onboarding_flow: params[:trial_onboarding_flow])} }
= link_to s_('InviteMember|Skip this for now'), new_users_sign_up_project_path(namespace_id: @group.id, trial: params[:trial], trial_onboarding_flow: params[:trial_onboarding_flow], hide_trial_activation_banner: true), class: "gl-mt-7"
%p.gl-mt-3= s_("InviteMember|Don't worry, you can always invite teammates later")
...@@ -44,7 +44,10 @@ ...@@ -44,7 +44,10 @@
- if !in_trial_onboarding_flow? && experiment_enabled?(:trial_during_signup) - if !in_trial_onboarding_flow? && experiment_enabled?(:trial_during_signup)
= render partial: 'shared/groups/trial_form' = render partial: 'shared/groups/trial_form'
- else - else
- experiment(:registrations_group_invite, actor: :user) do |experiment_instance|
- experiment_instance.use do
= render partial: 'shared/groups/invite_members' = render partial: 'shared/groups/invite_members'
- experiment_instance.try(:invite_page) {}
.row .row
.form-group.col-sm-12.mb-0 .form-group.col-sm-12.mb-0
= button_tag class: %w[btn gl-button btn-success w-100] do = button_tag class: %w[btn gl-button btn-success w-100] do
......
- page_title _('Your first project') - page_title _('Your first project')
- visibility_level = selected_visibility_level(@project, params.dig(:project, :visibility_level)) - visibility_level = selected_visibility_level(@project, params.dig(:project, :visibility_level))
- if in_trial_during_signup_flow? || in_trial_onboarding_flow? - if !already_showed_trial_activation? && (in_trial_during_signup_flow? || in_trial_onboarding_flow?)
.row .row
.d-flex.flex-column.align-items-center.w-100.gl-mt-3 .d-flex.flex-column.align-items-center.w-100.gl-mt-3
= render 'trial_is_activated_banner' = render 'registrations/trial_is_activated_banner'
.row.gl-flex-grow-1 .row.gl-flex-grow-1
.d-flex.flex-column.align-items-center.w-100.p-3 .d-flex.flex-column.align-items-center.w-100.p-3
.new-project.d-flex.flex-column.align-items-center.pt-5 .new-project.d-flex.flex-column.align-items-center.pt-5
......
---
name: registrations_group_invite
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52371
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/219544
milestone: '13.9'
type: experiment
group: group::expansion
default_enabled: false
...@@ -149,8 +149,22 @@ RSpec.describe Registrations::GroupsController do ...@@ -149,8 +149,22 @@ RSpec.describe Registrations::GroupsController do
expect(controller).to receive(:record_experiment_conversion_event).with(:trial_onboarding_issues) expect(controller).to receive(:record_experiment_conversion_event).with(:trial_onboarding_issues)
end end
context 'when registration_group_invite experiment is enabled' do
before do
stub_experiments(registrations_group_invite: :invite_page)
end
it { is_expected.to redirect_to(new_users_sign_up_group_invite_path(group_id: group.id, trial: false, trial_onboarding_flow: true)) }
end
context 'when registration_group_invite experiment is disabled' do
before do
stub_experiments(registrations_group_invite: :control)
end
it { is_expected.to redirect_to(new_users_sign_up_project_path(namespace_id: group.id, trial: false, trial_onboarding_flow: true)) } it { is_expected.to redirect_to(new_users_sign_up_project_path(namespace_id: group.id, trial: false, trial_onboarding_flow: true)) }
end end
end
context 'when failing to apply trial' do context 'when failing to apply trial' do
before do before do
...@@ -240,8 +254,33 @@ RSpec.describe Registrations::GroupsController do ...@@ -240,8 +254,33 @@ RSpec.describe Registrations::GroupsController do
end end
end end
context 'with registrations_group_invite experiment' do
it 'tracks experiment as expected', :experiment do
expect(experiment(:registrations_group_invite))
.to track(:created, { property: group.id.to_s })
.on_any_instance
.with_context(actor: :user)
subject
end
context 'when registrations_group_invite invite_page path is taken' do
before do
stub_experiments(registrations_group_invite: :invite_page)
end
it { is_expected.to redirect_to(new_users_sign_up_group_invite_path(group_id: group.id, trial: true)) }
end
context 'when registrations_group_invite experiment control path is taken' do
before do
stub_experiments(registrations_group_invite: :control)
end
it { is_expected.to redirect_to(new_users_sign_up_project_path(namespace_id: group.id, trial: true)) } it { is_expected.to redirect_to(new_users_sign_up_project_path(namespace_id: group.id, trial: true)) }
end end
end
end
context 'when failing to create a lead and apply trial' do context 'when failing to create a lead and apply trial' do
before do before do
...@@ -277,9 +316,38 @@ RSpec.describe Registrations::GroupsController do ...@@ -277,9 +316,38 @@ RSpec.describe Registrations::GroupsController do
subject subject
end end
context 'with registrations_group_invite experiment' do
it 'tracks experiment as expected', :experiment do
expect_next_instance_of(Groups::CreateService) do |service|
expect(service).to receive(:execute).and_return(group)
end
expect(experiment(:registrations_group_invite))
.to track(:created, { property: group.id.to_s })
.on_any_instance
.with_context(actor: :user)
subject
end
context 'when registrations_group_invite invite_page path is taken' do
before do
stub_experiments(registrations_group_invite: :invite_page)
end
it { is_expected.to redirect_to(new_users_sign_up_group_invite_path(group_id: user.groups.last.id, trial: false)) }
end
context 'when registrations_group_invite experiment control path is taken' do
before do
stub_experiments(registrations_group_invite: :control)
end
it { is_expected.to redirect_to(new_users_sign_up_project_path(namespace_id: user.groups.last.id, trial: false)) } it { is_expected.to redirect_to(new_users_sign_up_project_path(namespace_id: user.groups.last.id, trial: false)) }
end end
end end
end
end
context 'when not in experiment group for trial_during_signup' do context 'when not in experiment group for trial_during_signup' do
before do before do
...@@ -290,11 +358,27 @@ RSpec.describe Registrations::GroupsController do ...@@ -290,11 +358,27 @@ RSpec.describe Registrations::GroupsController do
expect_next_instance_of(Groups::CreateService) do |service| expect_next_instance_of(Groups::CreateService) do |service|
expect(service).to receive(:execute).and_return(group) expect(service).to receive(:execute).and_return(group)
end end
expect(experiment(:registrations_group_invite))
.to track(:created, { property: group.id.to_s })
.on_any_instance
.with_context(actor: :user)
subject subject
end end
context 'with invite on group creation' do context 'when registrations_group_invite invite_page path is taken' do
before do
stub_experiments(registrations_group_invite: :invite_page)
end
it { is_expected.to redirect_to(new_users_sign_up_group_invite_path(group_id: user.groups.last.id, trial: false)) }
end
context 'when registrations_group_invite experiment control path is taken' do
before do
stub_experiments(registrations_group_invite: :control)
end
it { is_expected.to redirect_to(new_users_sign_up_project_path(namespace_id: user.groups.last.id, trial: false)) } it { is_expected.to redirect_to(new_users_sign_up_project_path(namespace_id: user.groups.last.id, trial: false)) }
it_behaves_like GroupInviteMembers it_behaves_like GroupInviteMembers
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'User is able to invite members to group during signup', :js, :experiment do
include Select2Helper
let_it_be(:user) { create(:user) }
let(:trial_during_signup) { true }
let(:path_params) { {} }
before do
allow(Gitlab).to receive(:dev_env_or_com?).and_return(true)
stub_experiment(trial_during_signup: trial_during_signup)
stub_experiment_for_subject(trial_during_signup: trial_during_signup)
stub_experiments(registrations_group_invite: :invite_page)
sign_in(user)
end
context 'when all feature flags are enabled in group creation' do
it 'shows and allows inviting of users on separate screen' do
invite_email = 'bob@example.com'
create_group_through_form
expect_group_invites_page
fill_in 'Email 1', with: invite_email
click_on 'Send invitations'
aggregate_failures do
expect(page).to have_content('Create/import your first project')
expect(Member.last.invite_email).to eq invite_email
end
end
it 'allows skipping inviting members' do
create_group_through_form
expect_group_invites_page
click_on 'Skip this for now'
expect(page).to have_content('Create/import your first project')
end
end
context 'when trial during signup is not enabled' do
let(:trial_during_signup) { false }
it 'validates group invites are displayed as separate page' do
create_group_through_form
expect_group_invites_page
end
end
context 'when in trial_onboarding_flow' do
let(:path_params) { { trial_onboarding_flow: true } }
it 'validates group invites are displayed as separate page' do
expect_next_instance_of(GitlabSubscriptions::ApplyTrialService) do |service|
expect(service).to receive(:execute).and_return({ success: true })
end
create_group_through_form
expect_group_invites_page
end
end
context 'when in trial_during_signup_flow' do
let(:path_params) { { trial: true } }
it 'validates group invites are displayed as separate page', :aggregate_failures do
expect_next_instance_of(GitlabSubscriptions::CreateLeadService) do |service|
expect(service).to receive(:execute).and_return(success: true)
end
expect_next_instance_of(GitlabSubscriptions::ApplyTrialService) do |service|
expect(service).to receive(:execute).and_return({ success: true })
end
create_group_for_trial
expect_group_invites_page
expect_group_invites_with_trial_activation
end
end
def create_group_for_trial
visit new_users_sign_up_group_path(path_params)
fill_in 'group_name', with: 'test'
fill_in 'company_name', with: 'GitLab'
select2 '1-99', from: '#company_size'
fill_in 'number_of_users', with: '1'
fill_in 'phone_number', with: '+1234567890'
select2 'US', from: '#country_select'
click_on 'Create group'
end
def create_group_through_form
visit new_users_sign_up_group_path(path_params)
fill_in 'group_name', with: 'test'
click_on 'Create group'
end
def expect_group_invites_page
expect(page).to have_content('Invite your teammates')
end
def expect_group_invites_with_trial_activation
expect_group_invites_page
expect(page).to have_content('Congratulations, your free trial is activated.')
end
end
...@@ -10,6 +10,7 @@ RSpec.describe 'User sees new onboarding flow', :js do ...@@ -10,6 +10,7 @@ RSpec.describe 'User sees new onboarding flow', :js do
allow(Gitlab).to receive(:com?).and_return(true) allow(Gitlab).to receive(:com?).and_return(true)
stub_experiment(trial_during_signup: true) stub_experiment(trial_during_signup: true)
stub_experiment_for_subject(trial_during_signup: true) stub_experiment_for_subject(trial_during_signup: true)
stub_feature_flags(registrations_group_invite: false)
sign_in(user) sign_in(user)
visit users_sign_up_welcome_path visit users_sign_up_welcome_path
......
...@@ -5,6 +5,7 @@ require 'spec_helper' ...@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe 'User sees new onboarding flow', :js do RSpec.describe 'User sees new onboarding flow', :js do
before do before do
stub_const('Gitlab::QueryLimiting::Transaction::THRESHOLD', 200) stub_const('Gitlab::QueryLimiting::Transaction::THRESHOLD', 200)
stub_feature_flags(registration_group_invite: false)
allow(Gitlab).to receive(:com?).and_return(true) allow(Gitlab).to receive(:com?).and_return(true)
gitlab_sign_in(:user) gitlab_sign_in(:user)
visit users_sign_up_welcome_path visit users_sign_up_welcome_path
......
import { GlFormInput, GlButton } from '@gitlab/ui'; import { GlFormInput, GlButton } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils'; import { shallowMount, mount } from '@vue/test-utils';
import Component from 'ee/groups/components/invite_members.vue'; import Component from 'ee/groups/components/invite_members.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
describe('User invites', () => { describe('User invites', () => {
let wrapper; let wrapper;
const createComponent = (propsData) => { const createComponent = (propsData = {}) => {
wrapper = shallowMount(Component, { wrapper = extendedWrapper(
propsData, shallowMount(Component, {
}); propsData: {
emails: [],
docsPath: 'https://some.doc.path',
...propsData,
},
}),
);
}; };
beforeEach(() => { const inputs = () => wrapper.findAll(GlFormInput);
createComponent({ emails: [], docsPath: 'https://some.doc.path' }); const addButton = () => wrapper.find(GlButton);
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
}); });
const inputs = () => wrapper.findAll(GlFormInput);
const clickButton = () => wrapper.find(GlButton).vm.$emit('click');
describe('Default state', () => { describe('Default state', () => {
beforeEach(() => {
createComponent();
});
const clickButton = () => addButton().vm.$emit('click');
describe('Initial state', () => {
it('creates input field', () => { it('creates input field', () => {
expect(inputs().length).toBe(1); expect(inputs().length).toBe(1);
}); });
...@@ -71,4 +80,37 @@ describe('User invites', () => { ...@@ -71,4 +80,37 @@ describe('User invites', () => {
expect(inputs().at(1).element).toBe(document.activeElement); expect(inputs().at(1).element).toBe(document.activeElement);
}); });
}); });
});
describe('Configurable for multi-use', () => {
it('number of initial inputs can be configured', () => {
createComponent({ initialEmailInputs: 2 });
expect(inputs().length).toBe(2);
});
it('placeholder can be configured', () => {
createComponent({ emailPlaceholderPrefix: '_placeholder_' });
expect(inputs().at(0).attributes('placeholder')).toBe('_placeholder_1@company.com');
});
it('input name can be configured', () => {
createComponent({ inputName: 'emails[]' });
expect(inputs().at(0).attributes('name')).toBe('emails[]');
});
it('adding another email button can be configured', () => {
createComponent({ addAnotherText: '_addAnotherText_' });
expect(addButton().text()).toBe('_addAnotherText_');
});
it('label for component can be configured', () => {
createComponent({ inviteLabel: '_inviteLabel_' });
expect(wrapper.findByTestId('no-input-form-group').attributes('label')).toBe('_inviteLabel_');
});
});
}); });
import { GlForm, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import InviteMembers from 'ee/groups/components/invite_members.vue';
import InviteMembersForm from 'ee/registrations/components/invite_members_form.vue';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
describe('InviteMembersForm', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMount(InviteMembersForm, {
provide: { endpoint: '_endpoint_' },
propsData: {
docsPath: '_docs_path_',
emails: [],
},
});
};
const form = () => wrapper.find(GlForm);
const submitButton = () => form().find(GlButton);
const inviteMembers = () => form().find(InviteMembers);
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('displays form with correct action and inputs', () => {
expect(form().attributes('action')).toBe('_endpoint_');
expect(form().find('input[name="authenticity_token"]').attributes('value')).toBe(
'mock-csrf-token',
);
});
it('includes the invite members component', () => {
expect(inviteMembers().exists()).toBe(true);
expect(inviteMembers().props('docsPath')).toBe('_docs_path_');
expect(inviteMembers().props('emails')).toEqual([]);
expect(inviteMembers().props('initialEmailInputs')).toBe(3);
expect(inviteMembers().props('emailPlaceholderPrefix')).toBe('teammate');
expect(inviteMembers().props('addAnotherText')).toBe('Invite another teammate');
expect(inviteMembers().props('inputName')).toBe('emails[]');
});
it('has correct text on submit button', () => {
expect(submitButton().text()).toBe('Send invitations');
});
});
...@@ -302,4 +302,20 @@ RSpec.describe EE::WelcomeHelper do ...@@ -302,4 +302,20 @@ RSpec.describe EE::WelcomeHelper do
it { is_expected.to eq(result) } it { is_expected.to eq(result) }
end end
end end
describe '#already_showed_trial_activation?' do
subject { helper.already_showed_trial_activation? }
it 'returns true if query param hide_trial_activation_banner is set to true' do
allow(helper).to receive(:params).and_return({ hide_trial_activation_banner: 'true' })
is_expected.to eq(true)
end
it 'returns true if query param hide_trial_activation_banner is not set' do
allow(helper).to receive(:params).and_return({})
is_expected.to eq(false)
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'view group invites' do
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
let_it_be(:not_authorized_group) { create(:group) }
before_all do
group.add_owner(user)
end
before do
login_as(user)
end
describe 'GET /users/sign_up/group_invites/new' do
subject(:get_request) { get new_users_sign_up_group_invite_path(group_params) }
let(:group_params) { { group_id: group.id } }
context 'with an authorized user' do
it 'returns 200 response' do
get_request
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'when user is not authorized to invite for the group' do
let(:group_params) { { group_id: not_authorized_group.id } }
it 'returns not_found' do
get_request
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
describe 'POST /users/sign_up/group_invites' do
subject(:post_request) { post users_sign_up_group_invites_path(group_params) }
let(:group_params) { { group_id: group.id } }
context 'with an authorized user' do
specify do
is_expected.to redirect_to(new_users_sign_up_project_path(namespace_id: group.id,
trial: false,
trial_onboarding_flow: false,
hide_trial_activation_banner: true))
end
context 'when inviting members' do
context 'without valid emails in the params' do
it 'no invites generated by default' do
post_request
expect(group.members.invite).to be_empty
end
it 'does not track experiment', :experiment do
expect(experiment(:registrations_group_invite)).not_to track(:invites_sent)
post_request
end
end
context 'with valid emails in the params' do
let(:valid_emails) { %w[a@a.a b@b.b] }
let(:group_params) { { group_id: group.id, emails: valid_emails + ['', '', 'x', 'y'] } }
it 'adds users with developer access and ignores blank and invalid emails', :aggregate_failures do
post_request
invited_members = group.members.invite
expect(invited_members.pluck(:invite_email)).to match_array(valid_emails)
expect(invited_members.pluck(:access_level).uniq).to match([Gitlab::Access::DEVELOPER])
end
it 'tracks experiment as expected', :experiment do
expect(experiment(:registrations_group_invite))
.to track(:invites_sent, { property: group.id.to_s, value: valid_emails.size })
.on_any_instance
.with_context(actor: :user)
post_request
end
end
end
context 'when considering trial parameters' do
let(:group_params) { { group_id: group.id, trial: true, trial_onboarding_flow: true } }
specify do
is_expected.to redirect_to(new_users_sign_up_project_path(namespace_id: group.id,
trial: true,
trial_onboarding_flow: true,
hide_trial_activation_banner: true))
end
end
end
context 'when user is not authorized to invite for the group' do
let(:group_params) { { group_id: not_authorized_group.id } }
it 'returns not_found' do
post_request
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'registrations/group_invites/new' do
let(:group) { build(:group) }
let(:trial_onboarding_flow) { false }
let(:trial_during_signup_flow) { false }
before do
assign(:group, group)
allow(view).to receive(:in_trial_onboarding_flow?).and_return(trial_onboarding_flow)
allow(view).to receive(:in_trial_during_signup_flow?).and_return(trial_during_signup_flow)
render
end
it 'shows standard markup', :aggregate_failures do
expect(rendered).to have_selector('#progress-bar')
expect(rendered).to have_content('Invite your teammates')
expect(rendered).to have_link('Skip this for now')
expect(rendered).to have_content("Don't worry, you can always invite teammates later")
end
context 'in trial onboarding' do
let(:trial_onboarding_flow) { true }
it 'show the trial activation' do
expect(rendered).to have_content('Congratulations, your free trial is activated.')
end
end
context 'in trial flow' do
let(:trial_during_signup_flow) { true }
it 'show the trial activation' do
expect(rendered).to have_content('Congratulations, your free trial is activated.')
end
end
end
...@@ -8,12 +8,14 @@ RSpec.describe 'registrations/projects/new' do ...@@ -8,12 +8,14 @@ RSpec.describe 'registrations/projects/new' do
let_it_be(:project) { create(:project, namespace: namespace) } let_it_be(:project) { create(:project, namespace: namespace) }
let_it_be(:trial_onboarding_flow) { false } let_it_be(:trial_onboarding_flow) { false }
let_it_be(:trial_during_signup_flow) { false } let_it_be(:trial_during_signup_flow) { false }
let_it_be(:already_shown_banner) { false }
before do before do
assign(:project, project) assign(:project, project)
allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:current_user).and_return(user)
allow(view).to receive(:in_trial_onboarding_flow?).and_return(trial_onboarding_flow) allow(view).to receive(:in_trial_onboarding_flow?).and_return(trial_onboarding_flow)
allow(view).to receive(:in_trial_during_signup_flow?).and_return(trial_during_signup_flow) allow(view).to receive(:in_trial_during_signup_flow?).and_return(trial_during_signup_flow)
allow(view).to receive(:already_showed_trial_activation?).and_return(already_shown_banner)
allow(view).to receive(:import_sources_enabled?).and_return(false) allow(view).to receive(:import_sources_enabled?).and_return(false)
render render
...@@ -33,6 +35,14 @@ RSpec.describe 'registrations/projects/new' do ...@@ -33,6 +35,14 @@ RSpec.describe 'registrations/projects/new' do
it 'show the trial activation' do it 'show the trial activation' do
expect(rendered).to have_content('Congratulations, your free trial is activated.') expect(rendered).to have_content('Congratulations, your free trial is activated.')
end end
context 'when already shown activation banner' do
let_it_be(:already_shown_banner) { true }
it 'does not show trial activation banner' do
expect(rendered).not_to have_content('Congratulations, your free trial is activated.')
end
end
end end
context 'in trial flow' do context 'in trial flow' do
...@@ -41,5 +51,13 @@ RSpec.describe 'registrations/projects/new' do ...@@ -41,5 +51,13 @@ RSpec.describe 'registrations/projects/new' do
it 'show the trial activation' do it 'show the trial activation' do
expect(rendered).to have_content('Congratulations, your free trial is activated.') expect(rendered).to have_content('Congratulations, your free trial is activated.')
end end
context 'when already shown activation banner' do
let_it_be(:already_shown_banner) { true }
it 'does not show trial activation banner' do
expect(rendered).not_to have_content('Congratulations, your free trial is activated.')
end
end
end end
end end
...@@ -505,6 +505,9 @@ msgstr "" ...@@ -505,6 +505,9 @@ msgstr ""
msgid "%{edit_in_new_fork_notice} Try to upload a file again." msgid "%{edit_in_new_fork_notice} Try to upload a file again."
msgstr "" msgstr ""
msgid "%{emailPrefix}@company.com"
msgstr ""
msgid "%{extra} more downstream pipelines" msgid "%{extra} more downstream pipelines"
msgstr "" msgstr ""
...@@ -16587,18 +16590,33 @@ msgstr "" ...@@ -16587,18 +16590,33 @@ msgstr ""
msgid "InviteMember|Add members to this project and start collaborating with your team." msgid "InviteMember|Add members to this project and start collaborating with your team."
msgstr "" msgstr ""
msgid "InviteMember|Don't worry, you can always invite teammates later"
msgstr ""
msgid "InviteMember|Invite Members (optional)" msgid "InviteMember|Invite Members (optional)"
msgstr "" msgstr ""
msgid "InviteMember|Invite another member" msgid "InviteMember|Invite another member"
msgstr "" msgstr ""
msgid "InviteMember|Invite another teammate"
msgstr ""
msgid "InviteMember|Invite members" msgid "InviteMember|Invite members"
msgstr "" msgstr ""
msgid "InviteMember|Invite teammates to your GitLab group"
msgstr ""
msgid "InviteMember|Invite users to your group %{group_name} so you can collaborate on your projects"
msgstr ""
msgid "InviteMember|Invite your team" msgid "InviteMember|Invite your team"
msgstr "" msgstr ""
msgid "InviteMember|Invite your teammates"
msgstr ""
msgid "InviteMember|Invited users will be added with developer level permissions. %{linkStart}View the documentation%{linkEnd} to see how to change this later." msgid "InviteMember|Invited users will be added with developer level permissions. %{linkStart}View the documentation%{linkEnd} to see how to change this later."
msgstr "" msgstr ""
...@@ -16608,6 +16626,12 @@ msgstr "" ...@@ -16608,6 +16626,12 @@ msgstr ""
msgid "InviteMember|See who can invite members for you" msgid "InviteMember|See who can invite members for you"
msgstr "" 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" msgid "InviteMember|Until then, ask an owner to invite new project members for you"
msgstr "" msgstr ""
...@@ -35485,9 +35509,6 @@ msgstr "" ...@@ -35485,9 +35509,6 @@ msgstr ""
msgid "math|There was an error rendering this math block" msgid "math|There was an error rendering this math block"
msgstr "" msgstr ""
msgid "member%{number}@company.com"
msgstr ""
msgid "merge request" msgid "merge request"
msgid_plural "merge requests" msgid_plural "merge requests"
msgstr[0] "" msgstr[0] ""
......
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