Skip to content
Snippets Groups Projects
Commit 07a8c543 authored by Jacob Schatz's avatar Jacob Schatz Committed by Filipa Lacerda
Browse files

Create new branch from dropdown.

parent 3f9022cf
No related branches found
No related tags found
No related merge requests found
<script>
import flash, { hideFlash } from '../../flash';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import eventHub from '../event_hub';
export default {
components: {
loadingIcon,
},
props: {
currentBranch: {
type: String,
required: true,
},
},
data() {
return {
branchName: '',
loading: false,
};
},
computed: {
btnDisabled() {
return this.loading || this.branchName === '';
},
},
methods: {
toggleDropdown() {
this.$dropdown.dropdown('toggle');
},
submitNewBranch() {
// need to query as the element is appended outside of Vue
const flashEl = this.$refs.flashContainer.querySelector('.flash-alert');
this.loading = true;
if (flashEl) {
hideFlash(flashEl, false);
}
eventHub.$emit('createNewBranch', this.branchName);
},
showErrorMessage(message) {
this.loading = false;
flash(message, 'alert', this.$el);
},
createdNewBranch(newBranchName) {
this.loading = false;
this.branchName = '';
if (this.dropdownText) {
this.dropdownText.textContent = newBranchName;
}
},
},
created() {
// Dropdown is outside of Vue instance & is controlled by Bootstrap
this.$dropdown = $('.git-revision-dropdown');
// text element is outside Vue app
this.dropdownText = document.querySelector('.project-refs-form .dropdown-toggle-text');
eventHub.$on('createNewBranchSuccess', this.createdNewBranch);
eventHub.$on('createNewBranchError', this.showErrorMessage);
eventHub.$on('toggleNewBranchDropdown', this.toggleDropdown);
},
destroyed() {
eventHub.$off('createNewBranchSuccess', this.createdNewBranch);
eventHub.$off('toggleNewBranchDropdown', this.toggleDropdown);
eventHub.$off('createNewBranchError', this.showErrorMessage);
},
};
</script>
<template>
<div>
<div
class="flash-container"
ref="flashContainer"
>
</div>
<p>
Create from:
<code>{{ currentBranch }}</code>
</p>
<input
class="form-control js-new-branch-name"
type="text"
placeholder="Name new branch"
v-model="branchName"
@keyup.enter.stop.prevent="submitNewBranch"
/>
<div class="prepend-top-default clearfix">
<button
type="button"
class="btn btn-primary pull-left"
:disabled="btnDisabled"
@click.stop.prevent="submitNewBranch"
>
<loading-icon
v-if="loading"
:inline="true"
/>
<span>Create</span>
</button>
<button
type="button"
class="btn btn-default pull-right"
@click.stop.prevent="toggleDropdown"
>
Cancel
</button>
</div>
</div>
</template>
Loading
Loading
@@ -8,7 +8,9 @@ import RepoMixin from '../mixins/repo_mixin';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
import Store from '../stores/repo_store';
import Helper from '../helpers/repo_helper';
import Service from '../services/repo_service';
import MonacoLoaderHelper from '../helpers/monaco_loader_helper';
import eventHub from '../event_hub';
 
export default {
data() {
Loading
Loading
@@ -24,12 +26,19 @@ export default {
PopupDialog,
RepoPreview,
},
created() {
eventHub.$on('createNewBranch', this.createNewBranch);
},
mounted() {
Helper.getContent().catch(Helper.loadingError);
},
destroyed() {
eventHub.$off('createNewBranch', this.createNewBranch);
},
methods: {
getCurrentLocation() {
return location.href;
},
toggleDialogOpen(toggle) {
this.dialog.open = toggle;
},
Loading
Loading
@@ -38,8 +47,25 @@ export default {
this.toggleDialogOpen(false);
this.dialog.status = status;
},
toggleBlobView: Store.toggleBlobView,
createNewBranch(branch) {
Service.createBranch({
branch,
ref: Store.currentBranch,
}).then((res) => {
const newBranchName = res.data.name;
const newUrl = this.getCurrentLocation().replace(Store.currentBranch, newBranchName);
Store.currentBranch = newBranchName;
history.pushState({ key: Helper.key }, '', newUrl);
eventHub.$emit('createNewBranchSuccess', newBranchName);
eventHub.$emit('toggleNewBranchDropdown');
}).catch((err) => {
eventHub.$emit('createNewBranchError', err.response.data.message);
});
},
},
};
</script>
Loading
Loading
Loading
Loading
@@ -5,6 +5,7 @@ import Service from './services/repo_service';
import Store from './stores/repo_store';
import Repo from './components/repo.vue';
import RepoEditButton from './components/repo_edit_button.vue';
import newBranchForm from './components/new_branch_form.vue';
import Translate from '../vue_shared/translate';
 
function initDropdowns() {
Loading
Loading
@@ -62,6 +63,26 @@ function initRepoEditButton(el) {
});
}
 
