Commit cc170670 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'ph-multi-file-editor-new-file-folder-dropdown' into 'master'

Add new files & directories in the multi-file editor

Closes #38614

See merge request gitlab-org/gitlab-ce!14839
parents 4aaf4774 c147bccc
<script>
import RepoStore from '../../stores/repo_store';
import RepoHelper from '../../helpers/repo_helper';
import eventHub from '../../event_hub';
import newModal from './modal.vue';
export default {
components: {
newModal,
},
data() {
return {
openModal: false,
modalType: '',
currentPath: RepoStore.path,
};
},
methods: {
createNewItem(type) {
this.modalType = type;
this.toggleModalOpen();
},
toggleModalOpen() {
this.openModal = !this.openModal;
},
createNewEntryInStore(name, type) {
RepoHelper.createNewEntry(name, type);
this.toggleModalOpen();
},
},
created() {
eventHub.$on('createNewEntry', this.createNewEntryInStore);
},
beforeDestroy() {
eventHub.$off('createNewEntry', this.createNewEntryInStore);
},
};
</script>
<template>
<div>
<ul class="breadcrumb repo-breadcrumb">
<li class="dropdown">
<button
type="button"
class="btn btn-default dropdown-toggle add-to-tree"
data-toggle="dropdown"
aria-label="Create new file or directory"
>
<i
class="fa fa-plus"
aria-hidden="true"
>
</i>
</button>
<ul class="dropdown-menu">
<li>
<a
href="#"
role="button"
@click.prevent="createNewItem('blob')"
>
{{ __('New file') }}
</a>
</li>
<li>
<a
href="#"
role="button"
@click.prevent="createNewItem('tree')"
>
{{ __('New directory') }}
</a>
</li>
</ul>
</li>
</ul>
<new-modal
v-if="openModal"
:type="modalType"
:current-path="currentPath"
@toggle="toggleModalOpen"
/>
</div>
</template>
<script>
import { __ } from '../../../locale';
import popupDialog from '../../../vue_shared/components/popup_dialog.vue';
import eventHub from '../../event_hub';
export default {
props: {
currentPath: {
type: String,
required: true,
},
type: {
type: String,
required: true,
},
},
data() {
return {
entryName: this.currentPath !== '' ? `${this.currentPath}/` : '',
};
},
components: {
popupDialog,
},
methods: {
createEntryInStore() {
eventHub.$emit('createNewEntry', this.entryName, this.type);
},
toggleModalOpen() {
this.$emit('toggle');
},
},
computed: {
modalTitle() {
if (this.type === 'tree') {
return __('Create new directory');
}
return __('Create new file');
},
buttonLabel() {
if (this.type === 'tree') {
return __('Create directory');
}
return __('Create file');
},
formLabelName() {
if (this.type === 'tree') {
return __('Directory name');
}
return __('File name');
},
},
mounted() {
this.$refs.fieldName.focus();
},
};
</script>
<template>
<popup-dialog
:title="modalTitle"
:primary-button-label="buttonLabel"
kind="success"
@toggle="toggleModalOpen"
@submit="createEntryInStore"
>
<form
class="form-horizontal"
slot="body"
@submit.prevent="createEntryInStore"
>
<fieldset class="form-group append-bottom-0">
<label class="label-light col-sm-3">
{{ formLabelName }}
</label>
<div class="col-sm-9">
<input
type="text"
class="form-control"
v-model="entryName"
ref="fieldName"
/>
</div>
</fieldset>
</form>
</popup-dialog>
</template>
...@@ -46,6 +46,10 @@ export default { ...@@ -46,6 +46,10 @@ export default {
dialogSubmitted(status) { dialogSubmitted(status) {
this.toggleDialogOpen(false); this.toggleDialogOpen(false);
this.dialog.status = status; this.dialog.status = status;
// remove tmp files
Helper.removeAllTmpFiles('openedFiles');
Helper.removeAllTmpFiles('files');
}, },
toggleBlobView: Store.toggleBlobView, toggleBlobView: Store.toggleBlobView,
createNewBranch(branch) { createNewBranch(branch) {
......
...@@ -49,7 +49,7 @@ export default { ...@@ -49,7 +49,7 @@ export default {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const commitMessage = this.commitMessage; const commitMessage = this.commitMessage;
const actions = this.changedFiles.map(f => ({ const actions = this.changedFiles.map(f => ({
action: 'update', action: f.tempFile ? 'create' : 'update',
file_path: f.path, file_path: f.path,
content: f.newContent, content: f.newContent,
})); }));
...@@ -62,7 +62,6 @@ export default { ...@@ -62,7 +62,6 @@ export default {
if (newBranch) { if (newBranch) {
payload.start_branch = this.currentBranch; payload.start_branch = this.currentBranch;
} }
this.submitCommitsLoading = true;
Service.commitFiles(payload) Service.commitFiles(payload)
.then(() => { .then(() => {
this.resetCommitState(); this.resetCommitState();
...@@ -78,6 +77,8 @@ export default { ...@@ -78,6 +77,8 @@ export default {
}, },
tryCommit(e, skipBranchCheck = false, newBranch = false) { tryCommit(e, skipBranchCheck = false, newBranch = false) {
this.submitCommitsLoading = true;
if (skipBranchCheck) { if (skipBranchCheck) {
this.makeCommit(newBranch); this.makeCommit(newBranch);
} else { } else {
...@@ -90,6 +91,7 @@ export default { ...@@ -90,6 +91,7 @@ export default {
this.makeCommit(newBranch); this.makeCommit(newBranch);
}) })
.catch(() => { .catch(() => {
this.submitCommitsLoading = false;
Flash('An error occurred while committing your changes'); Flash('An error occurred while committing your changes');
}); });
} }
......
...@@ -16,7 +16,7 @@ const RepoEditor = { ...@@ -16,7 +16,7 @@ const RepoEditor = {
}, },
mounted() { mounted() {
Service.getRaw(this.activeFile.raw_path) Service.getRaw(this.activeFile)
.then((rawResponse) => { .then((rawResponse) => {
Store.blobRaw = rawResponse.data; Store.blobRaw = rawResponse.data;
Store.activeFile.plain = rawResponse.data; Store.activeFile.plain = rawResponse.data;
......
...@@ -11,7 +11,12 @@ const RepoFileButtons = { ...@@ -11,7 +11,12 @@ const RepoFileButtons = {
mixins: [RepoMixin], mixins: [RepoMixin],
computed: { computed: {
showButtons() {
return this.activeFile.raw_path ||
this.activeFile.blame_path ||
this.activeFile.commits_path ||
this.activeFile.permalink;
},
rawDownloadButtonLabel() { rawDownloadButtonLabel() {
return this.binary ? 'Download' : 'Raw'; return this.binary ? 'Download' : 'Raw';
}, },
...@@ -30,7 +35,10 @@ export default RepoFileButtons; ...@@ -30,7 +35,10 @@ export default RepoFileButtons;
</script> </script>
<template> <template>
<div id="repo-file-buttons"> <div
v-if="showButtons"
class="repo-file-buttons"
>
<a <a
:href="activeFile.raw_path" :href="activeFile.raw_path"
target="_blank" target="_blank"
......
...@@ -18,8 +18,8 @@ const RepoTab = { ...@@ -18,8 +18,8 @@ const RepoTab = {
}, },
changedClass() { changedClass() {
const tabChangedObj = { const tabChangedObj = {
'fa-times close-icon': !this.tab.changed, 'fa-times close-icon': !this.tab.changed && !this.tab.tempFile,
'fa-circle unsaved-icon': this.tab.changed, 'fa-circle unsaved-icon': this.tab.changed || this.tab.tempFile,
}; };
return tabChangedObj; return tabChangedObj;
}, },
...@@ -30,7 +30,7 @@ const RepoTab = { ...@@ -30,7 +30,7 @@ const RepoTab = {
Store.setActiveFiles(file); Store.setActiveFiles(file);
}, },
closeTab(file) { closeTab(file) {
if (file.changed) return; if (file.changed || file.tempFile) return;
Store.removeFromOpenedFiles(file); Store.removeFromOpenedFiles(file);
}, },
......
import { convertPermissionToBoolean } from '../../lib/utils/common_utils';
import Service from '../services/repo_service'; import Service from '../services/repo_service';
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import Flash from '../../flash'; import Flash from '../../flash';
...@@ -8,6 +7,7 @@ const RepoHelper = { ...@@ -8,6 +7,7 @@ const RepoHelper = {
getDefaultActiveFile() { getDefaultActiveFile() {
return { return {
id: '',
active: true, active: true,
binary: false, binary: false,
extension: '', extension: '',
...@@ -62,6 +62,7 @@ const RepoHelper = { ...@@ -62,6 +62,7 @@ const RepoHelper = {
}); });
RepoHelper.updateHistoryEntry(tree.url, title); RepoHelper.updateHistoryEntry(tree.url, title);
Store.path = tree.path;
}, },
setDirectoryToClosed(entry) { setDirectoryToClosed(entry) {
...@@ -96,8 +97,8 @@ const RepoHelper = { ...@@ -96,8 +97,8 @@ const RepoHelper = {
.then((response) => { .then((response) => {
const data = response.data; const data = response.data;
if (response.headers && response.headers['page-title']) data.pageTitle = decodeURI(response.headers['page-title']); if (response.headers && response.headers['page-title']) data.pageTitle = decodeURI(response.headers['page-title']);
if (response.headers && response.headers['is-root'] && !Store.isInitialRoot) { if (data.path && !Store.isInitialRoot) {
Store.isRoot = convertPermissionToBoolean(response.headers['is-root']); Store.isRoot = data.path === '/';
Store.isInitialRoot = Store.isRoot; Store.isInitialRoot = Store.isRoot;
} }
...@@ -110,7 +111,7 @@ const RepoHelper = { ...@@ -110,7 +111,7 @@ const RepoHelper = {
RepoHelper.setBinaryDataAsBase64(data); RepoHelper.setBinaryDataAsBase64(data);
Store.setViewToPreview(); Store.setViewToPreview();
} else if (!Store.isPreviewView() && !data.render_error) { } else if (!Store.isPreviewView() && !data.render_error) {
Service.getRaw(data.raw_path) Service.getRaw(data)
.then((rawResponse) => { .then((rawResponse) => {
Store.blobRaw = rawResponse.data; Store.blobRaw = rawResponse.data;
data.plain = rawResponse.data; data.plain = rawResponse.data;
...@@ -138,6 +139,10 @@ const RepoHelper = { ...@@ -138,6 +139,10 @@ const RepoHelper = {
addToDirectory(file, data) { addToDirectory(file, data) {
const tree = file || Store; const tree = file || Store;
// TODO: Figure out why `popstate` is being trigger in the specs
if (!tree.files) return;
const files = tree.files.concat(this.dataToListOfFiles(data, file ? file.level + 1 : 0)); const files = tree.files.concat(this.dataToListOfFiles(data, file ? file.level + 1 : 0));
tree.files = files; tree.files = files;
...@@ -157,7 +162,18 @@ const RepoHelper = { ...@@ -157,7 +162,18 @@ const RepoHelper = {
}, },
serializeRepoEntity(type, entity, level = 0) { serializeRepoEntity(type, entity, level = 0) {
const { id, url, name, icon, last_commit, tree_url } = entity; const {
id,
url,
name,
icon,
last_commit,
tree_url,
path,
tempFile,
active,
opened,
} = entity;
return { return {
id, id,
...@@ -165,11 +181,14 @@ const RepoHelper = { ...@@ -165,11 +181,14 @@ const RepoHelper = {
name, name,
url, url,
tree_url, tree_url,
path,
level, level,
tempFile,
icon: `fa-${icon}`, icon: `fa-${icon}`,
files: [], files: [],
loading: false, loading: false,
opened: false, opened,
active,
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
lastCommit: last_commit ? { lastCommit: last_commit ? {
url: `${Store.projectUrl}/commit/${last_commit.id}`, url: `${Store.projectUrl}/commit/${last_commit.id}`,
...@@ -213,7 +232,7 @@ const RepoHelper = { ...@@ -213,7 +232,7 @@ const RepoHelper = {
}, },
findOpenedFileFromActive() { findOpenedFileFromActive() {
return Store.openedFiles.find(openedFile => Store.activeFile.url === openedFile.url); return Store.openedFiles.find(openedFile => Store.activeFile.id === openedFile.id);
}, },
getFileFromPath(path) { getFileFromPath(path) {
...@@ -223,6 +242,76 @@ const RepoHelper = { ...@@ -223,6 +242,76 @@ const RepoHelper = {
loadingError() { loadingError() {
Flash('Unable to load this content at this time.'); Flash('Unable to load this content at this time.');
}, },
openEditMode() {
Store.editMode = true;
Store.currentBlobView = 'repo-editor';
},
updateStorePath(path) {
Store.path = path;
},
findOrCreateEntry(type, tree, name) {
let exists = true;
let foundEntry = tree.files.find(dir => dir.type === type && dir.name === name);
if (!foundEntry) {
foundEntry = RepoHelper.serializeRepoEntity(type, {
id: name,
name,
path: tree.path ? `${tree.path}/${name}` : name,
icon: type === 'tree' ? 'folder' : 'file-text-o',
tempFile: true,
opened: true,
active: true,
}, tree.level !== undefined ? tree.level + 1 : 0);
exists = false;
tree.files.push(foundEntry);
}
return {
entry: foundEntry,
exists,
};
},
removeAllTmpFiles(storeFilesKey) {
Store[storeFilesKey] = Store[storeFilesKey].filter(f => !f.tempFile);
},
createNewEntry(name, type) {
const originalPath = Store.path;
let entryName = name;
if (entryName.indexOf(`${originalPath}/`) !== 0) {
this.updateStorePath('');
} else {
entryName = entryName.replace(`${originalPath}/`, '');
}
if (entryName === '') return;
const fileName = type === 'tree' ? '.gitkeep' : entryName;
let tree = Store;
if (type === 'tree') {
const dirNames = entryName.split('/');
dirNames.forEach((dirName) => {
if (dirName === '') return;
tree = this.findOrCreateEntry('tree', tree, dirName).entry;
});
}
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();
}
}
this.updateStorePath(originalPath);
},
}; };
export default RepoHelper; export default RepoHelper;
...@@ -6,6 +6,7 @@ import Store from './stores/repo_store'; ...@@ -6,6 +6,7 @@ import Store from './stores/repo_store';
import Repo from './components/repo.vue'; import Repo from './components/repo.vue';
import RepoEditButton from './components/repo_edit_button.vue'; import RepoEditButton from './components/repo_edit_button.vue';
import newBranchForm from './components/new_branch_form.vue'; import newBranchForm from './components/new_branch_form.vue';
import newDropdown from './components/new_dropdown/index.vue';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
function initDropdowns() { function initDropdowns() {
...@@ -28,6 +29,7 @@ function setInitialStore(data) { ...@@ -28,6 +29,7 @@ function setInitialStore(data) {
Store.service = Service; Store.service = Service;
Store.service.url = data.url; Store.service.url = data.url;
Store.service.refsUrl = data.refsUrl; Store.service.refsUrl = data.refsUrl;
Store.path = data.currentPath;
Store.projectId = data.projectId; Store.projectId = data.projectId;
Store.projectName = data.projectName; Store.projectName = data.projectName;
Store.projectUrl = data.projectUrl; Store.projectUrl = data.projectUrl;
...@@ -63,6 +65,18 @@ function initRepoEditButton(el) { ...@@ -63,6 +65,18 @@ function initRepoEditButton(el) {
}); });
} }
function initNewDropdown(el) {
return new Vue({
el,
components: {
newDropdown,
},
render(createElement) {
return createElement('new-dropdown');
},
});
}
function initNewBranchForm() { function initNewBranchForm() {
const el = document.querySelector('.js-new-branch-dropdown'); const el = document.querySelector('.js-new-branch-dropdown');
...@@ -86,6 +100,7 @@ function initNewBranchForm() { ...@@ -86,6 +100,7 @@ function initNewBranchForm() {
function initRepoBundle() { function initRepoBundle() {
const repo = document.getElementById('repo'); const repo = document.getElementById('repo');
const editButton = document.querySelector('.editable-mode'); const editButton = document.querySelector('.editable-mode');
const newDropdownHolder = document.querySelector('.js-new-dropdown');
setInitialStore(repo.dataset); setInitialStore(repo.dataset);
addEventsForNonVueEls(); addEventsForNonVueEls();
initDropdowns(); initDropdowns();
...@@ -95,6 +110,7 @@ function initRepoBundle() { ...@@ -95,6 +110,7 @@ function initRepoBundle() {
initRepo(repo); initRepo(repo);
initRepoEditButton(editButton); initRepoEditButton(editButton);
initNewBranchForm(); initNewBranchForm();
initNewDropdown(newDropdownHolder);
} }
$(initRepoBundle); $(initRepoBundle);
......
...@@ -8,7 +8,7 @@ const RepoMixin = { ...@@ -8,7 +8,7 @@ const RepoMixin = {
changedFiles() { changedFiles() {
const changedFileList = this.openedFiles const changedFileList = this.openedFiles
.filter(file => file.changed); .filter(file => file.changed || file.tempFile);
return changedFileList; return changedFileList;
}, },
}, },
......
...@@ -16,8 +16,14 @@ const RepoService = { ...@@ -16,8 +16,14 @@ const RepoService = {
createBranchPath: '/api/:version/projects/:id/repository/branches', createBranchPath: '/api/:version/projects/:id/repository/branches',
richExtensionRegExp: /md/, richExtensionRegExp: /md/,
getRaw(url) { getRaw(file) {
return axios.get(url, { if (file.tempFile) {
return Promise.resolve({
data: '',
});
}
return axios.get(file.raw_path, {
// Stop Axios from parsing a JSON file into a JS object // Stop Axios from parsing a JSON file into a JS object
transformResponse: [res => res], transformResponse: [res => res],
}); });
......
...@@ -39,6 +39,7 @@ const RepoStore = { ...@@ -39,6 +39,7 @@ const RepoStore = {
newMrTemplateUrl: '', newMrTemplateUrl: '',
branchChanged: false, branchChanged: false,
commitMessage: '', commitMessage: '',
path: '',
loading: { loading: {
tree: false, tree: false,
blob: false, blob: false,
...@@ -77,21 +78,23 @@ const RepoStore = { ...@@ -77,21 +78,23 @@ const RepoStore = {
} else if (file.newContent || file.plain) { } else if (file.newContent || file.plain) {
RepoStore.blobRaw = file.newContent || file.plain; RepoStore.blobRaw = file.newContent || file.plain;
} else { } else {
Service.getRaw(file.raw_path) Service.getRaw(file)
.then((rawResponse) => { .then((rawResponse) => {
RepoStore.blobRaw = rawResponse.data; RepoStore.blobRaw = rawResponse.data;
Helper.findOpenedFileFromActive().plain = rawResponse.data; Helper.findOpenedFileFromActive().plain = rawResponse.data;
}).catch(Helper.loadingError); }).catch(Helper.loadingError);
} }
if (!file.loading) Helper.updateHistoryEntry(file.url, file.pageTitle || file.name); if (!file.loading && !file.tempFile) {
Helper.updateHistoryEntry(file.url, file.pageTitle || file.name);
}
RepoStore.binary = file.binary; RepoStore.binary = file.binary;
RepoStore.setActiveLine(-1); RepoStore.setActiveLine(-1);
}, },
setFileActivity(file, openedFile, i) { setFileActivity(file, openedFile, i) {
const activeFile = openedFile; const activeFile = openedFile;
activeFile.active = file.url === activeFile.url; activeFile.active = file.id === activeFile.id;
if (activeFile.active) RepoStore.setActiveFile(activeFile, i); if (activeFile.active) RepoStore.setActiveFile(activeFile, i);
...@@ -99,7 +102,7 @@ const RepoStore = { ...@@ -99,7 +102,7 @@ const RepoStore = {
}, },
setActiveFile(activeFile, i) { setActiveFile(activeFile, i) {
RepoStore.activeFile = Object.assign({}, RepoStore.activeFile, activeFile); RepoStore.activeFile = Object.assign({}, Helper.getDefaultActiveFile(), activeFile);
RepoStore.activeFileIndex = i; RepoStore.activeFileIndex = i;
}, },
...@@ -121,6 +124,11 @@ const RepoStore = { ...@@ -121,6 +124,11 @@ const RepoStore = {
return openedFile.path !== file.path; return openedFile.path !== file.path;
}); });
// remove the file from the sidebar if it is a tempFile
if (file.tempFile) {
RepoStore.files = RepoStore.files.filter(f => !(f.tempFile && f.path === file.path));
}
// now activate the right tab based on what you closed. // now activate the right tab based on what you closed.
if (RepoStore.openedFiles.length === 0) { if (RepoStore.openedFiles.length === 0) {
RepoStore.activeFile = {}; RepoStore.activeFile = {};
...@@ -170,7 +178,7 @@ const RepoStore = { ...@@ -170,7 +178,7 @@ const RepoStore = {
// getters // getters
isActiveFile(file) { isActiveFile(file) {
return file && file.url === RepoStore.activeFile.url; return file && file.id === RepoStore.activeFile.id;
}, },
isPreviewView() { isPreviewView() {
......
...@@ -9,7 +9,7 @@ export default { ...@@ -9,7 +9,7 @@ export default {
}, },
text: { text: {
type: String, type: String,
required: true, required: false,
}, },
kind: { kind: {
type: String, type: String,
...@@ -82,14 +82,15 @@ export default { ...@@ -82,14 +82,15 @@ export default {
type="button" type="button"
class="btn" class="btn"
:class="btnCancelKindClass" :class="btnCancelKindClass"
@click="emitSubmit(false)"> @click="close">
{{closeButtonLabel}} {{ closeButtonLabel }}
</button> </button>
<button type="button" <button
type="button"
class="btn" class="btn"
:class="btnKindClass" :class="btnKindClass"
@click="emitSubmit(true)"> @click="emitSubmit(true)">
{{primaryButtonLabel}} {{ primaryButtonLabel }}
</button> </button>
</div> </div>
</div> </div>
......
...@@ -201,7 +201,7 @@ ...@@ -201,7 +201,7 @@
} }
} }
#repo-file-buttons { .repo-file-buttons {
background-color: $white-light; background-color: $white-light;
padding: 5px 10px; padding: 5px 10px;
border-top: 1px solid $white-normal; border-top: 1px solid $white-normal;
......
...@@ -205,6 +205,7 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -205,6 +205,7 @@ class Projects::BlobController < Projects::ApplicationController
tree_path = path_segments.join('/') tree_path = path_segments.join('/')
render json: json.merge( render json: json.merge(
id: @blob.id,
path: blob.path, path: blob.path,
name: blob.name, name: blob.name,
extension: blob.extension, extension: blob.extension,
......
...@@ -36,7 +36,6 @@ class Projects::TreeController < Projects::ApplicationController ...@@ -36,7 +36,6 @@ class Projects::TreeController < Projects::ApplicationController
format.json do format.json do
page_title @path.presence || _("Files"), @ref, @project.name_with_namespace page_title @path.presence || _("Files"), @ref, @project.name_with_namespace
response.header['is-root'] = @path.empty?
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38261 # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38261
Gitlab::GitalyClient.allow_n_plus_1_calls do Gitlab::GitalyClient.allow_n_plus_1_calls do
......
...@@ -2,7 +2,9 @@ ...@@ -2,7 +2,9 @@
.tree-ref-holder .tree-ref-holder
= render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true = render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true
- unless show_new_repo? - if show_new_repo?
.js-new-dropdown
- else
= render 'projects/tree/old_tree_header' = render 'projects/tree/old_tree_header'
.tree-controls .tree-controls
......
...@@ -7,4 +7,5 @@ ...@@ -7,4 +7,5 @@
blob_url: namespace_project_blob_path(project.namespace, project, '{{branch}}'), blob_url: namespace_project_blob_path(project.namespace, project, '{{branch}}'),
new_mr_template_url: namespace_project_new_merge_request_path(project.namespace, project, merge_request: { source_branch: '{{source_branch}}' }), new_mr_template_url: namespace_project_new_merge_request_path(project.namespace, project, merge_request: { source_branch: '{{source_branch}}' }),
can_commit: (!!can_push_branch?(project, @ref)).to_s, can_commit: (!!can_push_branch?(project, @ref)).to_s,
on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s } } on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s,
current_path: @path } }
require 'spec_helper'
feature 'Multi-file editor new directory', :js do
include WaitForRequests
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
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 'creates directory in current directory' do
find('.add-to-tree').click
click_link('New directory')
page.within('.popup-dialog') do
find('.form-control').set('foldername')
click_button('Create directory')
end
fill_in('commit-message', with: 'commit message')
click_button('Commit 1 file')
expect(page).to have_content('Your changes have been committed')
expect(page).to have_selector('td', text: 'commit message')
click_link('foldername')
expect(page).to have_selector('td', text: 'commit message', count: 2)
expect(page).to have_selector('td', text: '.gitkeep')
end
end
require 'spec_helper'
feature 'Multi-file editor new file', :js do
include WaitForRequests
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
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 'creates file in current directory' do
find('.add-to-tree').click
click_link('New file')
page.within('.popup-dialog') do
find('.form-control').set('filename')
click_button('Create file')
end
find('.inputarea').send_keys('file content')
fill_in('commit-message', with: 'commit message')
click_button('Commit 1 file')
expect(page).to have_content('Your changes have been committed')
expect(page).to have_selector('td', text: 'commit message')
end
end
export default (Component, props = {}) => new Component({ export default (Component, props = {}, el = null) => new Component({
propsData: props, propsData: props,
}).$mount(); }).$mount(el);
import Vue from 'vue';
import newDropdown from '~/repo/components/new_dropdown/index.vue';
import RepoStore from '~/repo/stores/repo_store';
import RepoHelper from '~/repo/helpers/repo_helper';
import eventHub from '~/repo/event_hub';
import createComponent from '../../../helpers/vue_mount_component_helper';
describe('new dropdown component', () => {
let vm;
beforeEach(() => {
const component = Vue.extend(newDropdown);
vm = createComponent(component);
});
afterEach(() => {
vm.$destroy();
RepoStore.files = [];
RepoStore.openedFiles = [];
RepoStore.setViewToPreview();
});
it('renders new file and new directory links', () => {
expect(vm.$el.querySelectorAll('a')[0].textContent.trim()).toBe('New file');
expect(vm.$el.querySelectorAll('a')[1].textContent.trim()).toBe('New directory');
});
describe('createNewItem', () => {
it('sets modalType to blob when new file is clicked', () => {
vm.$el.querySelectorAll('a')[0].click();
expect(vm.modalType).toBe('blob');
});
it('sets modalType to tree when new directory is clicked', () => {
vm.$el.querySelectorAll('a')[1].click();
expect(vm.modalType).toBe('tree');
});
it('opens modal when link is clicked', (done) => {
vm.$el.querySelectorAll('a')[0].click();
Vue.nextTick(() => {
expect(vm.$el.querySelector('.modal')).not.toBeNull();
done();
});
});
});
describe('toggleModalOpen', () => {
it('closes modal after toggling', (done) => {
vm.toggleModalOpen();
Vue.nextTick()
.then(() => {
expect(vm.$el.querySelector('.modal')).not.toBeNull();
})
.then(vm.toggleModalOpen)
.then(() => {
expect(vm.$el.querySelector('.modal')).toBeNull();
})
.then(done)
.catch(done.fail);
});
});
describe('createEntryInStore', () => {
['tree', 'blob'].forEach((type) => {
describe(type, () => {
it('closes modal after creating file', () => {
vm.openModal = true;
eventHub.$emit('createNewEntry', 'testing', type);
expect(vm.openModal).toBeFalsy();
});
it('sets editMode to true', () => {
eventHub.$emit('createNewEntry', 'testing', type);
expect(RepoStore.editMode).toBeTruthy();
});
it('toggles blob view', () => {
eventHub.$emit('createNewEntry', 'testing', type);
expect(RepoStore.isPreviewView()).toBeFalsy();
});
it('adds file into activeFiles', () => {
eventHub.$emit('createNewEntry', 'testing', type);
expect(RepoStore.openedFiles.length).toBe(1);
});
it(`creates ${type} in the current stores path`, () => {
RepoStore.path = 'testing';
eventHub.$emit('createNewEntry', 'testing/app', type);
expect(RepoStore.files[0].path).toBe('testing/app');
expect(RepoStore.files[0].name).toBe('app');
if (type === 'tree') {
expect(RepoStore.files[0].files.length).toBe(1);
}
RepoStore.path = '';
});
});
});
describe('file', () => {
it('creates new file', () => {
eventHub.$emit('createNewEntry', 'testing', 'blob');
expect(RepoStore.files.length).toBe(1);
expect(RepoStore.files[0].name).toBe('testing');
expect(RepoStore.files[0].type).toBe('blob');
expect(RepoStore.files[0].tempFile).toBeTruthy();
});
it('does not create temp file when file already exists', () => {
RepoStore.files.push(RepoHelper.serializeRepoEntity('blob', {
name: 'testing',
}));
eventHub.$emit('createNewEntry', 'testing', 'blob');
expect(RepoStore.files.length).toBe(1);
expect(RepoStore.files[0].name).toBe('testing');
expect(RepoStore.files[0].type).toBe('blob');
expect(RepoStore.files[0].tempFile).toBeUndefined();
});
});
describe('tree', () => {
it('creates new tree', () => {
eventHub.$emit('createNewEntry', 'testing', 'tree');
expect(RepoStore.files.length).toBe(1);
expect(RepoStore.files[0].name).toBe('testing');
expect(RepoStore.files[0].type).toBe('tree');
expect(RepoStore.files[0].tempFile).toBeTruthy();
expect(RepoStore.files[0].files.length).toBe(1);
expect(RepoStore.files[0].files[0].name).toBe('.gitkeep');
});
it('creates multiple trees when entryName has slashes', () => {
eventHub.$emit('createNewEntry', 'app/test', 'tree');
expect(RepoStore.files.length).toBe(1);
expect(RepoStore.files[0].name).toBe('app');
expect(RepoStore.files[0].files[0].name).toBe('test');
expect(RepoStore.files[0].files[0].files[0].name).toBe('.gitkeep');
});
it('creates tree in existing tree', () => {
RepoStore.files.push(RepoHelper.serializeRepoEntity('tree', {
name: 'app',
}));
eventHub.$emit('createNewEntry', 'app/test', 'tree');
expect(RepoStore.files.length).toBe(1);
expect(RepoStore.files[0].name).toBe('app');
expect(RepoStore.files[0].tempFile).toBeUndefined();
expect(RepoStore.files[0].files[0].tempFile).toBeTruthy();
expect(RepoStore.files[0].files[0].name).toBe('test');
expect(RepoStore.files[0].files[0].files[0].name).toBe('.gitkeep');
});
it('does not create new tree when already exists', () => {
RepoStore.files.push(RepoHelper.serializeRepoEntity('tree', {
name: 'app',
}));
eventHub.$emit('createNewEntry', 'app', 'tree');
expect(RepoStore.files.length).toBe(1);
expect(RepoStore.files[0].name).toBe('app');
expect(RepoStore.files[0].tempFile).toBeUndefined();
expect(RepoStore.files[0].files.length).toBe(0);
});
});
});
});
import Vue from 'vue';
import RepoStore from '~/repo/stores/repo_store';
import modal from '~/repo/components/new_dropdown/modal.vue';
import eventHub from '~/repo/event_hub';
import createComponent from '../../../helpers/vue_mount_component_helper';
describe('new file modal component', () => {
const Component = Vue.extend(modal);
let vm;
afterEach(() => {
vm.$destroy();
RepoStore.files = [];
RepoStore.openedFiles = [];
RepoStore.setViewToPreview();
});
['tree', 'blob'].forEach((type) => {
describe(type, () => {
beforeEach(() => {
vm = createComponent(Component, {
type,
currentPath: RepoStore.path,
});
});
it(`sets modal title as ${type}`, () => {
const title = type === 'tree' ? 'directory' : 'file';
expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe(`Create new ${title}`);
});
it(`sets button label as ${type}`, () => {
const title = type === 'tree' ? 'directory' : 'file';
expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe(`Create ${title}`);
});
it(`sets form label as ${type}`, () => {
const title = type === 'tree' ? 'Directory' : 'File';
expect(vm.$el.querySelector('.label-light').textContent.trim()).toBe(`${title} name`);
});
});
});
it('focuses field on mount', () => {
document.body.innerHTML += '<div class="js-test"></div>';
vm = createComponent(Component, {
type: 'tree',
currentPath: RepoStore.path,
}, '.js-test');
expect(document.activeElement).toBe(vm.$refs.fieldName);
vm.$el.remove();
});
describe('createEntryInStore', () => {
it('emits createNewEntry event', () => {
spyOn(eventHub, '$emit');
vm = createComponent(Component, {
type: 'tree',
currentPath: RepoStore.path,
});
vm.entryName = 'testing';
vm.createEntryInStore();
expect(eventHub.$emit).toHaveBeenCalledWith('createNewEntry', 'testing', 'tree');
});
});
});
...@@ -3,6 +3,15 @@ import repoFileButtons from '~/repo/components/repo_file_buttons.vue'; ...@@ -3,6 +3,15 @@ import repoFileButtons from '~/repo/components/repo_file_buttons.vue';
import RepoStore from '~/repo/stores/repo_store'; import RepoStore from '~/repo/stores/repo_store';
describe('RepoFileButtons', () => { describe('RepoFileButtons', () => {
const activeFile = {
extension: 'md',
url: 'url',
raw_path: 'raw_path',
blame_path: 'blame_path',
commits_path: 'commits_path',
permalink: 'permalink',
};
function createComponent() { function createComponent() {
const RepoFileButtons = Vue.extend(repoFileButtons); const RepoFileButtons = Vue.extend(repoFileButtons);
...@@ -14,14 +23,6 @@ describe('RepoFileButtons', () => { ...@@ -14,14 +23,6 @@ describe('RepoFileButtons', () => {
}); });
it('renders Raw, Blame, History, Permalink and Preview toggle', () => { it('renders Raw, Blame, History, Permalink and Preview toggle', () => {
const activeFile = {
extension: 'md',
url: 'url',
raw_path: 'raw_path',
blame_path: 'blame_path',
commits_path: 'commits_path',
permalink: 'permalink',
};
const activeFileLabel = 'activeFileLabel'; const activeFileLabel = 'activeFileLabel';
RepoStore.openedFiles = new Array(1); RepoStore.openedFiles = new Array(1);
RepoStore.activeFile = activeFile; RepoStore.activeFile = activeFile;
...@@ -34,7 +35,6 @@ describe('RepoFileButtons', () => { ...@@ -34,7 +35,6 @@ describe('RepoFileButtons', () => {
const blame = vm.$el.querySelector('.blame'); const blame = vm.$el.querySelector('.blame');
const history = vm.$el.querySelector('.history'); const history = vm.$el.querySelector('.history');
expect(vm.$el.id).toEqual('repo-file-buttons');
expect(raw.href).toMatch(`/${activeFile.raw_path}`); expect(raw.href).toMatch(`/${activeFile.raw_path}`);
expect(raw.textContent.trim()).toEqual('Raw'); expect(raw.textContent.trim()).toEqual('Raw');
expect(blame.href).toMatch(`/${activeFile.blame_path}`); expect(blame.href).toMatch(`/${activeFile.blame_path}`);
...@@ -46,10 +46,6 @@ describe('RepoFileButtons', () => { ...@@ -46,10 +46,6 @@ describe('RepoFileButtons', () => {
}); });
it('triggers rawPreviewToggle on preview click', () => { it('triggers rawPreviewToggle on preview click', () => {
const activeFile = {
extension: 'md',
url: 'url',
};
RepoStore.openedFiles = new Array(1); RepoStore.openedFiles = new Array(1);
RepoStore.activeFile = activeFile; RepoStore.activeFile = activeFile;
RepoStore.editMode = true; RepoStore.editMode = true;
...@@ -65,10 +61,7 @@ describe('RepoFileButtons', () => { ...@@ -65,10 +61,7 @@ describe('RepoFileButtons', () => {
}); });
it('does not render preview toggle if not canPreview', () => { it('does not render preview toggle if not canPreview', () => {
const activeFile = { activeFile.extension = 'js';
extension: 'abcd',
url: 'url',
};
RepoStore.openedFiles = new Array(1); RepoStore.openedFiles = new Array(1);
RepoStore.activeFile = activeFile; RepoStore.activeFile = activeFile;
......
...@@ -7,6 +7,7 @@ import { file } from '../mock_data'; ...@@ -7,6 +7,7 @@ import { file } from '../mock_data';
describe('RepoFile', () => { describe('RepoFile', () => {
const updated = 'updated'; const updated = 'updated';
const otherFile = { const otherFile = {
id: 'test',
html: '<p class="file-content">html</p>', html: '<p class="file-content">html</p>',
pageTitle: 'otherpageTitle', pageTitle: 'otherpageTitle',
}; };
......
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