Commit c978c0b2 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'jswain_simplify_group_creation' into 'master'

Simplify Group and Project creation

See merge request gitlab-org/gitlab!67614
parents 732ce8b5 67117851
......@@ -71,6 +71,17 @@ const deriveProjectPathFromUrl = ($projectImportUrl) => {
}
};
const bindHowToImport = () => {
$('.how_to_import_link').on('click', (e) => {
e.preventDefault();
$(e.currentTarget).next('.modal').show();
});
$('.modal-header .close').on('click', () => {
$('.modal').hide();
});
};
const bindEvents = () => {
const $newProjectForm = $('#new_project');
const $projectImportUrl = $('#project_import_url');
......@@ -88,14 +99,7 @@ const bindEvents = () => {
return;
}
$('.how_to_import_link').on('click', (e) => {
e.preventDefault();
$(e.currentTarget).next('.modal').show();
});
$('.modal-header .close').on('click', () => {
$('.modal').hide();
});
bindHowToImport();
$('.btn_import_gitlab_project').on('click', () => {
const importHref = $('a.btn_import_gitlab_project').attr('href');
......@@ -174,3 +178,5 @@ export default {
onProjectNameChange,
onProjectPathChange,
};
export { bindHowToImport };
......@@ -16,7 +16,7 @@ module Registrations
result = ::Users::SignupService.new(current_user, update_params).execute
if result[:status] == :success
return redirect_to new_users_sign_up_group_path(trial_params) if show_signup_onboarding?
return redirect_to experiment(:combined_registration, user: current_user).redirect_path(trial_params) if show_signup_onboarding?
members = current_user.members
......
# frozen_string_literal: true
class CombinedRegistrationExperiment < ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass
include Rails.application.routes.url_helpers
def redirect_path(trial_params)
@trial_params = trial_params
run
end
def control_behavior
new_users_sign_up_group_path(@trial_params)
end
def candidate_behavior
new_users_sign_up_groups_project_path
end
end
......@@ -90,10 +90,14 @@ module Projects
def after_create_actions
log_info("#{@project.owner.name} created a new project \"#{@project.full_name}\"")
if @project.import?
experiment(:combined_registration, user: current_user).track(:import_project)
else
# Skip writing the config for project imports/forks because it
# will always fail since the Git directory doesn't exist until
# a background job creates it (see Project#add_import_job).
@project.set_full_path unless @project.import?
@project.set_full_path
end
unless @project.gitlab_project_import?
@project.create_wiki unless skip_wiki?
......
......@@ -8,7 +8,7 @@
.form-group.project-name.col-sm-12
= f.label :name, class: 'label-bold' do
%span= _("Project name")
= f.text_field :name, placeholder: "My awesome project", class: "form-control gl-form-input input-lg", autofocus: true, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_name", track_value: "" }, required: true, aria: { required: true }
= f.text_field :name, placeholder: "My awesome project", class: "form-control gl-form-input input-lg", data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_name", track_value: "" }, required: true, aria: { required: true }
.form-group.project-path.col-sm-6
= f.label :namespace_id, class: 'label-bold' do
%span= _('Project URL')
......
---
name: combined_registration
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67614
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/285533
milestone: '14.3'
type: experiment
group: group::adoption
default_enabled: false
......@@ -68,6 +68,9 @@ Rails.application.routes.draw do
Gitlab.ee do
resources :groups, only: [:new, :create]
resources :projects, only: [:new, :create]
resources :groups_projects, only: [:new, :create] do
post :import, on: :collection
end
end
end
......
/* eslint-disable no-new */
import mountComponents from 'ee/registrations/groups_projects/new';
import Group from '~/group';
new Group();
mountComponents();
......@@ -10,3 +10,5 @@ export const STEPS = {
export const SUBSCRIPTON_FLOW_STEPS = [STEPS.yourProfile, STEPS.checkout, STEPS.yourGroup];
export const SIGNUP_ONBOARDING_FLOW_STEPS = [STEPS.yourProfile, STEPS.yourGroup, STEPS.yourProject];
export const COMBINED_SIGNUP_FLOW_STEPS = [STEPS.yourProfile, STEPS.yourProject];
import $ from 'jquery';
import { bindHowToImport } from '~/projects/project_new';
import { displayGroupPath, displayProjectPath } from './path_display';
import mountProgressBar from './progress_bar';
import showTooltip from './show_tooltip';
const importButtonsSubmit = () => {
const buttons = document.querySelectorAll('.js-import-project-buttons a');
const form = document.querySelector('.js-import-project-form');
const submit = form.querySelector('input[type="submit"]');
const importUrlField = form.querySelector('.js-import-url');
const clickHandler = (e) => {
e.preventDefault();
importUrlField.value = e.target.getAttribute('href');
submit.click();
};
buttons.forEach((button) => button.addEventListener('click', clickHandler));
};
const setAutofocus = () => {
const setInputfocus = () => {
document
.querySelector('.js-group-project-tab-contents .tab-pane.active .js-group-name-field')
?.focus();
};
setInputfocus();
$('.js-group-project-tabs').on('shown.bs.tab', setInputfocus);
};
export default () => {
mountProgressBar();
displayGroupPath('.js-group-path-source', '.js-group-path-display');
displayGroupPath('.js-import-group-path-source', '.js-import-group-path-display');
displayProjectPath('.js-project-path-source', '.js-project-path-display');
showTooltip('.js-group-name-tooltip');
importButtonsSubmit();
bindHowToImport();
setAutofocus();
};
import { slugify, convertUnicodeToAscii } from '~/lib/utils/text_utility';
class DisplayInputValue {
constructor(sourceElementSelector, targetElementSelector, transformer) {
this.sourceElement = document.querySelector(sourceElementSelector);
this.targetElement = document.querySelector(targetElementSelector);
this.originalTargetValue = this.targetElement?.textContent;
this.transformer = transformer;
this.updateHandler = this.update.bind(this);
}
update() {
let { value } = this.sourceElement;
if (value.length === 0) {
value = this.originalTargetValue;
} else if (this.transformer) {
value = this.transformer(value);
}
this.targetElement.textContent = value;
}
listen(callback) {
if (!this.sourceElement || !this.targetElement) return null;
this.updateHandler();
return callback(this.sourceElement, this.updateHandler);
}
}
export const displayGroupPath = (sourceSelector, targetSelector) => {
const display = new DisplayInputValue(sourceSelector, targetSelector);
if (!display) return null;
const callback = (sourceElement, updateHandler) => {
const observer = new MutationObserver((mutationList) => {
mutationList.forEach((mutation) => {
if (mutation.attributeName === 'value') {
updateHandler();
}
});
});
observer.observe(sourceElement, { attributes: true });
};
return display.listen(callback);
};
export const displayProjectPath = (sourceSelector, displaySelector) => {
const transformer = (value) => slugify(convertUnicodeToAscii(value));
const display = new DisplayInputValue(sourceSelector, displaySelector, transformer);
if (!display) return null;
const callback = (sourceElement, updateHandler) => {
sourceElement.addEventListener('input', updateHandler);
};
return display.listen(callback);
};
import Vue from 'vue';
import ProgressBar from '../../components/progress_bar.vue';
import { STEPS, COMBINED_SIGNUP_FLOW_STEPS } from '../../constants';
export default function mountProgressBar() {
const el = document.getElementById('progress-bar');
if (!el) {
return null;
}
return new Vue({
el,
render(createElement) {
return createElement(ProgressBar, {
props: { steps: COMBINED_SIGNUP_FLOW_STEPS, currentStep: STEPS.yourProject },
});
},
});
}
import { initTooltips, add } from '~/tooltips';
export default function showTooltip(tooltipSelector) {
const tooltip = document.querySelector(tooltipSelector);
if (!tooltip) return null;
initTooltips({ selector: tooltipSelector });
return add([tooltip], { show: true });
}
import Vue from 'vue';
import 'ee/registrations/welcome/other_role';
import 'ee/registrations/welcome/jobs_to_be_done';
import { experiment } from '~/experimentation/utils';
import { parseBoolean } from '~/lib/utils/common_utils';
import ProgressBar from '../components/progress_bar.vue';
import { STEPS, SUBSCRIPTON_FLOW_STEPS, SIGNUP_ONBOARDING_FLOW_STEPS } from '../constants';
import {
STEPS,
SUBSCRIPTON_FLOW_STEPS,
SIGNUP_ONBOARDING_FLOW_STEPS,
COMBINED_SIGNUP_FLOW_STEPS,
} from '../constants';
export default () => {
const el = document.getElementById('progress-bar');
......@@ -18,7 +24,14 @@ export default () => {
if (isInSubscriptionFlow) {
steps = SUBSCRIPTON_FLOW_STEPS;
} else if (isSignupOnboardingEnabled) {
experiment('combined_registration', {
use: () => {
steps = SIGNUP_ONBOARDING_FLOW_STEPS;
},
try: () => {
steps = COMBINED_SIGNUP_FLOW_STEPS;
},
});
}
return new Vue({
......
# frozen_string_literal: true
module Registrations::CreateGroup
extend ActiveSupport::Concern
included do
before_action :check_if_gl_com_or_dev
before_action :authorize_create_group!, only: :new
protected
def show_confirm_warning?
false
end
private
def authorize_create_group!
access_denied! unless can?(current_user, :create_group)
end
def group_params
params.require(:group).permit(:name, :path, :visibility_level)
end
def learn_gitlab_context
strong_memoize(:learn_gitlab_context) do
in_experiment_group_a = Gitlab::Experimentation.in_experiment_group?(:learn_gitlab_a, subject: current_user)
in_experiment_group_b = !in_experiment_group_a && Gitlab::Experimentation.in_experiment_group?(:learn_gitlab_b, subject: current_user)
{ in_experiment_group_a: in_experiment_group_a, in_experiment_group_b: in_experiment_group_b }
end
end
end
end
# frozen_string_literal: true
module Registrations::CreateProject
extend ActiveSupport::Concern
include LearnGitlabHelper
LEARN_GITLAB_TEMPLATE = 'learn_gitlab.tar.gz'
LEARN_GITLAB_ULTIMATE_TEMPLATE = 'learn_gitlab_ultimate_trial.tar.gz'
included do
private
def learn_gitlab_experiment_enabled?
Gitlab::Experimentation.in_experiment_group?(:learn_gitlab_a, subject: current_user) ||
Gitlab::Experimentation.in_experiment_group?(:learn_gitlab_b, subject: current_user)
end
def learn_gitlab_template_path
file = if helpers.in_trial_onboarding_flow?
LEARN_GITLAB_ULTIMATE_TEMPLATE
else
LEARN_GITLAB_TEMPLATE
end
Rails.root.join('vendor', 'project_templates', file)
end
def create_learn_gitlab_project
File.open(learn_gitlab_template_path) do |archive|
::Projects::GitlabProjectsImportService.new(
current_user,
namespace_id: @project.namespace_id,
file: archive,
name: learn_gitlab_project_name
).execute
end
end
def learn_gitlab_project_name
helpers.in_trial_onboarding_flow? ? s_('Learn GitLab - Ultimate trial') : s_('Learn GitLab')
end
def project_params
params.require(:project).permit(project_params_attributes)
end
def project_params_attributes
[
:namespace_id,
:name,
:path,
:visibility_level
]
end
end
end
......@@ -17,6 +17,7 @@ module EE
]
before_action only: :show do
publish_combined_registration_experiment
experiment(:trial_registration_with_reassurance, actor: current_user)
.track(:render, label: 'registrations:welcome:show', user: current_user)
end
......@@ -84,6 +85,10 @@ module EE
::Project.find(params[:learn_gitlab_project_id])
end
end
def publish_combined_registration_experiment
experiment(:combined_registration, user: current_user).publish_to_client if show_signup_onboarding?
end
end
end
end
......@@ -2,10 +2,10 @@
module Registrations
class GroupsController < ApplicationController
layout 'minimal'
include Registrations::CreateGroup
include ::Gitlab::Utils::StrongMemoize
before_action :check_if_gl_com_or_dev
before_action :authorize_create_group!, only: :new
layout 'minimal'
feature_category :onboarding
......@@ -13,6 +13,7 @@ module Registrations
experiment(:trial_registration_with_reassurance, actor: current_user)
.track(:render, label: 'registrations:groups:new', user: current_user)
@group = Group.new(visibility_level: helpers.default_group_visibility)
experiment(:combined_registration, user: current_user).track(:view_new_group_action)
end
def create
......@@ -22,6 +23,8 @@ module Registrations
experiment(:jobs_to_be_done, user: current_user)
.track(:create_group, namespace: @group)
experiment(:combined_registration, user: current_user).track(:create_group, namespace: @group)
force_company_trial_experiment.track(:create_group, namespace: @group, user: current_user)
create_successful_flow
......@@ -30,12 +33,6 @@ module Registrations
end
end
protected
def show_confirm_warning?
false
end
private
def force_company_trial_experiment
......@@ -51,14 +48,6 @@ module Registrations
end
end
def authorize_create_group!
access_denied! unless can?(current_user, :create_group)
end
def group_params
params.require(:group).permit(:name, :path, :visibility_level)
end
def apply_trial_for_trial_onboarding_flow
if apply_trial
record_experiment_user(:remove_known_trial_form_fields_welcoming, namespace_id: @group.id)
......
# frozen_string_literal: true
module Registrations
class GroupsProjectsController < ApplicationController
include Registrations::CreateProject
include Registrations::CreateGroup
layout 'minimal'
feature_category :onboarding
def new
@group = Group.new(visibility_level: helpers.default_group_visibility)
@project = Project.new(namespace: @group)
combined_registration_experiment.track(:view_new_group_action)
experiment(:trial_registration_with_reassurance, actor: current_user)
.track(:render, label: 'registrations:groups:new', user: current_user)
end
def create
@group = if group_id = params[:group][:id]
Group.find_by_id(group_id)
else
Groups::CreateService.new(current_user, group_params).execute
end
if @group.persisted?
if @group.previously_new_record?
combined_registration_experiment.track(:create_group, namespace: @group)
experiment(:jobs_to_be_done, user: current_user).track(:create_group, namespace: @group)
end
@project = ::Projects::CreateService.new(current_user, project_params).execute
if @project.saved?
combined_registration_experiment.track(:create_project, namespace: @project.namespace)
learn_gitlab_project = create_learn_gitlab_project
experiment(:jobs_to_be_done, user: current_user)
.track(:create_project, project: @project)
if helpers.in_trial_onboarding_flow?
record_experiment_user(:remove_known_trial_form_fields_welcoming, namespace_id: @group.id)
record_experiment_conversion_event(:remove_known_trial_form_fields_welcoming)
redirect_to trial_getting_started_users_sign_up_welcome_path(learn_gitlab_project_id: learn_gitlab_project.id)
else
success_url = current_user.setup_for_company ? new_trial_path : nil
redirect_to success_url || continuous_onboarding_getting_started_users_sign_up_welcome_path(project_id: @project.id)
end
else
render :new
end
else
@project = Project.new(project_params) # #new requires a Project
render :new
end
end
def import
@group = Groups::CreateService.new(current_user, group_params).execute
if @group.persisted?
combined_registration_experiment.track(:create_group, namespace: @group)
experiment(:jobs_to_be_done, user: current_user).track(:create_group, namespace: @group)
import_url = URI.join(root_url, params[:import_url], "?namespace_id=#{@group.id}").to_s
redirect_to import_url
else
@project = Project.new(namespace: @group) # #new requires a Project
render :new
end
end
private
def combined_registration_experiment
@combined_registration_experiment ||= experiment(:combined_registration, user: current_user)
end
def project_params
params.require(:project).permit(project_params_attributes).merge(namespace_id: @group.id)
end
end
end
......@@ -2,13 +2,11 @@
module Registrations
class ProjectsController < ApplicationController
include LearnGitlabHelper
include Registrations::CreateProject
layout 'minimal'
LEARN_GITLAB_TEMPLATE = 'learn_gitlab.tar.gz'
LEARN_GITLAB_ULTIMATE_TEMPLATE = 'learn_gitlab_ultimate_trial.tar.gz'
before_action :check_if_gl_com_or_dev
before_action only: [:new] do
set_namespace
authorize_create_project!
......@@ -24,6 +22,8 @@ module Registrations
@project = ::Projects::CreateService.new(current_user, project_params).execute
if @project.saved?
experiment(:combined_registration, user: current_user).track(:create_project, namespace: @project.namespace)
learn_gitlab_project = create_learn_gitlab_project
experiment(:jobs_to_be_done, user: current_user)
......@@ -44,17 +44,6 @@ module Registrations
private
def create_learn_gitlab_project
File.open(learn_gitlab_template_path) do |archive|
::Projects::GitlabProjectsImportService.new(
current_user,
namespace_id: @project.namespace_id,
file: archive,
name: learn_gitlab_project_name
).execute
end
end
def authorize_create_project!
access_denied! unless can?(current_user, :create_projects, @namespace)
end
......@@ -62,32 +51,5 @@ module Registrations
def set_namespace
@namespace = Namespace.find_by_id(params[:namespace_id])
end
def project_params
params.require(:project).permit(project_params_attributes)
end
def project_params_attributes
[
:namespace_id,
:name,
:path,
:visibility_level
]
end
def learn_gitlab_project_name
helpers.in_trial_onboarding_flow? ? s_('Learn GitLab - Ultimate trial') : s_('Learn GitLab')
end
def learn_gitlab_template_path
file = if helpers.in_trial_onboarding_flow?
LEARN_GITLAB_ULTIMATE_TEMPLATE
else
LEARN_GITLAB_TEMPLATE
end
Rails.root.join('vendor', 'project_templates', file)
end
end
end
......@@ -52,6 +52,8 @@ class TrialsController < ApplicationController
.track(:apply_trial, label: 'trials:apply', namespace: @namespace, user: current_user)
experiment(:force_company_trial, user: current_user).track(:create_trial, namespace: @namespace, user: current_user, label: 'trials_controller') if @namespace.created_at > 24.hours.ago
experiment(:combined_registration, user: current_user).track(:create_trial)
if discover_group_security_flow?
redirect_trial_user_to_feature_experiment_flow
else
......
- @html_class = "subscriptions-layout-html"
- page_title _('Your GitLab group')
- form_params = { trial_onboarding_flow: params[:trial_onboarding_flow], glm_source: params[:glm_source], glm_content: params[:glm_content] }
- if 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.gl-px-5.gl-pb-5
.new-project.gl-display-flex.gl-flex-direction-column.gl-align-items-center
- unless in_trial_onboarding_flow?
#progress-bar
%h2.gl-text-center= _('Create or import your first project')
%p.gl-text-center= _('Projects help you organize your work. They contain your file repository, issues, merge requests, and so much more.')
.js-toggle-container.gl-w-full
%ul.nav.nav-tabs.nav-links.gitlab-tabs.js-group-project-tabs{ role: 'tablist' }
%li.nav-item{ role: 'presentation' }
%a#blank-project-tab.nav-link.active{ href: '#blank-project-pane', data: { toggle: 'tab', track_label: 'blank_project', track_event: 'click_tab', track_value: '' }, role: 'tab' }
%span= s_('ProjectsNew|Create')
%li.nav-item{ role: 'presentation' }
%a#import-project-tab.nav-link{ href: '#import-project-pane', data: { toggle: 'tab', track_label: 'import_project', track_event: 'click_tab', track_value: '' }, role: 'tab' }
%span= s_('ProjectsNew|Import')
.tab-content.gitlab-tab-content.gl-bg-white.js-group-project-tab-contents
#blank-project-pane.tab-pane.js-toggle-container.active{ role: 'tabpanel' }
= form_tag users_sign_up_groups_projects_path(form_params), class: 'gl-show-field-errors gl-w-full gl-p-4' do
= form_errors(@group, type: "Group")
= form_errors(@project, type: "Project")
= render 'layouts/flash'
= fields_for :group do |gf|
.row
.form-group.group-name-holder.col-sm-12
= gf.label :name, class: 'gl-font-weight-bold' do
= _('Group name')
- if @group.persisted?
= gf.text_field :name, class: 'form-control js-group-path-source',
disabled: true
= gf.hidden_field :id
- else
= gf.text_field :name, class: 'form-control js-validate-group-path js-autofill-group-name js-group-name-tooltip js-group-name-field',
required: true,
autofocus: true,
data: { title: _('Projects are organized into groups'), placement: 'right', show: true }
= gf.hidden_field :path, class: 'form-control js-autofill-group-path js-group-path-source'
= gf.hidden_field :parent_id, id: 'group_parent_id'
= fields_for :project do |pf|
#blank-project-name.row
.form-group.project-name.col-sm-12
= pf.label :name, class: 'gl-font-weight-bold' do
%span= _('Project name')
= pf.text_field :name, id: 'blank_project_name', class: 'form-control js-project-path-source', required: true, data: { track_label: 'blank_project', track_event: 'activate_form_input', track_property: 'project_name', track_value: '' }
%p.form-text.gl-text-center
= _('Your project will be created at:')
%p.form-text.gl-text-center.monospace
= root_url
%span.js-group-path-display>= _('{group}')
%span>= _('/')
%span.js-project-path-display>= _('{project}')
%p.form-text.text-muted.gl-text-center{ class: 'gl-mb-5!' }
= _('You can always change your URL later')
= submit_tag _('Create project'), class: 'btn gl-button btn-success btn-block', data: { track_label: 'blank_project', track_event: 'click_button', track_property: 'create_project', track_value: '' }
#import-project-pane.tab-pane.import-project-pane.js-toggle-container{ role: 'tabpanel' }
- if import_sources_enabled?
= form_tag import_users_sign_up_groups_projects_path, class: 'gl-show-field-errors gl-w-full gl-p-4 js-import-project-form' do
= form_errors(@group, type: "Group")
= render 'layouts/flash'
= fields_for :group do |gf|
.row
.form-group.group-name-holder.col-sm-12
= gf.label :name, class: 'gl-font-weight-bold' do
= _('Group name')
= gf.text_field :name, id: 'import_group_name', class: 'form-control js-validate-group-path js-autofill-group-name js-group-name-field has-tooltip',
required: true,
data: { title: _('Projects are organized into groups'), placement: 'right' }
= gf.hidden_field :path, id: 'import_group_path', class: 'form-control js-autofill-group-path js-import-group-path-source'
= hidden_field_tag :import_url, nil, class: 'js-import-url'
= submit_tag nil, class: 'gl-display-none'
%p.form-text.gl-text-center
= _('Your project will be created at:')
%p.form-text.gl-text-center.monospace
= root_url
%span.js-import-group-path-display>= _('{group}')
%span>= _('/')
%span>= _('{project}')
%p.form-text.text-muted.gl-text-center{ class: 'gl-mb-5!' }
= _('You can always change your URL later')
.js-import-project-buttons
= render 'projects/import_project_pane'
- else
.nothing-here-block
%h4= s_('ProjectsNew|No import options available')
%p= s_('ProjectsNew|Contact an administrator to enable options for importing your project.')
......@@ -3,91 +3,12 @@
require 'spec_helper'
RSpec.describe Registrations::GroupsController do
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
shared_examples 'hides email confirmation warning' do
RSpec::Matchers.define :set_confirm_warning_for do |email|
match do |response|
expect(controller).to set_flash.now[:warning].to include("Please check your email (#{email}) to verify that you own this address and unlock the power of CI/CD.")
end
end
context 'with an unconfirmed email address present' do
let(:user) { create(:user, confirmed_at: nil, unconfirmed_email: 'unconfirmed@gitlab.com') }
it { is_expected.not_to set_confirm_warning_for(user.unconfirmed_email) }
end
context 'without an unconfirmed email address present' do
let(:user) { create(:user, confirmed_at: nil) }
it { is_expected.not_to set_confirm_warning_for(user.email) }
end
end
describe 'GET #new', :aggregate_failures do
let(:dev_env_or_com) { true }
subject(:get_new) { get :new }
context 'with an unauthenticated user' do
it { is_expected.to have_gitlab_http_status(:redirect) }
it { is_expected.to redirect_to(new_user_session_path) }
end
context 'with an authenticated user' do
before do
sign_in(user)
allow(::Gitlab).to receive(:dev_env_or_com?).and_return(dev_env_or_com)
end
context 'when on .com' do
it { is_expected.to have_gitlab_http_status(:ok) }
it { is_expected.to render_template(:new) }
it 'assigns the group variable to a new Group with the default group visibility' do
get_new
expect(assigns(:group)).to be_a_new(Group)
expect(assigns(:group).visibility_level).to eq(Gitlab::CurrentSettings.default_group_visibility)
end
context 'when the trial_registration_with_reassurance experiment is active', :experiment do
before do
stub_experiments(trial_registration_with_reassurance: :control)
end
it 'tracks a "render" event' do
expect(experiment(:trial_registration_with_reassurance)).to track(
:render,
user: user,
label: 'registrations:groups:new'
).with_context(actor: user).on_next_instance
get_new
end
end
context 'user without the ability to create a group' do
let(:user) { create(:user, can_create_group: false) }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
end
context 'when not on .com' do
let(:dev_env_or_com) { false }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
it_behaves_like 'hides email confirmation warning'
end
describe 'GET #new' do
it_behaves_like "Registrations::GroupsController GET #new"
end
describe 'POST #create', :aggregate_failure do
let_it_be(:user) { create(:user) }
let_it_be(:glm_params) { {} }
let_it_be(:trial_form_params) { { trial: 'false' } }
let_it_be(:trial_onboarding_flow_params) { {} }
......@@ -269,6 +190,11 @@ RSpec.describe Registrations::GroupsController do
post_create
end
it 'tracks for the combined_registration experiment', :experiment do
expect(experiment(:combined_registration)).to track(:create_group, namespace: an_instance_of(Group)).on_next_instance
subject
end
end
context 'when failing to create a lead and apply trial' do
......
......@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe Registrations::ProjectsController do
include AfterNextHelpers
let_it_be(:user) { create(:user) }
let_it_be(:namespace) { create(:group) }
let_it_be(:project) { create(:project) }
......@@ -53,48 +51,20 @@ RSpec.describe Registrations::ProjectsController do
end
describe 'POST #create' do
subject { post :create, params: { project: params }.merge(trial_onboarding_flow_params) }
it_behaves_like "Registrations::ProjectsController POST #create"
let_it_be(:trial_onboarding_flow_params) { {} }
context 'force_company_trial_experiment' do
let(:project) { create(:project, namespace: namespace) }
let(:params) { { namespace_id: namespace.id, name: 'New project', path: 'project-path', visibility_level: Gitlab::VisibilityLevel::PRIVATE } }
let(:dev_env_or_com) { true }
context 'with an unauthenticated user' do
it { is_expected.to have_gitlab_http_status(:redirect) }
it { is_expected.to redirect_to(new_user_session_path) }
end
context 'with an authenticated user', :sidekiq_inline do
let_it_be(:first_project) { create(:project) }
before do
namespace.add_owner(user)
sign_in(user)
allow(::Gitlab).to receive(:dev_env_or_com?).and_return(dev_env_or_com)
end
it 'creates a new project, a "Learn GitLab" project, sets a cookie and redirects to the continuous onboarding page' do
allow(::Gitlab).to receive(:dev_env_or_com?).and_return(true)
allow_next_instance_of(::Projects::CreateService) do |service|
allow(service).to receive(:execute).and_return(first_project)
end
allow_next_instance_of(::Projects::GitlabProjectsImportService) do |service|
allow(service).to receive(:execute).and_return(project)
end
expect(subject).to have_gitlab_http_status(:redirect)
expect(subject).to redirect_to(continuous_onboarding_getting_started_users_sign_up_welcome_path(project_id: first_project.id))
end
it 'tracks an event for the jobs_to_be_done experiment', :experiment do
stub_experiments(jobs_to_be_done: :candidate)
expect(experiment(:jobs_to_be_done)).to track(:create_project, project: an_instance_of(Project))
.on_next_instance
.for(:candidate)
.with_context(user: user)
subject
end
it 'tracks an event for the force_company_trial experiment', :experiment do
......@@ -102,72 +72,7 @@ RSpec.describe Registrations::ProjectsController do
.with_context(user: user)
.on_next_instance
subject
end
context 'learn gitlab project' do
using RSpec::Parameterized::TableSyntax
where(:trial, :project_name, :template) do
false | 'Learn GitLab' | described_class::LEARN_GITLAB_TEMPLATE
true | 'Learn GitLab - Ultimate trial' | described_class::LEARN_GITLAB_ULTIMATE_TEMPLATE
end
with_them do
let(:path) { Rails.root.join('vendor', 'project_templates', template) }
let(:expected_arguments) { { namespace_id: namespace.id, file: handle, name: project_name } }
let(:handle) { double }
let(:trial_onboarding_flow_params) { { trial_onboarding_flow: trial } }
before do
allow(File).to receive(:open).and_call_original
expect(File).to receive(:open).with(path).and_yield(handle)
end
specify do
expect_next(::Projects::GitlabProjectsImportService, user, expected_arguments)
.to receive(:execute).and_return(project)
subject
end
end
end
context 'when the trial onboarding is active' do
let_it_be(:trial_onboarding_flow_params) { { trial_onboarding_flow: true } }
it 'creates a new project, a "Learn GitLab - Ultimate trial" project, does not set a cookie' do
expect { subject }.to change { namespace.projects.pluck(:name) }.from([]).to(['New project', s_('Learn GitLab - Ultimate trial')])
expect(subject).to have_gitlab_http_status(:redirect)
expect(namespace.projects.find_by_name(s_('Learn GitLab - Ultimate trial'))).to be_import_finished
end
it 'records context and redirects to the trial getting started page' do
expect_next_instance_of(::Projects::CreateService) do |service|
expect(service).to receive(:execute).and_return(first_project)
end
expect_next_instance_of(::Projects::GitlabProjectsImportService) do |service|
expect(service).to receive(:execute).and_return(project)
end
expect(subject).to redirect_to(trial_getting_started_users_sign_up_welcome_path(learn_gitlab_project_id: project.id))
end
end
context 'when the project cannot be saved' do
let(:params) { { name: '', path: '' } }
it 'does not create a project' do
expect { subject }.not_to change { Project.count }
end
it { is_expected.to have_gitlab_http_status(:ok) }
it { is_expected.to render_template(:new) }
end
context 'with signup onboarding not enabled' do
let(:dev_env_or_com) { false }
it { is_expected.to have_gitlab_http_status(:not_found) }
post :create, params: { project: params }
end
end
end
......
......@@ -8,23 +8,17 @@ RSpec.describe Registrations::WelcomeController do
let_it_be(:project) { create(:project) }
describe '#show' do
context 'when the trial_registration_with_reassurance experiment is active', :experiment do
before do
it 'publishes combined_registration experiment data to the client' do
sign_in(user)
stub_experiments(trial_registration_with_reassurance: :control)
end
allow(controller.helpers).to receive(:signup_onboarding_enabled?).and_return(true)
it 'tracks a "render" event' do
expect(experiment(:trial_registration_with_reassurance)).to track(
:render,
user: user,
label: 'registrations:welcome:show'
).with_context(actor: user).on_next_instance
wrapped_experiment(experiment(:combined_registration)) do |e|
expect(e).to receive(:publish_to_client)
end
get :show
end
end
end
describe '#continuous_onboarding_getting_started' do
let_it_be(:project) { create(:project, group: group) }
......@@ -248,6 +242,14 @@ RSpec.describe Registrations::WelcomeController do
allow(controller.helpers).to receive(:signup_onboarding_enabled?).and_return(true)
end
context 'when combined_registration is candidate variant' do
before do
stub_experiments(combined_registration: :candidate)
end
it { is_expected.to redirect_to new_users_sign_up_groups_project_path }
end
context 'and force_company_trial experiment is candidate' do
let(:setup_for_company) { 'true' }
......
......@@ -236,6 +236,12 @@ RSpec.describe TrialsController do
end
end
it 'calls tracking event for combined_registration experiment', :experiment do
expect(experiment(:combined_registration)).to track(:create_trial).on_next_instance
subject
end
context 'redirect trial user to feature' do
using RSpec::Parameterized::TableSyntax
......@@ -276,6 +282,8 @@ RSpec.describe TrialsController do
context 'with an old namespace' do
it 'does not track for the force_company_trial experiment' do
allow(controller).to receive(:experiment).and_call_original
namespace.update!(created_at: 2.days.ago)
expect(controller).not_to receive(:experiment).with(:force_company_trial, user: user)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe CombinedRegistrationExperiment, :experiment do
subject { described_class.new(user: user) }
let_it_be(:user) { create(:user) }
describe '#redirect_path' do
it 'when control passes trial_params to path' do
stub_experiments(combined_registration: :control)
expect(subject.redirect_path(trial: true)).to eq(Rails.application.routes.url_helpers.new_users_sign_up_group_path(trial: true))
end
it 'when candidate returns path' do
stub_experiments(combined_registration: :candidate)
expect(subject.redirect_path(trial: true)).to eq(Rails.application.routes.url_helpers.new_users_sign_up_groups_project_path)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Combined registration flow', :js do
let_it_be(:user) { create(:user) }
let(:experiments) { {} }
before do
# https://gitlab.com/gitlab-org/gitlab/-/issues/338737
stub_const('Gitlab::QueryLimiting::Transaction::THRESHOLD', 250)
stub_experiments(experiments)
allow(Gitlab).to receive(:com?).and_return(true)
sign_in(user)
visit users_sign_up_welcome_path
expect(page).to have_content('Welcome to GitLab')
page.within('.bar') do
expect(page).to have_content('Your profile')
expect(page).to have_content('Your first project')
expect(page).not_to have_content('Your GitLab group')
end
choose 'My company or team'
click_on 'Continue'
end
context 'when combined_registration experiment variant is candidate' do
let(:experiments) { { combined_registration: :candidate } }
it 'A user can create a group and project' do
expect(page).to have_content('Your first project')
page.within '.js-group-path-display' do
expect(page).to have_content('{group}')
end
page.within '.js-project-path-display' do
expect(page).to have_content('{project}')
end
fill_in 'group_name', with: 'test group'
fill_in 'blank_project_name', with: 'test project'
page.within '.js-group-path-display' do
expect(page).to have_content('test-group')
end
page.within '.js-project-path-display' do
expect(page).to have_content('test-project')
end
click_on 'Create project'
expect(page).to have_content('Start your Free Ultimate Trial')
end
it 'a user can create a group and import a project' do
expect(page).to have_content('Your first project')
click_on 'Import'
page.within '.js-import-group-path-display' do
expect(page).to have_content('{group}')
end
click_on 'GitHub'
page.within('.gl-field-error') do
expect(page).to have_content('This field is required.')
end
fill_in 'import_group_name', with: 'test group'
page.within '.js-import-group-path-display' do
expect(page).to have_content('test-group')
end
click_on 'GitHub'
expect(page).to have_content('To connect GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories.')
end
end
end
import mountComponents from 'ee/registrations/groups_projects/new';
describe('importButtonsSubmit', () => {
const fixture = `
<div class="js-import-project-buttons">
<a href="/import/github">github</a>
</div>
<div class="js-import-project-form">
<input type="hidden" class="js-import-url" />
<input type="submit" />
</form>
`;
beforeEach(() => {
setFixtures(fixture);
mountComponents();
});
const findSubmit = () => document.querySelector('.js-import-project-form input[type="submit"]');
const findImportUrlValue = () => document.querySelector('.js-import-url').value;
const findImportGithubButton = () => document.querySelector('.js-import-project-buttons a');
it('sets the import-url field with the value of the href and clicks submit', () => {
const submitSpy = jest.spyOn(findSubmit(), 'click');
findImportGithubButton().click();
expect(findImportUrlValue()).toBe('/import/github');
expect(submitSpy).toHaveBeenCalled();
});
});
import {
displayGroupPath,
displayProjectPath,
} from 'ee/registrations/groups_projects/new/path_display';
import { useMockMutationObserver } from 'helpers/mock_dom_observer';
const fixture = `<input type='text' class='source'><div class='display'>original value<div>`;
beforeEach(() => {
setFixtures(fixture);
});
const findSource = () => document.querySelector('.source');
const displayValue = () => document.querySelector('.display').textContent;
describe('displayGroupPath', () => {
const { trigger: triggerMutate } = useMockMutationObserver();
beforeEach(() => {
displayGroupPath('.source', '.display');
});
const inputSource = (value) => {
const source = findSource();
source.value = value;
triggerMutate(source, {
entry: { attributeName: 'value' },
options: { attributes: true },
});
};
it('coppies values from the source to the display', () => {
inputSource('peanut-butter-jelly-time');
expect(displayValue()).toBe('peanut-butter-jelly-time');
});
});
describe('displayProjectPath', () => {
beforeEach(() => {
displayProjectPath('.source', '.display');
});
const inputSource = (value) => {
const source = findSource();
source.value = value;
source.dispatchEvent(new Event('input'));
};
it('displays the default display value when source is empty', () => {
expect(displayValue()).toBe('original value');
inputSource('its a peanut butter jelly');
expect(displayValue()).not.toBe('original value');
inputSource('');
expect(displayValue()).toBe('original value');
});
it('sluggifies values from the source to the display', () => {
inputSource('peanut butter jelly time');
expect(displayValue()).toBe('peanut-butter-jelly-time');
});
});
import { nextTick } from 'vue';
import showTooltip from 'ee/registrations/groups_projects/new/show_tooltip';
const fixture = `<div class='my-tooltip' title='this is a tooltip!'></div>`;
beforeEach(() => {
setFixtures(fixture);
});
const findBodyText = () => document.body.innerText;
describe('showTooltip', () => {
it('renders a tooltip immediately', async () => {
expect(findBodyText()).toBe('');
showTooltip('.my-tooltip');
await nextTick();
expect(findBodyText()).toBe('this is a tooltip!');
});
});
# frozen_string_literal: true
RSpec.shared_examples 'hides email confirmation warning' do
RSpec::Matchers.define :set_confirm_warning_for do |email|
match do |response|
expect(controller).to set_flash.now[:warning].to include("Please check your email (#{email}) to verify that you own this address and unlock the power of CI/CD.")
end
end
context 'with an unconfirmed email address present' do
let(:user) { create(:user, confirmed_at: nil, unconfirmed_email: 'unconfirmed@gitlab.com') }
it { is_expected.not_to set_confirm_warning_for(user.unconfirmed_email) }
end
context 'without an unconfirmed email address present' do
let(:user) { create(:user, confirmed_at: nil) }
it { is_expected.not_to set_confirm_warning_for(user.email) }
end
end
RSpec.shared_examples "Registrations::GroupsController GET #new" do
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
let(:dev_env_or_com) { true }
let(:learn_gitlab_context) do
{
in_experiment_group_a: false,
in_experiment_group_b: false
}
end
subject { get :new }
context 'with an unauthenticated user' do
it { is_expected.to have_gitlab_http_status(:redirect) }
it { is_expected.to redirect_to(new_user_session_path) }
end
context 'with an authenticated user' do
before do
sign_in(user)
allow(::Gitlab).to receive(:dev_env_or_com?).and_return(dev_env_or_com)
end
context 'when on .com' do
it { is_expected.to have_gitlab_http_status(:ok) }
it { is_expected.to render_template(:new) }
it 'assigns the group variable to a new Group with the default group visibility', :aggregate_failures do
subject
expect(assigns(:group)).to be_a_new(Group)
expect(assigns(:group).visibility_level).to eq(Gitlab::CurrentSettings.default_group_visibility)
end
context 'when the trial_registration_with_reassurance experiment is active', :experiment do
before do
stub_experiments(trial_registration_with_reassurance: :control)
end
it 'tracks a "render" event' do
expect(experiment(:trial_registration_with_reassurance)).to track(
:render,
user: user,
label: 'registrations:groups:new'
).with_context(actor: user).on_next_instance
get :new
end
end
context 'user without the ability to create a group' do
let(:user) { create(:user, can_create_group: false) }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
it 'tracks an event for the combined_registration experiment', :experiment do
expect(experiment(:combined_registration)).to track(:view_new_group_action).on_next_instance
subject
end
end
context 'when not on .com' do
let(:dev_env_or_com) { false }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
it_behaves_like 'hides email confirmation warning'
end
end
# frozen_string_literal: true
RSpec.shared_examples "Registrations::ProjectsController POST #create" do
include AfterNextHelpers
subject { post :create, params: { project: params }.merge(trial_onboarding_flow_params).merge(extra_params) }
let_it_be(:trial_onboarding_flow_params) { {} }
let(:params) { { namespace_id: namespace.id, name: 'New project', path: 'project-path', visibility_level: Gitlab::VisibilityLevel::PRIVATE } }
let(:dev_env_or_com) { true }
let(:extra_params) { {} }
let(:success_path) { nil }
context 'with an unauthenticated user' do
it { is_expected.to have_gitlab_http_status(:redirect) }
it { is_expected.to redirect_to(new_user_session_path) }
end
context 'with an authenticated user', :sidekiq_inline do
let_it_be(:first_project) { create(:project) }
before do
namespace.add_owner(user)
sign_in(user)
allow(::Gitlab).to receive(:dev_env_or_com?).and_return(dev_env_or_com)
allow(controller).to receive(:experiment).and_call_original
end
it 'creates a new project, a "Learn GitLab" project, sets a cookie and redirects to the success_path' do
allow_next_instance_of(::Projects::CreateService) do |service|
allow(service).to receive(:execute).and_return(first_project)
end
allow_next_instance_of(::Projects::GitlabProjectsImportService) do |service|
allow(service).to receive(:execute).and_return(project)
end
expect(subject).to have_gitlab_http_status(:redirect)
expect(subject).to redirect_to(success_path || continuous_onboarding_getting_started_users_sign_up_welcome_path(project_id: first_project.id))
end
context 'jobs_to_be_done experiment' do
let(:jobs_to_be_done_experiment) { experiment(:jobs_to_be_done_experiment) }
it 'tracks an event for the jobs_to_be_done experiment', :experiment do
allow(controller).to receive(:experiment).with(:jobs_to_be_done, user: user).and_return(jobs_to_be_done_experiment)
allow(jobs_to_be_done_experiment).to receive(:track).and_call_original
expect(jobs_to_be_done_experiment).to receive(:track).with(:create_project, project: an_instance_of(Project))
subject
end
end
context 'learn gitlab project' do
using RSpec::Parameterized::TableSyntax
where(:trial, :project_name, :template) do
false | 'Learn GitLab' | described_class::LEARN_GITLAB_TEMPLATE
true | 'Learn GitLab - Ultimate trial' | described_class::LEARN_GITLAB_ULTIMATE_TEMPLATE
end
with_them do
let(:path) { Rails.root.join('vendor', 'project_templates', template) }
let(:expected_arguments) { { namespace_id: namespace.id, file: handle, name: project_name } }
let(:handle) { double }
let(:trial_onboarding_flow_params) { { trial_onboarding_flow: trial } }
before do
allow(File).to receive(:open).and_call_original
expect(File).to receive(:open).with(path).and_yield(handle)
end
specify do
expect_next(::Projects::GitlabProjectsImportService, user, expected_arguments)
.to receive(:execute).and_return(project)
subject
end
end
end
context 'when the trial onboarding is active' do
let_it_be(:trial_onboarding_flow_params) { { trial_onboarding_flow: true } }
it 'creates a new project, a "Learn GitLab - Ultimate trial" project, does not set a cookie' do
expect { subject }.to change { namespace.projects.pluck(:name) }.from([]).to(['New project', s_('Learn GitLab - Ultimate trial')])
expect(subject).to have_gitlab_http_status(:redirect)
expect(namespace.projects.find_by_name(s_('Learn GitLab - Ultimate trial'))).to be_import_finished
end
it 'records context and redirects to the success page' do
expect_next_instance_of(::Projects::CreateService) do |service|
expect(service).to receive(:execute).and_return(first_project)
end
expect_next_instance_of(::Projects::GitlabProjectsImportService) do |service|
expect(service).to receive(:execute).and_return(project)
end
expect(subject).to redirect_to(trial_getting_started_users_sign_up_welcome_path(learn_gitlab_project_id: project.id))
end
end
context 'when the project cannot be saved' do
let(:params) { { name: '', path: '' } }
it 'does not create a project' do
expect { subject }.not_to change { Project.count }
end
it { is_expected.to have_gitlab_http_status(:ok) }
it { is_expected.to render_template(:new) }
end
context 'with signup onboarding not enabled' do
let(:dev_env_or_com) { false }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
end
end
......@@ -1203,6 +1203,9 @@ msgstr ""
msgid "."
msgstr ""
msgid "/"
msgstr ""
msgid "0 bytes"
msgstr ""
......@@ -9528,6 +9531,9 @@ msgstr ""
msgid "Create new..."
msgstr ""
msgid "Create or import your first project"
msgstr ""
msgid "Create project"
msgstr ""
......@@ -26621,9 +26627,15 @@ msgstr ""
msgid "Projects are graded based on the highest severity vulnerability present"
msgstr ""
msgid "Projects are organized into groups"
msgstr ""
msgid "Projects contributed to"
msgstr ""
msgid "Projects help you organize your work. They contain your file repository, issues, merge requests, and so much more."
msgstr ""
msgid "Projects shared with %{group_name}"
msgstr ""
......@@ -38189,6 +38201,9 @@ msgstr ""
msgid "You can also use project access tokens with Git to authenticate over HTTP(S). %{link_start}Learn more.%{link_end}"
msgstr ""
msgid "You can always change your URL later"
msgstr ""
msgid "You can always edit this later"
msgstr ""
......@@ -38888,6 +38903,9 @@ msgstr ""
msgid "Your project limit is %{limit} projects! Please contact your administrator to increase it"
msgstr ""
msgid "Your project will be created at:"
msgstr ""
msgid "Your projects"
msgstr ""
......@@ -40666,3 +40684,9 @@ msgstr ""
msgid "your settings"
msgstr ""
msgid "{group}"
msgstr ""
msgid "{project}"
msgstr ""
......@@ -346,6 +346,12 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(imported_project.import_data.data).to eq(import_data[:data])
expect(imported_project.import_url).to eq('http://import-url')
end
it 'tracks for the combined_registration experiment', :experiment do
expect(experiment(:combined_registration)).to track(:import_project).on_next_instance
imported_project
end
end
context 'builds_enabled global setting' do
......
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