summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFilipa Lacerda <filipa@gitlab.com>2017-10-31 10:47:12 +0000
committerFilipa Lacerda <filipa@gitlab.com>2017-10-31 10:47:12 +0000
commita9446093b12a9dffef97541915e2b6872875ad3f (patch)
tree5bf4287612a089292ea950aff0e86f2ee158b2e2
parent7e78db6e04c3760c90d676e9a797b1b16b1dff7b (diff)
parent68d1e9d1c42f34d3fb2d58779d862dcf47d5f17e (diff)
downloadgitlab-ce-a9446093b12a9dffef97541915e2b6872875ad3f.tar.gz
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
-rw-r--r--app/assets/javascripts/repo/components/new_dropdown/index.vue15
-rw-r--r--app/assets/javascripts/repo/components/new_dropdown/modal.vue6
-rw-r--r--app/assets/javascripts/repo/components/new_dropdown/upload.vue67
-rw-r--r--app/assets/javascripts/repo/components/repo_commit_section.vue1
-rw-r--r--app/assets/javascripts/repo/components/repo_preview.vue7
-rw-r--r--app/assets/javascripts/repo/helpers/repo_helper.js31
-rw-r--r--app/assets/javascripts/repo/services/repo_service.js2
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss3
-rw-r--r--changelogs/unreleased/ph-multi-file-upload-file.yml5
-rw-r--r--spec/features/projects/tree/upload_file_spec.rb48
-rw-r--r--spec/javascripts/repo/components/new_dropdown/index_spec.js56
-rw-r--r--spec/javascripts/repo/components/new_dropdown/modal_spec.js6
-rw-r--r--spec/javascripts/repo/components/new_dropdown/upload_spec.js100
13 files changed, 325 insertions, 22 deletions
diff --git a/app/assets/javascripts/repo/components/new_dropdown/index.vue b/app/assets/javascripts/repo/components/new_dropdown/index.vue
index 3ccb50213ab..3a331ed805f 100644
--- a/app/assets/javascripts/repo/components/new_dropdown/index.vue
+++ b/app/assets/javascripts/repo/components/new_dropdown/index.vue
@@ -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() {
@@ -65,6 +69,11 @@
</a>
</li>
<li>
+ <upload
+ :current-path="currentPath"
+ />
+ </li>
+ <li>
<a
href="#"
role="button"
diff --git a/app/assets/javascripts/repo/components/new_dropdown/modal.vue b/app/assets/javascripts/repo/components/new_dropdown/modal.vue
index 5ef629e0dde..b49586de6bb 100644
--- a/app/assets/javascripts/repo/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/repo/components/new_dropdown/modal.vue
@@ -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');
diff --git a/app/assets/javascripts/repo/components/new_dropdown/upload.vue b/app/assets/javascripts/repo/components/new_dropdown/upload.vue
new file mode 100644
index 00000000000..cbea9c08249
--- /dev/null
+++ b/app/assets/javascripts/repo/components/new_dropdown/upload.vue
@@ -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>
diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/repo/components/repo_commit_section.vue
index 0d6259a37a8..649c69c43fd 100644
--- a/app/assets/javascripts/repo/components/repo_commit_section.vue
+++ b/app/assets/javascripts/repo/components/repo_commit_section.vue
@@ -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 = {
diff --git a/app/assets/javascripts/repo/components/repo_preview.vue b/app/assets/javascripts/repo/components/repo_preview.vue
index b5be771d539..264694f01a2 100644
--- a/app/assets/javascripts/repo/components/repo_preview.vue
+++ b/app/assets/javascripts/repo/components/repo_preview.vue
@@ -50,6 +50,13 @@ export default {
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">
<p class="text-center">
diff --git a/app/assets/javascripts/repo/helpers/repo_helper.js b/app/assets/javascripts/repo/helpers/repo_helper.js
index fb26f3b7380..1c677049b31 100644
--- a/app/assets/javascripts/repo/helpers/repo_helper.js
+++ b/app/assets/javascripts/repo/helpers/repo_helper.js
@@ -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';
+ }
}
}
diff --git a/app/assets/javascripts/repo/services/repo_service.js b/app/assets/javascripts/repo/services/repo_service.js
index c9fa5cc8bf8..d003e2b0a5e 100644
--- a/app/assets/javascripts/repo/services/repo_service.js
+++ b/app/assets/javascripts/repo/services/repo_service.js
@@ -19,7 +19,7 @@ const RepoService = {
getRaw(file) {
if (file.tempFile) {
return Promise.resolve({
- data: '',
+ data: file.newContent ? file.newContent : '',
});
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index a9d804e735d..1aa53b8f8cf 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -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;
diff --git a/changelogs/unreleased/ph-multi-file-upload-file.yml b/changelogs/unreleased/ph-multi-file-upload-file.yml
new file mode 100644
index 00000000000..a2bd3cfe459
--- /dev/null
+++ b/changelogs/unreleased/ph-multi-file-upload-file.yml
@@ -0,0 +1,5 @@
+---
+title: Allow files to uploaded in the multi-file editor
+merge_request:
+author:
+type: added
diff --git a/spec/features/projects/tree/upload_file_spec.rb b/spec/features/projects/tree/upload_file_spec.rb
new file mode 100644
index 00000000000..7dbe4fd0aa5
--- /dev/null
+++ b/spec/features/projects/tree/upload_file_spec.rb
@@ -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
diff --git a/spec/javascripts/repo/components/new_dropdown/index_spec.js b/spec/javascripts/repo/components/new_dropdown/index_spec.js
index ddbfdab582d..ddffef53300 100644
--- a/spec/javascripts/repo/components/new_dropdown/index_spec.js
+++ b/spec/javascripts/repo/components/new_dropdown/index_spec.js
@@ -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');
diff --git a/spec/javascripts/repo/components/new_dropdown/modal_spec.js b/spec/javascripts/repo/components/new_dropdown/modal_spec.js
index 4c5cdc47c6e..d9fd9b9a595 100644
--- a/spec/javascripts/repo/components/new_dropdown/modal_spec.js
+++ b/spec/javascripts/repo/components/new_dropdown/modal_spec.js
@@ -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,
+ });
});
});
});
diff --git a/spec/javascripts/repo/components/new_dropdown/upload_spec.js b/spec/javascripts/repo/components/new_dropdown/upload_spec.js
new file mode 100644
index 00000000000..31878e9d327
--- /dev/null
+++ b/spec/javascripts/repo/components/new_dropdown/upload_spec.js
@@ -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);
+ });
+ });
+});