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
This commit is contained in:
commit
cc17067085
|
@ -0,0 +1,86 @@
|
|||
<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>
|
|
@ -0,0 +1,90 @@
|
|||
<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 {
|
|||
dialogSubmitted(status) {
|
||||
this.toggleDialogOpen(false);
|
||||
this.dialog.status = status;
|
||||
|
||||
// remove tmp files
|
||||
Helper.removeAllTmpFiles('openedFiles');
|
||||
Helper.removeAllTmpFiles('files');
|
||||
},
|
||||
toggleBlobView: Store.toggleBlobView,
|
||||
createNewBranch(branch) {
|
||||
|
|
|
@ -49,7 +49,7 @@ export default {
|
|||
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
|
||||
const commitMessage = this.commitMessage;
|
||||
const actions = this.changedFiles.map(f => ({
|
||||
action: 'update',
|
||||
action: f.tempFile ? 'create' : 'update',
|
||||
file_path: f.path,
|
||||
content: f.newContent,
|
||||
}));
|
||||
|
@ -62,7 +62,6 @@ export default {
|
|||
if (newBranch) {
|
||||
payload.start_branch = this.currentBranch;
|
||||
}
|
||||
this.submitCommitsLoading = true;
|
||||
Service.commitFiles(payload)
|
||||
.then(() => {
|
||||
this.resetCommitState();
|
||||
|
@ -78,6 +77,8 @@ export default {
|
|||
},
|
||||
|
||||
tryCommit(e, skipBranchCheck = false, newBranch = false) {
|
||||
this.submitCommitsLoading = true;
|
||||
|
||||
if (skipBranchCheck) {
|
||||
this.makeCommit(newBranch);
|
||||
} else {
|
||||
|
@ -90,6 +91,7 @@ export default {
|
|||
this.makeCommit(newBranch);
|
||||
})
|
||||
.catch(() => {
|
||||
this.submitCommitsLoading = false;
|
||||
Flash('An error occurred while committing your changes');
|
||||
});
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ const RepoEditor = {
|
|||
},
|
||||
|
||||
mounted() {
|
||||
Service.getRaw(this.activeFile.raw_path)
|
||||
Service.getRaw(this.activeFile)
|
||||
.then((rawResponse) => {
|
||||
Store.blobRaw = rawResponse.data;
|
||||
Store.activeFile.plain = rawResponse.data;
|
||||
|
|
|
@ -11,7 +11,12 @@ const RepoFileButtons = {
|
|||
mixins: [RepoMixin],
|
||||
|
||||
computed: {
|
||||
|
||||
showButtons() {
|
||||
return this.activeFile.raw_path ||
|
||||
this.activeFile.blame_path ||
|
||||
this.activeFile.commits_path ||
|
||||
this.activeFile.permalink;
|
||||
},
|
||||
rawDownloadButtonLabel() {
|
||||
return this.binary ? 'Download' : 'Raw';
|
||||
},
|
||||
|
@ -30,7 +35,10 @@ export default RepoFileButtons;
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div id="repo-file-buttons">
|
||||
<div
|
||||
v-if="showButtons"
|
||||
class="repo-file-buttons"
|
||||
>
|
||||
<a
|
||||
:href="activeFile.raw_path"
|
||||
target="_blank"
|
||||
|
|
|
@ -18,8 +18,8 @@ const RepoTab = {
|
|||
},
|
||||
changedClass() {
|
||||
const tabChangedObj = {
|
||||
'fa-times close-icon': !this.tab.changed,
|
||||
'fa-circle unsaved-icon': this.tab.changed,
|
||||
'fa-times close-icon': !this.tab.changed && !this.tab.tempFile,
|
||||
'fa-circle unsaved-icon': this.tab.changed || this.tab.tempFile,
|
||||
};
|
||||
return tabChangedObj;
|
||||
},
|
||||
|
@ -30,7 +30,7 @@ const RepoTab = {
|
|||
Store.setActiveFiles(file);
|
||||
},
|
||||
closeTab(file) {
|
||||
if (file.changed) return;
|
||||
if (file.changed || file.tempFile) return;
|
||||
|
||||
Store.removeFromOpenedFiles(file);
|
||||
},
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { convertPermissionToBoolean } from '../../lib/utils/common_utils';
|
||||
import Service from '../services/repo_service';
|
||||
import Store from '../stores/repo_store';
|
||||
import Flash from '../../flash';
|
||||
|
@ -8,6 +7,7 @@ const RepoHelper = {
|
|||
|
||||
getDefaultActiveFile() {
|
||||
return {
|
||||
id: '',
|
||||
active: true,
|
||||
binary: false,
|
||||
extension: '',
|
||||
|
@ -62,6 +62,7 @@ const RepoHelper = {
|
|||
});
|
||||
|
||||
RepoHelper.updateHistoryEntry(tree.url, title);
|
||||
Store.path = tree.path;
|
||||
},
|
||||
|
||||
setDirectoryToClosed(entry) {
|
||||
|
@ -96,8 +97,8 @@ const RepoHelper = {
|
|||
.then((response) => {
|
||||
const data = response.data;
|
||||
if (response.headers && response.headers['page-title']) data.pageTitle = decodeURI(response.headers['page-title']);
|
||||
if (response.headers && response.headers['is-root'] && !Store.isInitialRoot) {
|
||||
Store.isRoot = convertPermissionToBoolean(response.headers['is-root']);
|
||||
if (data.path && !Store.isInitialRoot) {
|
||||
Store.isRoot = data.path === '/';
|
||||
Store.isInitialRoot = Store.isRoot;
|
||||
}
|
||||
|
||||
|
@ -110,7 +111,7 @@ const RepoHelper = {
|
|||
RepoHelper.setBinaryDataAsBase64(data);
|
||||
Store.setViewToPreview();
|
||||
} else if (!Store.isPreviewView() && !data.render_error) {
|
||||
Service.getRaw(data.raw_path)
|
||||
Service.getRaw(data)
|
||||
.then((rawResponse) => {
|
||||
Store.blobRaw = rawResponse.data;
|
||||
data.plain = rawResponse.data;
|
||||
|
@ -138,6 +139,10 @@ const RepoHelper = {
|
|||
|
||||
addToDirectory(file, data) {
|
||||
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));
|
||||
|
||||
tree.files = files;
|
||||
|
@ -157,7 +162,18 @@ const RepoHelper = {
|
|||
},
|
||||
|
||||
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 {
|
||||
id,
|
||||
|
@ -165,11 +181,14 @@ const RepoHelper = {
|
|||
name,
|
||||
url,
|
||||
tree_url,
|
||||
path,
|
||||
level,
|
||||
tempFile,
|
||||
icon: `fa-${icon}`,
|
||||
files: [],
|
||||
loading: false,
|
||||
opened: false,
|
||||
opened,
|
||||
active,
|
||||
// eslint-disable-next-line camelcase
|
||||
lastCommit: last_commit ? {
|
||||
url: `${Store.projectUrl}/commit/${last_commit.id}`,
|
||||
|
@ -213,7 +232,7 @@ const RepoHelper = {
|
|||
},
|
||||
|
||||
findOpenedFileFromActive() {
|
||||
return Store.openedFiles.find(openedFile => Store.activeFile.url === openedFile.url);
|
||||
return Store.openedFiles.find(openedFile => Store.activeFile.id === openedFile.id);
|
||||
},
|
||||
|
||||
getFileFromPath(path) {
|
||||
|
@ -223,6 +242,76 @@ const RepoHelper = {
|
|||
loadingError() {
|
||||
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;
|
||||
|
|
|
@ -6,6 +6,7 @@ import Store from './stores/repo_store';
|
|||
import Repo from './components/repo.vue';
|
||||
import RepoEditButton from './components/repo_edit_button.vue';
|
||||
import newBranchForm from './components/new_branch_form.vue';
|
||||
import newDropdown from './components/new_dropdown/index.vue';
|
||||
import Translate from '../vue_shared/translate';
|
||||
|
||||
function initDropdowns() {
|
||||
|
@ -28,6 +29,7 @@ function setInitialStore(data) {
|
|||
Store.service = Service;
|
||||
Store.service.url = data.url;
|
||||
Store.service.refsUrl = data.refsUrl;
|
||||
Store.path = data.currentPath;
|
||||
Store.projectId = data.projectId;
|
||||
Store.projectName = data.projectName;
|
||||
Store.projectUrl = data.projectUrl;
|
||||
|
@ -63,6 +65,18 @@ function initRepoEditButton(el) {
|
|||
});
|
||||
}
|
||||
|
||||
function initNewDropdown(el) {
|
||||
return new Vue({
|
||||
el,
|
||||
components: {
|
||||
newDropdown,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('new-dropdown');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function initNewBranchForm() {
|
||||
const el = document.querySelector('.js-new-branch-dropdown');
|
||||
|
||||
|
@ -86,6 +100,7 @@ function initNewBranchForm() {
|
|||
function initRepoBundle() {
|
||||
const repo = document.getElementById('repo');
|
||||
const editButton = document.querySelector('.editable-mode');
|
||||
const newDropdownHolder = document.querySelector('.js-new-dropdown');
|
||||
setInitialStore(repo.dataset);
|
||||
addEventsForNonVueEls();
|
||||
initDropdowns();
|
||||
|
@ -95,6 +110,7 @@ function initRepoBundle() {
|
|||
initRepo(repo);
|
||||
initRepoEditButton(editButton);
|
||||
initNewBranchForm();
|
||||
initNewDropdown(newDropdownHolder);
|
||||
}
|
||||
|
||||
$(initRepoBundle);
|
||||
|
|
|
@ -8,7 +8,7 @@ const RepoMixin = {
|
|||
|
||||
changedFiles() {
|
||||
const changedFileList = this.openedFiles
|
||||
.filter(file => file.changed);
|
||||
.filter(file => file.changed || file.tempFile);
|
||||
return changedFileList;
|
||||
},
|
||||
},
|
||||
|
|
|
@ -16,8 +16,14 @@ const RepoService = {
|
|||
createBranchPath: '/api/:version/projects/:id/repository/branches',
|
||||
richExtensionRegExp: /md/,
|
||||
|
||||
getRaw(url) {
|
||||
return axios.get(url, {
|
||||
getRaw(file) {
|
||||
if (file.tempFile) {
|
||||
return Promise.resolve({
|
||||
data: '',
|
||||
});
|
||||
}
|
||||
|
||||
return axios.get(file.raw_path, {
|
||||
// Stop Axios from parsing a JSON file into a JS object
|
||||
transformResponse: [res => res],
|
||||
});
|
||||
|
|
|
@ -39,6 +39,7 @@ const RepoStore = {
|
|||
newMrTemplateUrl: '',
|
||||
branchChanged: false,
|
||||
commitMessage: '',
|
||||
path: '',
|
||||
loading: {
|
||||
tree: false,
|
||||
blob: false,
|
||||
|
@ -77,21 +78,23 @@ const RepoStore = {
|
|||
} else if (file.newContent || file.plain) {
|
||||
RepoStore.blobRaw = file.newContent || file.plain;
|
||||
} else {
|
||||
Service.getRaw(file.raw_path)
|
||||
Service.getRaw(file)
|
||||
.then((rawResponse) => {
|
||||
RepoStore.blobRaw = rawResponse.data;
|
||||
Helper.findOpenedFileFromActive().plain = rawResponse.data;
|
||||
}).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.setActiveLine(-1);
|
||||
},
|
||||
|
||||
setFileActivity(file, openedFile, i) {
|
||||
const activeFile = openedFile;
|
||||
activeFile.active = file.url === activeFile.url;
|
||||
activeFile.active = file.id === activeFile.id;
|
||||
|
||||
if (activeFile.active) RepoStore.setActiveFile(activeFile, i);
|
||||
|
||||
|
@ -99,7 +102,7 @@ const RepoStore = {
|
|||
},
|
||||
|
||||
setActiveFile(activeFile, i) {
|
||||
RepoStore.activeFile = Object.assign({}, RepoStore.activeFile, activeFile);
|
||||
RepoStore.activeFile = Object.assign({}, Helper.getDefaultActiveFile(), activeFile);
|
||||
RepoStore.activeFileIndex = i;
|
||||
},
|
||||
|
||||
|
@ -121,6 +124,11 @@ const RepoStore = {
|
|||
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.
|
||||
if (RepoStore.openedFiles.length === 0) {
|
||||
RepoStore.activeFile = {};
|
||||
|
@ -170,7 +178,7 @@ const RepoStore = {
|
|||
// getters
|
||||
|
||||
isActiveFile(file) {
|
||||
return file && file.url === RepoStore.activeFile.url;
|
||||
return file && file.id === RepoStore.activeFile.id;
|
||||
},
|
||||
|
||||
isPreviewView() {
|
||||
|
|
|
@ -9,7 +9,7 @@ export default {
|
|||
},
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: false,
|
||||
},
|
||||
kind: {
|
||||
type: String,
|
||||
|
@ -82,14 +82,15 @@ export default {
|
|||
type="button"
|
||||
class="btn"
|
||||
:class="btnCancelKindClass"
|
||||
@click="emitSubmit(false)">
|
||||
{{closeButtonLabel}}
|
||||
@click="close">
|
||||
{{ closeButtonLabel }}
|
||||
</button>
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
class="btn"
|
||||
:class="btnKindClass"
|
||||
@click="emitSubmit(true)">
|
||||
{{primaryButtonLabel}}
|
||||
{{ primaryButtonLabel }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -201,7 +201,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
#repo-file-buttons {
|
||||
.repo-file-buttons {
|
||||
background-color: $white-light;
|
||||
padding: 5px 10px;
|
||||
border-top: 1px solid $white-normal;
|
||||
|
|
|
@ -205,6 +205,7 @@ class Projects::BlobController < Projects::ApplicationController
|
|||
tree_path = path_segments.join('/')
|
||||
|
||||
render json: json.merge(
|
||||
id: @blob.id,
|
||||
path: blob.path,
|
||||
name: blob.name,
|
||||
extension: blob.extension,
|
||||
|
|
|
@ -36,7 +36,6 @@ class Projects::TreeController < Projects::ApplicationController
|
|||
|
||||
format.json do
|
||||
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
|
||||
Gitlab::GitalyClient.allow_n_plus_1_calls do
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
.tree-ref-holder
|
||||
= 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'
|
||||
|
||||
.tree-controls
|
||||
|
|
|
@ -7,4 +7,5 @@
|
|||
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}}' }),
|
||||
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 } }
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
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
|
|
@ -0,0 +1,40 @@
|
|||
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
|
|
@ -1,4 +1,3 @@
|
|||
export default (Component, props = {}) => new Component({
|
||||
export default (Component, props = {}, el = null) => new Component({
|
||||
propsData: props,
|
||||
}).$mount();
|
||||
|
||||
}).$mount(el);
|
||||
|
|
|
@ -0,0 +1,191 @@
|
|||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,76 @@
|
|||
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';
|
|||
import RepoStore from '~/repo/stores/repo_store';
|
||||
|
||||
describe('RepoFileButtons', () => {
|
||||
const activeFile = {
|
||||
extension: 'md',
|
||||
url: 'url',
|
||||
raw_path: 'raw_path',
|
||||
blame_path: 'blame_path',
|
||||
commits_path: 'commits_path',
|
||||
permalink: 'permalink',
|
||||
};
|
||||
|
||||
function createComponent() {
|
||||
const RepoFileButtons = Vue.extend(repoFileButtons);
|
||||
|
||||
|
@ -14,14 +23,6 @@ describe('RepoFileButtons', () => {
|
|||
});
|
||||
|
||||
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';
|
||||
RepoStore.openedFiles = new Array(1);
|
||||
RepoStore.activeFile = activeFile;
|
||||
|
@ -34,7 +35,6 @@ describe('RepoFileButtons', () => {
|
|||
const blame = vm.$el.querySelector('.blame');
|
||||
const history = vm.$el.querySelector('.history');
|
||||
|
||||
expect(vm.$el.id).toEqual('repo-file-buttons');
|
||||
expect(raw.href).toMatch(`/${activeFile.raw_path}`);
|
||||
expect(raw.textContent.trim()).toEqual('Raw');
|
||||
expect(blame.href).toMatch(`/${activeFile.blame_path}`);
|
||||
|
@ -46,10 +46,6 @@ describe('RepoFileButtons', () => {
|
|||
});
|
||||
|
||||
it('triggers rawPreviewToggle on preview click', () => {
|
||||
const activeFile = {
|
||||
extension: 'md',
|
||||
url: 'url',
|
||||
};
|
||||
RepoStore.openedFiles = new Array(1);
|
||||
RepoStore.activeFile = activeFile;
|
||||
RepoStore.editMode = true;
|
||||
|
@ -65,10 +61,7 @@ describe('RepoFileButtons', () => {
|
|||
});
|
||||
|
||||
it('does not render preview toggle if not canPreview', () => {
|
||||
const activeFile = {
|
||||
extension: 'abcd',
|
||||
url: 'url',
|
||||
};
|
||||
activeFile.extension = 'js';
|
||||
RepoStore.openedFiles = new Array(1);
|
||||
RepoStore.activeFile = activeFile;
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import { file } from '../mock_data';
|
|||
describe('RepoFile', () => {
|
||||
const updated = 'updated';
|
||||
const otherFile = {
|
||||
id: 'test',
|
||||
html: '<p class="file-content">html</p>',
|
||||
pageTitle: 'otherpageTitle',
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue