Resolve "Decouple multi-file editor from file list"
This commit is contained in:
parent
889c7081f1
commit
213e91d439
|
@ -1,4 +1,5 @@
|
|||
import $ from 'jquery';
|
||||
import axios from './lib/utils/axios_utils';
|
||||
|
||||
const Api = {
|
||||
groupsPath: '/api/:version/groups.json',
|
||||
|
@ -6,6 +7,7 @@ const Api = {
|
|||
namespacesPath: '/api/:version/namespaces.json',
|
||||
groupProjectsPath: '/api/:version/groups/:id/projects.json',
|
||||
projectsPath: '/api/:version/projects.json',
|
||||
projectPath: '/api/:version/projects/:id',
|
||||
projectLabelsPath: '/:namespace_path/:project_path/labels',
|
||||
groupLabelsPath: '/groups/:namespace_path/labels',
|
||||
licensePath: '/api/:version/templates/licenses/:key',
|
||||
|
@ -76,6 +78,14 @@ const Api = {
|
|||
.done(projects => callback(projects));
|
||||
},
|
||||
|
||||
// Return single project
|
||||
project(projectPath) {
|
||||
const url = Api.buildUrl(Api.projectPath)
|
||||
.replace(':id', encodeURIComponent(projectPath));
|
||||
|
||||
return axios.get(url);
|
||||
},
|
||||
|
||||
newLabel(namespacePath, projectPath, data, callback) {
|
||||
let url;
|
||||
|
||||
|
@ -115,7 +125,7 @@ const Api = {
|
|||
commitMultiple(id, data) {
|
||||
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
|
||||
const url = Api.buildUrl(Api.commitPath)
|
||||
.replace(':id', id);
|
||||
.replace(':id', encodeURIComponent(id));
|
||||
return this.wrapAjaxCall({
|
||||
url,
|
||||
type: 'POST',
|
||||
|
@ -127,7 +137,7 @@ const Api = {
|
|||
|
||||
branchSingle(id, branch) {
|
||||
const url = Api.buildUrl(Api.branchSinglePath)
|
||||
.replace(':id', id)
|
||||
.replace(':id', encodeURIComponent(id))
|
||||
.replace(':branch', branch);
|
||||
|
||||
return this.wrapAjaxCall({
|
||||
|
|
|
@ -73,7 +73,6 @@ import initLegacyFilters from './init_legacy_filters';
|
|||
import initIssuableSidebar from './init_issuable_sidebar';
|
||||
import initProjectVisibilitySelector from './project_visibility';
|
||||
import GpgBadges from './gpg_badges';
|
||||
import UserFeatureHelper from './helpers/user_feature_helper';
|
||||
import initChangesDropdown from './init_changes_dropdown';
|
||||
import NewGroupChild from './groups/new_group_child';
|
||||
import AbuseReports from './abuse_reports';
|
||||
|
@ -447,9 +446,6 @@ import Activities from './activities';
|
|||
break;
|
||||
case 'projects:tree:show':
|
||||
shortcut_handler = new ShortcutsNavigation();
|
||||
|
||||
if (UserFeatureHelper.isNewRepoEnabled()) break;
|
||||
|
||||
new TreeView();
|
||||
new BlobViewer();
|
||||
new NewCommitForm($('.js-create-dir-form'));
|
||||
|
@ -468,7 +464,6 @@ import Activities from './activities';
|
|||
shortcut_handler = true;
|
||||
break;
|
||||
case 'projects:blob:show':
|
||||
if (UserFeatureHelper.isNewRepoEnabled()) break;
|
||||
new BlobViewer();
|
||||
initBlob();
|
||||
break;
|
||||
|
|
|
@ -161,13 +161,16 @@ export default () => {
|
|||
|
||||
const items = [...sidebar.querySelectorAll('.sidebar-top-level-items > li')];
|
||||
|
||||
sidebar.querySelector('.sidebar-top-level-items').addEventListener('mouseleave', () => {
|
||||
clearTimeout(timeoutId);
|
||||
const topItems = sidebar.querySelector('.sidebar-top-level-items');
|
||||
if (topItems) {
|
||||
sidebar.querySelector('.sidebar-top-level-items').addEventListener('mouseleave', () => {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
if (currentOpenMenu) hideMenu(currentOpenMenu);
|
||||
}, getHideSubItemsInterval());
|
||||
});
|
||||
timeoutId = setTimeout(() => {
|
||||
if (currentOpenMenu) hideMenu(currentOpenMenu);
|
||||
}, getHideSubItemsInterval());
|
||||
});
|
||||
}
|
||||
|
||||
headerHeight = document.querySelector('.nav-sidebar').offsetTop;
|
||||
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
import Cookies from 'js-cookie';
|
||||
|
||||
export default {
|
||||
isNewRepoEnabled() {
|
||||
return Cookies.get('new_repo') === 'true';
|
||||
},
|
||||
};
|
|
@ -0,0 +1,66 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import icon from '../../../vue_shared/components/icon.vue';
|
||||
import listItem from './list_item.vue';
|
||||
import listCollapsed from './list_collapsed.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
listItem,
|
||||
listCollapsed,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
fileList: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'currentProjectId',
|
||||
'currentBranchId',
|
||||
'rightPanelCollapsed',
|
||||
]),
|
||||
},
|
||||
methods: {
|
||||
toggleCollapsed() {
|
||||
this.$emit('toggleCollapsed');
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="multi-file-commit-list">
|
||||
<list-collapsed
|
||||
v-if="rightPanelCollapsed"
|
||||
/>
|
||||
<template v-else>
|
||||
<ul
|
||||
v-if="fileList.length"
|
||||
class="list-unstyled append-bottom-0"
|
||||
>
|
||||
<li
|
||||
v-for="file in fileList"
|
||||
:key="file.key"
|
||||
>
|
||||
<list-item
|
||||
:file="file"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
v-else
|
||||
class="help-block prepend-top-0"
|
||||
>
|
||||
No changes
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,73 @@
|
|||
<script>
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import ideSidebar from './ide_side_bar.vue';
|
||||
import ideContextbar from './ide_context_bar.vue';
|
||||
import repoTabs from './repo_tabs.vue';
|
||||
import repoFileButtons from './repo_file_buttons.vue';
|
||||
import ideStatusBar from './ide_status_bar.vue';
|
||||
import repoPreview from './repo_preview.vue';
|
||||
import repoEditor from './repo_editor.vue';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapState([
|
||||
'currentBlobView',
|
||||
'selectedFile',
|
||||
]),
|
||||
...mapGetters([
|
||||
'changedFiles',
|
||||
'activeFile',
|
||||
]),
|
||||
},
|
||||
components: {
|
||||
ideSidebar,
|
||||
ideContextbar,
|
||||
repoTabs,
|
||||
repoFileButtons,
|
||||
ideStatusBar,
|
||||
repoEditor,
|
||||
repoPreview,
|
||||
},
|
||||
mounted() {
|
||||
const returnValue = 'Are you sure you want to lose unsaved changes?';
|
||||
window.onbeforeunload = (e) => {
|
||||
if (!this.changedFiles.length) return undefined;
|
||||
|
||||
Object.assign(e, {
|
||||
returnValue,
|
||||
});
|
||||
return returnValue;
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="ide-view"
|
||||
>
|
||||
<ide-sidebar/>
|
||||
<div
|
||||
class="multi-file-edit-pane"
|
||||
>
|
||||
<template
|
||||
v-if="activeFile">
|
||||
<repo-tabs/>
|
||||
<component
|
||||
class="multi-file-edit-pane-content"
|
||||
:is="currentBlobView"
|
||||
/>
|
||||
<repo-file-buttons/>
|
||||
<ide-status-bar
|
||||
:file="selectedFile"/>
|
||||
</template>
|
||||
<template
|
||||
v-else>
|
||||
<div class="ide-empty-state">
|
||||
<h2 class="clgray">Welcome to the GitLab IDE</h2>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<ide-contextbar/>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,75 @@
|
|||
<script>
|
||||
import { mapGetters, mapState, mapActions } from 'vuex';
|
||||
import repoCommitSection from './repo_commit_section.vue';
|
||||
import icon from '../../vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
repoCommitSection,
|
||||
icon,
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'rightPanelCollapsed',
|
||||
]),
|
||||
...mapGetters([
|
||||
'changedFiles',
|
||||
]),
|
||||
currentIcon() {
|
||||
return this.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'setPanelCollapsedStatus',
|
||||
]),
|
||||
toggleCollapsed() {
|
||||
this.setPanelCollapsedStatus({
|
||||
side: 'right',
|
||||
collapsed: !this.rightPanelCollapsed,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="multi-file-commit-panel"
|
||||
:class="{
|
||||
'is-collapsed': rightPanelCollapsed,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="multi-file-commit-panel-section">
|
||||
<header
|
||||
class="multi-file-commit-panel-header"
|
||||
:class="{
|
||||
'is-collapsed': rightPanelCollapsed,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="multi-file-commit-panel-header-title"
|
||||
v-if="!rightPanelCollapsed">
|
||||
<icon
|
||||
name="list-bulleted"
|
||||
:size="18"
|
||||
/>
|
||||
Staged
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
|
||||
@click="toggleCollapsed"
|
||||
>
|
||||
<icon
|
||||
:name="currentIcon"
|
||||
:size="18"
|
||||
/>
|
||||
</button>
|
||||
</header>
|
||||
<repo-commit-section
|
||||
class=""/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,47 @@
|
|||
<script>
|
||||
import repoTree from './ide_repo_tree.vue';
|
||||
import icon from '../../vue_shared/components/icon.vue';
|
||||
import newDropdown from './new_dropdown/index.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
repoTree,
|
||||
icon,
|
||||
newDropdown,
|
||||
},
|
||||
props: {
|
||||
projectId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
branch: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="branch-container">
|
||||
<div class="branch-header">
|
||||
<div class="branch-header-title">
|
||||
<icon
|
||||
name="branch"
|
||||
:size="12">
|
||||
</icon>
|
||||
{{ branch.name }}
|
||||
</div>
|
||||
<div class="branch-header-btns">
|
||||
<new-dropdown
|
||||
:project-id="projectId"
|
||||
:branch="branch.name"
|
||||
path=""/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<repo-tree
|
||||
:treeId="branch.treeId"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,47 @@
|
|||
<script>
|
||||
import branchesTree from './ide_project_branches_tree.vue';
|
||||
import projectAvatarImage from '../../vue_shared/components/project_avatar/image.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
branchesTree,
|
||||
projectAvatarImage,
|
||||
},
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="projects-sidebar">
|
||||
<div class="context-header">
|
||||
<a
|
||||
:title="project.name"
|
||||
:href="project.web_url">
|
||||
<div class="avatar-container s40 project-avatar">
|
||||
<project-avatar-image
|
||||
class="avatar-container project-avatar"
|
||||
:link-href="project.path"
|
||||
:img-src="project.avatar_url"
|
||||
:img-alt="project.name"
|
||||
:img-size="40"
|
||||
/>
|
||||
</div>
|
||||
<div class="sidebar-context-title">
|
||||
{{ project.name }}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="multi-file-commit-panel-inner-scroll">
|
||||
<branches-tree
|
||||
v-for="(branch, index) in project.branches"
|
||||
:key="branch.name"
|
||||
:project-id="project.path_with_namespace"
|
||||
:branch="branch"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,66 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import RepoPreviousDirectory from './repo_prev_directory.vue';
|
||||
import RepoFile from './repo_file.vue';
|
||||
import RepoLoadingFile from './repo_loading_file.vue';
|
||||
import { treeList } from '../stores/utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'repo-previous-directory': RepoPreviousDirectory,
|
||||
'repo-file': RepoFile,
|
||||
'repo-loading-file': RepoLoadingFile,
|
||||
},
|
||||
props: {
|
||||
treeId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'loading',
|
||||
'isRoot',
|
||||
]),
|
||||
...mapState({
|
||||
projectName(state) {
|
||||
return state.project.name;
|
||||
},
|
||||
}),
|
||||
fetchedList() {
|
||||
return treeList(this.$store.state, this.treeId);
|
||||
},
|
||||
hasPreviousDirectory() {
|
||||
return !this.isRoot && this.fetchedList.length;
|
||||
},
|
||||
showLoading() {
|
||||
return this.loading;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="ide-file-list">
|
||||
<table class="table">
|
||||
<tbody
|
||||
v-if="treeId">
|
||||
<repo-previous-directory
|
||||
v-if="hasPreviousDirectory"
|
||||
/>
|
||||
<repo-loading-file
|
||||
v-if="showLoading"
|
||||
v-for="n in 5"
|
||||
:key="n"
|
||||
/>
|
||||
<repo-file
|
||||
v-for="file in fetchedList"
|
||||
:key="file.key"
|
||||
:file="file"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,62 @@
|
|||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import projectTree from './ide_project_tree.vue';
|
||||
import icon from '../../vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
projectTree,
|
||||
icon,
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'projects',
|
||||
'leftPanelCollapsed',
|
||||
]),
|
||||
currentIcon() {
|
||||
return this.leftPanelCollapsed ? 'angle-double-right' : 'angle-double-left';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'setPanelCollapsedStatus',
|
||||
]),
|
||||
toggleCollapsed() {
|
||||
this.setPanelCollapsedStatus({
|
||||
side: 'left',
|
||||
collapsed: !this.leftPanelCollapsed,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="multi-file-commit-panel"
|
||||
:class="{
|
||||
'is-collapsed': leftPanelCollapsed,
|
||||
}"
|
||||
>
|
||||
<div class="multi-file-commit-panel-inner">
|
||||
<project-tree
|
||||
v-for="(project, index) in projects"
|
||||
:key="project.id"
|
||||
:project="project"/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-transparent left-collapse-btn"
|
||||
@click="toggleCollapsed"
|
||||
>
|
||||
<icon
|
||||
:name="currentIcon"
|
||||
:size="18"
|
||||
/>
|
||||
<span
|
||||
v-if="!leftPanelCollapsed"
|
||||
class="collapse-text"
|
||||
>Collapse sidebar</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,71 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import icon from '../../vue_shared/components/icon.vue';
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
import timeAgoMixin from '../../vue_shared/mixins/timeago';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
icon,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
mixins: [
|
||||
timeAgoMixin,
|
||||
],
|
||||
computed: {
|
||||
...mapState([
|
||||
'selectedFile',
|
||||
]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="ide-status-bar">
|
||||
<div>
|
||||
<icon
|
||||
name="branch"
|
||||
:size="12">
|
||||
</icon>
|
||||
{{ selectedFile.branchId }}
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
v-if="selectedFile.lastCommit && selectedFile.lastCommit.id">
|
||||
Last commit:
|
||||
<a
|
||||
v-tooltip
|
||||
:title="selectedFile.lastCommit.message"
|
||||
:href="selectedFile.lastCommit.url">
|
||||
{{ timeFormated(selectedFile.lastCommit.updatedAt) }} by
|
||||
{{ selectedFile.lastCommit.author }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="text-right">
|
||||
{{ selectedFile.name }}
|
||||
</div>
|
||||
<div
|
||||
class="text-right">
|
||||
{{ selectedFile.eol }}
|
||||
</div>
|
||||
<div
|
||||
class="text-right">
|
||||
{{ file.editorRow }}:{{ file.editorColumn }}
|
||||
</div>
|
||||
<div
|
||||
class="text-right">
|
||||
{{ selectedFile.fileLanguage }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -44,7 +44,7 @@
|
|||
this.branchName = '';
|
||||
|
||||
if (this.dropdownText) {
|
||||
this.dropdownText.textContent = this.currentBranch;
|
||||
this.dropdownText.textContent = this.currentBranchId;
|
||||
}
|
||||
|
||||
this.toggleDropdown();
|
|
@ -0,0 +1,101 @@
|
|||
<script>
|
||||
import newModal from './modal.vue';
|
||||
import upload from './upload.vue';
|
||||
import icon from '../../../vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
branch: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
parent: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
icon,
|
||||
newModal,
|
||||
upload,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
openModal: false,
|
||||
modalType: '',
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
createNewItem(type) {
|
||||
this.modalType = type;
|
||||
this.toggleModalOpen();
|
||||
},
|
||||
toggleModalOpen() {
|
||||
this.openModal = !this.openModal;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="repo-new-btn pull-right">
|
||||
<div class="dropdown">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-default dropdown-toggle add-to-tree"
|
||||
data-toggle="dropdown"
|
||||
aria-label="Create new file or directory"
|
||||
>
|
||||
<icon
|
||||
name="plus"
|
||||
:size="12"
|
||||
css-classes="pull-left"
|
||||
/>
|
||||
<icon
|
||||
name="arrow-down"
|
||||
:size="12"
|
||||
css-classes="pull-left"
|
||||
/>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
role="button"
|
||||
@click.prevent="createNewItem('blob')"
|
||||
>
|
||||
{{ __('New file') }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<upload
|
||||
:branch-id="branch"
|
||||
:path="path"
|
||||
:parent="parent"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
role="button"
|
||||
@click.prevent="createNewItem('tree')"
|
||||
>
|
||||
{{ __('New directory') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<new-modal
|
||||
v-if="openModal"
|
||||
:type="modalType"
|
||||
:branch-id="branch"
|
||||
:path="path"
|
||||
:parent="parent"
|
||||
@toggle="toggleModalOpen"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -1,10 +1,18 @@
|
|||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import { __ } from '../../../locale';
|
||||
import modal from '../../../vue_shared/components/modal.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
branchId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
parent: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
@ -28,6 +36,9 @@
|
|||
]),
|
||||
createEntryInStore() {
|
||||
this.createTempEntry({
|
||||
projectId: this.currentProjectId,
|
||||
branchId: this.branchId,
|
||||
parent: this.parent,
|
||||
name: this.entryName.replace(new RegExp(`^${this.path}/`), ''),
|
||||
type: this.type,
|
||||
});
|
||||
|
@ -39,6 +50,9 @@
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'currentProjectId',
|
||||
]),
|
||||
modalTitle() {
|
||||
if (this.type === 'tree') {
|
||||
return __('Create new directory');
|
|
@ -1,12 +1,22 @@
|
|||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
path: {
|
||||
branchId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
parent: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'trees',
|
||||
'currentProjectId',
|
||||
]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
|
@ -22,6 +32,9 @@
|
|||
|
||||
this.createTempEntry({
|
||||
name,
|
||||
projectId: this.currentProjectId,
|
||||
branchId: this.branchId,
|
||||
parent: this.parent,
|
||||
type: 'blob',
|
||||
content: result,
|
||||
base64: !isText,
|
||||
|
@ -42,6 +55,9 @@
|
|||
openFile() {
|
||||
Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file));
|
||||
},
|
||||
startFileUpload() {
|
||||
this.$refs.fileUpload.click();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.fileUpload.addEventListener('change', this.openFile);
|
||||
|
@ -53,16 +69,19 @@
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<label
|
||||
role="button"
|
||||
class="menu-item"
|
||||
>
|
||||
{{ __('Upload file') }}
|
||||
<div>
|
||||
<a
|
||||
href="#"
|
||||
role="button"
|
||||
@click.prevent="startFileUpload"
|
||||
>
|
||||
{{ __('Upload file') }}
|
||||
</a>
|
||||
<input
|
||||
id="file-upload"
|
||||
type="file"
|
||||
class="hidden"
|
||||
ref="fileUpload"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
|
@ -20,12 +20,13 @@ export default {
|
|||
submitCommitsLoading: false,
|
||||
startNewMR: false,
|
||||
commitMessage: '',
|
||||
collapsed: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'currentBranch',
|
||||
'currentProjectId',
|
||||
'currentBranchId',
|
||||
'rightPanelCollapsed',
|
||||
]),
|
||||
...mapGetters([
|
||||
'changedFiles',
|
||||
|
@ -42,12 +43,13 @@ export default {
|
|||
'checkCommitStatus',
|
||||
'commitChanges',
|
||||
'getTreeData',
|
||||
'setPanelCollapsedStatus',
|
||||
]),
|
||||
makeCommit(newBranch = false) {
|
||||
const createNewBranch = newBranch || this.startNewMR;
|
||||
|
||||
const payload = {
|
||||
branch: createNewBranch ? `${this.currentBranch}-${new Date().getTime().toString()}` : this.currentBranch,
|
||||
branch: createNewBranch ? `${this.currentBranchId}-${new Date().getTime().toString()}` : this.currentBranchId,
|
||||
commit_message: this.commitMessage,
|
||||
actions: this.changedFiles.map(f => ({
|
||||
action: f.tempFile ? 'create' : 'update',
|
||||
|
@ -55,7 +57,7 @@ export default {
|
|||
content: f.content,
|
||||
encoding: f.base64 ? 'base64' : 'text',
|
||||
})),
|
||||
start_branch: createNewBranch ? this.currentBranch : undefined,
|
||||
start_branch: createNewBranch ? this.currentBranchId : undefined,
|
||||
};
|
||||
|
||||
this.showNewBranchModal = false;
|
||||
|
@ -64,7 +66,12 @@ export default {
|
|||
this.commitChanges({ payload, newMr: this.startNewMR })
|
||||
.then(() => {
|
||||
this.submitCommitsLoading = false;
|
||||
this.getTreeData();
|
||||
this.$store.dispatch('getTreeData', {
|
||||
projectId: this.currentProjectId,
|
||||
branch: this.currentBranchId,
|
||||
endpoint: `/tree/${this.currentBranchId}`,
|
||||
force: true,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.submitCommitsLoading = false;
|
||||
|
@ -86,19 +93,17 @@ export default {
|
|||
});
|
||||
},
|
||||
toggleCollapsed() {
|
||||
this.collapsed = !this.collapsed;
|
||||
this.setPanelCollapsedStatus({
|
||||
side: 'right',
|
||||
collapsed: !this.rightPanelCollapsed,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="multi-file-commit-panel"
|
||||
:class="{
|
||||
'is-collapsed': collapsed,
|
||||
}"
|
||||
>
|
||||
<div class="multi-file-commit-panel-section">
|
||||
<modal
|
||||
v-if="showNewBranchModal"
|
||||
:primary-button-label="__('Create new branch')"
|
||||
|
@ -108,28 +113,16 @@ export default {
|
|||
@toggle="showNewBranchModal = false"
|
||||
@submit="makeCommit(true)"
|
||||
/>
|
||||
<button
|
||||
v-if="collapsed"
|
||||
type="button"
|
||||
class="btn btn-transparent multi-file-commit-panel-collapse-btn is-collapsed prepend-top-10 append-bottom-10"
|
||||
@click="toggleCollapsed"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="fa fa-angle-double-left"
|
||||
>
|
||||
</i>
|
||||
</button>
|
||||
<commit-files-list
|
||||
title="Staged"
|
||||
:file-list="changedFiles"
|
||||
:collapsed="collapsed"
|
||||
:collapsed="rightPanelCollapsed"
|
||||
@toggleCollapsed="toggleCollapsed"
|
||||
/>
|
||||
<form
|
||||
class="form-horizontal multi-file-commit-form"
|
||||
@submit.prevent="tryCommit"
|
||||
v-if="!collapsed"
|
||||
v-if="!rightPanelCollapsed"
|
||||
>
|
||||
<div class="multi-file-commit-fieldset">
|
||||
<textarea
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
/* global monaco */
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
import flash from '../../flash';
|
||||
import monacoLoader from '../monaco_loader';
|
||||
import Editor from '../lib/editor';
|
||||
|
@ -24,6 +24,9 @@ export default {
|
|||
...mapActions([
|
||||
'getRawFileData',
|
||||
'changeFileContent',
|
||||
'setFileLanguage',
|
||||
'setEditorPosition',
|
||||
'setFileEOL',
|
||||
]),
|
||||
initMonaco() {
|
||||
if (this.shouldHideEditor) return;
|
||||
|
@ -43,12 +46,36 @@ export default {
|
|||
const model = this.editor.createModel(this.activeFile);
|
||||
|
||||
this.editor.attachModel(model);
|
||||
|
||||
model.onChange((m) => {
|
||||
this.changeFileContent({
|
||||
file: this.activeFile,
|
||||
content: m.getValue(),
|
||||
});
|
||||
});
|
||||
|
||||
// Handle Cursor Position
|
||||
this.editor.onPositionChange((instance, e) => {
|
||||
this.setEditorPosition({
|
||||
editorRow: e.position.lineNumber,
|
||||
editorColumn: e.position.column,
|
||||
});
|
||||
});
|
||||
|
||||
this.editor.setPosition({
|
||||
lineNumber: this.activeFile.editorRow,
|
||||
column: this.activeFile.editorColumn,
|
||||
});
|
||||
|
||||
// Handle File Language
|
||||
this.setFileLanguage({
|
||||
fileLanguage: model.language,
|
||||
});
|
||||
|
||||
// Get File eol
|
||||
this.setFileEOL({
|
||||
eol: model.eol,
|
||||
});
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
@ -57,12 +84,22 @@ export default {
|
|||
this.initMonaco();
|
||||
}
|
||||
},
|
||||
leftPanelCollapsed() {
|
||||
this.editor.updateDimensions();
|
||||
},
|
||||
rightPanelCollapsed() {
|
||||
this.editor.updateDimensions();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'activeFile',
|
||||
'activeFileExtension',
|
||||
]),
|
||||
...mapState([
|
||||
'leftPanelCollapsed',
|
||||
'rightPanelCollapsed',
|
||||
]),
|
||||
shouldHideEditor() {
|
||||
return this.activeFile.binary && !this.activeFile.raw;
|
||||
},
|
||||
|
@ -76,13 +113,14 @@ export default {
|
|||
class="blob-viewer-container blob-editor-container"
|
||||
>
|
||||
<div
|
||||
v-show="shouldHideEditor"
|
||||
v-if="shouldHideEditor"
|
||||
v-html="activeFile.html"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-show="!shouldHideEditor"
|
||||
ref="editor"
|
||||
class="multi-file-editor-holder"
|
||||
>
|
||||
</div>
|
||||
</div>
|
|
@ -1,7 +1,8 @@
|
|||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import { mapState } from 'vuex';
|
||||
import timeAgoMixin from '../../vue_shared/mixins/timeago';
|
||||
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
|
||||
import newDropdown from './new_dropdown/index.vue';
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
|
@ -9,20 +10,22 @@
|
|||
],
|
||||
components: {
|
||||
skeletonLoadingContainer,
|
||||
newDropdown,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
showExtraColumns: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'isCollapsed',
|
||||
...mapState([
|
||||
'leftPanelCollapsed',
|
||||
]),
|
||||
isSubmodule() {
|
||||
return this.file.type === 'submodule';
|
||||
},
|
||||
fileIcon() {
|
||||
return {
|
||||
'fa-spinner fa-spin': this.file.loading,
|
||||
|
@ -30,6 +33,12 @@
|
|||
'fa-folder-open': !this.file.loading && this.file.opened,
|
||||
};
|
||||
},
|
||||
isSubmodule() {
|
||||
return this.file.type === 'submodule';
|
||||
},
|
||||
isTree() {
|
||||
return this.file.type === 'tree';
|
||||
},
|
||||
levelIndentation() {
|
||||
return {
|
||||
marginLeft: `${this.file.level * 16}px`,
|
||||
|
@ -39,13 +48,39 @@
|
|||
return this.file.id.substr(0, 8);
|
||||
},
|
||||
submoduleColSpan() {
|
||||
return !this.isCollapsed && this.isSubmodule ? 3 : 1;
|
||||
return !this.leftPanelCollapsed && this.isSubmodule ? 3 : 1;
|
||||
},
|
||||
fileClass() {
|
||||
if (this.file.type === 'blob') {
|
||||
if (this.file.active) {
|
||||
return 'file-open file-active';
|
||||
}
|
||||
return this.file.opened ? 'file-open' : '';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
changedClass() {
|
||||
return {
|
||||
'fa-circle unsaved-icon': this.file.changed || this.file.tempFile,
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'clickedTreeRow',
|
||||
]),
|
||||
clickFile(row) {
|
||||
// Manual Action if a tree is selected/opened
|
||||
if (this.file.type === 'tree' && this.$router.currentRoute.path === `/project${row.url}`) {
|
||||
this.$store.dispatch('toggleTreeOpen', {
|
||||
endpoint: this.file.url,
|
||||
tree: this.file,
|
||||
});
|
||||
}
|
||||
this.$router.push(`/project${row.url}`);
|
||||
},
|
||||
},
|
||||
updated() {
|
||||
if (this.file.type === 'blob' && this.file.active) {
|
||||
this.$el.scrollIntoView();
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -53,7 +88,8 @@
|
|||
<template>
|
||||
<tr
|
||||
class="file"
|
||||
@click.prevent="clickedTreeRow(file)">
|
||||
:class="fileClass"
|
||||
@click="clickFile(file)">
|
||||
<td
|
||||
class="multi-file-table-name"
|
||||
:colspan="submoduleColSpan"
|
||||
|
@ -66,11 +102,23 @@
|
|||
>
|
||||
</i>
|
||||
<a
|
||||
:href="file.url"
|
||||
class="repo-file-name"
|
||||
>
|
||||
{{ file.name }}
|
||||
</a>
|
||||
<new-dropdown
|
||||
v-if="isTree"
|
||||
:project-id="file.projectId"
|
||||
:branch="file.branchId"
|
||||
:path="file.path"
|
||||
:parent="file"/>
|
||||
<i
|
||||
class="fa"
|
||||
v-if="changedClass"
|
||||
:class="changedClass"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</i>
|
||||
<template v-if="isSubmodule && file.id">
|
||||
@
|
||||
<span class="commit-sha">
|
||||
|
@ -84,7 +132,7 @@
|
|||
</template>
|
||||
</td>
|
||||
|
||||
<template v-if="!isCollapsed && !isSubmodule">
|
||||
<template v-if="showExtraColumns && !isSubmodule">
|
||||
<td class="multi-file-table-col-commit-message hidden-sm hidden-xs">
|
||||
<a
|
||||
v-if="file.lastCommit.message"
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { mapState } from 'vuex';
|
||||
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
|
||||
|
||||
export default {
|
||||
|
@ -7,8 +7,8 @@
|
|||
skeletonLoadingContainer,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'isCollapsed',
|
||||
...mapState([
|
||||
'leftPanelCollapsed',
|
||||
]),
|
||||
},
|
||||
};
|
||||
|
@ -24,7 +24,7 @@
|
|||
:small="true"
|
||||
/>
|
||||
</td>
|
||||
<template v-if="!isCollapsed">
|
||||
<template v-if="!leftPanelCollapsed">
|
||||
<td
|
||||
class="hidden-sm hidden-xs">
|
||||
<skeleton-loading-container
|
|
@ -1,16 +1,14 @@
|
|||
<script>
|
||||
import { mapGetters, mapState, mapActions } from 'vuex';
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapState([
|
||||
'parentTreeUrl',
|
||||
]),
|
||||
...mapGetters([
|
||||
'isCollapsed',
|
||||
'leftPanelCollapsed',
|
||||
]),
|
||||
colSpanCondition() {
|
||||
return this.isCollapsed ? undefined : 3;
|
||||
return this.leftPanelCollapsed ? undefined : 3;
|
||||
},
|
||||
},
|
||||
methods: {
|
|
@ -27,16 +27,18 @@ export default {
|
|||
|
||||
methods: {
|
||||
...mapActions([
|
||||
'setFileActive',
|
||||
'closeFile',
|
||||
]),
|
||||
clickFile(tab) {
|
||||
this.$router.push(`/project${tab.url}`);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li
|
||||
@click="setFileActive(tab)"
|
||||
@click="clickFile(tab)"
|
||||
>
|
||||
<button
|
||||
type="button"
|
|
@ -0,0 +1,101 @@
|
|||
import Vue from 'vue';
|
||||
import VueRouter from 'vue-router';
|
||||
import store from './stores';
|
||||
import flash from '../flash';
|
||||
import {
|
||||
getTreeEntry,
|
||||
} from './stores/utils';
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
/**
|
||||
* Routes below /-/ide/:
|
||||
|
||||
/project/h5bp/html5-boilerplate/blob/master
|
||||
/project/h5bp/html5-boilerplate/blob/master/app/js/test.js
|
||||
|
||||
/project/h5bp/html5-boilerplate/mr/123
|
||||
/project/h5bp/html5-boilerplate/mr/123/app/js/test.js
|
||||
|
||||
/workspace/123
|
||||
/workspace/project/h5bp/html5-boilerplate/blob/my-special-branch
|
||||
/workspace/project/h5bp/html5-boilerplate/mr/123
|
||||
|
||||
/ = /workspace
|
||||
|
||||
/settings
|
||||
*/
|
||||
|
||||
// Unfortunately Vue Router doesn't work without at least a fake component
|
||||
// If you do only data handling
|
||||
const EmptyRouterComponent = {
|
||||
render(createElement) {
|
||||
return createElement('div');
|
||||
},
|
||||
};
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
base: `${gon.relative_url_root}/-/ide/`,
|
||||
routes: [
|
||||
{
|
||||
path: '/project/:namespace/:project',
|
||||
component: EmptyRouterComponent,
|
||||
children: [
|
||||
{
|
||||
path: ':targetmode/:branch/*',
|
||||
component: EmptyRouterComponent,
|
||||
},
|
||||
{
|
||||
path: 'mr/:mrid',
|
||||
component: EmptyRouterComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.params.namespace && to.params.project) {
|
||||
store.dispatch('getProjectData', {
|
||||
namespace: to.params.namespace,
|
||||
projectId: to.params.project,
|
||||
})
|
||||
.then(() => {
|
||||
const fullProjectId = `${to.params.namespace}/${to.params.project}`;
|
||||
|
||||
if (to.params.branch) {
|
||||
store.dispatch('getBranchData', {
|
||||
projectId: fullProjectId,
|
||||
branchId: to.params.branch,
|
||||
});
|
||||
|
||||
store.dispatch('getTreeData', {
|
||||
projectId: fullProjectId,
|
||||
branch: to.params.branch,
|
||||
endpoint: `/tree/${to.params.branch}`,
|
||||
})
|
||||
.then(() => {
|
||||
if (to.params[0]) {
|
||||
const treeEntry = getTreeEntry(store, `${to.params.namespace}/${to.params.project}/${to.params.branch}`, to.params[0]);
|
||||
if (treeEntry) {
|
||||
store.dispatch('handleTreeEntryAction', treeEntry);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
flash('Error while loading the branch files. Please try again.');
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
flash('Error while loading the project data. Please try again.');
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,55 @@
|
|||
import Vue from 'vue';
|
||||
import { mapActions } from 'vuex';
|
||||
import { convertPermissionToBoolean } from '../lib/utils/common_utils';
|
||||
import ide from './components/ide.vue';
|
||||
|
||||
import store from './stores';
|
||||
import router from './ide_router';
|
||||
import Translate from '../vue_shared/translate';
|
||||
import ContextualSidebar from '../contextual_sidebar';
|
||||
|
||||
function initIde(el) {
|
||||
if (!el) return null;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
store,
|
||||
router,
|
||||
components: {
|
||||
ide,
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'setInitialData',
|
||||
]),
|
||||
},
|
||||
created() {
|
||||
const data = el.dataset;
|
||||
|
||||
this.setInitialData({
|
||||
endpoints: {
|
||||
rootEndpoint: data.url,
|
||||
newMergeRequestUrl: data.newMergeRequestUrl,
|
||||
rootUrl: data.rootUrl,
|
||||
},
|
||||
canCommit: convertPermissionToBoolean(data.canCommit),
|
||||
onTopOfBranch: convertPermissionToBoolean(data.onTopOfBranch),
|
||||
path: data.currentPath,
|
||||
isRoot: convertPermissionToBoolean(data.root),
|
||||
isInitialRoot: convertPermissionToBoolean(data.root),
|
||||
});
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('ide');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const ideElement = document.getElementById('ide');
|
||||
|
||||
Vue.use(Translate);
|
||||
|
||||
initIde(ideElement);
|
||||
|
||||
const contextualSidebar = new ContextualSidebar();
|
||||
contextualSidebar.bindEvents();
|
|
@ -28,6 +28,14 @@ export default class Model {
|
|||
return this.model.uri.toString();
|
||||
}
|
||||
|
||||
get language() {
|
||||
return this.model.getModeId();
|
||||
}
|
||||
|
||||
get eol() {
|
||||
return this.model.getEOL() === '\n' ? 'LF' : 'CRLF';
|
||||
}
|
||||
|
||||
get path() {
|
||||
return this.file.path;
|
||||
}
|
|
@ -22,6 +22,11 @@ export default class Editor {
|
|||
this.modelManager = new ModelManager(this.monaco),
|
||||
this.decorationsController = new DecorationsController(this),
|
||||
);
|
||||
|
||||
this.debouncedUpdate = _.debounce(() => {
|
||||
this.updateDimensions();
|
||||
}, 200);
|
||||
window.addEventListener('resize', this.debouncedUpdate, false);
|
||||
}
|
||||
|
||||
createInstance(domElement) {
|
||||
|
@ -32,6 +37,9 @@ export default class Editor {
|
|||
readOnly: false,
|
||||
contextmenu: true,
|
||||
scrollBeyondLastLine: false,
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
}),
|
||||
this.dirtyDiffController = new DirtyDiffController(
|
||||
this.modelManager, this.decorationsController,
|
||||
|
@ -70,10 +78,32 @@ export default class Editor {
|
|||
|
||||
dispose() {
|
||||
this.disposable.dispose();
|
||||
window.removeEventListener('resize', this.debouncedUpdate);
|
||||
|
||||
// dispose main monaco instance
|
||||
if (this.instance) {
|
||||
this.instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
updateDimensions() {
|
||||
this.instance.layout();
|
||||
}
|
||||
|
||||
setPosition({ lineNumber, column }) {
|
||||
this.instance.revealPositionInCenter({
|
||||
lineNumber,
|
||||
column,
|
||||
});
|
||||
this.instance.setPosition({
|
||||
lineNumber,
|
||||
column,
|
||||
});
|
||||
}
|
||||
|
||||
onPositionChange(cb) {
|
||||
this.disposable.add(
|
||||
this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -23,8 +23,11 @@ export default {
|
|||
return Vue.http.get(file.rawPath, { params: { format: 'json' } })
|
||||
.then(res => res.text());
|
||||
},
|
||||
getBranchData(projectId, currentBranch) {
|
||||
return Api.branchSingle(projectId, currentBranch);
|
||||
getProjectData(namespace, project) {
|
||||
return Api.project(`${namespace}/${project}`);
|
||||
},
|
||||
getBranchData(projectId, currentBranchId) {
|
||||
return Api.branchSingle(projectId, currentBranchId);
|
||||
},
|
||||
createBranch(projectId, payload) {
|
||||
const url = Api.buildUrl(Api.createBranchPath).replace(':id', projectId);
|
|
@ -0,0 +1,179 @@
|
|||
import Vue from 'vue';
|
||||
import { visitUrl } from '../../lib/utils/url_utility';
|
||||
import flash from '../../flash';
|
||||
import service from '../services';
|
||||
import * as types from './mutation_types';
|
||||
|
||||
export const redirectToUrl = (_, url) => visitUrl(url);
|
||||
|
||||
export const setInitialData = ({ commit }, data) =>
|
||||
commit(types.SET_INITIAL_DATA, data);
|
||||
|
||||
export const closeDiscardPopup = ({ commit }) =>
|
||||
commit(types.TOGGLE_DISCARD_POPUP, false);
|
||||
|
||||
export const discardAllChanges = ({ commit, getters, dispatch }) => {
|
||||
const changedFiles = getters.changedFiles;
|
||||
|
||||
changedFiles.forEach((file) => {
|
||||
commit(types.DISCARD_FILE_CHANGES, file);
|
||||
|
||||
if (file.tempFile) {
|
||||
dispatch('closeFile', { file, force: true });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const closeAllFiles = ({ state, dispatch }) => {
|
||||
state.openFiles.forEach(file => dispatch('closeFile', { file }));
|
||||
};
|
||||
|
||||
export const toggleEditMode = (
|
||||
{ state, commit, getters, dispatch },
|
||||
force = false,
|
||||
) => {
|
||||
const changedFiles = getters.changedFiles;
|
||||
|
||||
if (changedFiles.length && !force) {
|
||||
commit(types.TOGGLE_DISCARD_POPUP, true);
|
||||
} else {
|
||||
commit(types.TOGGLE_EDIT_MODE);
|
||||
commit(types.TOGGLE_DISCARD_POPUP, false);
|
||||
dispatch('toggleBlobView');
|
||||
|
||||
if (!state.editMode) {
|
||||
dispatch('discardAllChanges');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const toggleBlobView = ({ commit, state }) => {
|
||||
if (state.editMode) {
|
||||
commit(types.SET_EDIT_MODE);
|
||||
} else {
|
||||
commit(types.SET_PREVIEW_MODE);
|
||||
}
|
||||
};
|
||||
|
||||
export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
|
||||
if (side === 'left') {
|
||||
commit(types.SET_LEFT_PANEL_COLLAPSED, collapsed);
|
||||
} else {
|
||||
commit(types.SET_RIGHT_PANEL_COLLAPSED, collapsed);
|
||||
}
|
||||
};
|
||||
|
||||
export const checkCommitStatus = ({ state }) =>
|
||||
service
|
||||
.getBranchData(state.currentProjectId, state.currentBranchId)
|
||||
.then((data) => {
|
||||
const { id } = data.commit;
|
||||
const selectedBranch =
|
||||
state.projects[state.currentProjectId].branches[state.currentBranchId];
|
||||
|
||||
if (selectedBranch.workingReference !== id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
.catch(() => flash('Error checking branch data. Please try again.'));
|
||||
|
||||
export const commitChanges = (
|
||||
{ commit, state, dispatch, getters },
|
||||
{ payload, newMr },
|
||||
) =>
|
||||
service
|
||||
.commit(state.currentProjectId, payload)
|
||||
.then((data) => {
|
||||
const { branch } = payload;
|
||||
if (!data.short_id) {
|
||||
flash(data.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedProject = state.projects[state.currentProjectId];
|
||||
const lastCommit = {
|
||||
commit_path: `${selectedProject.web_url}/commit/${data.id}`,
|
||||
commit: {
|
||||
message: data.message,
|
||||
authored_date: data.committed_date,
|
||||
},
|
||||
};
|
||||
|
||||
flash(
|
||||
`Your changes have been committed. Commit ${data.short_id} with ${
|
||||
data.stats.additions
|
||||
} additions, ${data.stats.deletions} deletions.`,
|
||||
'notice',
|
||||
);
|
||||
|
||||
if (newMr) {
|
||||
dispatch(
|
||||
'redirectToUrl',
|
||||
`${
|
||||
selectedProject.web_url
|
||||
}/merge_requests/new?merge_request%5Bsource_branch%5D=${branch}`,
|
||||
);
|
||||
} else {
|
||||
commit(types.SET_BRANCH_WORKING_REFERENCE, {
|
||||
projectId: state.currentProjectId,
|
||||
branchId: state.currentBranchId,
|
||||
reference: data.id,
|
||||
});
|
||||
|
||||
getters.changedFiles.forEach((entry) => {
|
||||
commit(types.SET_LAST_COMMIT_DATA, {
|
||||
entry,
|
||||
lastCommit,
|
||||
});
|
||||
});
|
||||
|
||||
dispatch('discardAllChanges');
|
||||
dispatch('closeAllFiles');
|
||||
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
})
|
||||
.catch(() => flash('Error committing changes. Please try again.'));
|
||||
|
||||
export const createTempEntry = (
|
||||
{ state, dispatch },
|
||||
{ projectId, branchId, parent, name, type, content = '', base64 = false },
|
||||
) => {
|
||||
const selectedParent = parent || state.trees[`${projectId}/${branchId}`];
|
||||
if (type === 'tree') {
|
||||
dispatch('createTempTree', {
|
||||
projectId,
|
||||
branchId,
|
||||
parent: selectedParent,
|
||||
name,
|
||||
});
|
||||
} else if (type === 'blob') {
|
||||
dispatch('createTempFile', {
|
||||
projectId,
|
||||
branchId,
|
||||
parent: selectedParent,
|
||||
name,
|
||||
base64,
|
||||
content,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const scrollToTab = () => {
|
||||
Vue.nextTick(() => {
|
||||
const tabs = document.getElementById('tabs');
|
||||
|
||||
if (tabs) {
|
||||
const tabEl = tabs.querySelector('.active .repo-tab');
|
||||
|
||||
tabEl.focus();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export * from './actions/tree';
|
||||
export * from './actions/file';
|
||||
export * from './actions/project';
|
||||
export * from './actions/branch';
|
|
@ -0,0 +1,43 @@
|
|||
import service from '../../services';
|
||||
import flash from '../../../flash';
|
||||
import * as types from '../mutation_types';
|
||||
|
||||
export const getBranchData = (
|
||||
{ commit, state, dispatch },
|
||||
{ projectId, branchId, force = false } = {},
|
||||
) => new Promise((resolve, reject) => {
|
||||
if ((typeof state.projects[`${projectId}`] === 'undefined' ||
|
||||
!state.projects[`${projectId}`].branches[branchId])
|
||||
|| force) {
|
||||
service.getBranchData(`${projectId}`, branchId)
|
||||
.then((data) => {
|
||||
const { id } = data.commit;
|
||||
commit(types.SET_BRANCH, { projectPath: `${projectId}`, branchName: branchId, branch: data });
|
||||
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
|
||||
resolve(data);
|
||||
})
|
||||
.catch(() => {
|
||||
flash('Error loading branch data. Please try again.');
|
||||
reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
|
||||
});
|
||||
} else {
|
||||
resolve(state.projects[`${projectId}`].branches[branchId]);
|
||||
}
|
||||
});
|
||||
|
||||
export const createNewBranch = ({ state, commit }, branch) => service.createBranch(
|
||||
state.currentProjectId,
|
||||
{
|
||||
branch,
|
||||
ref: state.currentBranchId,
|
||||
},
|
||||
)
|
||||
.then(res => res.json())
|
||||
.then((data) => {
|
||||
const branchName = data.name;
|
||||
const url = location.href.replace(state.currentBranchId, branchName);
|
||||
|
||||
if (this.$router) this.$router.push(url);
|
||||
|
||||
commit(types.SET_CURRENT_BRANCH, branchName);
|
||||
});
|
|
@ -2,9 +2,9 @@ import { normalizeHeaders } from '../../../lib/utils/common_utils';
|
|||
import flash from '../../../flash';
|
||||
import service from '../../services';
|
||||
import * as types from '../mutation_types';
|
||||
import router from '../../ide_router';
|
||||
import {
|
||||
findEntry,
|
||||
pushState,
|
||||
setPageTitle,
|
||||
createTemp,
|
||||
findIndexOfFile,
|
||||
|
@ -25,7 +25,7 @@ export const closeFile = ({ commit, state, dispatch }, { file, force = false })
|
|||
|
||||
dispatch('setFileActive', nextFileToOpen);
|
||||
} else if (!state.openFiles.length) {
|
||||
pushState(file.parentTreeUrl);
|
||||
router.push(`/project/${file.projectId}/tree/${file.branchId}/`);
|
||||
}
|
||||
|
||||
dispatch('getLastCommitData');
|
||||
|
@ -45,6 +45,9 @@ export const setFileActive = ({ commit, state, getters, dispatch }, file) => {
|
|||
|
||||
// reset hash for line highlighting
|
||||
location.hash = '';
|
||||
|
||||
commit(types.SET_CURRENT_PROJECT, file.projectId);
|
||||
commit(types.SET_CURRENT_BRANCH, file.branchId);
|
||||
};
|
||||
|
||||
export const getFileData = ({ state, commit, dispatch }, file) => {
|
||||
|
@ -63,8 +66,6 @@ export const getFileData = ({ state, commit, dispatch }, file) => {
|
|||
commit(types.TOGGLE_FILE_OPEN, file);
|
||||
dispatch('setFileActive', file);
|
||||
commit(types.TOGGLE_LOADING, file);
|
||||
|
||||
pushState(file.url);
|
||||
})
|
||||
.catch(() => {
|
||||
commit(types.TOGGLE_LOADING, file);
|
||||
|
@ -82,21 +83,39 @@ export const changeFileContent = ({ commit }, { file, content }) => {
|
|||
commit(types.UPDATE_FILE_CONTENT, { file, content });
|
||||
};
|
||||
|
||||
export const createTempFile = ({ state, commit, dispatch }, { tree, name, content = '', base64 = '' }) => {
|
||||
export const setFileLanguage = ({ state, commit }, { fileLanguage }) => {
|
||||
commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage });
|
||||
};
|
||||
|
||||
export const setFileEOL = ({ state, commit }, { eol }) => {
|
||||
commit(types.SET_FILE_EOL, { file: state.selectedFile, eol });
|
||||
};
|
||||
|
||||
export const setEditorPosition = ({ state, commit }, { editorRow, editorColumn }) => {
|
||||
commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn });
|
||||
};
|
||||
|
||||
export const createTempFile = ({ state, commit, dispatch }, { projectId, branchId, parent, name, content = '', base64 = '' }) => {
|
||||
const path = parent.path !== undefined ? parent.path : '';
|
||||
// We need to do the replacement otherwise the web_url + file.url duplicate
|
||||
const newUrl = `/${projectId}/blob/${branchId}/${path}${path ? '/' : ''}${name}`;
|
||||
const file = createTemp({
|
||||
name: name.replace(`${state.path}/`, ''),
|
||||
path: tree.path,
|
||||
projectId,
|
||||
branchId,
|
||||
name: name.replace(`${path}/`, ''),
|
||||
path,
|
||||
type: 'blob',
|
||||
level: tree.level !== undefined ? tree.level + 1 : 0,
|
||||
level: parent.level !== undefined ? parent.level + 1 : 0,
|
||||
changed: true,
|
||||
content,
|
||||
base64,
|
||||
url: newUrl,
|
||||
});
|
||||
|
||||
if (findEntry(tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`);
|
||||
if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`);
|
||||
|
||||
commit(types.CREATE_TMP_FILE, {
|
||||
parent: tree,
|
||||
parent,
|
||||
file,
|
||||
});
|
||||
commit(types.TOGGLE_FILE_OPEN, file);
|
||||
|
@ -106,5 +125,7 @@ export const createTempFile = ({ state, commit, dispatch }, { tree, name, conten
|
|||
dispatch('toggleEditMode', true);
|
||||
}
|
||||
|
||||
router.push(`/project${file.url}`);
|
||||
|
||||
return Promise.resolve(file);
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
import service from '../../services';
|
||||
import flash from '../../../flash';
|
||||
import * as types from '../mutation_types';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const getProjectData = (
|
||||
{ commit, state, dispatch },
|
||||
{ namespace, projectId, force = false } = {},
|
||||
) => new Promise((resolve, reject) => {
|
||||
if (!state.projects[`${namespace}/${projectId}`] || force) {
|
||||
service.getProjectData(namespace, projectId)
|
||||
.then(res => res.data)
|
||||
.then((data) => {
|
||||
commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
|
||||
if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
|
||||
resolve(data);
|
||||
})
|
||||
.catch(() => {
|
||||
flash('Error loading project data. Please try again.');
|
||||
reject(new Error(`Project not loaded ${namespace}/${projectId}`));
|
||||
});
|
||||
} else {
|
||||
resolve(state.projects[`${namespace}/${projectId}`]);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,188 @@
|
|||
import { visitUrl } from '../../../lib/utils/url_utility';
|
||||
import { normalizeHeaders } from '../../../lib/utils/common_utils';
|
||||
import flash from '../../../flash';
|
||||
import service from '../../services';
|
||||
import * as types from '../mutation_types';
|
||||
import router from '../../ide_router';
|
||||
import {
|
||||
setPageTitle,
|
||||
findEntry,
|
||||
createTemp,
|
||||
createOrMergeEntry,
|
||||
} from '../utils';
|
||||
|
||||
export const getTreeData = (
|
||||
{ commit, state, dispatch },
|
||||
{ endpoint, tree = null, projectId, branch, force = false } = {},
|
||||
) => new Promise((resolve, reject) => {
|
||||
// We already have the base tree so we resolve immediately
|
||||
if (!tree && state.trees[`${projectId}/${branch}`] && !force) {
|
||||
resolve();
|
||||
} else {
|
||||
if (tree) commit(types.TOGGLE_LOADING, tree);
|
||||
const selectedProject = state.projects[projectId];
|
||||
// We are merging the web_url that we got on the project info with the endpoint
|
||||
// we got on the tree entry, as both contain the projectId, we replace it in the tree endpoint
|
||||
const completeEndpoint = selectedProject.web_url + (endpoint).replace(projectId, '');
|
||||
if (completeEndpoint && (!tree || !tree.tempFile)) {
|
||||
service.getTreeData(completeEndpoint)
|
||||
.then((res) => {
|
||||
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
|
||||
|
||||
setPageTitle(pageTitle);
|
||||
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (!state.isInitialRoot) {
|
||||
commit(types.SET_ROOT, data.path === '/');
|
||||
}
|
||||
|
||||
dispatch('updateDirectoryData', { data, tree, projectId, branch });
|
||||
const selectedTree = tree || state.trees[`${projectId}/${branch}`];
|
||||
|
||||
commit(types.SET_PARENT_TREE_URL, data.parent_tree_url);
|
||||
commit(types.SET_LAST_COMMIT_URL, { tree: selectedTree, url: data.last_commit_path });
|
||||
if (tree) commit(types.TOGGLE_LOADING, selectedTree);
|
||||
|
||||
const prevLastCommitPath = selectedTree.lastCommitPath;
|
||||
if (prevLastCommitPath !== null) {
|
||||
dispatch('getLastCommitData', selectedTree);
|
||||
}
|
||||
resolve(data);
|
||||
})
|
||||
.catch((e) => {
|
||||
flash('Error loading tree data. Please try again.');
|
||||
if (tree) commit(types.TOGGLE_LOADING, tree);
|
||||
reject(e);
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const toggleTreeOpen = ({ commit, dispatch }, { endpoint, tree }) => {
|
||||
if (tree.opened) {
|
||||
// send empty data to clear the tree
|
||||
const data = { trees: [], blobs: [], submodules: [] };
|
||||
|
||||
dispatch('updateDirectoryData', { data, tree, projectId: tree.projectId, branchId: tree.branchId });
|
||||
} else {
|
||||
dispatch('getTreeData', { endpoint, tree, projectId: tree.projectId, branch: tree.branchId });
|
||||
}
|
||||
|
||||
commit(types.TOGGLE_TREE_OPEN, tree);
|
||||
};
|
||||
|
||||
export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
|
||||
if (row.type === 'tree') {
|
||||
dispatch('toggleTreeOpen', {
|
||||
endpoint: row.url,
|
||||
tree: row,
|
||||
});
|
||||
} else if (row.type === 'submodule') {
|
||||
commit(types.TOGGLE_LOADING, row);
|
||||
visitUrl(row.url);
|
||||
} else if (row.type === 'blob' && row.opened) {
|
||||
dispatch('setFileActive', row);
|
||||
} else {
|
||||
dispatch('getFileData', row);
|
||||
}
|
||||
};
|
||||
|
||||
export const createTempTree = (
|
||||
{ state, commit, dispatch },
|
||||
{ projectId, branchId, parent, name },
|
||||
) => {
|
||||
let selectedTree = parent;
|
||||
const dirNames = name.replace(new RegExp(`^${state.path}/`), '').split('/');
|
||||
|
||||
dirNames.forEach((dirName) => {
|
||||
const foundEntry = findEntry(selectedTree.tree, 'tree', dirName);
|
||||
|
||||
if (!foundEntry) {
|
||||
const path = selectedTree.path !== undefined ? selectedTree.path : '';
|
||||
const tmpEntry = createTemp({
|
||||
projectId,
|
||||
branchId,
|
||||
name: dirName,
|
||||
path,
|
||||
type: 'tree',
|
||||
level: selectedTree.level !== undefined ? selectedTree.level + 1 : 0,
|
||||
tree: [],
|
||||
url: `/${projectId}/blob/${branchId}/${path}${path ? '/' : ''}${dirName}`,
|
||||
});
|
||||
|
||||
commit(types.CREATE_TMP_TREE, {
|
||||
parent: selectedTree,
|
||||
tmpEntry,
|
||||
});
|
||||
commit(types.TOGGLE_TREE_OPEN, tmpEntry);
|
||||
|
||||
router.push(`/project${tmpEntry.url}`);
|
||||
|
||||
selectedTree = tmpEntry;
|
||||
} else {
|
||||
selectedTree = foundEntry;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
|
||||
if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
|
||||
|
||||
service.getTreeLastCommit(tree.lastCommitPath)
|
||||
.then((res) => {
|
||||
const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
|
||||
|
||||
commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
|
||||
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
data.forEach((lastCommit) => {
|
||||
const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name);
|
||||
|
||||
if (entry) {
|
||||
commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit });
|
||||
}
|
||||
});
|
||||
|
||||
dispatch('getLastCommitData', tree);
|
||||
})
|
||||
.catch(() => flash('Error fetching log data.'));
|
||||
};
|
||||
|
||||
export const updateDirectoryData = (
|
||||
{ commit, state },
|
||||
{ data, tree, projectId, branch },
|
||||
) => {
|
||||
if (!tree) {
|
||||
const existingTree = state.trees[`${projectId}/${branch}`];
|
||||
if (!existingTree) {
|
||||
commit(types.CREATE_TREE, { treePath: `${projectId}/${branch}` });
|
||||
}
|
||||
}
|
||||
|
||||
const selectedTree = tree || state.trees[`${projectId}/${branch}`];
|
||||
const level = selectedTree.level !== undefined ? selectedTree.level + 1 : 0;
|
||||
const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl;
|
||||
const createEntry = (entry, type) => createOrMergeEntry({
|
||||
tree: selectedTree,
|
||||
projectId: `${projectId}`,
|
||||
branchId: branch,
|
||||
entry,
|
||||
level,
|
||||
type,
|
||||
parentTreeUrl,
|
||||
});
|
||||
|
||||
const formattedData = [
|
||||
...data.trees.map(t => createEntry(t, 'tree')),
|
||||
...data.submodules.map(m => createEntry(m, 'submodule')),
|
||||
...data.blobs.map(b => createEntry(b, 'blob')),
|
||||
];
|
||||
|
||||
commit(types.SET_DIRECTORY_DATA, { tree: selectedTree, data: formattedData });
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
export const changedFiles = state => state.openFiles.filter(file => file.changed);
|
||||
|
||||
export const activeFile = state => state.openFiles.find(file => file.active) || null;
|
||||
|
||||
export const activeFileExtension = (state) => {
|
||||
const file = activeFile(state);
|
||||
return file ? `.${file.path.split('.').pop()}` : '';
|
||||
};
|
||||
|
||||
export const canEditFile = (state) => {
|
||||
const currentActiveFile = activeFile(state);
|
||||
|
||||
return state.canCommit &&
|
||||
(currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary);
|
||||
};
|
||||
|
||||
export const addedFiles = state => changedFiles(state).filter(f => f.tempFile);
|
||||
|
||||
export const modifiedFiles = state => changedFiles(state).filter(f => !f.tempFile);
|
|
@ -1,16 +1,27 @@
|
|||
export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
|
||||
export const TOGGLE_LOADING = 'TOGGLE_LOADING';
|
||||
export const SET_COMMIT_REF = 'SET_COMMIT_REF';
|
||||
export const SET_PARENT_TREE_URL = 'SET_PARENT_TREE_URL';
|
||||
export const SET_ROOT = 'SET_ROOT';
|
||||
export const SET_PREVIOUS_URL = 'SET_PREVIOUS_URL';
|
||||
export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA';
|
||||
export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
|
||||
export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
|
||||
|
||||
// Project Mutation Types
|
||||
export const SET_PROJECT = 'SET_PROJECT';
|
||||
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
|
||||
export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
|
||||
|
||||
// Branch Mutation Types
|
||||
export const SET_BRANCH = 'SET_BRANCH';
|
||||
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
|
||||
export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
|
||||
|
||||
// Tree mutation types
|
||||
export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
|
||||
export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN';
|
||||
export const CREATE_TMP_TREE = 'CREATE_TMP_TREE';
|
||||
export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL';
|
||||
export const CREATE_TREE = 'CREATE_TREE';
|
||||
|
||||
// File mutation types
|
||||
export const SET_FILE_DATA = 'SET_FILE_DATA';
|
||||
|
@ -18,6 +29,9 @@ export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
|
|||
export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
|
||||
export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
|
||||
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
|
||||
export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
|
||||
export const SET_FILE_POSITION = 'SET_FILE_POSITION';
|
||||
export const SET_FILE_EOL = 'SET_FILE_EOL';
|
||||
export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
|
||||
export const CREATE_TMP_FILE = 'CREATE_TMP_FILE';
|
||||
|
||||
|
@ -28,3 +42,4 @@ export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE';
|
|||
export const TOGGLE_DISCARD_POPUP = 'TOGGLE_DISCARD_POPUP';
|
||||
|
||||
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import * as types from './mutation_types';
|
||||
import projectMutations from './mutations/project';
|
||||
import fileMutations from './mutations/file';
|
||||
import treeMutations from './mutations/tree';
|
||||
import branchMutations from './mutations/branch';
|
||||
|
@ -32,29 +33,32 @@ export default {
|
|||
discardPopupOpen,
|
||||
});
|
||||
},
|
||||
[types.SET_COMMIT_REF](state, ref) {
|
||||
Object.assign(state, {
|
||||
currentRef: ref,
|
||||
});
|
||||
},
|
||||
[types.SET_ROOT](state, isRoot) {
|
||||
Object.assign(state, {
|
||||
isRoot,
|
||||
isInitialRoot: isRoot,
|
||||
});
|
||||
},
|
||||
[types.SET_PREVIOUS_URL](state, previousUrl) {
|
||||
[types.SET_LEFT_PANEL_COLLAPSED](state, collapsed) {
|
||||
Object.assign(state, {
|
||||
previousUrl,
|
||||
leftPanelCollapsed: collapsed,
|
||||
});
|
||||
},
|
||||
[types.SET_RIGHT_PANEL_COLLAPSED](state, collapsed) {
|
||||
Object.assign(state, {
|
||||
rightPanelCollapsed: collapsed,
|
||||
});
|
||||
},
|
||||
[types.SET_LAST_COMMIT_DATA](state, { entry, lastCommit }) {
|
||||
Object.assign(entry.lastCommit, {
|
||||
id: lastCommit.commit.id,
|
||||
url: lastCommit.commit_path,
|
||||
message: lastCommit.commit.message,
|
||||
author: lastCommit.commit.author_name,
|
||||
updatedAt: lastCommit.commit.authored_date,
|
||||
});
|
||||
},
|
||||
...projectMutations,
|
||||
...fileMutations,
|
||||
...treeMutations,
|
||||
...branchMutations,
|
|
@ -0,0 +1,28 @@
|
|||
import * as types from '../mutation_types';
|
||||
|
||||
export default {
|
||||
[types.SET_CURRENT_BRANCH](state, currentBranchId) {
|
||||
Object.assign(state, {
|
||||
currentBranchId,
|
||||
});
|
||||
},
|
||||
[types.SET_BRANCH](state, { projectPath, branchName, branch }) {
|
||||
// Add client side properties
|
||||
Object.assign(branch, {
|
||||
treeId: `${projectPath}/${branchName}`,
|
||||
active: true,
|
||||
workingReference: '',
|
||||
});
|
||||
|
||||
Object.assign(state.projects[projectPath], {
|
||||
branches: {
|
||||
[branchName]: branch,
|
||||
},
|
||||
});
|
||||
},
|
||||
[types.SET_BRANCH_WORKING_REFERENCE](state, { projectId, branchId, reference }) {
|
||||
Object.assign(state.projects[projectId].branches[branchId], {
|
||||
workingReference: reference,
|
||||
});
|
||||
},
|
||||
};
|
|
@ -6,6 +6,10 @@ export default {
|
|||
Object.assign(file, {
|
||||
active,
|
||||
});
|
||||
|
||||
Object.assign(state, {
|
||||
selectedFile: file,
|
||||
});
|
||||
},
|
||||
[types.TOGGLE_FILE_OPEN](state, file) {
|
||||
Object.assign(file, {
|
||||
|
@ -42,6 +46,22 @@ export default {
|
|||
changed,
|
||||
});
|
||||
},
|
||||
[types.SET_FILE_LANGUAGE](state, { file, fileLanguage }) {
|
||||
Object.assign(file, {
|
||||
fileLanguage,
|
||||
});
|
||||
},
|
||||
[types.SET_FILE_EOL](state, { file, eol }) {
|
||||
Object.assign(file, {
|
||||
eol,
|
||||
});
|
||||
},
|
||||
[types.SET_FILE_POSITION](state, { file, editorRow, editorColumn }) {
|
||||
Object.assign(file, {
|
||||
editorRow,
|
||||
editorColumn,
|
||||
});
|
||||
},
|
||||
[types.DISCARD_FILE_CHANGES](state, file) {
|
||||
Object.assign(file, {
|
||||
content: '',
|
|
@ -0,0 +1,23 @@
|
|||
import * as types from '../mutation_types';
|
||||
|
||||
export default {
|
||||
[types.SET_CURRENT_PROJECT](state, currentProjectId) {
|
||||
Object.assign(state, {
|
||||
currentProjectId,
|
||||
});
|
||||
},
|
||||
[types.SET_PROJECT](state, { projectPath, project }) {
|
||||
// Add client side properties
|
||||
Object.assign(project, {
|
||||
tree: [],
|
||||
branches: {},
|
||||
active: true,
|
||||
});
|
||||
|
||||
Object.assign(state, {
|
||||
projects: Object.assign({}, state.projects, {
|
||||
[projectPath]: project,
|
||||
}),
|
||||
});
|
||||
},
|
||||
};
|
|
@ -6,6 +6,15 @@ export default {
|
|||
opened: !tree.opened,
|
||||
});
|
||||
},
|
||||
[types.CREATE_TREE](state, { treePath }) {
|
||||
Object.assign(state, {
|
||||
trees: Object.assign({}, state.trees, {
|
||||
[treePath]: {
|
||||
tree: [],
|
||||
},
|
||||
}),
|
||||
});
|
||||
},
|
||||
[types.SET_DIRECTORY_DATA](state, { data, tree }) {
|
||||
Object.assign(tree, {
|
||||
tree: data,
|
|
@ -1,10 +1,10 @@
|
|||
export default () => ({
|
||||
canCommit: false,
|
||||
currentBranch: '',
|
||||
currentBlobView: 'repo-preview',
|
||||
currentRef: '',
|
||||
currentProjectId: '',
|
||||
currentBranchId: '',
|
||||
currentBlobView: 'repo-editor',
|
||||
discardPopupOpen: false,
|
||||
editMode: false,
|
||||
editMode: true,
|
||||
endpoints: {},
|
||||
isRoot: false,
|
||||
isInitialRoot: false,
|
||||
|
@ -12,13 +12,11 @@ export default () => ({
|
|||
loading: false,
|
||||
onTopOfBranch: false,
|
||||
openFiles: [],
|
||||
selectedFile: null,
|
||||
path: '',
|
||||
project: {
|
||||
id: 0,
|
||||
name: '',
|
||||
url: '',
|
||||
},
|
||||
parentTreeUrl: '',
|
||||
previousUrl: '',
|
||||
tree: [],
|
||||
trees: {},
|
||||
projects: {},
|
||||
leftPanelCollapsed: false,
|
||||
rightPanelCollapsed: true,
|
||||
});
|
|
@ -2,6 +2,8 @@ export const dataStructure = () => ({
|
|||
id: '',
|
||||
key: '',
|
||||
type: '',
|
||||
projectId: '',
|
||||
branchId: '',
|
||||
name: '',
|
||||
url: '',
|
||||
path: '',
|
||||
|
@ -15,9 +17,11 @@ export const dataStructure = () => ({
|
|||
changed: false,
|
||||
lastCommitPath: '',
|
||||
lastCommit: {
|
||||
id: '',
|
||||
url: '',
|
||||
message: '',
|
||||
updatedAt: '',
|
||||
author: '',
|
||||
},
|
||||
tree_url: '',
|
||||
blamePath: '',
|
||||
|
@ -31,11 +35,17 @@ export const dataStructure = () => ({
|
|||
parentTreeUrl: '',
|
||||
renderError: false,
|
||||
base64: false,
|
||||
editorRow: 1,
|
||||
editorColumn: 1,
|
||||
fileLanguage: '',
|
||||
eol: '',
|
||||
});
|
||||
|
||||
export const decorateData = (entity) => {
|
||||
const {
|
||||
id,
|
||||
projectId,
|
||||
branchId,
|
||||
type,
|
||||
url,
|
||||
name,
|
||||
|
@ -56,6 +66,8 @@ export const decorateData = (entity) => {
|
|||
return {
|
||||
...dataStructure(),
|
||||
id,
|
||||
projectId,
|
||||
branchId,
|
||||
key: `${name}-${type}-${id}`,
|
||||
type,
|
||||
name,
|
||||
|
@ -75,24 +87,51 @@ export const decorateData = (entity) => {
|
|||
};
|
||||
};
|
||||
|
||||
export const findEntry = (state, type, name) => state.tree.find(
|
||||
/*
|
||||
Takes the multi-dimensional tree and returns a flattened array.
|
||||
This allows for the table to recursively render the table rows but keeps the data
|
||||
structure nested to make it easier to add new files/directories.
|
||||
*/
|
||||
export const treeList = (state, treeId) => {
|
||||
const baseTree = state.trees[treeId];
|
||||
if (baseTree) {
|
||||
const mapTree = arr => (!arr.tree || !arr.tree.length ?
|
||||
[] : _.map(arr.tree, a => [a, mapTree(a)]));
|
||||
|
||||
return _.chain(baseTree.tree)
|
||||
.map(arr => [arr, mapTree(arr)])
|
||||
.flatten()
|
||||
.value();
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getTree = state => (namespace, projectId, branch) => state.trees[`${namespace}/${projectId}/${branch}`];
|
||||
|
||||
export const getTreeEntry = (store, treeId, path) => {
|
||||
const fileList = treeList(store.state, treeId);
|
||||
return fileList ? fileList.find(file => file.path === path) : null;
|
||||
};
|
||||
|
||||
export const findEntry = (tree, type, name) => tree.find(
|
||||
f => f.type === type && f.name === name,
|
||||
);
|
||||
|
||||
export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path);
|
||||
|
||||
export const setPageTitle = (title) => {
|
||||
document.title = title;
|
||||
};
|
||||
|
||||
export const pushState = (url) => {
|
||||
history.pushState({ url }, '', url);
|
||||
};
|
||||
|
||||
export const createTemp = ({ name, path, type, level, changed, content, base64 }) => {
|
||||
export const createTemp = ({
|
||||
projectId, branchId, name, path, type, level, changed, content, base64, url,
|
||||
}) => {
|
||||
const treePath = path ? `${path}/${name}` : name;
|
||||
|
||||
return decorateData({
|
||||
id: new Date().getTime().toString(),
|
||||
projectId,
|
||||
branchId,
|
||||
name,
|
||||
type,
|
||||
tempFile: true,
|
||||
|
@ -104,11 +143,18 @@ export const createTemp = ({ name, path, type, level, changed, content, base64 }
|
|||
level,
|
||||
base64,
|
||||
renderError: base64,
|
||||
url,
|
||||
});
|
||||
};
|
||||
|
||||
export const createOrMergeEntry = ({ tree, entry, type, parentTreeUrl, level }) => {
|
||||
const found = findEntry(tree, type, entry.name);
|
||||
export const createOrMergeEntry = ({ tree,
|
||||
projectId,
|
||||
branchId,
|
||||
entry,
|
||||
type,
|
||||
parentTreeUrl,
|
||||
level }) => {
|
||||
const found = findEntry(tree.tree || tree, type, entry.name);
|
||||
|
||||
if (found) {
|
||||
return Object.assign({}, found, {
|
||||
|
@ -120,6 +166,8 @@ export const createOrMergeEntry = ({ tree, entry, type, parentTreeUrl, level })
|
|||
|
||||
return decorateData({
|
||||
...entry,
|
||||
projectId,
|
||||
branchId,
|
||||
type,
|
||||
parentTreeUrl,
|
||||
level,
|
|
@ -6,11 +6,12 @@ export default class NewCommitForm {
|
|||
this.branchName = form.find('.js-branch-name');
|
||||
this.originalBranch = form.find('.js-original-branch');
|
||||
this.createMergeRequest = form.find('.js-create-merge-request');
|
||||
this.createMergeRequestContainer = form.find('.js-create-merge-request-container');
|
||||
this.createMergeRequestContainer = form.find(
|
||||
'.js-create-merge-request-container',
|
||||
);
|
||||
this.branchName.keyup(this.renderDestination);
|
||||
this.renderDestination();
|
||||
}
|
||||
|
||||
renderDestination() {
|
||||
var different;
|
||||
different = this.branchName.val() !== this.originalBranch.val();
|
||||
|
@ -23,6 +24,6 @@ export default class NewCommitForm {
|
|||
this.createMergeRequestContainer.hide();
|
||||
this.createMergeRequest.prop('checked', false);
|
||||
}
|
||||
return this.wasDifferent = different;
|
||||
return (this.wasDifferent = different);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
<script>
|
||||
import icon from '../../../vue_shared/components/icon.vue';
|
||||
import listItem from './list_item.vue';
|
||||
import listCollapsed from './list_collapsed.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
listItem,
|
||||
listCollapsed,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
fileList: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleCollapsed() {
|
||||
this.$emit('toggleCollapsed');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="multi-file-commit-panel-section">
|
||||
<header
|
||||
class="multi-file-commit-panel-header"
|
||||
:class="{
|
||||
'is-collapsed': collapsed,
|
||||
}"
|
||||
>
|
||||
<icon
|
||||
name="list-bulleted"
|
||||
:size="18"
|
||||
css-classes="append-right-default"
|
||||
/>
|
||||
<template v-if="!collapsed">
|
||||
{{ title }}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
|
||||
@click="toggleCollapsed"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="fa fa-angle-double-right"
|
||||
>
|
||||
</i>
|
||||
</button>
|
||||
</template>
|
||||
</header>
|
||||
<div class="multi-file-commit-list">
|
||||
<list-collapsed
|
||||
v-if="collapsed"
|
||||
/>
|
||||
<template v-else>
|
||||
<ul
|
||||
v-if="fileList.length"
|
||||
class="list-unstyled append-bottom-0"
|
||||
>
|
||||
<li
|
||||
v-for="file in fileList"
|
||||
:key="file.key"
|
||||
>
|
||||
<list-item
|
||||
:file="file"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
v-else
|
||||
class="help-block prepend-top-0"
|
||||
>
|
||||
No changes
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,89 +0,0 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import newModal from './modal.vue';
|
||||
import upload from './upload.vue';
|
||||
import icon from '../../../vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
newModal,
|
||||
upload,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
openModal: false,
|
||||
modalType: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'path',
|
||||
]),
|
||||
},
|
||||
methods: {
|
||||
createNewItem(type) {
|
||||
this.modalType = type;
|
||||
this.toggleModalOpen();
|
||||
},
|
||||
toggleModalOpen() {
|
||||
this.openModal = !this.openModal;
|
||||
},
|
||||
},
|
||||
};
|
||||
</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"
|
||||
>
|
||||
<icon
|
||||
name="plus"
|
||||
css-classes="pull-left"
|
||||
/>
|
||||
<icon
|
||||
name="arrow-down"
|
||||
css-classes="pull-left"
|
||||
/>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
role="button"
|
||||
@click.prevent="createNewItem('blob')"
|
||||
>
|
||||
{{ __('New file') }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<upload
|
||||
:path="path"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
role="button"
|
||||
@click.prevent="createNewItem('tree')"
|
||||
>
|
||||
{{ __('New directory') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<new-modal
|
||||
v-if="openModal"
|
||||
:type="modalType"
|
||||
:path="path"
|
||||
@toggle="toggleModalOpen"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -1,63 +0,0 @@
|
|||
<script>
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import RepoSidebar from './repo_sidebar.vue';
|
||||
import RepoCommitSection from './repo_commit_section.vue';
|
||||
import RepoTabs from './repo_tabs.vue';
|
||||
import RepoFileButtons from './repo_file_buttons.vue';
|
||||
import RepoPreview from './repo_preview.vue';
|
||||
import repoEditor from './repo_editor.vue';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapState([
|
||||
'currentBlobView',
|
||||
]),
|
||||
...mapGetters([
|
||||
'isCollapsed',
|
||||
'changedFiles',
|
||||
]),
|
||||
},
|
||||
components: {
|
||||
RepoSidebar,
|
||||
RepoTabs,
|
||||
RepoFileButtons,
|
||||
repoEditor,
|
||||
RepoCommitSection,
|
||||
RepoPreview,
|
||||
},
|
||||
mounted() {
|
||||
const returnValue = 'Are you sure you want to lose unsaved changes?';
|
||||
window.onbeforeunload = (e) => {
|
||||
if (!this.changedFiles.length) return undefined;
|
||||
|
||||
Object.assign(e, {
|
||||
returnValue,
|
||||
});
|
||||
return returnValue;
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="multi-file"
|
||||
:class="{
|
||||
'is-collapsed': isCollapsed
|
||||
}"
|
||||
>
|
||||
<repo-sidebar/>
|
||||
<div
|
||||
v-if="isCollapsed"
|
||||
class="multi-file-edit-pane"
|
||||
>
|
||||
<repo-tabs />
|
||||
<component
|
||||
class="multi-file-edit-pane-content"
|
||||
:is="currentBlobView"
|
||||
/>
|
||||
<repo-file-buttons />
|
||||
</div>
|
||||
<repo-commit-section />
|
||||
</div>
|
||||
</template>
|
|
@ -1,85 +0,0 @@
|
|||
<script>
|
||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
import RepoPreviousDirectory from './repo_prev_directory.vue';
|
||||
import RepoFile from './repo_file.vue';
|
||||
import RepoLoadingFile from './repo_loading_file.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'repo-previous-directory': RepoPreviousDirectory,
|
||||
'repo-file': RepoFile,
|
||||
'repo-loading-file': RepoLoadingFile,
|
||||
},
|
||||
created() {
|
||||
window.addEventListener('popstate', this.popHistoryState);
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('popstate', this.popHistoryState);
|
||||
},
|
||||
mounted() {
|
||||
this.getTreeData();
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'loading',
|
||||
'isRoot',
|
||||
]),
|
||||
...mapState({
|
||||
projectName(state) {
|
||||
return state.project.name;
|
||||
},
|
||||
}),
|
||||
...mapGetters([
|
||||
'treeList',
|
||||
'isCollapsed',
|
||||
]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'getTreeData',
|
||||
'popHistoryState',
|
||||
]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ide-file-list">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
v-if="isCollapsed"
|
||||
>
|
||||
</th>
|
||||
<template v-else>
|
||||
<th class="name multi-file-table-name">
|
||||
Name
|
||||
</th>
|
||||
<th class="hidden-sm hidden-xs last-commit">
|
||||
Last commit
|
||||
</th>
|
||||
<th class="hidden-xs last-update text-right">
|
||||
Last update
|
||||
</th>
|
||||
</template>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<repo-previous-directory
|
||||
v-if="!isRoot && treeList.length"
|
||||
/>
|
||||
<repo-loading-file
|
||||
v-if="!treeList.length && loading"
|
||||
v-for="n in 5"
|
||||
:key="n"
|
||||
/>
|
||||
<repo-file
|
||||
v-for="file in treeList"
|
||||
:key="file.key"
|
||||
:file="file"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
|
@ -1,106 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import { mapActions } from 'vuex';
|
||||
import { convertPermissionToBoolean } from '../lib/utils/common_utils';
|
||||
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 store from './stores';
|
||||
import Translate from '../vue_shared/translate';
|
||||
|
||||
function initRepo(el) {
|
||||
if (!el) return null;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
store,
|
||||
components: {
|
||||
repo: Repo,
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'setInitialData',
|
||||
]),
|
||||
},
|
||||
created() {
|
||||
const data = el.dataset;
|
||||
|
||||
this.setInitialData({
|
||||
project: {
|
||||
id: data.projectId,
|
||||
name: data.projectName,
|
||||
url: data.projectUrl,
|
||||
},
|
||||
endpoints: {
|
||||
rootEndpoint: data.url,
|
||||
newMergeRequestUrl: data.newMergeRequestUrl,
|
||||
rootUrl: data.rootUrl,
|
||||
},
|
||||
canCommit: convertPermissionToBoolean(data.canCommit),
|
||||
onTopOfBranch: convertPermissionToBoolean(data.onTopOfBranch),
|
||||
currentRef: data.ref,
|
||||
path: data.currentPath,
|
||||
currentBranch: data.currentBranch,
|
||||
isRoot: convertPermissionToBoolean(data.root),
|
||||
isInitialRoot: convertPermissionToBoolean(data.root),
|
||||
});
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('repo');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function initRepoEditButton(el) {
|
||||
return new Vue({
|
||||
el,
|
||||
store,
|
||||
components: {
|
||||
repoEditButton: RepoEditButton,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('repo-edit-button');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function initNewDropdown(el) {
|
||||
return new Vue({
|
||||
el,
|
||||
store,
|
||||
components: {
|
||||
newDropdown,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('new-dropdown');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function initNewBranchForm() {
|
||||
const el = document.querySelector('.js-new-branch-dropdown');
|
||||
|
||||
if (!el) return null;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
components: {
|
||||
newBranchForm,
|
||||
},
|
||||
store,
|
||||
render(createElement) {
|
||||
return createElement('new-branch-form');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const repo = document.getElementById('repo');
|
||||
const editButton = document.querySelector('.editable-mode');
|
||||
const newDropdownHolder = document.querySelector('.js-new-dropdown');
|
||||
|
||||
Vue.use(Translate);
|
||||
|
||||
initRepo(repo);
|
||||
initRepoEditButton(editButton);
|
||||
initNewBranchForm();
|
||||
initNewDropdown(newDropdownHolder);
|
|
@ -1,146 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import { visitUrl } from '../../lib/utils/url_utility';
|
||||
import flash from '../../flash';
|
||||
import service from '../services';
|
||||
import * as types from './mutation_types';
|
||||
|
||||
export const redirectToUrl = (_, url) => visitUrl(url);
|
||||
|
||||
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
|
||||
|
||||
export const closeDiscardPopup = ({ commit }) => commit(types.TOGGLE_DISCARD_POPUP, false);
|
||||
|
||||
export const discardAllChanges = ({ commit, getters, dispatch }) => {
|
||||
const changedFiles = getters.changedFiles;
|
||||
|
||||
changedFiles.forEach((file) => {
|
||||
commit(types.DISCARD_FILE_CHANGES, file);
|
||||
|
||||
if (file.tempFile) {
|
||||
dispatch('closeFile', { file, force: true });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const closeAllFiles = ({ state, dispatch }) => {
|
||||
state.openFiles.forEach(file => dispatch('closeFile', { file }));
|
||||
};
|
||||
|
||||
export const toggleEditMode = ({ state, commit, getters, dispatch }, force = false) => {
|
||||
const changedFiles = getters.changedFiles;
|
||||
|
||||
if (changedFiles.length && !force) {
|
||||
commit(types.TOGGLE_DISCARD_POPUP, true);
|
||||
} else {
|
||||
commit(types.TOGGLE_EDIT_MODE);
|
||||
commit(types.TOGGLE_DISCARD_POPUP, false);
|
||||
dispatch('toggleBlobView');
|
||||
|
||||
if (!state.editMode) {
|
||||
dispatch('discardAllChanges');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const toggleBlobView = ({ commit, state }) => {
|
||||
if (state.editMode) {
|
||||
commit(types.SET_EDIT_MODE);
|
||||
} else {
|
||||
commit(types.SET_PREVIEW_MODE);
|
||||
}
|
||||
};
|
||||
|
||||
export const checkCommitStatus = ({ state }) => service.getBranchData(
|
||||
state.project.id,
|
||||
state.currentBranch,
|
||||
)
|
||||
.then((data) => {
|
||||
const { id } = data.commit;
|
||||
|
||||
if (state.currentRef !== id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
.catch(() => flash('Error checking branch data. Please try again.'));
|
||||
|
||||
export const commitChanges = ({ commit, state, dispatch, getters }, { payload, newMr }) =>
|
||||
service.commit(state.project.id, payload)
|
||||
.then((data) => {
|
||||
const { branch } = payload;
|
||||
if (!data.short_id) {
|
||||
flash(data.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const lastCommit = {
|
||||
commit_path: `${state.project.url}/commit/${data.id}`,
|
||||
commit: {
|
||||
message: data.message,
|
||||
authored_date: data.committed_date,
|
||||
},
|
||||
};
|
||||
|
||||
flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
|
||||
|
||||
if (newMr) {
|
||||
dispatch('redirectToUrl', `${state.endpoints.newMergeRequestUrl}${branch}`);
|
||||
} else {
|
||||
commit(types.SET_COMMIT_REF, data.id);
|
||||
|
||||
getters.changedFiles.forEach((entry) => {
|
||||
commit(types.SET_LAST_COMMIT_DATA, {
|
||||
entry,
|
||||
lastCommit,
|
||||
});
|
||||
});
|
||||
|
||||
dispatch('discardAllChanges');
|
||||
dispatch('closeAllFiles');
|
||||
dispatch('toggleEditMode');
|
||||
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
})
|
||||
.catch(() => flash('Error committing changes. Please try again.'));
|
||||
|
||||
export const createTempEntry = ({ state, dispatch }, { name, type, content = '', base64 = false }) => {
|
||||
if (type === 'tree') {
|
||||
dispatch('createTempTree', name);
|
||||
} else if (type === 'blob') {
|
||||
dispatch('createTempFile', {
|
||||
tree: state,
|
||||
name,
|
||||
base64,
|
||||
content,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const popHistoryState = ({ state, dispatch, getters }) => {
|
||||
const treeList = getters.treeList;
|
||||
const tree = treeList.find(file => file.url === state.previousUrl);
|
||||
|
||||
if (!tree) return;
|
||||
|
||||
if (tree.type === 'tree') {
|
||||
dispatch('toggleTreeOpen', { endpoint: tree.url, tree });
|
||||
}
|
||||
};
|
||||
|
||||
export const scrollToTab = () => {
|
||||
Vue.nextTick(() => {
|
||||
const tabs = document.getElementById('tabs');
|
||||
|
||||
if (tabs) {
|
||||
const tabEl = tabs.querySelector('.active .repo-tab');
|
||||
|
||||
tabEl.focus();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export * from './actions/tree';
|
||||
export * from './actions/file';
|
||||
export * from './actions/branch';
|
|
@ -1,20 +0,0 @@
|
|||
import service from '../../services';
|
||||
import * as types from '../mutation_types';
|
||||
import { pushState } from '../utils';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const createNewBranch = ({ state, commit }, branch) => service.createBranch(
|
||||
state.project.id,
|
||||
{
|
||||
branch,
|
||||
ref: state.currentBranch,
|
||||
},
|
||||
).then(res => res.json())
|
||||
.then((data) => {
|
||||
const branchName = data.name;
|
||||
const url = location.href.replace(state.currentBranch, branchName);
|
||||
|
||||
pushState(url);
|
||||
|
||||
commit(types.SET_CURRENT_BRANCH, branchName);
|
||||
});
|
|
@ -1,163 +0,0 @@
|
|||
import { visitUrl } from '../../../lib/utils/url_utility';
|
||||
import { normalizeHeaders } from '../../../lib/utils/common_utils';
|
||||
import flash from '../../../flash';
|
||||
import service from '../../services';
|
||||
import * as types from '../mutation_types';
|
||||
import {
|
||||
pushState,
|
||||
setPageTitle,
|
||||
findEntry,
|
||||
createTemp,
|
||||
createOrMergeEntry,
|
||||
} from '../utils';
|
||||
|
||||
export const getTreeData = (
|
||||
{ commit, state, dispatch },
|
||||
{ endpoint = state.endpoints.rootEndpoint, tree = state } = {},
|
||||
) => {
|
||||
commit(types.TOGGLE_LOADING, tree);
|
||||
|
||||
service.getTreeData(endpoint)
|
||||
.then((res) => {
|
||||
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
|
||||
|
||||
setPageTitle(pageTitle);
|
||||
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
const prevLastCommitPath = tree.lastCommitPath;
|
||||
if (!state.isInitialRoot) {
|
||||
commit(types.SET_ROOT, data.path === '/');
|
||||
}
|
||||
|
||||
dispatch('updateDirectoryData', { data, tree });
|
||||
commit(types.SET_PARENT_TREE_URL, data.parent_tree_url);
|
||||
commit(types.SET_LAST_COMMIT_URL, { tree, url: data.last_commit_path });
|
||||
commit(types.TOGGLE_LOADING, tree);
|
||||
|
||||
if (prevLastCommitPath !== null) {
|
||||
dispatch('getLastCommitData', tree);
|
||||
}
|
||||
|
||||
pushState(endpoint);
|
||||
})
|
||||
.catch(() => {
|
||||
flash('Error loading tree data. Please try again.');
|
||||
commit(types.TOGGLE_LOADING, tree);
|
||||
});
|
||||
};
|
||||
|
||||
export const toggleTreeOpen = ({ commit, dispatch }, { endpoint, tree }) => {
|
||||
if (tree.opened) {
|
||||
// send empty data to clear the tree
|
||||
const data = { trees: [], blobs: [], submodules: [] };
|
||||
|
||||
pushState(tree.parentTreeUrl);
|
||||
|
||||
commit(types.SET_PREVIOUS_URL, tree.parentTreeUrl);
|
||||
dispatch('updateDirectoryData', { data, tree });
|
||||
} else {
|
||||
commit(types.SET_PREVIOUS_URL, endpoint);
|
||||
dispatch('getTreeData', { endpoint, tree });
|
||||
}
|
||||
|
||||
commit(types.TOGGLE_TREE_OPEN, tree);
|
||||
};
|
||||
|
||||
export const clickedTreeRow = ({ commit, dispatch }, row) => {
|
||||
if (row.type === 'tree') {
|
||||
dispatch('toggleTreeOpen', {
|
||||
endpoint: row.url,
|
||||
tree: row,
|
||||
});
|
||||
} else if (row.type === 'submodule') {
|
||||
commit(types.TOGGLE_LOADING, row);
|
||||
|
||||
visitUrl(row.url);
|
||||
} else if (row.type === 'blob' && row.opened) {
|
||||
dispatch('setFileActive', row);
|
||||
} else {
|
||||
dispatch('getFileData', row);
|
||||
}
|
||||
};
|
||||
|
||||
export const createTempTree = ({ state, commit, dispatch }, name) => {
|
||||
let tree = state;
|
||||
const dirNames = name.replace(new RegExp(`^${state.path}/`), '').split('/');
|
||||
|
||||
dirNames.forEach((dirName) => {
|
||||
const foundEntry = findEntry(tree, 'tree', dirName);
|
||||
|
||||
if (!foundEntry) {
|
||||
const tmpEntry = createTemp({
|
||||
name: dirName,
|
||||
path: tree.path,
|
||||
type: 'tree',
|
||||
level: tree.level !== undefined ? tree.level + 1 : 0,
|
||||
});
|
||||
|
||||
commit(types.CREATE_TMP_TREE, {
|
||||
parent: tree,
|
||||
tmpEntry,
|
||||
});
|
||||
commit(types.TOGGLE_TREE_OPEN, tmpEntry);
|
||||
|
||||
tree = tmpEntry;
|
||||
} else {
|
||||
tree = foundEntry;
|
||||
}
|
||||
});
|
||||
|
||||
if (tree.tempFile) {
|
||||
dispatch('createTempFile', {
|
||||
tree,
|
||||
name: '.gitkeep',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
|
||||
if (tree.lastCommitPath === null || getters.isCollapsed) return;
|
||||
|
||||
service.getTreeLastCommit(tree.lastCommitPath)
|
||||
.then((res) => {
|
||||
const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
|
||||
|
||||
commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
|
||||
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
data.forEach((lastCommit) => {
|
||||
const entry = findEntry(tree, lastCommit.type, lastCommit.file_name);
|
||||
|
||||
if (entry) {
|
||||
commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit });
|
||||
}
|
||||
});
|
||||
|
||||
dispatch('getLastCommitData', tree);
|
||||
})
|
||||
.catch(() => flash('Error fetching log data.'));
|
||||
};
|
||||
|
||||
export const updateDirectoryData = ({ commit, state }, { data, tree }) => {
|
||||
const level = tree.level !== undefined ? tree.level + 1 : 0;
|
||||
const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl;
|
||||
const createEntry = (entry, type) => createOrMergeEntry({
|
||||
tree,
|
||||
entry,
|
||||
level,
|
||||
type,
|
||||
parentTreeUrl,
|
||||
});
|
||||
|
||||
const formattedData = [
|
||||
...data.trees.map(t => createEntry(t, 'tree')),
|
||||
...data.submodules.map(m => createEntry(m, 'submodule')),
|
||||
...data.blobs.map(b => createEntry(b, 'blob')),
|
||||
];
|
||||
|
||||
commit(types.SET_DIRECTORY_DATA, { tree, data: formattedData });
|
||||
};
|
|
@ -1,40 +0,0 @@
|
|||
import _ from 'underscore';
|
||||
|
||||
/*
|
||||
Takes the multi-dimensional tree and returns a flattened array.
|
||||
This allows for the table to recursively render the table rows but keeps the data
|
||||
structure nested to make it easier to add new files/directories.
|
||||
*/
|
||||
export const treeList = (state) => {
|
||||
const mapTree = arr => (!arr.tree.length ? [] : _.map(arr.tree, a => [a, mapTree(a)]));
|
||||
|
||||
return _.chain(state.tree)
|
||||
.map(arr => [arr, mapTree(arr)])
|
||||
.flatten()
|
||||
.value();
|
||||
};
|
||||
|
||||
export const changedFiles = state => state.openFiles.filter(file => file.changed);
|
||||
|
||||
export const activeFile = state => state.openFiles.find(file => file.active);
|
||||
|
||||
export const activeFileExtension = (state) => {
|
||||
const file = activeFile(state);
|
||||
return file ? `.${file.path.split('.').pop()}` : '';
|
||||
};
|
||||
|
||||
export const isCollapsed = state => !!state.openFiles.length;
|
||||
|
||||
export const canEditFile = (state) => {
|
||||
const currentActiveFile = activeFile(state);
|
||||
const openedFiles = state.openFiles;
|
||||
|
||||
return state.canCommit &&
|
||||
state.onTopOfBranch &&
|
||||
openedFiles.length &&
|
||||
(currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary);
|
||||
};
|
||||
|
||||
export const addedFiles = state => changedFiles(state).filter(f => f.tempFile);
|
||||
|
||||
export const modifiedFiles = state => changedFiles(state).filter(f => !f.tempFile);
|
|
@ -1,9 +0,0 @@
|
|||
import * as types from '../mutation_types';
|
||||
|
||||
export default {
|
||||
[types.SET_CURRENT_BRANCH](state, currentBranch) {
|
||||
Object.assign(state, {
|
||||
currentBranch,
|
||||
});
|
||||
},
|
||||
};
|
|
@ -0,0 +1,103 @@
|
|||
<script>
|
||||
|
||||
/* This is a re-usable vue component for rendering a project avatar that
|
||||
does not need to link to the project's profile. The image and an optional
|
||||
tooltip can be configured by props passed to this component.
|
||||
|
||||
Sample configuration:
|
||||
|
||||
<project-avatar-image
|
||||
:lazy="true"
|
||||
:img-src="projectAvatarSrc"
|
||||
:img-alt="tooltipText"
|
||||
:tooltip-text="tooltipText"
|
||||
tooltip-placement="top"
|
||||
/>
|
||||
|
||||
*/
|
||||
|
||||
import defaultAvatarUrl from 'images/no_avatar.png';
|
||||
import { placeholderImage } from '../../../lazy_loader';
|
||||
import tooltip from '../../directives/tooltip';
|
||||
|
||||
export default {
|
||||
name: 'ProjectAvatarImage',
|
||||
props: {
|
||||
lazy: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
imgSrc: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: defaultAvatarUrl,
|
||||
},
|
||||
cssClasses: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
imgAlt: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'project avatar',
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 20,
|
||||
},
|
||||
tooltipText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
tooltipPlacement: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'top',
|
||||
},
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
computed: {
|
||||
// API response sends null when gravatar is disabled and
|
||||
// we provide an empty string when we use it inside project avatar link.
|
||||
// In both cases we should render the defaultAvatarUrl
|
||||
sanitizedSource() {
|
||||
return this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
|
||||
},
|
||||
resultantSrcAttribute() {
|
||||
return this.lazy ? placeholderImage : this.sanitizedSource;
|
||||
},
|
||||
tooltipContainer() {
|
||||
return this.tooltipText ? 'body' : null;
|
||||
},
|
||||
avatarSizeClass() {
|
||||
return `s${this.size}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img
|
||||
v-tooltip
|
||||
class="avatar"
|
||||
:class="{
|
||||
lazy,
|
||||
[avatarSizeClass]: true,
|
||||
[cssClasses]: true
|
||||
}"
|
||||
:src="resultantSrcAttribute"
|
||||
:width="size"
|
||||
:height="size"
|
||||
:alt="imgAlt"
|
||||
:data-src="sanitizedSource"
|
||||
:data-container="tooltipContainer"
|
||||
:data-placement="tooltipPlacement"
|
||||
:title="tooltipText"
|
||||
/>
|
||||
</template>
|
|
@ -23,7 +23,6 @@
|
|||
.context-header {
|
||||
position: relative;
|
||||
margin-right: 2px;
|
||||
width: $contextual-sidebar-width;
|
||||
|
||||
a {
|
||||
transition: padding $sidebar-transition-duration;
|
||||
|
|
|
@ -219,6 +219,7 @@ $gl-input-padding: 10px;
|
|||
$gl-vert-padding: 6px;
|
||||
$gl-padding-top: 10px;
|
||||
$gl-sidebar-padding: 22px;
|
||||
$gl-bar-padding: 3px;
|
||||
|
||||
/*
|
||||
* Misc
|
||||
|
|
|
@ -22,9 +22,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.multi-file {
|
||||
.ide-view {
|
||||
display: flex;
|
||||
height: calc(100vh - 145px);
|
||||
height: calc(100vh - #{$header-height});
|
||||
color: $almost-black;
|
||||
border-top: 1px solid $white-dark;
|
||||
border-bottom: 1px solid $white-dark;
|
||||
|
||||
|
@ -35,12 +36,47 @@
|
|||
}
|
||||
}
|
||||
|
||||
.with-performance-bar .ide-view {
|
||||
height: calc(100vh - #{$header-height});
|
||||
}
|
||||
|
||||
.ide-file-list {
|
||||
flex: 1;
|
||||
overflow: scroll;
|
||||
|
||||
.file {
|
||||
cursor: pointer;
|
||||
|
||||
&.file-open {
|
||||
background: $white-normal;
|
||||
}
|
||||
|
||||
.repo-file-name {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.unsaved-icon {
|
||||
color: $indigo-700;
|
||||
float: right;
|
||||
font-size: smaller;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.repo-new-btn {
|
||||
display: none;
|
||||
margin-top: -4px;
|
||||
margin-bottom: -4px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.repo-new-btn {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.unsaved-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
|
@ -55,10 +91,9 @@
|
|||
|
||||
.multi-file-table-name,
|
||||
.multi-file-table-col-commit-message {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
overflow: visible;
|
||||
max-width: 0;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.multi-file-table-name {
|
||||
|
@ -66,6 +101,7 @@
|
|||
}
|
||||
|
||||
.multi-file-table-col-commit-message {
|
||||
white-space: nowrap;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
|
@ -79,7 +115,7 @@
|
|||
|
||||
.multi-file-tabs {
|
||||
display: flex;
|
||||
overflow: scroll;
|
||||
overflow-x: auto;
|
||||
background-color: $white-normal;
|
||||
box-shadow: inset 0 -1px $white-dark;
|
||||
|
||||
|
@ -128,9 +164,38 @@
|
|||
height: 0;
|
||||
}
|
||||
|
||||
.blob-editor-container {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.vertical-center {
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.multi-file-editor-holder {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.multi-file-editor-btn-group {
|
||||
padding: $grid-size;
|
||||
padding: $gl-bar-padding $gl-padding;
|
||||
border-top: 1px solid $white-dark;
|
||||
border-bottom: 1px solid $white-dark;
|
||||
background: $white-light;
|
||||
}
|
||||
|
||||
.ide-status-bar {
|
||||
padding: $gl-bar-padding $gl-padding;
|
||||
background: $white-light;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
// Not great, but this is to deal with our current output
|
||||
|
@ -138,10 +203,6 @@
|
|||
height: 100%;
|
||||
overflow: scroll;
|
||||
|
||||
.blob-viewer {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.file-content.code {
|
||||
display: flex;
|
||||
|
||||
|
@ -162,18 +223,101 @@
|
|||
}
|
||||
}
|
||||
|
||||
.file-content.blob-no-preview {
|
||||
a {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.multi-file-commit-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 290px;
|
||||
padding: $gl-padding;
|
||||
padding: 0;
|
||||
background-color: $gray-light;
|
||||
border-left: 1px solid $white-dark;
|
||||
|
||||
.projects-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.multi-file-commit-panel-inner {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.multi-file-commit-panel-inner-scroll {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
&.is-collapsed {
|
||||
width: 60px;
|
||||
padding: 0;
|
||||
|
||||
.multi-file-commit-list {
|
||||
padding-top: $gl-padding;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.multi-file-context-bar-icon {
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
float: none;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.branch-container {
|
||||
border-left: 4px solid $indigo-700;
|
||||
margin-bottom: $gl-bar-padding;
|
||||
}
|
||||
|
||||
.branch-header {
|
||||
background: $white-dark;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.branch-header-title {
|
||||
flex: 1;
|
||||
padding: $grid-size $gl-padding;
|
||||
color: $indigo-700;
|
||||
font-weight: $gl-font-weight-bold;
|
||||
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.branch-header-btns {
|
||||
padding: $gl-vert-padding $gl-padding;
|
||||
}
|
||||
|
||||
.left-collapse-btn {
|
||||
display: none;
|
||||
background: $gray-light;
|
||||
text-align: left;
|
||||
border-top: 1px solid $white-dark;
|
||||
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.multi-file-context-bar-icon {
|
||||
padding: 10px;
|
||||
|
||||
svg {
|
||||
margin-right: 10px;
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -186,9 +330,9 @@
|
|||
.multi-file-commit-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 0 12px;
|
||||
margin-bottom: 12px;
|
||||
border-bottom: 1px solid $white-dark;
|
||||
padding: $gl-btn-padding 0;
|
||||
|
||||
&.is-collapsed {
|
||||
border-bottom: 1px solid $white-dark;
|
||||
|
@ -197,23 +341,33 @@
|
|||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.multi-file-commit-panel-collapse-btn {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.multi-file-commit-panel-header-title {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
padding: $gl-btn-padding;
|
||||
|
||||
svg {
|
||||
margin-right: $gl-btn-padding;
|
||||
}
|
||||
}
|
||||
|
||||
.multi-file-commit-panel-collapse-btn {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
margin-left: auto;
|
||||
font-size: 20px;
|
||||
|
||||
&.is-collapsed {
|
||||
margin-right: auto;
|
||||
}
|
||||
border-left: 1px solid $white-dark;
|
||||
}
|
||||
|
||||
.multi-file-commit-list {
|
||||
flex: 1;
|
||||
overflow: scroll;
|
||||
overflow: auto;
|
||||
padding: $gl-padding;
|
||||
}
|
||||
|
||||
.multi-file-commit-list-item {
|
||||
|
@ -244,7 +398,7 @@
|
|||
}
|
||||
|
||||
.multi-file-commit-form {
|
||||
padding-top: 12px;
|
||||
padding: $gl-padding;
|
||||
border-top: 1px solid $white-dark;
|
||||
}
|
||||
|
||||
|
@ -295,3 +449,40 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ide-loading {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ide-empty-state {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.repo-new-btn {
|
||||
.dropdown-toggle svg {
|
||||
margin-top: -2px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
left: auto;
|
||||
right: 0;
|
||||
|
||||
label {
|
||||
font-weight: $gl-font-weight-normal;
|
||||
padding: 5px 8px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ide-flash-container.flash-container {
|
||||
margin-top: $header-height;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
class IdeController < ApplicationController
|
||||
layout 'nav_only'
|
||||
|
||||
def index
|
||||
end
|
||||
end
|
|
@ -306,7 +306,7 @@ module ApplicationHelper
|
|||
cookies["sidebar_collapsed"] == "true"
|
||||
end
|
||||
|
||||
def show_new_repo?
|
||||
def show_new_ide?
|
||||
cookies["new_repo"] == "true" && body_data_page != 'projects:show'
|
||||
end
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ module BlobHelper
|
|||
%w(credits changelog news copying copyright license authors)
|
||||
end
|
||||
|
||||
def edit_path(project = @project, ref = @ref, path = @path, options = {})
|
||||
def edit_blob_path(project = @project, ref = @ref, path = @path, options = {})
|
||||
project_edit_blob_path(project,
|
||||
tree_join(ref, path),
|
||||
options[:link_opts])
|
||||
|
@ -26,10 +26,10 @@ module BlobHelper
|
|||
button_tag 'Edit', class: "#{common_classes} disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' }
|
||||
# This condition applies to anonymous or users who can edit directly
|
||||
elsif !current_user || (current_user && can_modify_blob?(blob, project, ref))
|
||||
link_to 'Edit', edit_path(project, ref, path, options), class: "#{common_classes} btn-sm"
|
||||
link_to 'Edit', edit_blob_path(project, ref, path, options), class: "#{common_classes} btn-sm"
|
||||
elsif current_user && can?(current_user, :fork_project, project)
|
||||
continue_params = {
|
||||
to: edit_path(project, ref, path, options),
|
||||
to: edit_blob_path(project, ref, path, options),
|
||||
notice: edit_in_new_fork_notice,
|
||||
notice_now: edit_in_new_fork_notice_now
|
||||
}
|
||||
|
@ -41,6 +41,43 @@ module BlobHelper
|
|||
end
|
||||
end
|
||||
|
||||
def ide_edit_path(project = @project, ref = @ref, path = @path, options = {})
|
||||
"#{ide_path}/project#{edit_blob_path(project, ref, path, options)}"
|
||||
end
|
||||
|
||||
def ide_edit_text
|
||||
"#{_('Multi Edit')} <span class='label label-primary'>#{_('Beta')}</span>".html_safe
|
||||
end
|
||||
|
||||
def ide_blob_link(project = @project, ref = @ref, path = @path, options = {})
|
||||
return unless show_new_ide?
|
||||
|
||||
blob = options.delete(:blob)
|
||||
blob ||= project.repository.blob_at(ref, path) rescue nil
|
||||
|
||||
return unless blob && blob.readable_text?
|
||||
|
||||
common_classes = "btn js-edit-ide #{options[:extra_class]}"
|
||||
|
||||
if !on_top_of_branch?(project, ref)
|
||||
button_tag ide_edit_text, class: "#{common_classes} disabled has-tooltip", title: _('You can only edit files when you are on a branch'), data: { container: 'body' }
|
||||
# This condition applies to anonymous or users who can edit directly
|
||||
elsif current_user && can_modify_blob?(blob, project, ref)
|
||||
link_to ide_edit_text, ide_edit_path(project, ref, path, options), class: "#{common_classes} btn-sm"
|
||||
elsif current_user && can?(current_user, :fork_project, project)
|
||||
continue_params = {
|
||||
to: ide_edit_path(project, ref, path, options),
|
||||
notice: edit_in_new_fork_notice,
|
||||
notice_now: edit_in_new_fork_notice_now
|
||||
}
|
||||
fork_path = project_forks_path(project, namespace_key: current_user.namespace.id, continue: continue_params)
|
||||
|
||||
button_tag ide_edit_text,
|
||||
class: common_classes,
|
||||
data: { fork_path: fork_path }
|
||||
end
|
||||
end
|
||||
|
||||
def modify_file_link(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:)
|
||||
return unless current_user
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
- page_title 'IDE'
|
||||
|
||||
- content_for :page_specific_javascripts do
|
||||
= webpack_bundle_tag 'common_vue'
|
||||
= webpack_bundle_tag 'ide'
|
||||
|
||||
.ide-flash-container.flash-container
|
||||
|
||||
#ide.ide-loading
|
||||
.text-center
|
||||
= icon('spinner spin 2x')
|
||||
%h2.clgray= _('IDE Loading ...')
|
|
@ -0,0 +1,13 @@
|
|||
!!! 5
|
||||
%html{ lang: I18n.locale, class: page_class }
|
||||
= render "layouts/head"
|
||||
%body{ class: "#{user_application_theme} #{@body_class}", data: { page: body_data_page } }
|
||||
= render 'peek/bar'
|
||||
= render "layouts/header/default"
|
||||
= render 'shared/outdated_browser'
|
||||
.mobile-overlay
|
||||
.alert-wrapper
|
||||
= render "layouts/broadcast"
|
||||
= yield :flash_message
|
||||
= render "layouts/flash"
|
||||
= yield
|
|
@ -7,7 +7,7 @@
|
|||
.nav-block
|
||||
= render 'projects/tree/tree_header', tree: @tree
|
||||
|
||||
- if !show_new_repo? && commit
|
||||
- if commit
|
||||
= render 'shared/commit_well', commit: commit, ref: ref, project: project
|
||||
|
||||
= render 'projects/tree/tree_content', tree: @tree, content_url: content_url
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
|
||||
.btn-group{ role: "group" }<
|
||||
= edit_blob_link
|
||||
= ide_blob_link
|
||||
- if current_user
|
||||
= replace_blob_link
|
||||
= delete_blob_link
|
||||
|
|
|
@ -6,21 +6,14 @@
|
|||
- content_for :page_specific_javascripts do
|
||||
= webpack_bundle_tag 'blob'
|
||||
|
||||
- if show_new_repo?
|
||||
= webpack_bundle_tag 'common_vue'
|
||||
= webpack_bundle_tag 'repo'
|
||||
|
||||
= render 'projects/last_push'
|
||||
|
||||
%div{ class: container_class }
|
||||
- if show_new_repo?
|
||||
= render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_blob_path(@project, @id)
|
||||
- else
|
||||
#tree-holder.tree-holder
|
||||
= render 'blob', blob: @blob
|
||||
#tree-holder.tree-holder
|
||||
= render 'blob', blob: @blob
|
||||
|
||||
- if can_modify_blob?(@blob)
|
||||
= render 'projects/blob/remove'
|
||||
|
||||
- title = "Replace #{@blob.name}"
|
||||
= render 'projects/blob/upload', title: title, placeholder: title, button_title: 'Replace file', form_path: project_update_blob_path(@project, @id), method: :put
|
||||
- title = "Replace #{@blob.name}"
|
||||
= render 'projects/blob/upload', title: title, placeholder: title, button_title: 'Replace file', form_path: project_update_blob_path(@project, @id), method: :put
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
.tree-content-holder.js-tree-content{ 'data-logs-path': @logs_path }
|
||||
.table-holder
|
||||
%table.table#tree-slider{ class: "table_#{@hex_path} tree-table" }
|
||||
%thead
|
||||
%tr
|
||||
%th= s_('ProjectFileTree|Name')
|
||||
%th.hidden-xs
|
||||
.pull-left= _('Last commit')
|
||||
%th.text-right= _('Last update')
|
||||
- if @path.present?
|
||||
%tr.tree-item
|
||||
%td.tree-item-file-name
|
||||
= link_to "..", project_tree_path(@project, up_dir_path), class: 'prepend-left-10'
|
||||
%td
|
||||
%td.hidden-xs
|
||||
|
||||
= render_tree(tree)
|
||||
|
||||
- if tree.readme
|
||||
= render "projects/tree/readme", readme: tree.readme
|
||||
|
||||
- if can_edit_tree?
|
||||
= render 'projects/blob/upload', title: _('Upload New File'), placeholder: _('Upload New File'), button_title: _('Upload file'), form_path: project_create_blob_path(@project, @id), method: :post
|
||||
= render 'projects/blob/new_dir'
|
|
@ -1,64 +0,0 @@
|
|||
- if on_top_of_branch?
|
||||
- addtotree_toggle_attributes = { href: '#', 'data-toggle': 'dropdown', 'data-target': '.add-to-tree-dropdown' }
|
||||
- else
|
||||
- addtotree_toggle_attributes = { title: _("You can only add files when you are on a branch"), data: { container: 'body' }, class: 'disabled has-tooltip' }
|
||||
|
||||
%ul.breadcrumb.repo-breadcrumb
|
||||
%li
|
||||
= link_to project_tree_path(@project, @ref) do
|
||||
= @project.path
|
||||
- path_breadcrumbs do |title, path|
|
||||
%li
|
||||
= link_to truncate(title, length: 40), project_tree_path(@project, tree_join(@ref, path))
|
||||
|
||||
- if current_user
|
||||
%li
|
||||
%a.btn.add-to-tree{ addtotree_toggle_attributes }
|
||||
= sprite_icon('plus', size: 16, css_class: 'pull-left')
|
||||
= sprite_icon('arrow-down', size: 16, css_class: 'pull-left')
|
||||
- if on_top_of_branch?
|
||||
.add-to-tree-dropdown
|
||||
%ul.dropdown-menu
|
||||
- if can_edit_tree?
|
||||
%li
|
||||
= link_to project_new_blob_path(@project, @id) do
|
||||
#{ _('New file') }
|
||||
%li
|
||||
= link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do
|
||||
#{ _('Upload file') }
|
||||
%li
|
||||
= link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do
|
||||
#{ _('New directory') }
|
||||
- elsif can?(current_user, :fork_project, @project)
|
||||
%li
|
||||
- continue_params = { to: project_new_blob_path(@project, @id),
|
||||
notice: edit_in_new_fork_notice,
|
||||
notice_now: edit_in_new_fork_notice_now }
|
||||
- fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
|
||||
continue: continue_params)
|
||||
= link_to fork_path, method: :post do
|
||||
#{ _('New file') }
|
||||
%li
|
||||
- continue_params = { to: request.fullpath,
|
||||
notice: edit_in_new_fork_notice + " Try to upload a file again.",
|
||||
notice_now: edit_in_new_fork_notice_now }
|
||||
- fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
|
||||
continue: continue_params)
|
||||
= link_to fork_path, method: :post do
|
||||
#{ _('Upload file') }
|
||||
%li
|
||||
- continue_params = { to: request.fullpath,
|
||||
notice: edit_in_new_fork_notice + " Try to create a new directory again.",
|
||||
notice_now: edit_in_new_fork_notice_now }
|
||||
- fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
|
||||
continue: continue_params)
|
||||
= link_to fork_path, method: :post do
|
||||
#{ _('New directory') }
|
||||
|
||||
%li.divider
|
||||
%li
|
||||
= link_to new_project_branch_path(@project) do
|
||||
#{ _('New branch') }
|
||||
%li
|
||||
= link_to new_project_tag_path(@project) do
|
||||
#{ _('New tag') }
|
|
@ -1,5 +1,24 @@
|
|||
- content_url = local_assigns.fetch(:content_url, nil)
|
||||
- if show_new_repo?
|
||||
= render 'shared/repo/repo', project: @project, content_url: content_url
|
||||
- else
|
||||
= render 'projects/tree/old_tree_content', tree: tree
|
||||
.tree-content-holder.js-tree-content{ 'data-logs-path': @logs_path }
|
||||
.table-holder
|
||||
%table.table#tree-slider{ class: "table_#{@hex_path} tree-table" }
|
||||
%thead
|
||||
%tr
|
||||
%th= s_('ProjectFileTree|Name')
|
||||
%th.hidden-xs
|
||||
.pull-left= _('Last commit')
|
||||
%th.text-right= _('Last update')
|
||||
- if @path.present?
|
||||
%tr.tree-item
|
||||
%td.tree-item-file-name
|
||||
= link_to "..", project_tree_path(@project, up_dir_path), class: 'prepend-left-10'
|
||||
%td
|
||||
%td.hidden-xs
|
||||
|
||||
= render_tree(tree)
|
||||
|
||||
- if tree.readme
|
||||
= render "projects/tree/readme", readme: tree.readme
|
||||
|
||||
- if can_edit_tree?
|
||||
= render 'projects/blob/upload', title: _('Upload New File'), placeholder: _('Upload New File'), button_title: _('Upload file'), form_path: project_create_blob_path(@project, @id), method: :post
|
||||
= render 'projects/blob/new_dir'
|
||||
|
|
|
@ -2,16 +2,78 @@
|
|||
.tree-ref-holder
|
||||
= render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true
|
||||
|
||||
- if show_new_repo? && can_push_branch?(@project, @ref)
|
||||
.js-new-dropdown
|
||||
- else
|
||||
= render 'projects/tree/old_tree_header'
|
||||
- if on_top_of_branch?
|
||||
- addtotree_toggle_attributes = { href: '#', 'data-toggle': 'dropdown', 'data-target': '.add-to-tree-dropdown' }
|
||||
- else
|
||||
- addtotree_toggle_attributes = { title: _("You can only add files when you are on a branch"), data: { container: 'body' }, class: 'disabled has-tooltip' }
|
||||
|
||||
%ul.breadcrumb.repo-breadcrumb
|
||||
%li
|
||||
= link_to project_tree_path(@project, @ref) do
|
||||
= @project.path
|
||||
- path_breadcrumbs do |title, path|
|
||||
%li
|
||||
= link_to truncate(title, length: 40), project_tree_path(@project, tree_join(@ref, path))
|
||||
|
||||
- if current_user
|
||||
%li
|
||||
%a.btn.add-to-tree{ addtotree_toggle_attributes }
|
||||
= sprite_icon('plus', size: 16, css_class: 'pull-left')
|
||||
= sprite_icon('arrow-down', size: 16, css_class: 'pull-left')
|
||||
- if on_top_of_branch?
|
||||
.add-to-tree-dropdown
|
||||
%ul.dropdown-menu
|
||||
- if can_edit_tree?
|
||||
%li
|
||||
= link_to project_new_blob_path(@project, @id) do
|
||||
#{ _('New file') }
|
||||
%li
|
||||
= link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do
|
||||
#{ _('Upload file') }
|
||||
%li
|
||||
= link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do
|
||||
#{ _('New directory') }
|
||||
- elsif can?(current_user, :fork_project, @project)
|
||||
%li
|
||||
- continue_params = { to: project_new_blob_path(@project, @id),
|
||||
notice: edit_in_new_fork_notice,
|
||||
notice_now: edit_in_new_fork_notice_now }
|
||||
- fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
|
||||
continue: continue_params)
|
||||
= link_to fork_path, method: :post do
|
||||
#{ _('New file') }
|
||||
%li
|
||||
- continue_params = { to: request.fullpath,
|
||||
notice: edit_in_new_fork_notice + " Try to upload a file again.",
|
||||
notice_now: edit_in_new_fork_notice_now }
|
||||
- fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
|
||||
continue: continue_params)
|
||||
= link_to fork_path, method: :post do
|
||||
#{ _('Upload file') }
|
||||
%li
|
||||
- continue_params = { to: request.fullpath,
|
||||
notice: edit_in_new_fork_notice + " Try to create a new directory again.",
|
||||
notice_now: edit_in_new_fork_notice_now }
|
||||
- fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
|
||||
continue: continue_params)
|
||||
= link_to fork_path, method: :post do
|
||||
#{ _('New directory') }
|
||||
|
||||
%li.divider
|
||||
%li
|
||||
= link_to new_project_branch_path(@project) do
|
||||
#{ _('New branch') }
|
||||
%li
|
||||
= link_to new_project_tag_path(@project) do
|
||||
#{ _('New tag') }
|
||||
|
||||
.tree-controls
|
||||
- if show_new_repo?
|
||||
.editable-mode
|
||||
- else
|
||||
= link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn'
|
||||
- if show_new_ide?
|
||||
= succeed " " do
|
||||
= link_to ide_edit_path(@project, @id), class: 'btn btn-default' do
|
||||
= ide_edit_text
|
||||
|
||||
= link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn'
|
||||
|
||||
= render 'projects/find_file_link'
|
||||
|
||||
|
|
|
@ -6,11 +6,6 @@
|
|||
= content_for :meta_tags do
|
||||
= auto_discovery_link_tag(:atom, project_commits_url(@project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits")
|
||||
|
||||
- if show_new_repo?
|
||||
- content_for :page_specific_javascripts do
|
||||
= webpack_bundle_tag 'common_vue'
|
||||
= webpack_bundle_tag 'repo'
|
||||
|
||||
%div{ class: [(container_class unless show_new_repo?), ("limit-container-width" unless fluid_layout)] }
|
||||
%div{ class: [(container_class), ("limit-container-width" unless fluid_layout)] }
|
||||
= render 'projects/last_push'
|
||||
= render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
- show_create = local_assigns.fetch(:show_create, false)
|
||||
|
||||
- show_new_branch_form = show_new_repo? && show_create && can?(current_user, :push_code, @project)
|
||||
- show_new_branch_form = show_new_ide? && show_create && can?(current_user, :push_code, @project)
|
||||
- dropdown_toggle_text = @ref || @project.default_branch
|
||||
= form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do
|
||||
= hidden_field_tag :destination, destination
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
- @no_container = true;
|
||||
#repo{ data: { root: @path.empty?.to_s,
|
||||
root_url: project_tree_path(project),
|
||||
url: content_url,
|
||||
current_branch: @ref,
|
||||
ref: @commit.id,
|
||||
project_name: project.name,
|
||||
project_url: project_path(project),
|
||||
project_id: project.id,
|
||||
new_merge_request_url: namespace_project_new_merge_request_path(project.namespace, project, merge_request: { source_branch: '' }),
|
||||
can_commit: (!!can_push_branch?(project, @ref)).to_s,
|
||||
on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s,
|
||||
current_path: @path } }
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Adds the multi file editor as a new beta feature
|
||||
merge_request: 15430
|
||||
author:
|
||||
type: feature
|
|
@ -43,6 +43,8 @@ Rails.application.routes.draw do
|
|||
get 'liveness' => 'health#liveness'
|
||||
get 'readiness' => 'health#readiness'
|
||||
post 'storage_check' => 'health#storage_check'
|
||||
get 'ide' => 'ide#index'
|
||||
get 'ide/*vueroute' => 'ide#index', format: false
|
||||
resources :metrics, only: [:index]
|
||||
mount Peek::Railtie => '/peek'
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@ var config = {
|
|||
protected_branches: './protected_branches',
|
||||
protected_tags: './protected_tags',
|
||||
registry_list: './registry/index.js',
|
||||
repo: './repo/index.js',
|
||||
ide: './ide/index.js',
|
||||
sidebar: './sidebar/sidebar_bundle.js',
|
||||
schedule_form: './pipeline_schedules/pipeline_schedule_form_bundle.js',
|
||||
schedules_index: './pipeline_schedules/pipeline_schedules_index_bundle.js',
|
||||
|
@ -204,7 +204,7 @@ var config = {
|
|||
'pipelines',
|
||||
'pipelines_details',
|
||||
'registry_list',
|
||||
'repo',
|
||||
'ide',
|
||||
'schedule_form',
|
||||
'schedules_index',
|
||||
'sidebar',
|
||||
|
|
|
@ -79,6 +79,7 @@
|
|||
"vue": "^2.5.8",
|
||||
"vue-loader": "^13.5.0",
|
||||
"vue-resource": "^1.3.4",
|
||||
"vue-router": "^3.0.1",
|
||||
"vue-template-compiler": "^2.5.8",
|
||||
"vuex": "^3.0.1",
|
||||
"webpack": "^3.5.5",
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
require 'rails_helper'
|
||||
|
||||
feature 'Ref switcher', :js do
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project, :public, :repository) }
|
||||
|
||||
before do
|
||||
project.team << [user, :master]
|
||||
set_cookie('new_repo', 'true')
|
||||
sign_in(user)
|
||||
visit project_tree_path(project, 'master')
|
||||
end
|
||||
|
||||
it 'allow user to change ref by enter key' do
|
||||
click_button 'master'
|
||||
wait_for_requests
|
||||
|
||||
page.within '.project-refs-form' do
|
||||
input = find('input[type="search"]')
|
||||
input.set 'binary'
|
||||
wait_for_requests
|
||||
|
||||
expect(find('.dropdown-content ul')).to have_selector('li', count: 7)
|
||||
|
||||
page.within '.dropdown-content ul' do
|
||||
input.native.send_keys :enter
|
||||
end
|
||||
end
|
||||
|
||||
expect(page).to have_title 'add-pdf-text-binary'
|
||||
end
|
||||
|
||||
it "user selects ref with special characters" do
|
||||
click_button 'master'
|
||||
wait_for_requests
|
||||
|
||||
page.within '.project-refs-form' do
|
||||
page.fill_in 'Search branches and tags', with: "'test'"
|
||||
click_link "'test'"
|
||||
end
|
||||
|
||||
expect(page).to have_title "'test'"
|
||||
end
|
||||
|
||||
context "create branch" do
|
||||
let(:input) { find('.js-new-branch-name') }
|
||||
|
||||
before do
|
||||
click_button 'master'
|
||||
wait_for_requests
|
||||
|
||||
page.within '.project-refs-form' do
|
||||
find(".dropdown-footer-list a").click
|
||||
end
|
||||
end
|
||||
|
||||
it "shows error message for the invalid branch name" do
|
||||
input.set 'foo bar'
|
||||
click_button('Create')
|
||||
wait_for_requests
|
||||
expect(page).to have_content 'Branch name is invalid'
|
||||
end
|
||||
|
||||
it "should create new branch properly" do
|
||||
input.set 'new-branch-name'
|
||||
click_button('Create')
|
||||
wait_for_requests
|
||||
expect(find('.js-project-refs-dropdown')).to have_content 'new-branch-name'
|
||||
end
|
||||
|
||||
it "should create new branch by Enter key" do
|
||||
input.set 'new-branch-name-2'
|
||||
input.native.send_keys :enter
|
||||
wait_for_requests
|
||||
expect(find('.js-project-refs-dropdown')).to have_content 'new-branch-name-2'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -13,6 +13,14 @@ feature 'Multi-file editor new directory', :js do
|
|||
visit project_tree_path(project, :master)
|
||||
|
||||
wait_for_requests
|
||||
|
||||
click_link('Multi Edit')
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
after do
|
||||
set_cookie('new_repo', 'false')
|
||||
end
|
||||
|
||||
it 'creates directory in current directory' do
|
||||
|
@ -21,17 +29,29 @@ feature 'Multi-file editor new directory', :js do
|
|||
click_link('New directory')
|
||||
|
||||
page.within('.modal') do
|
||||
find('.form-control').set('foldername')
|
||||
find('.form-control').set('folder name')
|
||||
|
||||
click_button('Create directory')
|
||||
end
|
||||
|
||||
find('.add-to-tree').click
|
||||
|
||||
click_link('New file')
|
||||
|
||||
page.within('.modal-dialog') do
|
||||
find('.form-control').set('file name')
|
||||
|
||||
click_button('Create file')
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
find('.multi-file-commit-panel-collapse-btn').click
|
||||
|
||||
fill_in('commit-message', with: 'commit message')
|
||||
fill_in('commit-message', with: 'commit message ide')
|
||||
|
||||
click_button('Commit')
|
||||
|
||||
expect(page).to have_selector('td', text: 'commit message')
|
||||
expect(page).to have_content('folder name')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,6 +13,14 @@ feature 'Multi-file editor new file', :js do
|
|||
visit project_tree_path(project, :master)
|
||||
|
||||
wait_for_requests
|
||||
|
||||
click_link('Multi Edit')
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
after do
|
||||
set_cookie('new_repo', 'false')
|
||||
end
|
||||
|
||||
it 'creates file in current directory' do
|
||||
|
@ -21,17 +29,19 @@ feature 'Multi-file editor new file', :js do
|
|||
click_link('New file')
|
||||
|
||||
page.within('.modal') do
|
||||
find('.form-control').set('filename')
|
||||
find('.form-control').set('file name')
|
||||
|
||||
click_button('Create file')
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
find('.multi-file-commit-panel-collapse-btn').click
|
||||
|
||||
fill_in('commit-message', with: 'commit message')
|
||||
fill_in('commit-message', with: 'commit message ide')
|
||||
|
||||
click_button('Commit')
|
||||
|
||||
expect(page).to have_selector('td', text: 'commit message')
|
||||
expect(page).to have_content('file name')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,6 +15,14 @@ feature 'Multi-file editor upload file', :js do
|
|||
visit project_tree_path(project, :master)
|
||||
|
||||
wait_for_requests
|
||||
|
||||
click_link('Multi Edit')
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
after do
|
||||
set_cookie('new_repo', 'false')
|
||||
end
|
||||
|
||||
it 'uploads text file' do
|
||||
|
@ -41,6 +49,5 @@ feature 'Multi-file editor upload file', :js do
|
|||
|
||||
expect(page).to have_selector('.multi-file-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
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Vue from 'vue';
|
||||
import store from '~/repo/stores';
|
||||
import listCollapsed from '~/repo/components/commit_sidebar/list_collapsed.vue';
|
||||
import store from '~/ide/stores';
|
||||
import listCollapsed from '~/ide/components/commit_sidebar/list_collapsed.vue';
|
||||
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
|
||||
import { file } from '../../helpers';
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Vue from 'vue';
|
||||
import listItem from '~/repo/components/commit_sidebar/list_item.vue';
|
||||
import listItem from '~/ide/components/commit_sidebar/list_item.vue';
|
||||
import mountComponent from '../../../helpers/vue_mount_component_helper';
|
||||
import { file } from '../../helpers';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Vue from 'vue';
|
||||
import store from '~/repo/stores';
|
||||
import commitSidebarList from '~/repo/components/commit_sidebar/list.vue';
|
||||
import store from '~/ide/stores';
|
||||
import commitSidebarList from '~/ide/components/commit_sidebar/list.vue';
|
||||
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
|
||||
import { file } from '../../helpers';
|
||||
|
||||
|
@ -13,8 +13,11 @@ describe('Multi-file editor commit sidebar list', () => {
|
|||
vm = createComponentWithStore(Component, store, {
|
||||
title: 'Staged',
|
||||
fileList: [],
|
||||
collapsed: false,
|
||||
}).$mount();
|
||||
});
|
||||
|
||||
vm.$store.state.rightPanelCollapsed = false;
|
||||
|
||||
vm.$mount();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -43,30 +46,14 @@ describe('Multi-file editor commit sidebar list', () => {
|
|||
|
||||
describe('collapsed', () => {
|
||||
beforeEach((done) => {
|
||||
vm.collapsed = true;
|
||||
vm.$store.state.rightPanelCollapsed = true;
|
||||
|
||||
Vue.nextTick(done);
|
||||
});
|
||||
|
||||
it('adds collapsed class', () => {
|
||||
expect(vm.$el.querySelector('.is-collapsed')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('hides list', () => {
|
||||
expect(vm.$el.querySelector('.list-unstyled')).toBeNull();
|
||||
expect(vm.$el.querySelector('.help-block')).toBeNull();
|
||||
});
|
||||
|
||||
it('hides collapse button', () => {
|
||||
expect(vm.$el.querySelector('.multi-file-commit-panel-collapse-btn')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('clicking toggle collapse button emits toggle event', () => {
|
||||
spyOn(vm, '$emit');
|
||||
|
||||
vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click();
|
||||
|
||||
expect(vm.$emit).toHaveBeenCalledWith('toggleCollapsed');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import Vue from 'vue';
|
||||
import store from '~/ide/stores';
|
||||
import ideContextBar from '~/ide/components/ide_context_bar.vue';
|
||||
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
|
||||
|
||||
describe('Multi-file editor right context bar', () => {
|
||||
let vm;
|
||||
|
||||
beforeEach(() => {
|
||||
const Component = Vue.extend(ideContextBar);
|
||||
|
||||
vm = createComponentWithStore(Component, store);
|
||||
|
||||
vm.$store.state.rightPanelCollapsed = false;
|
||||
|
||||
vm.$mount();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
describe('collapsed', () => {
|
||||
beforeEach((done) => {
|
||||
vm.$store.state.rightPanelCollapsed = true;
|
||||
|
||||
Vue.nextTick(done);
|
||||
});
|
||||
|
||||
it('adds collapsed class', () => {
|
||||
expect(vm.$el.querySelector('.is-collapsed')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('shows correct icon', () => {
|
||||
expect(vm.currentIcon).toBe('angle-double-left');
|
||||
});
|
||||
});
|
||||
|
||||
it('clicking toggle collapse button collapses the bar', () => {
|
||||
spyOn(vm, 'setPanelCollapsedStatus').and.returnValue(Promise.resolve());
|
||||
|
||||
vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click();
|
||||
|
||||
expect(vm.setPanelCollapsedStatus).toHaveBeenCalledWith({
|
||||
side: 'right',
|
||||
collapsed: true,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,20 +1,26 @@
|
|||
import Vue from 'vue';
|
||||
import store from '~/repo/stores';
|
||||
import repoSidebar from '~/repo/components/repo_sidebar.vue';
|
||||
import store from '~/ide/stores';
|
||||
import ideRepoTree from '~/ide/components/ide_repo_tree.vue';
|
||||
import { file, resetStore } from '../helpers';
|
||||
|
||||
describe('RepoSidebar', () => {
|
||||
describe('IdeRepoTree', () => {
|
||||
let vm;
|
||||
|
||||
beforeEach(() => {
|
||||
const RepoSidebar = Vue.extend(repoSidebar);
|
||||
const IdeRepoTree = Vue.extend(ideRepoTree);
|
||||
|
||||
vm = new RepoSidebar({
|
||||
vm = new IdeRepoTree({
|
||||
store,
|
||||
propsData: {
|
||||
treeId: 'abcproject/mybranch',
|
||||
},
|
||||
});
|
||||
|
||||
vm.$store.state.currentBranch = 'master';
|
||||
vm.$store.state.isRoot = true;
|
||||
vm.$store.state.tree.push(file());
|
||||
vm.$store.state.trees['abcproject/mybranch'] = {
|
||||
tree: [file()],
|
||||
};
|
||||
|
||||
vm.$mount();
|
||||
});
|
||||
|
@ -26,13 +32,9 @@ describe('RepoSidebar', () => {
|
|||
});
|
||||
|
||||
it('renders a sidebar', () => {
|
||||
const thead = vm.$el.querySelector('thead');
|
||||
const tbody = vm.$el.querySelector('tbody');
|
||||
|
||||
expect(vm.$el.classList.contains('sidebar-mini')).toBeFalsy();
|
||||
expect(thead.querySelector('.name').textContent.trim()).toEqual('Name');
|
||||
expect(thead.querySelector('.last-commit').textContent.trim()).toEqual('Last commit');
|
||||
expect(thead.querySelector('.last-update').textContent.trim()).toEqual('Last update');
|
||||
expect(tbody.querySelector('.repo-file-options')).toBeFalsy();
|
||||
expect(tbody.querySelector('.prev-directory')).toBeFalsy();
|
||||
expect(tbody.querySelector('.loading-file')).toBeFalsy();
|
||||
|
@ -40,7 +42,6 @@ describe('RepoSidebar', () => {
|
|||
});
|
||||
|
||||
it('renders 5 loading files if tree is loading', (done) => {
|
||||
vm.$store.state.tree = [];
|
||||
vm.$store.state.loading = true;
|
||||
|
||||
Vue.nextTick(() => {
|
|
@ -0,0 +1,43 @@
|
|||
import Vue from 'vue';
|
||||
import store from '~/ide/stores';
|
||||
import ideSidebar from '~/ide/components/ide_side_bar.vue';
|
||||
import { resetStore } from '../helpers';
|
||||
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
|
||||
|
||||
describe('IdeSidebar', () => {
|
||||
let vm;
|
||||
|
||||
beforeEach(() => {
|
||||
const Component = Vue.extend(ideSidebar);
|
||||
|
||||
vm = createComponentWithStore(Component, store).$mount();
|
||||
|
||||
vm.$store.state.leftPanelCollapsed = false;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
|
||||
resetStore(vm.$store);
|
||||
});
|
||||
|
||||
it('renders a sidebar', () => {
|
||||
expect(vm.$el.querySelector('.multi-file-commit-panel-inner')).not.toBeNull();
|
||||
});
|
||||
|
||||
describe('collapsed', () => {
|
||||
beforeEach((done) => {
|
||||
vm.$store.state.leftPanelCollapsed = true;
|
||||
|
||||
Vue.nextTick(done);
|
||||
});
|
||||
|
||||
it('adds collapsed class', () => {
|
||||
expect(vm.$el.classList).toContain('is-collapsed');
|
||||
});
|
||||
|
||||
it('shows correct icon', () => {
|
||||
expect(vm.currentIcon).toBe('angle-double-right');
|
||||
});
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue