From 68d1e9d1c42f34d3fb2d58779d862dcf47d5f17e Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 31 Oct 2017 10:47:10 +0000 Subject: Upload files through the multi-file editor --- .../repo/components/new_dropdown/index.vue | 15 +++- .../repo/components/new_dropdown/modal.vue | 6 +- .../repo/components/new_dropdown/upload.vue | 67 ++++++++++++++ .../repo/components/repo_commit_section.vue | 1 + .../javascripts/repo/components/repo_preview.vue | 7 ++ app/assets/javascripts/repo/helpers/repo_helper.js | 31 +++++-- .../javascripts/repo/services/repo_service.js | 2 +- app/assets/stylesheets/framework/dropdowns.scss | 3 + .../unreleased/ph-multi-file-upload-file.yml | 5 ++ spec/features/projects/tree/upload_file_spec.rb | 48 ++++++++++ .../repo/components/new_dropdown/index_spec.js | 56 +++++++++--- .../repo/components/new_dropdown/modal_spec.js | 6 +- .../repo/components/new_dropdown/upload_spec.js | 100 +++++++++++++++++++++ 13 files changed, 325 insertions(+), 22 deletions(-) create mode 100644 app/assets/javascripts/repo/components/new_dropdown/upload.vue create mode 100644 changelogs/unreleased/ph-multi-file-upload-file.yml create mode 100644 spec/features/projects/tree/upload_file_spec.rb create mode 100644 spec/javascripts/repo/components/new_dropdown/upload_spec.js 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() { @@ -64,6 +68,11 @@ {{ __('New file') }} +
  • + +
  • + 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); + }, + }; + + + 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 @@ -49,6 +49,13 @@ export default { v-if="!activeFile.render_error" v-html="activeFile.html"> +
    +

    + The source could not be displayed for this temporary file. +

    +
    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); + }); + }); +}); -- cgit v1.2.1