function initNewBranchForm() {
const el = document.querySelector('.js-new-branch-dropdown');
if (!el) return null;
return new Vue({
el,
components: {
newBranchForm,
},
render(createElement) {
return createElement('new-branch-form', {
props: {
currentBranch: Store.currentBranch,
},
});
},
});
}
function initRepoBundle() {
const repo = document.getElementById('repo');
const editButton = document.querySelector('.editable-mode');
Loading
Loading
@@ -73,6 +94,7 @@ function initRepoBundle() {
 
initRepo(repo);
initRepoEditButton(editButton);
initNewBranchForm();
}
 
$(initRepoBundle);
Loading
Loading
import axios from 'axios';
import csrf from '../../lib/utils/csrf';
import Store from '../stores/repo_store';
import Api from '../../api';
import Helper from '../helpers/repo_helper';
 
axios.defaults.headers.common[csrf.headerKey] = csrf.token;
const RepoService = {
url: '',
options: {
Loading
Loading
@@ -10,6 +13,7 @@ const RepoService = {
format: 'json',
},
},
createBranchPath: '/api/:version/projects/:id/repository/branches',
richExtensionRegExp: /md/,
 
getRaw(url) {
Loading
Loading
@@ -73,6 +77,12 @@ const RepoService = {
.then(this.commitFlash);
},
 
createBranch(payload) {
const url = Api.buildUrl(this.createBranchPath)
.replace(':id', Store.projectId);
return axios.post(url, payload);
},
commitFlash(data) {
if (data.short_id && data.stats) {
window.Flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
Loading
Loading
Loading
Loading
@@ -13,6 +13,7 @@ const RepoStore = {
projectId: '',
projectName: '',
projectUrl: '',
branchUrl: '',
blobRaw: '',
currentBlobView: 'repo-preview',
openedFiles: [],
Loading
Loading
.tree-ref-container
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'tree', path: @path
= render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true
 
- unless show_new_repo?
= render 'projects/tree/old_tree_header'
Loading
Loading
- show_new_branch_form = show_new_repo? && show_create && can?(current_user, :push_code, @project)
- dropdown_toggle_text = @ref || @project.default_branch
= form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do
= hidden_field_tag :destination, destination
Loading
Loading
@@ -7,8 +8,20 @@
= hidden_field_tag key, value, id: nil
.dropdown
= dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown" }
.dropdown-menu.dropdown-menu-selectable.git-revision-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
= dropdown_title _("Switch branch/tag")
= dropdown_filter _("Search branches and tags")
= dropdown_content
= dropdown_loading
.dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
.dropdown-page-one
= dropdown_title _("Switch branch/tag")
= dropdown_filter _("Search branches and tags")
= dropdown_content
= dropdown_loading
- if show_new_branch_form
= dropdown_footer do
%ul.dropdown-footer-list
%li
%a.dropdown-toggle-page{ href: "#" }
Create new branch
- if show_new_branch_form
.dropdown-page-two
= dropdown_title("Create new branch", options: { back: true })
= dropdown_content do
.js-new-branch-dropdown
Loading
Loading
@@ -6,6 +6,7 @@ feature 'Ref switcher', :js do
 
before do
project.team << [user, :master]
page.driver.set_cookie('new_repo', 'true')
sign_in(user)
visit project_tree_path(project, 'master')
end
Loading
Loading
@@ -40,4 +41,38 @@ feature 'Ref switcher', :js do
 
expect(page).to have_title "'test'"
end
context "create branch" do
let(:input) { find('.js-new-branch-name') }
before do
click_button 'master'
wait_for_requests
page.within '.project-refs-form' do
find(".dropdown-footer-list a").click
end
end
it "shows error message for the invalid branch name" do
input.set 'foo bar'
click_button('Create')
wait_for_requests
expect(page).to have_content 'Branch name is invalid'
end
it "should create new branch properly" do
input.set 'new-branch-name'
click_button('Create')
wait_for_requests
expect(find('.js-project-refs-dropdown')).to have_content 'new-branch-name'
end
it "should create new branch by Enter key" do
input.set 'new-branch-name-2'
input.native.send_keys :enter
wait_for_requests
expect(find('.js-project-refs-dropdown')).to have_content 'new-branch-name-2'
end
end
end
import Vue from 'vue';
import newBranchForm from '~/repo/components/new_branch_form.vue';
import eventHub from '~/repo/event_hub';
import RepoStore from '~/repo/stores/repo_store';
import createComponent from '../../helpers/vue_mount_component_helper';
describe('Multi-file editor new branch form', () => {
let vm;
beforeEach(() => {
const Component = Vue.extend(newBranchForm);
RepoStore.currentBranch = 'master';
vm = createComponent(Component, {
currentBranch: RepoStore.currentBranch,
});
});
afterEach(() => {
vm.$destroy();
RepoStore.currentBranch = '';
});
describe('template', () => {
it('renders submit as disabled', () => {
expect(vm.$el.querySelector('.btn').getAttribute('disabled')).toBe('disabled');
});
it('enables the submit button when branch is not empty', (done) => {
vm.branchName = 'testing';
Vue.nextTick(() => {
expect(vm.$el.querySelector('.btn').getAttribute('disabled')).toBeNull();
done();
});
});
it('displays current branch creating from', (done) => {
Vue.nextTick(() => {
expect(vm.$el.querySelector('p').textContent.replace(/\s+/g, ' ').trim()).toBe('Create from: master');
done();
});
});
});
describe('submitNewBranch', () => {
it('sets to loading', () => {
vm.submitNewBranch();
expect(vm.loading).toBeTruthy();
});
it('hides current flash element', (done) => {
vm.$refs.flashContainer.innerHTML = '<div class="flash-alert"></div>';
vm.submitNewBranch();
Vue.nextTick(() => {
expect(vm.$el.querySelector('.flash-alert')).toBeNull();
done();
});
});
it('emits an event with branchName', () => {
spyOn(eventHub, '$emit');
vm.branchName = 'testing';
vm.submitNewBranch();
expect(eventHub.$emit).toHaveBeenCalledWith('createNewBranch', 'testing');
});
});
describe('showErrorMessage', () => {
it('sets loading to false', () => {
vm.loading = true;
vm.showErrorMessage();
expect(vm.loading).toBeFalsy();
});
it('creates flash element', () => {
vm.showErrorMessage('error message');
expect(vm.$el.querySelector('.flash-alert')).not.toBeNull();
expect(vm.$el.querySelector('.flash-alert').textContent.trim()).toBe('error message');
});
});
describe('createdNewBranch', () => {
it('set loading to false', () => {
vm.loading = true;
vm.createdNewBranch();
expect(vm.loading).toBeFalsy();
});
it('resets branch name', () => {
vm.branchName = 'testing';
vm.createdNewBranch();
expect(vm.branchName).toBe('');
});
it('sets the dropdown toggle text', () => {
vm.dropdownText = document.createElement('span');
vm.createdNewBranch('branch name');
expect(vm.dropdownText.textContent).toBe('branch name');
});
});
});
import Vue from 'vue';
import repo from '~/repo/components/repo.vue';
import RepoStore from '~/repo/stores/repo_store';
import Service from '~/repo/services/repo_service';
import eventHub from '~/repo/event_hub';
import createComponent from '../../helpers/vue_mount_component_helper';
describe('repo component', () => {
let vm;
beforeEach(() => {
const Component = Vue.extend(repo);
RepoStore.currentBranch = 'master';
vm = createComponent(Component);
});
afterEach(() => {
vm.$destroy();
RepoStore.currentBranch = '';
});
describe('createNewBranch', () => {
beforeEach(() => {
spyOn(history, 'pushState');
});
describe('success', () => {
beforeEach(() => {
spyOn(Service, 'createBranch').and.returnValue(Promise.resolve({
data: {
name: 'test',
},
}));
});
it('calls createBranch with branchName', () => {
eventHub.$emit('createNewBranch', 'test');
expect(Service.createBranch).toHaveBeenCalledWith({
branch: 'test',
ref: RepoStore.currentBranch,
});
});
it('pushes new history state', (done) => {
RepoStore.currentBranch = 'master';
spyOn(vm, 'getCurrentLocation').and.returnValue('http://test.com/master');
eventHub.$emit('createNewBranch', 'test');
setTimeout(() => {
expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'http://test.com/test');
done();
});
});
it('updates stores currentBranch', (done) => {
eventHub.$emit('createNewBranch', 'test');
setTimeout(() => {
expect(RepoStore.currentBranch).toBe('test');
done();
});
});
});
describe('failure', () => {
beforeEach(() => {
spyOn(Service, 'createBranch').and.returnValue(Promise.reject({
response: {
data: {
message: 'test',
},
},
}));
});
it('emits createNewBranchError event', (done) => {
spyOn(eventHub, '$emit').and.callThrough();
eventHub.$emit('createNewBranch', 'test');
setTimeout(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('createNewBranchError', 'test');
done();
});
});
});
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment