Commit c147bccc authored by Phil Hughes's avatar Phil Hughes

Merge branch 'master' into ph-multi-file-editor-new-file-folder-dropdown

parents 133e66d2 9931ef4a
...@@ -1280,10 +1280,12 @@ export default class Notes { ...@@ -1280,10 +1280,12 @@ export default class Notes {
* Get data from Form attributes to use for saving/submitting comment. * Get data from Form attributes to use for saving/submitting comment.
*/ */
getFormData($form) { getFormData($form) {
const content = $form.find('.js-note-text').val();
return { return {
formData: $form.serialize(), formData: $form.serialize(),
formContent: _.escape($form.find('.js-note-text').val()), formContent: _.escape(content),
formAction: $form.attr('action'), formAction: $form.attr('action'),
formContentOriginal: content,
}; };
} }
...@@ -1415,7 +1417,7 @@ export default class Notes { ...@@ -1415,7 +1417,7 @@ export default class Notes {
const isMainForm = $form.hasClass('js-main-target-form'); const isMainForm = $form.hasClass('js-main-target-form');
const isDiscussionForm = $form.hasClass('js-discussion-note-form'); const isDiscussionForm = $form.hasClass('js-discussion-note-form');
const isDiscussionResolve = $submitBtn.hasClass('js-comment-resolve-button'); const isDiscussionResolve = $submitBtn.hasClass('js-comment-resolve-button');
const { formData, formContent, formAction } = this.getFormData($form); const { formData, formContent, formAction, formContentOriginal } = this.getFormData($form);
let noteUniqueId; let noteUniqueId;
let systemNoteUniqueId; let systemNoteUniqueId;
let hasQuickActions = false; let hasQuickActions = false;
...@@ -1574,7 +1576,7 @@ export default class Notes { ...@@ -1574,7 +1576,7 @@ export default class Notes {
$form = $notesContainer.parent().find('form'); $form = $notesContainer.parent().find('form');
} }
$form.find('.js-note-text').val(formContent); $form.find('.js-note-text').val(formContentOriginal);
this.reenableTargetFormSubmitButton(e); this.reenableTargetFormSubmitButton(e);
this.addNoteError($form); this.addNoteError($form);
}); });
......
<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'; ...@@ -8,7 +8,9 @@ import RepoMixin from '../mixins/repo_mixin';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue'; import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import Helper from '../helpers/repo_helper'; import Helper from '../helpers/repo_helper';
import Service from '../services/repo_service';
import MonacoLoaderHelper from '../helpers/monaco_loader_helper'; import MonacoLoaderHelper from '../helpers/monaco_loader_helper';
import eventHub from '../event_hub';
export default { export default {
data() { data() {
...@@ -24,12 +26,19 @@ export default { ...@@ -24,12 +26,19 @@ export default {
PopupDialog, PopupDialog,
RepoPreview, RepoPreview,
}, },
created() {
eventHub.$on('createNewBranch', this.createNewBranch);
},
mounted() { mounted() {
Helper.getContent().catch(Helper.loadingError); Helper.getContent().catch(Helper.loadingError);
}, },
destroyed() {
eventHub.$off('createNewBranch', this.createNewBranch);
},
methods: { methods: {
getCurrentLocation() {
return location.href;
},
toggleDialogOpen(toggle) { toggleDialogOpen(toggle) {
this.dialog.open = toggle; this.dialog.open = toggle;
}, },
...@@ -42,8 +51,25 @@ export default { ...@@ -42,8 +51,25 @@ export default {
Helper.removeAllTmpFiles('openedFiles'); Helper.removeAllTmpFiles('openedFiles');
Helper.removeAllTmpFiles('files'); Helper.removeAllTmpFiles('files');
}, },
toggleBlobView: Store.toggleBlobView, 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> </script>
......
...@@ -5,6 +5,7 @@ import Service from './services/repo_service'; ...@@ -5,6 +5,7 @@ import Service from './services/repo_service';
import Store from './stores/repo_store'; import Store from './stores/repo_store';
import Repo from './components/repo.vue'; import Repo from './components/repo.vue';
import RepoEditButton from './components/repo_edit_button.vue'; import RepoEditButton from './components/repo_edit_button.vue';
import newBranchForm from './components/new_branch_form.vue';
import newDropdown from './components/new_dropdown/index.vue'; import newDropdown from './components/new_dropdown/index.vue';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
...@@ -76,6 +77,26 @@ function initNewDropdown(el) { ...@@ -76,6 +77,26 @@ function initNewDropdown(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() { function initRepoBundle() {
const repo = document.getElementById('repo'); const repo = document.getElementById('repo');
const editButton = document.querySelector('.editable-mode'); const editButton = document.querySelector('.editable-mode');
...@@ -88,6 +109,7 @@ function initRepoBundle() { ...@@ -88,6 +109,7 @@ function initRepoBundle() {
initRepo(repo); initRepo(repo);
initRepoEditButton(editButton); initRepoEditButton(editButton);
initNewBranchForm();
initNewDropdown(newDropdownHolder); initNewDropdown(newDropdownHolder);
} }
......
import axios from 'axios'; import axios from 'axios';
import csrf from '../../lib/utils/csrf';
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import Api from '../../api'; import Api from '../../api';
import Helper from '../helpers/repo_helper'; import Helper from '../helpers/repo_helper';
axios.defaults.headers.common[csrf.headerKey] = csrf.token;
const RepoService = { const RepoService = {
url: '', url: '',
options: { options: {
...@@ -10,6 +13,7 @@ const RepoService = { ...@@ -10,6 +13,7 @@ const RepoService = {
format: 'json', format: 'json',
}, },
}, },
createBranchPath: '/api/:version/projects/:id/repository/branches',
richExtensionRegExp: /md/, richExtensionRegExp: /md/,
getRaw(file) { getRaw(file) {
...@@ -79,6 +83,12 @@ const RepoService = { ...@@ -79,6 +83,12 @@ const RepoService = {
.then(this.commitFlash); .then(this.commitFlash);
}, },
createBranch(payload) {
const url = Api.buildUrl(this.createBranchPath)
.replace(':id', Store.projectId);
return axios.post(url, payload);
},
commitFlash(data) { commitFlash(data) {
if (data.short_id && data.stats) { 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'); 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 = { ...@@ -13,6 +13,7 @@ const RepoStore = {
projectId: '', projectId: '',
projectName: '', projectName: '',
projectUrl: '', projectUrl: '',
branchUrl: '',
blobRaw: '', blobRaw: '',
currentBlobView: 'repo-preview', currentBlobView: 'repo-preview',
openedFiles: [], openedFiles: [],
......
- local_assigns.fetch(:view)
%strong
%span{ data: { defer_to: "#{view.defer_key}-duration" } } ...
\/
%span{ data: { defer_to: "#{view.defer_key}-calls" } } ...
Gitaly
.tree-ref-container .tree-ref-container
.tree-ref-holder .tree-ref-holder
= render 'shared/ref_switcher', destination: 'tree', path: @path = render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true
- if show_new_repo? - if show_new_repo?
.js-new-dropdown .js-new-dropdown
......
- show_new_branch_form = show_new_repo? && show_create && can?(current_user, :push_code, @project)
- dropdown_toggle_text = @ref || @project.default_branch - dropdown_toggle_text = @ref || @project.default_branch
= form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do = form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do
= hidden_field_tag :destination, destination = hidden_field_tag :destination, destination
...@@ -7,8 +8,20 @@ ...@@ -7,8 +8,20 @@
= hidden_field_tag key, value, id: nil = hidden_field_tag key, value, id: nil
.dropdown .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_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-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_title _("Switch branch/tag")
= dropdown_filter _("Search branches and tags") = dropdown_filter _("Search branches and tags")
= dropdown_content = dropdown_content
= dropdown_loading = 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
---
title: Add Gitaly metrics to the performance bar
merge_request:
author:
type: other
...@@ -16,6 +16,7 @@ Peek.into Peek::Views::Redis ...@@ -16,6 +16,7 @@ Peek.into Peek::Views::Redis
Peek.into Peek::Views::Sidekiq Peek.into Peek::Views::Sidekiq
Peek.into Peek::Views::Rblineprof Peek.into Peek::Views::Rblineprof
Peek.into Peek::Views::GC Peek.into Peek::Views::GC
Peek.into Peek::Views::Gitaly
# rubocop:disable Style/ClassAndModuleCamelCase # rubocop:disable Style/ClassAndModuleCamelCase
class PEEK_DB_CLIENT class PEEK_DB_CLIENT
......
...@@ -33,6 +33,12 @@ module Gitlab ...@@ -33,6 +33,12 @@ module Gitlab
MUTEX = Mutex.new MUTEX = Mutex.new
private_constant :MUTEX private_constant :MUTEX
class << self
attr_accessor :query_time
end
self.query_time = 0
def self.stub(name, storage) def self.stub(name, storage)
MUTEX.synchronize do MUTEX.synchronize do
@stubs ||= {} @stubs ||= {}
...@@ -83,11 +89,14 @@ module Gitlab ...@@ -83,11 +89,14 @@ module Gitlab
# end # end
# #
def self.call(storage, service, rpc, request) def self.call(storage, service, rpc, request)
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
enforce_gitaly_request_limits(:call) enforce_gitaly_request_limits(:call)
kwargs = request_kwargs(storage) kwargs = request_kwargs(storage)
kwargs = yield(kwargs) if block_given? kwargs = yield(kwargs) if block_given?
stub(service, storage).__send__(rpc, request, kwargs) # rubocop:disable GitlabSecurity/PublicSend stub(service, storage).__send__(rpc, request, kwargs) # rubocop:disable GitlabSecurity/PublicSend
ensure
self.query_time += Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
end end
def self.request_kwargs(storage) def self.request_kwargs(storage)
......
module Peek
module Views
class Gitaly < View
def duration
::Gitlab::GitalyClient.query_time
end
def calls
::Gitlab::GitalyClient.get_request_count
end
def results
{ duration: formatted_duration, calls: calls }
end
private
def formatted_duration
ms = duration * 1000
if ms >= 1000
"%.2fms" % ms
else
"%.0fms" % ms
end
end
def setup_subscribers
subscribe 'start_processing.action_controller' do
::Gitlab::GitalyClient.query_time = 0
end
end
end
end
end
...@@ -6,6 +6,7 @@ feature 'Ref switcher', :js do ...@@ -6,6 +6,7 @@ feature 'Ref switcher', :js do
before do before do
project.team << [user, :master] project.team << [user, :master]
page.driver.set_cookie('new_repo', 'true')
sign_in(user) sign_in(user)
visit project_tree_path(project, 'master') visit project_tree_path(project, 'master')
end end
...@@ -40,4 +41,38 @@ feature 'Ref switcher', :js do ...@@ -40,4 +41,38 @@ feature 'Ref switcher', :js do
expect(page).to have_title "'test'" expect(page).to have_title "'test'"
end 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 end
...@@ -103,6 +103,16 @@ import '~/notes'; ...@@ -103,6 +103,16 @@ import '~/notes';
$('.js-comment-button').click(); $('.js-comment-button').click();
expect(this.autoSizeSpy).toHaveBeenTriggered(); expect(this.autoSizeSpy).toHaveBeenTriggered();
}); });
it('should not place escaped text in the comment box in case of error', function() {
const deferred = $.Deferred();
spyOn($, 'ajax').and.returnValue(deferred.promise());
$(textarea).text('A comment with `markup`.');
deferred.reject();
$('.js-comment-button').click();
expect($(textarea).val()).toEqual('A comment with `markup`.');
});
}); });
describe('updateNote', () => { describe('updateNote', () => {
......
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();
});
});
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment