From e0fe2834ebe6d9678444c8a10b79ca093f944232 Mon Sep 17 00:00:00 2001 From: Jacopo Date: Thu, 22 Dec 2016 07:21:40 +0100 Subject: [PATCH] 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. --- .../javascripts/blob/blob_file_dropzone.js | 2 +- .../blob/create_branch_dropdown.js | 88 ++++++++++ .../blob/target_branch_dropdown.js | 152 ++++++++++++++++++ app/assets/javascripts/dispatcher.js | 26 ++- app/assets/javascripts/main.js | 2 + app/assets/javascripts/new_commit_form.js | 12 +- app/assets/stylesheets/pages/projects.scss | 3 +- app/controllers/projects/blob_controller.rb | 7 + .../projects/branches_controller.rb | 13 +- app/views/projects/commit/_change.html.haml | 2 +- app/views/shared/_branch_switcher.html.haml | 8 + app/views/shared/_new_commit_form.html.haml | 2 +- .../blob/_branch_page_create.html.haml | 8 + .../blob/_branch_page_default.html.haml | 10 ++ .../24501-new-file-existing-branch.yml | 4 + features/steps/project/source/browse_files.rb | 5 +- .../projects/branches_controller_spec.rb | 14 ++ .../projects/blobs/user_create_spec.rb | 107 ++++++++++++ .../blob/create_branch_dropdown_spec.js | 109 +++++++++++++ .../blob/target_branch_dropdown_spec.js | 121 ++++++++++++++ .../fixtures/project_branches.json | 5 + .../fixtures/target_branch_dropdown.html.haml | 28 ++++ 22 files changed, 710 insertions(+), 18 deletions(-) create mode 100644 app/assets/javascripts/blob/create_branch_dropdown.js create mode 100644 app/assets/javascripts/blob/target_branch_dropdown.js create mode 100644 app/views/shared/_branch_switcher.html.haml create mode 100644 app/views/shared/projects/blob/_branch_page_create.html.haml create mode 100644 app/views/shared/projects/blob/_branch_page_default.html.haml create mode 100644 changelogs/unreleased/24501-new-file-existing-branch.yml create mode 100644 spec/features/projects/blobs/user_create_spec.rb create mode 100644 spec/javascripts/blob/create_branch_dropdown_spec.js create mode 100644 spec/javascripts/blob/target_branch_dropdown_spec.js create mode 100644 spec/javascripts/fixtures/project_branches.json create mode 100644 spec/javascripts/fixtures/target_branch_dropdown.html.haml 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'}