summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJacopo <beschi.jacopo@gmail.com>2016-12-22 07:21:40 +0100
committerJacopo <beschi.jacopo@gmail.com>2017-03-13 16:36:11 +0100
commite0fe2834ebe6d9678444c8a10b79ca093f944232 (patch)
treea2d7c482f7271d8af7ba47e76c9d48a1d3e357f4
parent9ed3db915026c6e0cd266a1c276386e3e96d2151 (diff)
downloadgitlab-ce-e0fe2834ebe6d9678444c8a10b79ca093f944232.tar.gz
New file from interface on existing branch
Now you can create a new file and select a target_branch != source_branch. If the file that you want to create already exists on the target branch an error message is shown A glDropdown is used to select and create a new branch instead of a text field.
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js2
-rw-r--r--app/assets/javascripts/blob/create_branch_dropdown.js88
-rw-r--r--app/assets/javascripts/blob/target_branch_dropdown.js152
-rw-r--r--app/assets/javascripts/dispatcher.js26
-rw-r--r--app/assets/javascripts/main.js2
-rw-r--r--app/assets/javascripts/new_commit_form.js12
-rw-r--r--app/assets/stylesheets/pages/projects.scss3
-rw-r--r--app/controllers/projects/blob_controller.rb7
-rw-r--r--app/controllers/projects/branches_controller.rb13
-rw-r--r--app/views/projects/commit/_change.html.haml2
-rw-r--r--app/views/shared/_branch_switcher.html.haml8
-rw-r--r--app/views/shared/_new_commit_form.html.haml2
-rw-r--r--app/views/shared/projects/blob/_branch_page_create.html.haml8
-rw-r--r--app/views/shared/projects/blob/_branch_page_default.html.haml10
-rw-r--r--changelogs/unreleased/24501-new-file-existing-branch.yml4
-rw-r--r--features/steps/project/source/browse_files.rb5
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb14
-rw-r--r--spec/features/projects/blobs/user_create_spec.rb107
-rw-r--r--spec/javascripts/blob/create_branch_dropdown_spec.js109
-rw-r--r--spec/javascripts/blob/target_branch_dropdown_spec.js121
-rw-r--r--spec/javascripts/fixtures/project_branches.json5
-rw-r--r--spec/javascripts/fixtures/target_branch_dropdown.html.haml28
22 files changed, 710 insertions, 18 deletions
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index 5f14ff40eee..8f6bf162d6e 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -36,7 +36,7 @@
this.removeFile(file);
});
return this.on('sending', function(file, xhr, formData) {
- formData.append('target_branch', form.find('.js-target-branch').val());
+ formData.append('target_branch', form.find('input[name="target_branch"]').val());
formData.append('create_merge_request', form.find('.js-create-merge-request').val());
formData.append('commit_message', form.find('.js-commit-message').val());
});
diff --git a/app/assets/javascripts/blob/create_branch_dropdown.js b/app/assets/javascripts/blob/create_branch_dropdown.js
new file mode 100644
index 00000000000..95517f51b1c
--- /dev/null
+++ b/app/assets/javascripts/blob/create_branch_dropdown.js
@@ -0,0 +1,88 @@
+class CreateBranchDropdown {
+ constructor(el, targetBranchDropdown) {
+ this.targetBranchDropdown = targetBranchDropdown;
+ this.el = el;
+ this.dropdownBack = this.el.closest('.dropdown').querySelector('.dropdown-menu-back');
+ this.cancelButton = this.el.querySelector('.js-cancel-branch-btn');
+ this.newBranchField = this.el.querySelector('#new_branch_name');
+ this.newBranchCreateButton = this.el.querySelector('.js-new-branch-btn');
+
+ this.newBranchCreateButton.setAttribute('disabled', '');
+
+ this.addBindings();
+ this.cleanupWrapper = this.cleanup.bind(this);
+ document.addEventListener('beforeunload', this.cleanupWrapper);
+ }
+
+ cleanup() {
+ this.cleanBindings();
+ document.removeEventListener('beforeunload', this.cleanupWrapper);
+ }
+
+ cleanBindings() {
+ this.newBranchField.removeEventListener('keyup', this.enableBranchCreateButtonWrapper);
+ this.newBranchField.removeEventListener('change', this.enableBranchCreateButtonWrapper);
+ this.newBranchField.removeEventListener('keydown', this.handleNewBranchKeydownWrapper);
+ this.dropdownBack.removeEventListener('click', this.resetFormWrapper);
+ this.cancelButton.removeEventListener('click', this.handleCancelClickWrapper);
+ this.newBranchCreateButton.removeEventListener('click', this.createBranchWrapper);
+ }
+
+ addBindings() {
+ this.enableBranchCreateButtonWrapper = this.enableBranchCreateButton.bind(this);
+ this.handleNewBranchKeydownWrapper = this.handleNewBranchKeydown.bind(this);
+ this.resetFormWrapper = this.resetForm.bind(this);
+ this.handleCancelClickWrapper = this.handleCancelClick.bind(this);
+ this.createBranchWrapper = this.createBranch.bind(this);
+
+ this.newBranchField.addEventListener('keyup', this.enableBranchCreateButtonWrapper);
+ this.newBranchField.addEventListener('change', this.enableBranchCreateButtonWrapper);
+ this.newBranchField.addEventListener('keydown', this.handleNewBranchKeydownWrapper);
+ this.dropdownBack.addEventListener('click', this.resetFormWrapper);
+ this.cancelButton.addEventListener('click', this.handleCancelClickWrapper);
+ this.newBranchCreateButton.addEventListener('click', this.createBranchWrapper);
+ }
+
+ handleCancelClick(e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.resetForm();
+ this.dropdownBack.click();
+ }
+
+ handleNewBranchKeydown(e) {
+ const keyCode = e.which;
+ const ENTER_KEYCODE = 13;
+ if (keyCode === ENTER_KEYCODE) {
+ this.createBranch(e);
+ }
+ }
+
+ enableBranchCreateButton() {
+ if (this.newBranchField.value !== '') {
+ this.newBranchCreateButton.removeAttribute('disabled');
+ } else {
+ this.newBranchCreateButton.setAttribute('disabled', '');
+ }
+ }
+
+ resetForm() {
+ this.newBranchField.value = '';
+ this.enableBranchCreateButtonWrapper();
+ }
+
+ createBranch(e) {
+ e.preventDefault();
+
+ if (this.newBranchCreateButton.getAttribute('disabled') === '') {
+ return;
+ }
+ const newBranchName = this.newBranchField.value;
+ this.targetBranchDropdown.setNewBranch(newBranchName);
+ this.resetForm();
+ }
+}
+
+window.gl = window.gl || {};
+gl.CreateBranchDropdown = CreateBranchDropdown;
diff --git a/app/assets/javascripts/blob/target_branch_dropdown.js b/app/assets/javascripts/blob/target_branch_dropdown.js
new file mode 100644
index 00000000000..216f069ef71
--- /dev/null
+++ b/app/assets/javascripts/blob/target_branch_dropdown.js
@@ -0,0 +1,152 @@
+/* eslint-disable class-methods-use-this */
+const SELECT_ITEM_MSG = 'Select';
+
+class TargetBranchDropDown {
+ constructor(dropdown) {
+ this.dropdown = dropdown;
+ this.$dropdown = $(dropdown);
+ this.fieldName = this.dropdown.getAttribute('data-field-name');
+ this.form = this.dropdown.closest('form');
+ this.createDropdown();
+ }
+
+ static bootstrap() {
+ const dropdowns = document.querySelectorAll('.js-project-branches-dropdown');
+ [].forEach.call(dropdowns, dropdown => new TargetBranchDropDown(dropdown));
+ }
+
+ createDropdown() {
+ const self = this;
+ this.$dropdown.glDropdown({
+ selectable: true,
+ filterable: true,
+ search: {
+ fields: ['title'],
+ },
+ data: (term, callback) => $.ajax({
+ url: self.dropdown.getAttribute('data-refs-url'),
+ data: {
+ ref: self.dropdown.getAttribute('data-ref'),
+ show_all: true,
+ },
+ dataType: 'json',
+ }).done(refs => callback(self.dropdownData(refs))),
+ toggleLabel(item, el) {
+ if (el.is('.is-active')) {
+ return item.text;
+ }
+ return SELECT_ITEM_MSG;
+ },
+ clicked(item, el, e) {
+ e.preventDefault();
+ self.onClick.call(self);
+ },
+ fieldName: self.fieldName,
+ });
+ return new gl.CreateBranchDropdown(this.form.querySelector('.dropdown-new-branch'), this);
+ }
+
+ onClick() {
+ this.enableSubmit();
+ this.$dropdown.trigger('change.branch');
+ }
+
+ enableSubmit() {
+ const submitBtn = this.form.querySelector('[type="submit"]');
+ if (this.branchInput && this.branchInput.value) {
+ submitBtn.removeAttribute('disabled');
+ } else {
+ submitBtn.setAttribute('disabled', '');
+ }
+ }
+
+ dropdownData(refs) {
+ const branchList = this.dropdownItems(refs);
+ this.cachedRefs = refs;
+ this.addDefaultBranch(branchList);
+ this.addNewBranch(branchList);
+ return { Branches: branchList };
+ }
+
+ dropdownItems(refs) {
+ return refs.map(this.dropdownItem);
+ }
+
+ dropdownItem(ref) {
+ return { id: ref, text: ref, title: ref };
+ }
+
+ addDefaultBranch(branchList) {
+ // when no branch is selected do nothing
+ if (!this.branchInput) {
+ return;
+ }
+
+ const branchInputVal = this.branchInput.value;
+ const currentBranchIndex = this.searchBranch(branchList, branchInputVal);
+
+ if (currentBranchIndex === -1) {
+ this.unshiftBranch(branchList, this.dropdownItem(branchInputVal));
+ }
+ }
+
+ addNewBranch(branchList) {
+ if (this.newBranch) {
+ this.unshiftBranch(branchList, this.newBranch);
+ }
+ }
+
+ searchBranch(branchList, branchName) {
+ return _.findIndex(branchList, el => branchName === el.id);
+ }
+
+ unshiftBranch(branchList, branch) {
+ const branchIndex = this.searchBranch(branchList, branch.id);
+
+ if (branchIndex === -1) {
+ branchList.unshift(branch);
+ }
+ }
+
+ setNewBranch(newBranchName) {
+ this.newBranch = this.dropdownItem(newBranchName);
+ this.refreshData();
+ this.selectBranch(this.searchBranch(this.glDropdown.fullData.Branches, newBranchName));
+ }
+
+ refreshData() {
+ this.glDropdown.fullData = this.dropdownData(this.cachedRefs);
+ this.clearFilter();
+ }
+
+ clearFilter() {
+ // apply an empty filter in order to refresh the data
+ this.glDropdown.filter.filter('');
+ this.dropdown.closest('.dropdown').querySelector('.dropdown-page-one .dropdown-input-field').value = '';
+ }
+
+ selectBranch(index) {
+ const branch = this.dropdown.closest('.dropdown').querySelectorAll('li a')[index];
+
+ if (!branch.classList.contains('is-active')) {
+ branch.click();
+ } else {
+ this.closeDropdown();
+ }
+ }
+
+ closeDropdown() {
+ this.dropdown.closest('.dropdown').querySelector('.dropdown-menu-close').click();
+ }
+
+ get branchInput() {
+ return this.form.querySelector(`input[name="${this.fieldName}"]`);
+ }
+
+ get glDropdown() {
+ return this.$dropdown.data('glDropdown');
+ }
+}
+
+window.gl = window.gl || {};
+gl.TargetBranchDropDown = TargetBranchDropDown;
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 7b9b9123c31..5739a28699f 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -59,7 +59,7 @@ const UserCallout = require('./user_callout');
}
Dispatcher.prototype.initPageScripts = function() {
- var page, path, shortcut_handler;
+ var page, path, shortcut_handler, fileBlobPermalinkUrlElement, fileBlobPermalinkUrl;
page = $('body').attr('data-page');
if (!page) {
return false;
@@ -245,16 +245,36 @@ const UserCallout = require('./user_callout');
case 'projects:tree:show':
shortcut_handler = new ShortcutsNavigation();
new TreeView();
+ gl.TargetBranchDropDown.bootstrap();
break;
case 'projects:find_file:show':
shortcut_handler = true;
break;
+ case 'projects:blob:new':
+ gl.TargetBranchDropDown.bootstrap();
+ break;
+ case 'projects:blob:create':
+ gl.TargetBranchDropDown.bootstrap();
+ break;
case 'projects:blob:show':
+ gl.TargetBranchDropDown.bootstrap();
+ new LineHighlighter();
+ shortcut_handler = new ShortcutsNavigation();
+ fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url');
+ fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
+ new ShortcutsBlob({
+ skipResetBindings: true,
+ fileBlobPermalinkUrl,
+ });
+ break;
+ case 'projects:blob:edit':
+ gl.TargetBranchDropDown.bootstrap();
+ break;
case 'projects:blame:show':
new LineHighlighter();
shortcut_handler = new ShortcutsNavigation();
- const fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url');
- const fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
+ fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url');
+ fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
new ShortcutsBlob({
skipResetBindings: true,
fileBlobPermalinkUrl,
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 689a6c3a93a..604ed91627a 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -66,6 +66,8 @@ import './blob/blob_gitignore_selectors';
import './blob/blob_license_selector';
import './blob/blob_license_selectors';
import './blob/template_selector';
+import './blob/create_branch_dropdown';
+import './blob/target_branch_dropdown';
// templates
import './templates/issuable_template_selector';
diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js
index 747f693726e..ad36f08840d 100644
--- a/app/assets/javascripts/new_commit_form.js
+++ b/app/assets/javascripts/new_commit_form.js
@@ -3,19 +3,23 @@
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.NewCommitForm = (function() {
- function NewCommitForm(form) {
+ function NewCommitForm(form, targetBranchName = 'target_branch') {
+ this.form = form;
+ this.targetBranchName = targetBranchName;
this.renderDestination = bind(this.renderDestination, this);
- this.newBranch = form.find('.js-target-branch');
+ this.targetBranchDropdown = form.find('button.js-target-branch');
this.originalBranch = form.find('.js-original-branch');
this.createMergeRequest = form.find('.js-create-merge-request');
this.createMergeRequestContainer = form.find('.js-create-merge-request-container');
+ this.targetBranchDropdown.on('change.branch', this.renderDestination);
this.renderDestination();
- this.newBranch.keyup(this.renderDestination);
}
NewCommitForm.prototype.renderDestination = function() {
var different;
- different = this.newBranch.val() !== this.originalBranch.val();
+ var targetBranch = this.form.find(`input[name="${this.targetBranchName}"]`);
+
+ different = targetBranch.val() !== this.originalBranch.val();
if (different) {
this.createMergeRequestContainer.show();
if (!this.wasDifferent) {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 4914933430f..efa47be9a73 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -795,7 +795,8 @@ pre.light-well {
}
.project-refs-form .dropdown-menu,
-.dropdown-menu-projects {
+.dropdown-menu-projects,
+.dropdown-menu-branches {
width: 300px;
@media (min-width: $screen-sm-min) {
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 21ed0660762..52fc67d162c 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -23,6 +23,8 @@ class Projects::BlobController < Projects::ApplicationController
end
def create
+ update_ref
+
create_commit(Files::CreateService, success_notice: "The file has been successfully created.",
success_path: -> { namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) },
failure_view: :new,
@@ -87,6 +89,11 @@ class Projects::BlobController < Projects::ApplicationController
private
+ def update_ref
+ branch_exists = @repository.find_branch(@target_branch)
+ @ref = @target_branch if branch_exists
+ end
+
def blob
@blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path))
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index c40f9b7f75f..22714d9c5a4 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -10,15 +10,16 @@ class Projects::BranchesController < Projects::ApplicationController
def index
@sort = params[:sort].presence || sort_value_name
@branches = BranchesFinder.new(@repository, params).execute
- @branches = Kaminari.paginate_array(@branches).page(params[:page])
- @max_commits = @branches.reduce(0) do |memo, branch|
- diverging_commit_counts = repository.diverging_commit_counts(branch)
- [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
- end
+ @branches = Kaminari.paginate_array(@branches).page(params[:page]) unless params[:show_all].present?
respond_to do |format|
- format.html
+ format.html do
+ @max_commits = @branches.reduce(0) do |memo, branch|
+ diverging_commit_counts = repository.diverging_commit_counts(branch)
+ [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
+ end
+ end
format.json do
render json: @branches.map(&:name)
end
diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml
index 2ebd4f9069a..b5f67cae341 100644
--- a/app/views/projects/commit/_change.html.haml
+++ b/app/views/projects/commit/_change.html.haml
@@ -37,4 +37,4 @@
= commit_in_fork_help
:javascript
- new NewCommitForm($('.js-#{type}-form'))
+ new NewCommitForm($('.js-#{type}-form'), 'start_branch')
diff --git a/app/views/shared/_branch_switcher.html.haml b/app/views/shared/_branch_switcher.html.haml
new file mode 100644
index 00000000000..7799aff6b5b
--- /dev/null
+++ b/app/views/shared/_branch_switcher.html.haml
@@ -0,0 +1,8 @@
+- dropdown_toggle_text = @target_branch || tree_edit_branch
+= hidden_field_tag 'target_branch', dropdown_toggle_text
+
+.dropdown
+ = dropdown_toggle dropdown_toggle_text, { toggle: 'dropdown', selected: dropdown_toggle_text, field_name: 'target_branch', form_id: '.js-edit-blob-form', refs_url: namespace_project_branches_path(@project.namespace, @project) }, { toggle_class: 'js-project-branches-dropdown js-target-branch' }
+ .dropdown-menu.dropdown-menu-selectable.dropdown-menu-paging.dropdown-menu-branches
+ = render partial: 'shared/projects/blob/branch_page_default'
+ = render partial: 'shared/projects/blob/branch_page_create'
diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml
index 0c8ac48bb58..3ac5e15d1c4 100644
--- a/app/views/shared/_new_commit_form.html.haml
+++ b/app/views/shared/_new_commit_form.html.haml
@@ -7,7 +7,7 @@
.form-group.branch
= label_tag 'target_branch', 'Target branch', class: 'control-label'
.col-sm-10
- = text_field_tag 'target_branch', @target_branch || tree_edit_branch, required: true, class: "form-control js-target-branch"
+ = render 'shared/branch_switcher'
.js-create-merge-request-container
.checkbox
diff --git a/app/views/shared/projects/blob/_branch_page_create.html.haml b/app/views/shared/projects/blob/_branch_page_create.html.haml
new file mode 100644
index 00000000000..c279a0d8846
--- /dev/null
+++ b/app/views/shared/projects/blob/_branch_page_create.html.haml
@@ -0,0 +1,8 @@
+.dropdown-page-two.dropdown-new-branch
+ = dropdown_title('Create new branch', back: true)
+ = dropdown_content do
+ %input#new_branch_name.default-dropdown-input.append-bottom-10{ type: "text", placeholder: "Name new branch" }
+ %button.btn.btn-primary.pull-left.js-new-branch-btn{ type: "button" }
+ Create
+ %button.btn.btn-default.pull-right.js-cancel-branch-btn{ type: "button" }
+ Cancel
diff --git a/app/views/shared/projects/blob/_branch_page_default.html.haml b/app/views/shared/projects/blob/_branch_page_default.html.haml
new file mode 100644
index 00000000000..9bf78d10878
--- /dev/null
+++ b/app/views/shared/projects/blob/_branch_page_default.html.haml
@@ -0,0 +1,10 @@
+.dropdown-page-one
+ = dropdown_title "Select branch"
+ = dropdown_filter "Search branches"
+ = dropdown_content
+ = dropdown_loading
+ = dropdown_footer do
+ %ul.dropdown-footer-list
+ %li
+ %a.create-new-branch.dropdown-toggle-page{ href: "#" }
+ Create new branch
diff --git a/changelogs/unreleased/24501-new-file-existing-branch.yml b/changelogs/unreleased/24501-new-file-existing-branch.yml
new file mode 100644
index 00000000000..31c66b2a978
--- /dev/null
+++ b/changelogs/unreleased/24501-new-file-existing-branch.yml
@@ -0,0 +1,4 @@
+---
+title: New file from interface on existing branch
+merge_request: 8427
+author: Jacopo Beschi @jacopo-beschi
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index f18adcadcce..e84cd9193da 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -82,7 +82,10 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I fill the new branch name' do
- fill_in :target_branch, with: 'new_branch_name', visible: true
+ first('button.js-target-branch', visible: true).click
+ first('.create-new-branch', visible: true).click
+ first('#new_branch_name', visible: true).set('new_branch_name')
+ first('.js-new-branch-btn', visible: true).click
end
step 'I fill the new file name with an illegal name' do
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index 298a7ff179c..d20e7368086 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -266,5 +266,19 @@ describe Projects::BranchesController do
expect(parsed_response.first).to eq 'master'
end
end
+
+ context 'show_all = true' do
+ it 'returns all the branches name' do
+ get :index,
+ namespace_id: project.namespace,
+ project_id: project,
+ format: :json,
+ show_all: true
+
+ parsed_response = JSON.parse(response.body)
+
+ expect(parsed_response.length).to eq(project.repository.branches.count)
+ end
+ end
end
end
diff --git a/spec/features/projects/blobs/user_create_spec.rb b/spec/features/projects/blobs/user_create_spec.rb
new file mode 100644
index 00000000000..03d08c12612
--- /dev/null
+++ b/spec/features/projects/blobs/user_create_spec.rb
@@ -0,0 +1,107 @@
+require 'spec_helper'
+
+feature 'New blob creation', feature: true, js: true do
+ include WaitForAjax
+
+ given(:user) { create(:user) }
+ given(:role) { :developer }
+ given(:project) { create(:project) }
+ given(:content) { 'class NextFeature\nend\n' }
+
+ background do
+ login_as(user)
+ project.team << [user, role]
+ visit namespace_project_new_blob_path(project.namespace, project, 'master')
+ end
+
+ def edit_file
+ wait_for_ajax
+ fill_in 'file_name', with: 'feature.rb'
+ execute_script("ace.edit('editor').setValue('#{content}')")
+ end
+
+ def select_branch_index(index)
+ first('button.js-target-branch').click
+ wait_for_ajax
+ all('a[data-group="Branches"]')[index].click
+ end
+
+ def create_new_branch(name)
+ first('button.js-target-branch').click
+ click_link 'Create new branch'
+ fill_in 'new_branch_name', with: name
+ click_button 'Create'
+ end
+
+ def commit_file
+ click_button 'Commit Changes'
+ end
+
+ context 'with default target branch' do
+ background do
+ edit_file
+ commit_file
+ end
+
+ scenario 'creates the blob in the default branch' do
+ expect(page).to have_content 'master'
+ expect(page).to have_content 'successfully created'
+ expect(page).to have_content 'NextFeature'
+ end
+ end
+
+ context 'with different target branch' do
+ background do
+ edit_file
+ select_branch_index(0)
+ commit_file
+ end
+
+ scenario 'creates the blob in the different branch' do
+ expect(page).to have_content 'test'
+ expect(page).to have_content 'successfully created'
+ end
+ end
+
+ context 'with a new target branch' do
+ given(:new_branch_name) { 'new-feature' }
+
+ background do
+ edit_file
+ create_new_branch(new_branch_name)
+ commit_file
+ end
+
+ scenario 'creates the blob in the new branch' do
+ expect(page).to have_content new_branch_name
+ expect(page).to have_content 'successfully created'
+ end
+ scenario 'returns you to the mr' do
+ expect(page).to have_content 'New Merge Request'
+ expect(page).to have_content "From #{new_branch_name} into master"
+ expect(page).to have_content 'Add new file'
+ end
+ end
+
+ context 'the file already exist in the source branch' do
+ background do
+ Files::CreateService.new(
+ project,
+ user,
+ start_branch: 'master',
+ target_branch: 'master',
+ commit_message: 'Create file',
+ file_path: 'feature.rb',
+ file_content: content
+ ).execute
+ edit_file
+ commit_file
+ end
+
+ scenario 'shows error message' do
+ expect(page).to have_content('Your changes could not be committed because a file with the same name already exists')
+ expect(page).to have_content('New File')
+ expect(page).to have_content('NextFeature')
+ end
+ end
+end
diff --git a/spec/javascripts/blob/create_branch_dropdown_spec.js b/spec/javascripts/blob/create_branch_dropdown_spec.js
new file mode 100644
index 00000000000..dafb43761e0
--- /dev/null
+++ b/spec/javascripts/blob/create_branch_dropdown_spec.js
@@ -0,0 +1,109 @@
+require('jquery');
+require('~/extensions/jquery.js');
+require('~/gl_dropdown');
+require('~/lib/utils/type_utility');
+require('~/blob/create_branch_dropdown');
+require('~/blob/target_branch_dropdown');
+
+describe('CreateBranchDropdown', () => {
+ const fixtureTemplate = 'static/target_branch_dropdown.html.raw';
+ // selectors
+ const createBranchSel = '.js-new-branch-btn';
+ const backBtnSel = '.dropdown-menu-back';
+ const cancelBtnSel = '.js-cancel-branch-btn';
+ const branchNameSel = '#new_branch_name';
+ const branchName = 'new_name';
+ let dropdown;
+
+ function createDropdown() {
+ const dropdownEl = document.querySelector('.js-project-branches-dropdown');
+ const projectBranches = getJSONFixture('project_branches.json');
+ dropdown = new gl.TargetBranchDropDown(dropdownEl);
+ dropdown.cachedRefs = projectBranches;
+ return dropdown;
+ }
+
+ function createBranchBtn() {
+ return document.querySelector(createBranchSel);
+ }
+
+ function backBtn() {
+ return document.querySelector(backBtnSel);
+ }
+
+ function cancelBtn() {
+ return document.querySelector(cancelBtnSel);
+ }
+
+ function branchNameEl() {
+ return document.querySelector(branchNameSel);
+ }
+
+ function changeBranchName(text) {
+ branchNameEl().value = text;
+ branchNameEl().dispatchEvent(new Event('change'));
+ }
+
+ preloadFixtures(fixtureTemplate);
+
+ beforeEach(() => {
+ loadFixtures(fixtureTemplate);
+ createDropdown();
+ });
+
+ it('disable submit when branch name is empty', () => {
+ expect(createBranchBtn()).toBeDisabled();
+ });
+
+ it('enable submit when branch name is present', () => {
+ changeBranchName(branchName);
+
+ expect(createBranchBtn()).not.toBeDisabled();
+ });
+
+ it('resets the form when cancel btn is clicked and triggers dropdownback', () => {
+ const spyBackEvent = spyOnEvent(backBtnSel, 'click');
+ changeBranchName(branchName);
+
+ cancelBtn().click();
+
+ expect(branchNameEl()).toHaveValue('');
+ expect(spyBackEvent).toHaveBeenTriggered();
+ });
+
+ it('resets the form when back btn is clicked', () => {
+ changeBranchName(branchName);
+
+ backBtn().click();
+
+ expect(branchNameEl()).toHaveValue('');
+ });
+
+ describe('new branch creation', () => {
+ beforeEach(() => {
+ changeBranchName(branchName);
+ });
+ it('sets the new branch name and updates the dropdown', () => {
+ spyOn(dropdown, 'setNewBranch');
+
+ createBranchBtn().click();
+
+ expect(dropdown.setNewBranch).toHaveBeenCalledWith(branchName);
+ });
+
+ it('resets the form', () => {
+ createBranchBtn().click();
+
+ expect(branchNameEl()).toHaveValue('');
+ });
+
+ it('is triggered with enter keypress', () => {
+ spyOn(dropdown, 'setNewBranch');
+ const enterEvent = new Event('keydown');
+ enterEvent.which = 13;
+ branchNameEl().dispatchEvent(enterEvent);
+
+ expect(dropdown.setNewBranch).toHaveBeenCalledWith(branchName);
+ });
+ });
+});
diff --git a/spec/javascripts/blob/target_branch_dropdown_spec.js b/spec/javascripts/blob/target_branch_dropdown_spec.js
new file mode 100644
index 00000000000..6f3eb4cc7eb
--- /dev/null
+++ b/spec/javascripts/blob/target_branch_dropdown_spec.js
@@ -0,0 +1,121 @@
+require('jquery');
+require('~/extensions/jquery.js');
+require('~/gl_dropdown');
+require('~/lib/utils/type_utility');
+require('~/blob/create_branch_dropdown');
+require('~/blob/target_branch_dropdown');
+
+describe('TargetBranchDropdown', () => {
+ const fixtureTemplate = 'static/target_branch_dropdown.html.raw';
+ let dropdown;
+
+ function createDropdown() {
+ const projectBranches = getJSONFixture('project_branches.json');
+ const dropdownEl = document.querySelector('.js-project-branches-dropdown');
+ dropdown = new gl.TargetBranchDropDown(dropdownEl);
+ dropdown.cachedRefs = projectBranches;
+ dropdown.refreshData();
+ return dropdown;
+ }
+
+ function submitBtn() {
+ return document.querySelector('button[type="submit"]');
+ }
+
+ function searchField() {
+ return document.querySelector('.dropdown-page-one .dropdown-input-field');
+ }
+
+ function element() {
+ return document.querySelectorAll('div.dropdown-content li a');
+ }
+
+ function elementAtIndex(index) {
+ return element()[index];
+ }
+
+ function clickElementAtIndex(index) {
+ elementAtIndex(index).click();
+ }
+
+ preloadFixtures(fixtureTemplate);
+
+ beforeEach(() => {
+ loadFixtures(fixtureTemplate);
+ createDropdown();
+ });
+
+ it('disable submit when branch is not selected', () => {
+ document.querySelector('input[name="target_branch"]').value = null;
+ clickElementAtIndex(1);
+
+ expect(submitBtn().getAttribute('disabled')).toEqual('');
+ });
+
+ it('enable submit when a branch is selected', () => {
+ clickElementAtIndex(1);
+
+ expect(submitBtn().getAttribute('disabled')).toBe(null);
+ });
+
+ it('triggers change.branch event on a branch click', () => {
+ spyOnEvent(dropdown.$dropdown, 'change.branch');
+ clickElementAtIndex(0);
+
+ expect('change.branch').toHaveBeenTriggeredOn(dropdown.$dropdown);
+ });
+
+ describe('#dropdownData', () => {
+ it('cache the refs', () => {
+ const refs = dropdown.cachedRefs;
+ dropdown.cachedRefs = null;
+
+ dropdown.dropdownData(refs);
+
+ expect(dropdown.cachedRefs).toEqual(refs);
+ });
+
+ it('returns the Branches with the newBranch and defaultBranch', () => {
+ const refs = dropdown.cachedRefs;
+ dropdown.branchInput.value = 'master';
+ dropdown.newBranch = { id: 'new_branch', text: 'new_branch', title: 'new_branch' };
+
+ const branches = dropdown.dropdownData(refs).Branches;
+
+ expect(branches.length).toEqual(4);
+ expect(branches[0]).toEqual(dropdown.newBranch);
+ expect(branches[1]).toEqual({ id: 'master', text: 'master', title: 'master' });
+ expect(branches[2]).toEqual({ id: 'development', text: 'development', title: 'development' });
+ expect(branches[3]).toEqual({ id: 'staging', text: 'staging', title: 'staging' });
+ });
+ });
+
+ describe('#setNewBranch', () => {
+ it('adds the new branch and select it', () => {
+ const branchName = 'new_branch';
+
+ dropdown.setNewBranch(branchName);
+
+ expect(elementAtIndex(0)).toHaveClass('is-active');
+ expect(elementAtIndex(0)).toContainHtml(branchName);
+ });
+
+ it("doesn't add a new branch if already exists in the list", () => {
+ const branchName = elementAtIndex(0).text;
+ const initialLength = element().length;
+
+ dropdown.setNewBranch(branchName);
+
+ expect(element().length).toEqual(initialLength);
+ });
+
+ it('clears the search filter', () => {
+ const branchName = elementAtIndex(0).text;
+ searchField().value = 'searching';
+
+ dropdown.setNewBranch(branchName);
+
+ expect(searchField().value).toEqual('');
+ });
+ });
+});
diff --git a/spec/javascripts/fixtures/project_branches.json b/spec/javascripts/fixtures/project_branches.json
new file mode 100644
index 00000000000..a96a4c0c095
--- /dev/null
+++ b/spec/javascripts/fixtures/project_branches.json
@@ -0,0 +1,5 @@
+[
+ "master",
+ "development",
+ "staging"
+]
diff --git a/spec/javascripts/fixtures/target_branch_dropdown.html.haml b/spec/javascripts/fixtures/target_branch_dropdown.html.haml
new file mode 100644
index 00000000000..821fb7940a0
--- /dev/null
+++ b/spec/javascripts/fixtures/target_branch_dropdown.html.haml
@@ -0,0 +1,28 @@
+%form.js-edit-blob-form
+ %input{type: 'hidden', name: 'target_branch', value: 'master'}
+ %div
+ .dropdown
+ %button.dropdown-menu-toggle.js-project-branches-dropdown.js-target-branch{type: 'button', data: {toggle: 'dropdown', selected: 'master', field_name: 'target_branch', form_id: '.js-edit-blob-form'}}
+ .dropdown-menu.dropdown-menu-selectable.dropdown-menu-paging
+ .dropdown-page-one
+ .dropdown-title 'Select branch'
+ .dropdown-input
+ %input.dropdown-input-field{type: 'search', value: ''}
+ %i.fa.fa-search.dropdown-input-search
+ %i.fa.fa-times-dropdown-input-clear.js-dropdown-input-clear{role: 'button'}
+ .dropdown-content
+ .dropdown-footer
+ %ul.dropdown-footer-list
+ %li
+ %a.create-new-branch.dropdown-toggle-page{href: "#"}
+ Create new branch
+ .dropdown-page-two.dropdown-new-branch
+ %button.dropdown-title-button.dropdown-menu-back{type: 'button'}
+ .dropdown_title 'Create new branch'
+ .dropdown_content
+ %input#new_branch_name.default-dropdown-input{ type: "text", placeholder: "Name new branch" }
+ %button.btn.btn-primary.pull-left.js-new-branch-btn{ type: "button" }
+ Create
+ %button.btn.btn-default.pull-right.js-cancel-branch-btn{ type: "button" }
+ Cancel
+ %button{type: 'submit'}