Merge branch 'ph-multi-file-upload-file' into 'master'

Upload files through the multi-file editor

Closes #38629 and #38614

See merge request gitlab-org/gitlab-ce!14975
This commit is contained in:
Filipa Lacerda 2017-10-31 10:47:12 +00:00
commit a9446093b1
13 changed files with 325 additions and 22 deletions

View file

@ -3,10 +3,12 @@
import RepoHelper from '../../helpers/repo_helper';
import eventHub from '../../event_hub';
import newModal from './modal.vue';
import upload from './upload.vue';
export default {
components: {
newModal,
upload,
},
data() {
return {
@ -23,10 +25,12 @@
toggleModalOpen() {
this.openModal = !this.openModal;
},
createNewEntryInStore(name, type) {
RepoHelper.createNewEntry(name, type);
createNewEntryInStore(options, openEditMode = true) {
RepoHelper.createNewEntry(options, openEditMode);
this.toggleModalOpen();
if (options.toggleModal) {
this.toggleModalOpen();
}
},
},
created() {
@ -64,6 +68,11 @@
{{ __('New file') }}
</a>
</li>
<li>
<upload
:current-path="currentPath"
/>
</li>
<li>
<a
href="#"

View file

@ -24,7 +24,11 @@
},
methods: {
createEntryInStore() {
eventHub.$emit('createNewEntry', this.entryName, this.type);
eventHub.$emit('createNewEntry', {
name: this.entryName,
type: this.type,
toggleModal: true,
});
},
toggleModalOpen() {
this.$emit('toggle');

View file

@ -0,0 +1,67 @@
<script>
import eventHub from '../../event_hub';
export default {
props: {
currentPath: {
type: String,
required: true,
},
},
methods: {
createFile(target, file, isText) {
const { name } = file;
const nameWithPath = `${this.currentPath !== '' ? `${this.currentPath}/` : ''}${name}`;
let { result } = target;
if (!isText) {
result = result.split('base64,')[1];
}
eventHub.$emit('createNewEntry', {
name: nameWithPath,
type: 'blob',
content: result,
toggleModal: false,
base64: !isText,
}, isText);
},
readFile(file) {
const reader = new FileReader();
const isText = file.type.match(/text.*/) !== null;
reader.addEventListener('load', e => this.createFile(e.target, file, isText), { once: true });
if (isText) {
reader.readAsText(file);
} else {
reader.readAsDataURL(file);
}
},
openFile() {
Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file));
},
},
mounted() {
this.$refs.fileUpload.addEventListener('change', this.openFile);
},
beforeDestroy() {
this.$refs.fileUpload.removeEventListener('change', this.openFile);
},
};
</script>
<template>
<label
role="button"
class="menu-item"
>
{{ __('Upload file') }}
<input
id="file-upload"
type="file"
class="hidden"
ref="fileUpload"
/>
</label>
</template>

View file

@ -52,6 +52,7 @@ export default {
action: f.tempFile ? 'create' : 'update',
file_path: f.path,
content: f.newContent,
encoding: f.base64 ? 'base64' : 'text',
}));
const branch = newBranch ? `${this.currentBranch}-${this.currentShortHash}` : this.currentBranch;
const payload = {

View file

@ -49,6 +49,13 @@ export default {
v-if="!activeFile.render_error"
v-html="activeFile.html">
</div>
<div
v-else-if="activeFile.tempFile"
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed for this temporary file.
</p>
</div>
<div
v-else-if="activeFile.tooLarge"
class="vertical-center render-error">

View file

@ -155,7 +155,7 @@ const RepoHelper = {
if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') {
newFile.tooLarge = true;
}
newFile.newContent = '';
newFile.newContent = file.newContent ? file.newContent : '';
Store.addToOpenedFiles(newFile);
Store.setActiveFiles(newFile);
@ -276,7 +276,13 @@ const RepoHelper = {
removeAllTmpFiles(storeFilesKey) {
Store[storeFilesKey] = Store[storeFilesKey].filter(f => !f.tempFile);
},
createNewEntry(name, type) {
createNewEntry(options, openEditMode = true) {
const {
name,
type,
content = '',
base64 = false,
} = options;
const originalPath = Store.path;
let entryName = name;
@ -304,9 +310,24 @@ const RepoHelper = {
if ((type === 'tree' && tree.tempFile) || type === 'blob') {
const file = this.findOrCreateEntry('blob', tree, fileName);
if (!file.exists) {
this.setFile(file.entry, file.entry);
this.openEditMode();
if (file.exists) {
Flash(`The name "${file.entry.name}" is already taken in this directory.`);
} else {
const { entry } = file;
entry.newContent = content;
entry.base64 = base64;
if (entry.base64) {
entry.render_error = true;
}
this.setFile(entry, entry);
if (openEditMode) {
this.openEditMode();
} else {
file.entry.render_error = 'asdsad';
}
}
}

View file

@ -19,7 +19,7 @@ const RepoService = {
getRaw(file) {
if (file.tempFile) {
return Promise.resolve({
data: '',
data: file.newContent ? file.newContent : '',
});
}

View file

@ -776,12 +776,15 @@
a,
button,
.menu-item {
margin-bottom: 0;
border-radius: 0;
box-shadow: none;
padding: 8px 16px;
text-align: left;
white-space: normal;
width: 100%;
font-weight: $gl-font-weight-normal;
line-height: normal;
&.dropdown-menu-user-link {
white-space: nowrap;

View file

@ -0,0 +1,5 @@
---
title: Allow files to uploaded in the multi-file editor
merge_request:
author:
type: added

View file

@ -0,0 +1,48 @@
require 'spec_helper'
feature 'Multi-file editor upload file', :js do
include WaitForRequests
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:txt_file) { File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt') }
let(:img_file) { File.join(Rails.root, 'spec', 'fixtures', 'dk.png') }
before do
project.add_master(user)
sign_in(user)
page.driver.set_cookie('new_repo', 'true')
visit project_tree_path(project, :master)
wait_for_requests
end
it 'uploads text file' do
find('.add-to-tree').click
# make the field visible so capybara can use it
execute_script('document.querySelector("#file-upload").classList.remove("hidden")')
attach_file('file-upload', txt_file)
find('.add-to-tree').click
expect(page).to have_selector('.repo-tab', text: 'doc_sample.txt')
expect(page).to have_content(File.open(txt_file, &:readline))
end
it 'uploads image file' do
find('.add-to-tree').click
# make the field visible so capybara can use it
execute_script('document.querySelector("#file-upload").classList.remove("hidden")')
attach_file('file-upload', img_file)
find('.add-to-tree').click
expect(page).to have_selector('.repo-tab', text: 'dk.png')
expect(page).not_to have_selector('.monaco-editor')
expect(page).to have_content('The source could not be displayed for this temporary file.')
end
end

View file

@ -74,25 +74,38 @@ describe('new dropdown component', () => {
it('closes modal after creating file', () => {
vm.openModal = true;
eventHub.$emit('createNewEntry', 'testing', type);
eventHub.$emit('createNewEntry', {
name: 'testing',
type,
toggleModal: true,
});
expect(vm.openModal).toBeFalsy();
});
it('sets editMode to true', () => {
eventHub.$emit('createNewEntry', 'testing', type);
eventHub.$emit('createNewEntry', {
name: 'testing',
type,
});
expect(RepoStore.editMode).toBeTruthy();
});
it('toggles blob view', () => {
eventHub.$emit('createNewEntry', 'testing', type);
eventHub.$emit('createNewEntry', {
name: 'testing',
type,
});
expect(RepoStore.isPreviewView()).toBeFalsy();
});
it('adds file into activeFiles', () => {
eventHub.$emit('createNewEntry', 'testing', type);
eventHub.$emit('createNewEntry', {
name: 'testing',
type,
});
expect(RepoStore.openedFiles.length).toBe(1);
});
@ -100,7 +113,10 @@ describe('new dropdown component', () => {
it(`creates ${type} in the current stores path`, () => {
RepoStore.path = 'testing';
eventHub.$emit('createNewEntry', 'testing/app', type);
eventHub.$emit('createNewEntry', {
name: 'testing/app',
type,
});
expect(RepoStore.files[0].path).toBe('testing/app');
expect(RepoStore.files[0].name).toBe('app');
@ -116,7 +132,10 @@ describe('new dropdown component', () => {
describe('file', () => {
it('creates new file', () => {
eventHub.$emit('createNewEntry', 'testing', 'blob');
eventHub.$emit('createNewEntry', {
name: 'testing',
type: 'blob',
});
expect(RepoStore.files.length).toBe(1);
expect(RepoStore.files[0].name).toBe('testing');
@ -129,7 +148,10 @@ describe('new dropdown component', () => {
name: 'testing',
}));
eventHub.$emit('createNewEntry', 'testing', 'blob');
eventHub.$emit('createNewEntry', {
name: 'testing',
type: 'blob',
});
expect(RepoStore.files.length).toBe(1);
expect(RepoStore.files[0].name).toBe('testing');
@ -140,7 +162,10 @@ describe('new dropdown component', () => {
describe('tree', () => {
it('creates new tree', () => {
eventHub.$emit('createNewEntry', 'testing', 'tree');
eventHub.$emit('createNewEntry', {
name: 'testing',
type: 'tree',
});
expect(RepoStore.files.length).toBe(1);
expect(RepoStore.files[0].name).toBe('testing');
@ -151,7 +176,10 @@ describe('new dropdown component', () => {
});
it('creates multiple trees when entryName has slashes', () => {
eventHub.$emit('createNewEntry', 'app/test', 'tree');
eventHub.$emit('createNewEntry', {
name: 'app/test',
type: 'tree',
});
expect(RepoStore.files.length).toBe(1);
expect(RepoStore.files[0].name).toBe('app');
@ -164,7 +192,10 @@ describe('new dropdown component', () => {
name: 'app',
}));
eventHub.$emit('createNewEntry', 'app/test', 'tree');
eventHub.$emit('createNewEntry', {
name: 'app/test',
type: 'tree',
});
expect(RepoStore.files.length).toBe(1);
expect(RepoStore.files[0].name).toBe('app');
@ -179,7 +210,10 @@ describe('new dropdown component', () => {
name: 'app',
}));
eventHub.$emit('createNewEntry', 'app', 'tree');
eventHub.$emit('createNewEntry', {
name: 'app',
type: 'tree',
});
expect(RepoStore.files.length).toBe(1);
expect(RepoStore.files[0].name).toBe('app');

View file

@ -70,7 +70,11 @@ describe('new file modal component', () => {
vm.createEntryInStore();
expect(eventHub.$emit).toHaveBeenCalledWith('createNewEntry', 'testing', 'tree');
expect(eventHub.$emit).toHaveBeenCalledWith('createNewEntry', {
name: 'testing',
type: 'tree',
toggleModal: true,
});
});
});
});

View file

@ -0,0 +1,100 @@
import Vue from 'vue';
import upload from '~/repo/components/new_dropdown/upload.vue';
import eventHub from '~/repo/event_hub';
import createComponent from '../../../helpers/vue_mount_component_helper';
describe('new dropdown upload', () => {
let vm;
beforeEach(() => {
const Component = Vue.extend(upload);
vm = createComponent(Component, {
currentPath: '',
});
});
afterEach(() => {
vm.$destroy();
});
describe('readFile', () => {
beforeEach(() => {
spyOn(FileReader.prototype, 'readAsText');
spyOn(FileReader.prototype, 'readAsDataURL');
});
it('calls readAsText for text files', () => {
const file = {
type: 'text/html',
};
vm.readFile(file);
expect(FileReader.prototype.readAsText).toHaveBeenCalledWith(file);
});
it('calls readAsDataURL for non-text files', () => {
const file = {
type: 'images/png',
};
vm.readFile(file);
expect(FileReader.prototype.readAsDataURL).toHaveBeenCalledWith(file);
});
});
describe('createFile', () => {
const target = {
result: 'content',
};
const binaryTarget = {
result: 'base64,base64content',
};
const file = {
name: 'file',
};
beforeEach(() => {
spyOn(eventHub, '$emit');
});
it('emits createNewEntry event', () => {
vm.createFile(target, file, true);
expect(eventHub.$emit).toHaveBeenCalledWith('createNewEntry', {
name: 'file',
type: 'blob',
content: 'content',
toggleModal: false,
base64: false,
}, true);
});
it('createNewEntry event name contains current path', () => {
vm.currentPath = 'testing';
vm.createFile(target, file, true);
expect(eventHub.$emit).toHaveBeenCalledWith('createNewEntry', {
name: 'testing/file',
type: 'blob',
content: 'content',
toggleModal: false,
base64: false,
}, true);
});
it('splits content on base64 if binary', () => {
vm.createFile(binaryTarget, file, false);
expect(eventHub.$emit).toHaveBeenCalledWith('createNewEntry', {
name: 'file',
type: 'blob',
content: 'base64content',
toggleModal: false,
base64: true,
}, false);
});
});
});