Commit 4e1fbada authored by Peter Leitzen's avatar Peter Leitzen

Merge branch 'jswain_upload_button' into 'master'

Allow uploads from empty repo state [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!52755
parents 532a6c52 3fec2fa5
......@@ -3,6 +3,7 @@
import Dropzone from 'dropzone';
import $ from 'jquery';
import { sprintf, __ } from '~/locale';
import { trackUploadFileFormSubmitted } from '~/projects/upload_file_experiment';
import { HIDDEN_CLASS } from '../lib/utils/constants';
import csrf from '../lib/utils/csrf';
import { visitUrl } from '../lib/utils/url_utility';
......@@ -83,6 +84,9 @@ export default class BlobFileDropzone {
submitButton.on('click', (e) => {
e.preventDefault();
e.stopPropagation();
trackUploadFileFormSubmitted();
if (dropzone[0].dropzone.getQueuedFiles().length === 0) {
// eslint-disable-next-line no-alert
alert(__('Please select a file'));
......
......@@ -4,6 +4,7 @@ import $ from 'jquery';
import initPopover from '~/blob/suggest_gitlab_ci_yml';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils';
import { initUploadFileTrigger } from '~/projects/upload_file_experiment';
import Tracking from '~/tracking';
import BlobFileDropzone from '../blob/blob_file_dropzone';
import NewCommitForm from '../new_commit_form';
......@@ -47,6 +48,7 @@ export const initUploadForm = () => {
new NewCommitForm(uploadBlobForm);
disableButtonIfEmptyField(uploadBlobForm.find('.js-commit-message'), '.btn-upload-file');
initUploadFileTrigger();
}
};
......
import { get } from 'lodash';
import Tracking from '~/tracking';
const TRACKING_CONTEXT_SCHEMA = 'iglu:com.gitlab/gitlab_experiment/jsonschema/1-0-0';
export default class ExperimentTracking {
constructor(experimentName, { label } = {}) {
this.label = label;
this.experimentData = get(window, ['gon', 'global', 'experiment', experimentName]);
}
event(action) {
if (!this.experimentData) {
return false;
}
return Tracking.event(document.body.dataset.page, action, {
label: this.label,
context: {
schema: TRACKING_CONTEXT_SCHEMA,
data: this.experimentData,
},
});
}
}
......@@ -24,9 +24,12 @@ new UserCallout({
});
// Project show page loads different overview content based on user preferences
const treeSlider = document.getElementById('js-tree-list');
if (treeSlider) {
if (document.querySelector('.js-upload-blob-form')) {
initUploadForm();
}
if (document.getElementById('js-tree-list')) {
initTree();
}
......
import ExperimentTracking from '~/experiment_tracking';
function trackEvent(eventName) {
const Tracking = new ExperimentTracking('empty_repo_upload', { label: 'blob-upload-modal' });
Tracking.event(eventName);
}
export function initUploadFileTrigger() {
const uploadFileTriggerEl = document.querySelector('.js-upload-file-experiment-trigger');
if (uploadFileTriggerEl) {
uploadFileTriggerEl.addEventListener('click', () => {
trackEvent('click_upload_modal_trigger');
});
}
}
export function trackUploadFileFormSubmitted() {
trackEvent('click_upload_modal_form_submit');
}
......@@ -329,6 +329,8 @@ class ProjectsController < Projects::ApplicationController
if can?(current_user, :download_code, @project)
return render 'projects/no_repo' unless @project.repository_exists?
experiment(:empty_repo_upload, project: @project).track(:view_project_show) if @project.can_current_user_push_to_default_branch?
if @project.empty_repo?
record_experiment_user(:invite_members_empty_project_version_a)
......
......@@ -5,13 +5,14 @@ module StatAnchorsHelper
{}.tap do |attrs|
attrs[:class] = %w(nav-link gl-display-flex gl-align-items-center) << extra_classes(anchor)
attrs[:itemprop] = anchor.itemprop if anchor.itemprop
attrs[:data] = anchor.data if anchor.data
end
end
private
def button_attribute(anchor)
"btn-#{anchor.class_modifier || 'dashed'}"
anchor.class_modifier || 'btn-dashed'
end
def extra_classes(anchor)
......
......@@ -10,10 +10,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
include BlobHelper
include ChecksCollaboration
include Gitlab::Utils::StrongMemoize
include Gitlab::Experiment::Dsl
presents :project
AnchorData = Struct.new(:is_link, :label, :link, :class_modifier, :icon, :itemprop)
AnchorData = Struct.new(:is_link, :label, :link, :class_modifier, :icon, :itemprop, :data)
MAX_TOPICS_TO_SHOW = 3
def statistic_icon(icon_name = 'plus-square-o')
......@@ -33,6 +34,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def statistics_buttons(show_auto_devops_callout:)
[
upload_anchor_data,
readme_anchor_data,
license_anchor_data,
changelog_anchor_data,
......@@ -49,6 +51,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def empty_repo_statistics_buttons
[
upload_anchor_data,
new_file_anchor_data,
readme_anchor_data,
license_anchor_data,
......@@ -154,6 +157,8 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def can_current_user_push_to_branch?(branch)
return false unless current_user
user_access(project).can_push_to_branch?(branch)
end
......@@ -232,19 +237,47 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
empty_repo? ? nil : project_tags_path(project))
end
def upload_anchor_data
strong_memoize(:upload_anchor_data) do
next unless can_current_user_push_to_default_branch?
experiment(:empty_repo_upload, project: project) do |e|
e.use {}
e.try do
AnchorData.new(false,
statistic_icon('upload') + _('Upload file'),
'#modal-upload-blob',
'js-upload-file-experiment-trigger',
nil,
nil,
{
'toggle' => 'modal',
'target' => '#modal-upload-blob'
}
)
end
e.run
end
end
end
def empty_repo_upload_experiment?
upload_anchor_data.present?
end
def new_file_anchor_data
if current_user && can_current_user_push_to_default_branch?
if can_current_user_push_to_default_branch?
new_file_path = empty_repo? ? ide_edit_path(project, default_branch_or_master) : project_new_blob_path(project, default_branch_or_master)
AnchorData.new(false,
statistic_icon + _('New file'),
new_file_path,
'dashed')
'btn-dashed')
end
end
def readme_anchor_data
if current_user && can_current_user_push_to_default_branch? && readme_path.nil?
if can_current_user_push_to_default_branch? && readme_path.nil?
AnchorData.new(false,
statistic_icon + _('Add README'),
empty_repo? ? add_readme_ide_path : add_readme_path)
......@@ -252,13 +285,13 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
AnchorData.new(false,
statistic_icon('doc-text') + _('README'),
default_view != 'readme' ? readme_path : '#readme',
'default',
'btn-default',
'doc-text')
end
end
def changelog_anchor_data
if current_user && can_current_user_push_to_default_branch? && repository.changelog.blank?
if can_current_user_push_to_default_branch? && repository.changelog.blank?
AnchorData.new(false,
statistic_icon + _('Add CHANGELOG'),
empty_repo? ? add_changelog_ide_path : add_changelog_path)
......@@ -266,7 +299,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
AnchorData.new(false,
statistic_icon('doc-text') + _('CHANGELOG'),
changelog_path,
'default')
'btn-default')
end
end
......@@ -277,11 +310,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
AnchorData.new(false,
icon + content_tag(:span, license_short_name, class: 'project-stat-value'),
license_path,
'default',
'btn-default',
nil,
'license')
else
if current_user && can_current_user_push_to_default_branch?
if can_current_user_push_to_default_branch?
AnchorData.new(false,
content_tag(:span, statistic_icon + _('Add LICENSE'), class: 'add-license-link d-flex'),
empty_repo? ? add_license_ide_path : add_license_path)
......@@ -294,7 +327,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def contribution_guide_anchor_data
if current_user && can_current_user_push_to_default_branch? && repository.contribution_guide.blank?
if can_current_user_push_to_default_branch? && repository.contribution_guide.blank?
AnchorData.new(false,
statistic_icon + _('Add CONTRIBUTING'),
empty_repo? ? add_contribution_guide_ide_path : add_contribution_guide_path)
......@@ -302,7 +335,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
AnchorData.new(false,
statistic_icon('doc-text') + _('CONTRIBUTING'),
contribution_guide_path,
'default')
'btn-default')
end
end
......@@ -312,7 +345,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
AnchorData.new(false,
statistic_icon('settings') + _('Auto DevOps enabled'),
project_settings_ci_cd_path(project, anchor: 'autodevops-settings'),
'default')
'btn-default')
else
AnchorData.new(false,
statistic_icon + _('Enable Auto DevOps'),
......@@ -337,7 +370,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
AnchorData.new(false,
_('Kubernetes'),
cluster_link,
'default')
'btn-default')
end
end
end
......@@ -351,7 +384,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
AnchorData.new(false,
statistic_icon('doc-text') + _('CI/CD configuration'),
ci_configuration_path,
'default')
'btn-default')
end
end
......
......@@ -17,7 +17,7 @@
%br
.dropzone-alerts.gl-alert.gl-alert-danger.gl-mb-5.data{ style: "display:none" }
= render 'shared/new_commit_form', placeholder: placeholder
= render 'shared/new_commit_form', placeholder: placeholder, ref: local_assigns[:ref]
.form-actions
= button_tag class: 'btn gl-button btn-success btn-upload-file', id: 'submit-all', type: 'button' do
......
- @content_class = "limit-container-width" unless fluid_layout
- default_branch_name = @project.default_branch || "master"
- default_branch_name = @project.default_branch_or_master
- @skip_current_level_breadcrumb = true
= content_for :invite_members_sidebar do
......@@ -77,3 +77,6 @@
%span><
git push -u origin --all
git push -u origin --tags
- if @project.empty_repo_upload_experiment?
= render 'projects/blob/upload', title: _('Upload New File'), placeholder: _('Upload New File'), button_title: _('Upload file'), form_path: project_create_blob_path(@project, default_branch_name), ref: default_branch_name, method: :post
......@@ -3,8 +3,11 @@
= render 'shared/commit_message_container', placeholder: placeholder
- if @project.empty_repo?
= hidden_field_tag 'branch_name', @ref
- if project.empty_repo?
- ref = local_assigns[:ref] || @ref
- branch_name_class = project.empty_repo_upload_experiment? ? 'js-branch-name' : nil
= hidden_field_tag 'branch_name', ref, class: branch_name_class
- else
- if can?(current_user, :push_code, @project)
.form-group.row.branch
......
......@@ -123,6 +123,7 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
def after_project_changes_hooks(project, user, refs, changes)
experiment(:new_project_readme, actor: user).track_initial_writes(project)
experiment(:empty_repo_upload, project: project).track(:initial_write) if project.empty_repo?
repository_update_hook_data = Gitlab::DataBuilder::Repository.update(project, user, changes, refs)
SystemHooksService.new.execute_hooks(repository_update_hook_data, :repository_update_hooks)
Gitlab::UsageDataCounters::SourceCodeCounter.count(:pushes)
......
---
name: empty_repo_upload
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52755
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/285296
milestone: '13.9'
type: experiment
group: group::adoption
default_enabled: false
......@@ -221,6 +221,20 @@ RSpec.describe ProjectsController do
allow(controller).to receive(:record_experiment_user)
end
context 'when user can push to default branch' do
let(:user) { empty_project.owner }
it 'creates an "view_project_show" experiment tracking event', :snowplow do
allow_next_instance_of(ApplicationExperiment) do |e|
allow(e).to receive(:should_track?).and_return(true)
end
get :show, params: { namespace_id: empty_project.namespace, id: empty_project }
expect_snowplow_event(category: 'empty_repo_upload', action: 'view_project_show', context: [{ schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/0-3-0', data: anything }])
end
end
User.project_views.keys.each do |project_view|
context "with #{project_view} view set" do
before do
......
......@@ -33,4 +33,34 @@ RSpec.describe 'Projects > Show > User uploads files' do
include_examples 'it uploads and commit a new file to a forked project'
end
context 'with an empty repo' do
let(:project) { create(:project, :empty_repo, creator: user) }
context 'when in the empty_repo_upload experiment' do
before do
stub_experiments(empty_repo_upload: :candidate)
visit(project_path(project))
end
it 'uploads and commits a new text file', :js do
click_link('Upload file')
drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'))
page.within('#modal-upload-blob') do
fill_in(:commit_message, with: 'New commit message')
end
click_button('Upload file')
wait_for_requests
expect(page).to have_content('New commit message')
expect(page).to have_content('Lorem ipsum dolor sit amet')
expect(page).to have_content('Sed ut perspiciatis unde omnis')
end
end
end
end
import $ from 'jquery';
import BlobFileDropzone from '~/blob/blob_file_dropzone';
import { trackUploadFileFormSubmitted } from '~/projects/upload_file_experiment';
jest.mock('~/projects/upload_file_experiment', () => ({
trackUploadFileFormSubmitted: jest.fn(),
}));
describe('BlobFileDropzone', () => {
preloadFixtures('blob/show.html');
......@@ -41,5 +46,13 @@ describe('BlobFileDropzone', () => {
expect(replaceFileButton.is(':disabled')).toEqual(true);
expect(dropzone.processQueue).toHaveBeenCalled();
});
it('calls the tracking event', () => {
jest.spyOn(window, 'alert').mockImplementation(() => {});
replaceFileButton.click();
expect(trackUploadFileFormSubmitted).toHaveBeenCalled();
});
});
});
import ExperimentTracking from '~/experiment_tracking';
import Tracking from '~/tracking';
jest.mock('~/tracking');
const oldGon = window.gon;
let experimentTracking;
let label;
let newGon = {};
const setup = () => {
window.gon = newGon;
experimentTracking = new ExperimentTracking('sidebar_experiment', label);
};
beforeEach(() => {
document.body.dataset.page = 'issues-page';
});
afterEach(() => {
window.gon = oldGon;
Tracking.mockClear();
label = undefined;
});
describe('event', () => {
describe('when experiment data exists for experimentName', () => {
beforeEach(() => {
newGon = { global: { experiment: { sidebar_experiment: 'experiment-data' } } };
setup();
});
describe('when providing options', () => {
label = { label: 'sidebar-drawer' };
it('passes them to the tracking call', () => {
experimentTracking.event('click_sidebar_close');
expect(Tracking.event).toHaveBeenCalledTimes(1);
expect(Tracking.event).toHaveBeenCalledWith('issues-page', 'click_sidebar_close', {
label: 'sidebar-drawer',
context: {
schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/1-0-0',
data: 'experiment-data',
},
});
});
});
it('tracks with the correct context', () => {
experimentTracking.event('click_sidebar_trigger');
expect(Tracking.event).toHaveBeenCalledTimes(1);
expect(Tracking.event).toHaveBeenCalledWith('issues-page', 'click_sidebar_trigger', {
label: undefined,
context: {
schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/1-0-0',
data: 'experiment-data',
},
});
});
});
describe('when experiment data does NOT exists for the experimentName', () => {
beforeEach(() => {
newGon = { global: { experiment: { unrelated_experiment: 'not happening' } } };
setup();
});
it('does not track', () => {
experimentTracking.event('click_sidebar_close');
expect(Tracking.event).not.toHaveBeenCalled();
});
});
});
import ExperimentTracking from '~/experiment_tracking';
import * as UploadFileExperiment from '~/projects/upload_file_experiment';
const mockExperimentTrackingEvent = jest.fn();
jest.mock('~/experiment_tracking', () =>
jest.fn().mockImplementation(() => ({
event: mockExperimentTrackingEvent,
})),
);
const fixture = `<a class='js-upload-file-experiment-trigger' data-toggle='modal' data-target='#modal-upload-blob'></a><div id='modal-upload-blob'></div>`;
const findModal = () => document.querySelector('[aria-modal="true"]');
const findTrigger = () => document.querySelector('.js-upload-file-experiment-trigger');
beforeEach(() => {
ExperimentTracking.mockClear();
mockExperimentTrackingEvent.mockClear();
document.body.innerHTML = fixture;
});
afterEach(() => {
document.body.innerHTML = '';
});
describe('trackUploadFileFormSubmitted', () => {
it('initializes ExperimentTracking with the correct arguments and calls the tracking event with correct arguments', () => {
UploadFileExperiment.trackUploadFileFormSubmitted();
expect(ExperimentTracking).toHaveBeenCalledWith('empty_repo_upload', {
label: 'blob-upload-modal',
});
expect(mockExperimentTrackingEvent).toHaveBeenCalledWith('click_upload_modal_form_submit');
});
});
describe('initUploadFileTrigger', () => {
it('calls modal and tracks event', () => {
UploadFileExperiment.initUploadFileTrigger();
expect(findModal()).not.toExist();
findTrigger().click();
expect(findModal()).toExist();
expect(mockExperimentTrackingEvent).toHaveBeenCalledWith('click_upload_modal_trigger');
});
});
......@@ -18,7 +18,7 @@ RSpec.describe StatAnchorsHelper do
context 'when anchor is not a link' do
context 'when class_modifier is set' do
let(:anchor) { anchor_klass.new(false, nil, nil, 'default') }
let(:anchor) { anchor_klass.new(false, nil, nil, 'btn-default') }
it 'returns the proper attributes' do
expect(subject[:class]).to include('gl-button btn btn-default')
......@@ -49,5 +49,21 @@ RSpec.describe StatAnchorsHelper do
expect(subject[:itemprop]).to eq true
end
end
context 'when data is not set' do
let(:anchor) { anchor_klass.new(false, nil, nil, nil, nil, nil, nil) }
it 'returns the data attributes' do
expect(subject[:data]).to be_nil
end
end
context 'when itemprop is set' do
let(:anchor) { anchor_klass.new(false, nil, nil, nil, nil, nil, { 'toggle' => 'modal' }) }
it 'returns the data attributes' do
expect(subject[:data]).to eq({ 'toggle' => 'modal' })
end
end
end
end
......@@ -183,6 +183,14 @@ RSpec.describe ProjectPresenter do
context 'not empty repo' do
let(:project) { create(:project, :repository) }
context 'if no current user' do
let(:user) { nil }
it 'returns false' do
expect(presenter.can_current_user_push_code?).to be(false)
end
end
it 'returns true if user can push to default branch' do
project.add_developer(user)
......@@ -350,7 +358,7 @@ RSpec.describe ProjectPresenter do
is_link: false,
label: a_string_including("New file"),
link: presenter.project_new_blob_path(project, 'master'),
class_modifier: 'dashed'
class_modifier: 'btn-dashed'
)
end
......@@ -597,10 +605,12 @@ RSpec.describe ProjectPresenter do
context 'for a developer' do
before do
project.add_developer(user)
stub_experiments(empty_repo_upload: :candidate)
end
it 'orders the items correctly' do
expect(empty_repo_statistics_buttons.map(&:label)).to start_with(
a_string_including('Upload'),
a_string_including('New'),
a_string_including('README'),
a_string_including('LICENSE'),
......@@ -609,6 +619,16 @@ RSpec.describe ProjectPresenter do
a_string_including('CI/CD')
)
end
context 'when not in the upload experiment' do
before do
stub_experiments(empty_repo_upload: :control)
end
it 'does not include upload button' do
expect(empty_repo_statistics_buttons.map(&:label)).not_to start_with(a_string_including('Upload'))
end
end
end
end
......@@ -694,4 +714,20 @@ RSpec.describe ProjectPresenter do
end
end
end
describe 'empty_repo_upload_experiment?' do
subject { presenter.empty_repo_upload_experiment? }
it 'returns false when upload_anchor_data is nil' do
allow(presenter).to receive(:upload_anchor_data).and_return(nil)
expect(subject).to be false
end
it 'returns true when upload_anchor_data exists' do
allow(presenter).to receive(:upload_anchor_data).and_return(true)
expect(subject).to be true
end
end
end
......@@ -3,7 +3,13 @@
RSpec.shared_examples 'it uploads and commit a new text file' do
it 'uploads and commit a new text file', :js do
find('.add-to-tree').click
page.within('.dropdown-menu') do
click_link('Upload file')
wait_for_requests
end
drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'))
page.within('#modal-upload-blob') do
......@@ -29,7 +35,13 @@ end
RSpec.shared_examples 'it uploads and commit a new image file' do
it 'uploads and commit a new image file', :js do
find('.add-to-tree').click
page.within('.dropdown-menu') do
click_link('Upload file')
wait_for_requests
end
drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'logo_sample.svg'))
page.within('#modal-upload-blob') do
......
......@@ -93,6 +93,29 @@ RSpec.describe PostReceive do
perform
end
it 'tracks an event for the empty_repo_upload experiment', :snowplow do
allow_next_instance_of(ApplicationExperiment) do |e|
allow(e).to receive(:should_track?).and_return(true)
allow(e).to receive(:track_initial_writes)
end
perform
expect_snowplow_event(category: 'empty_repo_upload', action: 'initial_write', context: [{ schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/0-3-0', data: anything }])
end
it 'does not track an event for the empty_repo_upload experiment when project is not empty', :snowplow do
allow(empty_project).to receive(:empty_repo?).and_return(false)
allow_next_instance_of(ApplicationExperiment) do |e|
allow(e).to receive(:should_track?).and_return(true)
allow(e).to receive(:track_initial_writes)
end
perform
expect_no_snowplow_event
end
end
shared_examples 'not updating remote mirrors' do
......@@ -159,7 +182,7 @@ RSpec.describe PostReceive do
end
it 'expires the status cache' do
expect(project.repository).to receive(:empty?).and_return(true)
expect(project.repository).to receive(:empty?).at_least(:once).and_return(true)
expect(project.repository).to receive(:expire_status_cache)
perform
......
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