Commit a9446093 authored by Filipa Lacerda's avatar Filipa Lacerda

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
parents 7e78db6e 68d1e9d1
...@@ -3,10 +3,12 @@ ...@@ -3,10 +3,12 @@
import RepoHelper from '../../helpers/repo_helper'; import RepoHelper from '../../helpers/repo_helper';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import newModal from './modal.vue'; import newModal from './modal.vue';
import upload from './upload.vue';
export default { export default {
components: { components: {
newModal, newModal,
upload,
}, },
data() { data() {
return { return {
...@@ -23,10 +25,12 @@ ...@@ -23,10 +25,12 @@
toggleModalOpen() { toggleModalOpen() {
this.openModal = !this.openModal; this.openModal = !this.openModal;
}, },
createNewEntryInStore(name, type) { createNewEntryInStore(options, openEditMode = true) {
RepoHelper.createNewEntry(name, type); RepoHelper.createNewEntry(options, openEditMode);
this.toggleModalOpen(); if (options.toggleModal) {
this.toggleModalOpen();
}
}, },
}, },
created() { created() {
...@@ -64,6 +68,11 @@ ...@@ -64,6 +68,11 @@
{{ __('New file') }} {{ __('New file') }}
</a> </a>
</li> </li>
<li>
<upload
:current-path="currentPath"
/>
</li>
<li> <li>
<a <a
href="#" href="#"
......
...@@ -24,7 +24,11 @@ ...@@ -24,7 +24,11 @@
}, },
methods: { methods: {
createEntryInStore() { createEntryInStore() {
eventHub.$emit('createNewEntry', this.entryName, this.type); eventHub.$emit('createNewEntry', {
name: this.entryName,
type: this.type,
toggleModal: true,
});
}, },
toggleModalOpen() { toggleModalOpen() {
this.$emit('toggle'); this.$emit('toggle');
......
<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>
...@@ -52,6 +52,7 @@ export default { ...@@ -52,6 +52,7 @@ export default {
action: f.tempFile ? 'create' : 'update', action: f.tempFile ? 'create' : 'update',
file_path: f.path, file_path: f.path,
content: f.newContent, content: f.newContent,
encoding: f.base64 ? 'base64' : 'text',
})); }));
const branch = newBranch ? `${this.currentBranch}-${this.currentShortHash}` : this.currentBranch; const branch = newBranch ? `${this.currentBranch}-${this.currentShortHash}` : this.currentBranch;
const payload = { const payload = {
......
...@@ -49,6 +49,13 @@ export default { ...@@ -49,6 +49,13 @@ export default {
v-if="!activeFile.render_error" v-if="!activeFile.render_error"
v-html="activeFile.html"> v-html="activeFile.html">
</div> </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 <div
v-else-if="activeFile.tooLarge" v-else-if="activeFile.tooLarge"
class="vertical-center render-error"> class="vertical-center render-error">
......
...@@ -155,7 +155,7 @@ const RepoHelper = { ...@@ -155,7 +155,7 @@ const RepoHelper = {
if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') { if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') {
newFile.tooLarge = true; newFile.tooLarge = true;
} }
newFile.newContent = ''; newFile.newContent = file.newContent ? file.newContent : '';
Store.addToOpenedFiles(newFile); Store.addToOpenedFiles(newFile);
Store.setActiveFiles(newFile); Store.setActiveFiles(newFile);
...@@ -276,7 +276,13 @@ const RepoHelper = { ...@@ -276,7 +276,13 @@ const RepoHelper = {
removeAllTmpFiles(storeFilesKey) { removeAllTmpFiles(storeFilesKey) {
Store[storeFilesKey] = Store[storeFilesKey].filter(f => !f.tempFile); 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; const originalPath = Store.path;
let entryName = name; let entryName = name;
...@@ -304,9 +310,24 @@ const RepoHelper = { ...@@ -304,9 +310,24 @@ const RepoHelper = {
if ((type === 'tree' && tree.tempFile) || type === 'blob') { if ((type === 'tree' && tree.tempFile) || type === 'blob') {
const file = this.findOrCreateEntry('blob', tree, fileName); const file = this.findOrCreateEntry('blob', tree, fileName);
if (!file.exists) { if (file.exists) {
this.setFile(file.entry, file.entry); Flash(`The name "${file.entry.name}" is already taken in this directory.`);
this.openEditMode(); } 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';
}
} }
} }
......
...@@ -19,7 +19,7 @@ const RepoService = { ...@@ -19,7 +19,7 @@ const RepoService = {
getRaw(file) { getRaw(file) {
if (file.tempFile) { if (file.tempFile) {
return Promise.resolve({ return Promise.resolve({
data: '', data: file.newContent ? file.newContent : '',
}); });
} }
......
...@@ -776,12 +776,15 @@ ...@@ -776,12 +776,15 @@
a, a,
button, button,
.menu-item { .menu-item {
margin-bottom: 0;
border-radius: 0; border-radius: 0;
box-shadow: none; box-shadow: none;
padding: 8px 16px; padding: 8px 16px;
text-align: left; text-align: left;
white-space: normal; white-space: normal;
width: 100%; width: 100%;
font-weight: $gl-font-weight-normal;
line-height: normal;
&.dropdown-menu-user-link { &.dropdown-menu-user-link {
white-space: nowrap; white-space: nowrap;
......
---
title: Allow files to uploaded in the multi-file editor
merge_request:
author:
type: added
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
...@@ -74,25 +74,38 @@ describe('new dropdown component', () => { ...@@ -74,25 +74,38 @@ describe('new dropdown component', () => {
it('closes modal after creating file', () => { it('closes modal after creating file', () => {
vm.openModal = true; vm.openModal = true;
eventHub.$emit('createNewEntry', 'testing', type); eventHub.$emit('createNewEntry', {
name: 'testing',
type,
toggleModal: true,
});
expect(vm.openModal).toBeFalsy(); expect(vm.openModal).toBeFalsy();
}); });
it('sets editMode to true', () => { it('sets editMode to true', () => {
eventHub.$emit('createNewEntry', 'testing', type); eventHub.$emit('createNewEntry', {
name: 'testing',
type,
});
expect(RepoStore.editMode).toBeTruthy(); expect(RepoStore.editMode).toBeTruthy();
}); });
it('toggles blob view', () => { it('toggles blob view', () => {
eventHub.$emit('createNewEntry', 'testing', type); eventHub.$emit('createNewEntry', {
name: 'testing',
type,
});
expect(RepoStore.isPreviewView()).toBeFalsy(); expect(RepoStore.isPreviewView()).toBeFalsy();
}); });
it('adds file into activeFiles', () => { it('adds file into activeFiles', () => {
eventHub.$emit('createNewEntry', 'testing', type); eventHub.$emit('createNewEntry', {
name: 'testing',
type,
});
expect(RepoStore.openedFiles.length).toBe(1); expect(RepoStore.openedFiles.length).toBe(1);
}); });
...@@ -100,7 +113,10 @@ describe('new dropdown component', () => { ...@@ -100,7 +113,10 @@ describe('new dropdown component', () => {
it(`creates ${type} in the current stores path`, () => { it(`creates ${type} in the current stores path`, () => {
RepoStore.path = 'testing'; 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].path).toBe('testing/app');
expect(RepoStore.files[0].name).toBe('app'); expect(RepoStore.files[0].name).toBe('app');
...@@ -116,7 +132,10 @@ describe('new dropdown component', () => { ...@@ -116,7 +132,10 @@ describe('new dropdown component', () => {
describe('file', () => { describe('file', () => {
it('creates new 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.length).toBe(1);
expect(RepoStore.files[0].name).toBe('testing'); expect(RepoStore.files[0].name).toBe('testing');
...@@ -129,7 +148,10 @@ describe('new dropdown component', () => { ...@@ -129,7 +148,10 @@ describe('new dropdown component', () => {
name: 'testing', name: 'testing',
})); }));
eventHub.$emit('createNewEntry', 'testing', 'blob'); eventHub.$emit('createNewEntry', {
name: 'testing',
type: 'blob',
});
expect(RepoStore.files.length).toBe(1); expect(RepoStore.files.length).toBe(1);
expect(RepoStore.files[0].name).toBe('testing'); expect(RepoStore.files[0].name).toBe('testing');
...@@ -140,7 +162,10 @@ describe('new dropdown component', () => { ...@@ -140,7 +162,10 @@ describe('new dropdown component', () => {
describe('tree', () => { describe('tree', () => {
it('creates new 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.length).toBe(1);
expect(RepoStore.files[0].name).toBe('testing'); expect(RepoStore.files[0].name).toBe('testing');
...@@ -151,7 +176,10 @@ describe('new dropdown component', () => { ...@@ -151,7 +176,10 @@ describe('new dropdown component', () => {
}); });
it('creates multiple trees when entryName has slashes', () => { 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.length).toBe(1);
expect(RepoStore.files[0].name).toBe('app'); expect(RepoStore.files[0].name).toBe('app');
...@@ -164,7 +192,10 @@ describe('new dropdown component', () => { ...@@ -164,7 +192,10 @@ describe('new dropdown component', () => {
name: 'app', 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.length).toBe(1);
expect(RepoStore.files[0].name).toBe('app'); expect(RepoStore.files[0].name).toBe('app');
...@@ -179,7 +210,10 @@ describe('new dropdown component', () => { ...@@ -179,7 +210,10 @@ describe('new dropdown component', () => {
name: 'app', name: 'app',
})); }));
eventHub.$emit('createNewEntry', 'app', 'tree'); eventHub.$emit('createNewEntry', {
name: 'app',
type: 'tree',
});
expect(RepoStore.files.length).toBe(1); expect(RepoStore.files.length).toBe(1);
expect(RepoStore.files[0].name).toBe('app'); expect(RepoStore.files[0].name).toBe('app');
......
...@@ -70,7 +70,11 @@ describe('new file modal component', () => { ...@@ -70,7 +70,11 @@ describe('new file modal component', () => {
vm.createEntryInStore(); vm.createEntryInStore();
expect(eventHub.$emit).toHaveBeenCalledWith('createNewEntry', 'testing', 'tree'); expect(eventHub.$emit).toHaveBeenCalledWith('createNewEntry', {
name: 'testing',
type: 'tree',
toggleModal: true,
});
}); });
}); });
}); });
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);
});
});
});
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