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:
commit
a9446093b1
13 changed files with 325 additions and 22 deletions
|
@ -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="#"
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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>
|
|
@ -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 = {
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ const RepoService = {
|
|||
getRaw(file) {
|
||||
if (file.tempFile) {
|
||||
return Promise.resolve({
|
||||
data: '',
|
||||
data: file.newContent ? file.newContent : '',
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
5
changelogs/unreleased/ph-multi-file-upload-file.yml
Normal file
5
changelogs/unreleased/ph-multi-file-upload-file.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow files to uploaded in the multi-file editor
|
||||
merge_request:
|
||||
author:
|
||||
type: added
|
48
spec/features/projects/tree/upload_file_spec.rb
Normal file
48
spec/features/projects/tree/upload_file_spec.rb
Normal 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
|
|
@ -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');
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
100
spec/javascripts/repo/components/new_dropdown/upload_spec.js
Normal file
100
spec/javascripts/repo/components/new_dropdown/upload_spec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue