Create new branch from dropdown.
This commit is contained in:
parent
3f9022cf78
commit
07a8c54355
10 changed files with 449 additions and 9 deletions
115
app/assets/javascripts/repo/components/new_branch_form.vue
Normal file
115
app/assets/javascripts/repo/components/new_branch_form.vue
Normal file
|
@ -0,0 +1,115 @@
|
|||
<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>
|
|
@ -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() {
|
||||
|
@ -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;
|
||||
},
|
||||
|
@ -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>
|
||||
|
|
|
@ -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() {
|
||||
|
@ -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');
|
||||
|
@ -73,6 +94,7 @@ function initRepoBundle() {
|
|||
|
||||
initRepo(repo);
|
||||
initRepoEditButton(editButton);
|
||||
initNewBranchForm();
|
||||
}
|
||||
|
||||
$(initRepoBundle);
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
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: {
|
||||
|
@ -10,6 +13,7 @@ const RepoService = {
|
|||
format: 'json',
|
||||
},
|
||||
},
|
||||
createBranchPath: '/api/:version/projects/:id/repository/branches',
|
||||
richExtensionRegExp: /md/,
|
||||
|
||||
getRaw(url) {
|
||||
|
@ -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');
|
||||
|
|
|
@ -13,6 +13,7 @@ const RepoStore = {
|
|||
projectId: '',
|
||||
projectName: '',
|
||||
projectUrl: '',
|
||||
branchUrl: '',
|
||||
blobRaw: '',
|
||||
currentBlobView: 'repo-preview',
|
||||
openedFiles: [],
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.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'
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
- 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
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
@ -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
|
||||
|
|
122
spec/javascripts/repo/components/new_branch_form_spec.js
Normal file
122
spec/javascripts/repo/components/new_branch_form_spec.js
Normal file
|
@ -0,0 +1,122 @@
|
|||
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');
|
||||
});
|
||||
});
|
||||
});
|
96
spec/javascripts/repo/components/repo_spec.js
Normal file
96
spec/javascripts/repo/components/repo_spec.js
Normal file
|
@ -0,0 +1,96 @@
|
|||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue