Commit 45031729 authored by Phil Hughes's avatar Phil Hughes

Merge branch '40040-decouple-multi-file-editor-from-file-list' into 'master'

Resolve "Decouple multi-file editor from file list"

Closes #40040 and #40032

See merge request gitlab-org/gitlab-ce!15430
parents 889c7081 213e91d4
import $ from 'jquery'; import $ from 'jquery';
import axios from './lib/utils/axios_utils';
const Api = { const Api = {
groupsPath: '/api/:version/groups.json', groupsPath: '/api/:version/groups.json',
...@@ -6,6 +7,7 @@ const Api = { ...@@ -6,6 +7,7 @@ const Api = {
namespacesPath: '/api/:version/namespaces.json', namespacesPath: '/api/:version/namespaces.json',
groupProjectsPath: '/api/:version/groups/:id/projects.json', groupProjectsPath: '/api/:version/groups/:id/projects.json',
projectsPath: '/api/:version/projects.json', projectsPath: '/api/:version/projects.json',
projectPath: '/api/:version/projects/:id',
projectLabelsPath: '/:namespace_path/:project_path/labels', projectLabelsPath: '/:namespace_path/:project_path/labels',
groupLabelsPath: '/groups/:namespace_path/labels', groupLabelsPath: '/groups/:namespace_path/labels',
licensePath: '/api/:version/templates/licenses/:key', licensePath: '/api/:version/templates/licenses/:key',
...@@ -76,6 +78,14 @@ const Api = { ...@@ -76,6 +78,14 @@ const Api = {
.done(projects => callback(projects)); .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) { newLabel(namespacePath, projectPath, data, callback) {
let url; let url;
...@@ -115,7 +125,7 @@ const Api = { ...@@ -115,7 +125,7 @@ const Api = {
commitMultiple(id, data) { commitMultiple(id, data) {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const url = Api.buildUrl(Api.commitPath) const url = Api.buildUrl(Api.commitPath)
.replace(':id', id); .replace(':id', encodeURIComponent(id));
return this.wrapAjaxCall({ return this.wrapAjaxCall({
url, url,
type: 'POST', type: 'POST',
...@@ -127,7 +137,7 @@ const Api = { ...@@ -127,7 +137,7 @@ const Api = {
branchSingle(id, branch) { branchSingle(id, branch) {
const url = Api.buildUrl(Api.branchSinglePath) const url = Api.buildUrl(Api.branchSinglePath)
.replace(':id', id) .replace(':id', encodeURIComponent(id))
.replace(':branch', branch); .replace(':branch', branch);
return this.wrapAjaxCall({ return this.wrapAjaxCall({
......
...@@ -73,7 +73,6 @@ import initLegacyFilters from './init_legacy_filters'; ...@@ -73,7 +73,6 @@ import initLegacyFilters from './init_legacy_filters';
import initIssuableSidebar from './init_issuable_sidebar'; import initIssuableSidebar from './init_issuable_sidebar';
import initProjectVisibilitySelector from './project_visibility'; import initProjectVisibilitySelector from './project_visibility';
import GpgBadges from './gpg_badges'; import GpgBadges from './gpg_badges';
import UserFeatureHelper from './helpers/user_feature_helper';
import initChangesDropdown from './init_changes_dropdown'; import initChangesDropdown from './init_changes_dropdown';
import NewGroupChild from './groups/new_group_child'; import NewGroupChild from './groups/new_group_child';
import AbuseReports from './abuse_reports'; import AbuseReports from './abuse_reports';
...@@ -447,9 +446,6 @@ import Activities from './activities'; ...@@ -447,9 +446,6 @@ import Activities from './activities';
break; break;
case 'projects:tree:show': case 'projects:tree:show':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
if (UserFeatureHelper.isNewRepoEnabled()) break;
new TreeView(); new TreeView();
new BlobViewer(); new BlobViewer();
new NewCommitForm($('.js-create-dir-form')); new NewCommitForm($('.js-create-dir-form'));
...@@ -468,7 +464,6 @@ import Activities from './activities'; ...@@ -468,7 +464,6 @@ import Activities from './activities';
shortcut_handler = true; shortcut_handler = true;
break; break;
case 'projects:blob:show': case 'projects:blob:show':
if (UserFeatureHelper.isNewRepoEnabled()) break;
new BlobViewer(); new BlobViewer();
initBlob(); initBlob();
break; break;
......
...@@ -161,13 +161,16 @@ export default () => { ...@@ -161,13 +161,16 @@ export default () => {
const items = [...sidebar.querySelectorAll('.sidebar-top-level-items > li')]; const items = [...sidebar.querySelectorAll('.sidebar-top-level-items > li')];
sidebar.querySelector('.sidebar-top-level-items').addEventListener('mouseleave', () => { const topItems = sidebar.querySelector('.sidebar-top-level-items');
clearTimeout(timeoutId); if (topItems) {
sidebar.querySelector('.sidebar-top-level-items').addEventListener('mouseleave', () => {
timeoutId = setTimeout(() => { clearTimeout(timeoutId);
if (currentOpenMenu) hideMenu(currentOpenMenu);
}, getHideSubItemsInterval()); timeoutId = setTimeout(() => {
}); if (currentOpenMenu) hideMenu(currentOpenMenu);
}, getHideSubItemsInterval());
});
}
headerHeight = document.querySelector('.nav-sidebar').offsetTop; headerHeight = document.querySelector('.nav-sidebar').offsetTop;
......
import Cookies from 'js-cookie';
export default {
isNewRepoEnabled() {
return Cookies.get('new_repo') === 'true';
},
};
<script> <script>
import { mapState } from 'vuex';
import icon from '../../../vue_shared/components/icon.vue'; import icon from '../../../vue_shared/components/icon.vue';
import listItem from './list_item.vue'; import listItem from './list_item.vue';
import listCollapsed from './list_collapsed.vue'; import listCollapsed from './list_collapsed.vue';
...@@ -18,72 +19,48 @@ ...@@ -18,72 +19,48 @@
type: Array, type: Array,
required: true, required: true,
}, },
collapsed: { },
type: Boolean, computed: {
required: true, ...mapState([
}, 'currentProjectId',
'currentBranchId',
'rightPanelCollapsed',
]),
}, },
methods: { methods: {
toggleCollapsed() { toggleCollapsed() {
this.$emit('toggleCollapsed'); this.$emit('toggleCollapsed');
}, },
}, },
}; };
</script> </script>
<template> <template>
<div class="multi-file-commit-panel-section"> <div class="multi-file-commit-list">
<header <list-collapsed
class="multi-file-commit-panel-header" v-if="rightPanelCollapsed"
:class="{ />
'is-collapsed': collapsed, <template v-else>
}" <ul
> v-if="fileList.length"
<icon class="list-unstyled append-bottom-0"
name="list-bulleted" >
:size="18" <li
css-classes="append-right-default" v-for="file in fileList"
/> :key="file.key"
<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 <list-item
</div> :file="file"
</template> />
</div> </li>
</ul>
<div
v-else
class="help-block prepend-top-0"
>
No changes
</div>
</template>
</div> </div>
</template> </template>
<script> <script>
import { mapState, mapGetters } from 'vuex'; import { mapState, mapGetters } from 'vuex';
import RepoSidebar from './repo_sidebar.vue'; import ideSidebar from './ide_side_bar.vue';
import RepoCommitSection from './repo_commit_section.vue'; import ideContextbar from './ide_context_bar.vue';
import RepoTabs from './repo_tabs.vue'; import repoTabs from './repo_tabs.vue';
import RepoFileButtons from './repo_file_buttons.vue'; import repoFileButtons from './repo_file_buttons.vue';
import RepoPreview from './repo_preview.vue'; import ideStatusBar from './ide_status_bar.vue';
import repoPreview from './repo_preview.vue';
import repoEditor from './repo_editor.vue'; import repoEditor from './repo_editor.vue';
export default { export default {
computed: { computed: {
...mapState([ ...mapState([
'currentBlobView', 'currentBlobView',
'selectedFile',
]), ]),
...mapGetters([ ...mapGetters([
'isCollapsed',
'changedFiles', 'changedFiles',
'activeFile',
]), ]),
}, },
components: { components: {
RepoSidebar, ideSidebar,
RepoTabs, ideContextbar,
RepoFileButtons, repoTabs,
repoFileButtons,
ideStatusBar,
repoEditor, repoEditor,
RepoCommitSection, repoPreview,
RepoPreview,
}, },
mounted() { mounted() {
const returnValue = 'Are you sure you want to lose unsaved changes?'; const returnValue = 'Are you sure you want to lose unsaved changes?';
...@@ -40,24 +43,31 @@ export default { ...@@ -40,24 +43,31 @@ export default {
</script> </script>
<template> <template>
<div <div
class="multi-file" class="ide-view"
:class="{
'is-collapsed': isCollapsed
}"
> >
<repo-sidebar/> <ide-sidebar/>
<div <div
v-if="isCollapsed"
class="multi-file-edit-pane" class="multi-file-edit-pane"
> >
<repo-tabs /> <template
<component v-if="activeFile">
class="multi-file-edit-pane-content" <repo-tabs/>
:is="currentBlobView" <component
/> class="multi-file-edit-pane-content"
<repo-file-buttons /> :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> </div>
<repo-commit-section /> <ide-contextbar/>
</div> </div>
</template> </template>
<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>
<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>
<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>
<script> <script>
import { mapState, mapGetters, mapActions } from 'vuex'; import { mapState } from 'vuex';
import RepoPreviousDirectory from './repo_prev_directory.vue'; import RepoPreviousDirectory from './repo_prev_directory.vue';
import RepoFile from './repo_file.vue'; import RepoFile from './repo_file.vue';
import RepoLoadingFile from './repo_loading_file.vue'; import RepoLoadingFile from './repo_loading_file.vue';
import { treeList } from '../stores/utils';
export default { export default {
components: { components: {
...@@ -10,14 +11,11 @@ export default { ...@@ -10,14 +11,11 @@ export default {
'repo-file': RepoFile, 'repo-file': RepoFile,
'repo-loading-file': RepoLoadingFile, 'repo-loading-file': RepoLoadingFile,
}, },
created() { props: {
window.addEventListener('popstate', this.popHistoryState); treeId: {
}, type: String,
destroyed() { required: true,
window.removeEventListener('popstate', this.popHistoryState); },
},
mounted() {
this.getTreeData();
}, },
computed: { computed: {
...mapState([ ...mapState([
...@@ -29,57 +27,40 @@ export default { ...@@ -29,57 +27,40 @@ export default {
return state.project.name; return state.project.name;
}, },
}), }),
...mapGetters([ fetchedList() {
'treeList', return treeList(this.$store.state, this.treeId);
'isCollapsed', },
]), hasPreviousDirectory() {
}, return !this.isRoot && this.fetchedList.length;
methods: { },
...mapActions([ showLoading() {
'getTreeData', return this.loading;
'popHistoryState', },
]),
}, },
}; };
</script> </script>
<template> <template>
<div class="ide-file-list"> <div>
<table class="table"> <div class="ide-file-list">
<thead> <table class="table">
<tr> <tbody
<th v-if="treeId">
v-if="isCollapsed" <repo-previous-directory
> v-if="hasPreviousDirectory"
</th> />
<template v-else> <repo-loading-file
<th class="name multi-file-table-name"> v-if="showLoading"
Name v-for="n in 5"
</th> :key="n"
<th class="hidden-sm hidden-xs last-commit"> />
Last commit <repo-file
</th> v-for="file in fetchedList"
<th class="hidden-xs last-update text-right"> :key="file.key"
Last update :file="file"
</th> />
</template> </tbody>
</tr> </table>
</thead> </div>
<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> </div>
</template> </template>
<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>
<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 @@ ...@@ -44,7 +44,7 @@
this.branchName = ''; this.branchName = '';
if (this.dropdownText) { if (this.dropdownText) {
this.dropdownText.textContent = this.currentBranch; this.dropdownText.textContent = this.currentBranchId;
} }
this.toggleDropdown(); this.toggleDropdown();
......
<script> <script>
import { mapState } from 'vuex';
import newModal from './modal.vue'; import newModal from './modal.vue';
import upload from './upload.vue'; import upload from './upload.vue';
import icon from '../../../vue_shared/components/icon.vue'; import icon from '../../../vue_shared/components/icon.vue';
export default { export default {
props: {
branch: {
type: String,
required: true,
},
path: {
type: String,
required: true,
},
parent: {
type: Object,
default: null,
},
},
components: { components: {
icon, icon,
newModal, newModal,
...@@ -16,11 +29,6 @@ ...@@ -16,11 +29,6 @@
modalType: '', modalType: '',
}; };
}, },
computed: {
...mapState([
'path',
]),
},
methods: { methods: {
createNewItem(type) { createNewItem(type) {
this.modalType = type; this.modalType = type;
...@@ -34,55 +42,59 @@ ...@@ -34,55 +42,59 @@
</script> </script>
<template> <template>
<div> <div class="repo-new-btn pull-right">
<ul class="breadcrumb repo-breadcrumb"> <div class="dropdown">
<li class="dropdown"> <button
<button type="button"
type="button" class="btn btn-sm btn-default dropdown-toggle add-to-tree"
class="btn btn-default dropdown-toggle add-to-tree" data-toggle="dropdown"
data-toggle="dropdown" aria-label="Create new file or directory"
aria-label="Create new file or directory" >
> <icon
<icon name="plus"
name="plus" :size="12"
css-classes="pull-left" css-classes="pull-left"
/> />
<icon <icon
name="arrow-down" name="arrow-down"
css-classes="pull-left" :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"
/> />
</button> </li>
<ul class="dropdown-menu"> <li>
<li> <a
<a href="#"
href="#" role="button"
role="button" @click.prevent="createNewItem('tree')"
@click.prevent="createNewItem('blob')" >
> {{ __('New directory') }}
{{ __('New file') }} </a>
</a> </li>
</li> </ul>
<li> </div>
<upload
:path="path"
/>
</li>
<li>
<a
href="#"
role="button"
@click.prevent="createNewItem('tree')"
>
{{ __('New directory') }}
</a>
</li>
</ul>
</li>
</ul>
<new-modal <new-modal
v-if="openModal" v-if="openModal"
:type="modalType" :type="modalType"
:branch-id="branch"
:path="path" :path="path"
:parent="parent"
@toggle="toggleModalOpen" @toggle="toggleModalOpen"
/> />
</div> </div>
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions, mapState } from 'vuex';
import { __ } from '../../../locale'; import { __ } from '../../../locale';
import modal from '../../../vue_shared/components/modal.vue'; import modal from '../../../vue_shared/components/modal.vue';
export default { export default {
props: { props: {
branchId: {
type: String,
required: true,
},
parent: {
type: Object,
default: null,
},
type: { type: {
type: String, type: String,
required: true, required: true,
...@@ -28,6 +36,9 @@ ...@@ -28,6 +36,9 @@
]), ]),
createEntryInStore() { createEntryInStore() {
this.createTempEntry({ this.createTempEntry({
projectId: this.currentProjectId,
branchId: this.branchId,
parent: this.parent,
name: this.entryName.replace(new RegExp(`^${this.path}/`), ''), name: this.entryName.replace(new RegExp(`^${this.path}/`), ''),
type: this.type, type: this.type,
}); });
...@@ -39,6 +50,9 @@ ...@@ -39,6 +50,9 @@
}, },
}, },
computed: { computed: {
...mapState([
'currentProjectId',
]),
modalTitle() { modalTitle() {
if (this.type === 'tree') { if (this.type === 'tree') {
return __('Create new directory'); return __('Create new directory');
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions, mapState } from 'vuex';
export default { export default {
props: { props: {
path: { branchId: {
type: String, type: String,
required: true, required: true,
}, },
parent: {
type: Object,
default: null,
},
},
computed: {
...mapState([
'trees',
'currentProjectId',
]),
}, },
methods: { methods: {
...mapActions([ ...mapActions([
...@@ -22,6 +32,9 @@ ...@@ -22,6 +32,9 @@
this.createTempEntry({ this.createTempEntry({
name, name,
projectId: this.currentProjectId,
branchId: this.branchId,
parent: this.parent,
type: 'blob', type: 'blob',
content: result, content: result,
base64: !isText, base64: !isText,
...@@ -42,6 +55,9 @@ ...@@ -42,6 +55,9 @@
openFile() { openFile() {
Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file)); Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file));
}, },
startFileUpload() {
this.$refs.fileUpload.click();
},
}, },
mounted() { mounted() {
this.$refs.fileUpload.addEventListener('change', this.openFile); this.$refs.fileUpload.addEventListener('change', this.openFile);
...@@ -53,16 +69,19 @@ ...@@ -53,16 +69,19 @@
</script> </script>
<template> <template>
<label <div>
role="button" <a
class="menu-item" href="#"
> role="button"
{{ __('Upload file') }} @click.prevent="startFileUpload"
>
{{ __('Upload file') }}
</a>
<input <input
id="file-upload" id="file-upload"
type="file" type="file"
class="hidden" class="hidden"
ref="fileUpload" ref="fileUpload"
/> />
</label> </div>
</template> </template>
...@@ -20,12 +20,13 @@ export default { ...@@ -20,12 +20,13 @@ export default {
submitCommitsLoading: false, submitCommitsLoading: false,
startNewMR: false, startNewMR: false,
commitMessage: '', commitMessage: '',
collapsed: true,
}; };
}, },
computed: { computed: {
...mapState([ ...mapState([
'currentBranch', 'currentProjectId',
'currentBranchId',
'rightPanelCollapsed',
]), ]),
...mapGetters([ ...mapGetters([
'changedFiles', 'changedFiles',
...@@ -42,12 +43,13 @@ export default { ...@@ -42,12 +43,13 @@ export default {
'checkCommitStatus', 'checkCommitStatus',
'commitChanges', 'commitChanges',
'getTreeData', 'getTreeData',
'setPanelCollapsedStatus',
]), ]),
makeCommit(newBranch = false) { makeCommit(newBranch = false) {
const createNewBranch = newBranch || this.startNewMR; const createNewBranch = newBranch || this.startNewMR;
const payload = { 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, commit_message: this.commitMessage,
actions: this.changedFiles.map(f => ({ actions: this.changedFiles.map(f => ({
action: f.tempFile ? 'create' : 'update', action: f.tempFile ? 'create' : 'update',
...@@ -55,7 +57,7 @@ export default { ...@@ -55,7 +57,7 @@ export default {
content: f.content, content: f.content,
encoding: f.base64 ? 'base64' : 'text', encoding: f.base64 ? 'base64' : 'text',
})), })),
start_branch: createNewBranch ? this.currentBranch : undefined, start_branch: createNewBranch ? this.currentBranchId : undefined,
}; };
this.showNewBranchModal = false; this.showNewBranchModal = false;
...@@ -64,7 +66,12 @@ export default { ...@@ -64,7 +66,12 @@ export default {
this.commitChanges({ payload, newMr: this.startNewMR }) this.commitChanges({ payload, newMr: this.startNewMR })
.then(() => { .then(() => {
this.submitCommitsLoading = false; this.submitCommitsLoading = false;
this.getTreeData(); this.$store.dispatch('getTreeData', {
projectId: this.currentProjectId,
branch: this.currentBranchId,
endpoint: `/tree/${this.currentBranchId}`,
force: true,
});
}) })
.catch(() => { .catch(() => {
this.submitCommitsLoading = false; this.submitCommitsLoading = false;
...@@ -86,19 +93,17 @@ export default { ...@@ -86,19 +93,17 @@ export default {
}); });
}, },
toggleCollapsed() { toggleCollapsed() {
this.collapsed = !this.collapsed; this.setPanelCollapsedStatus({
side: 'right',
collapsed: !this.rightPanelCollapsed,
});
}, },
}, },
}; };
</script> </script>
<template> <template>
<div <div class="multi-file-commit-panel-section">
class="multi-file-commit-panel"
:class="{
'is-collapsed': collapsed,
}"
>
<modal <modal
v-if="showNewBranchModal" v-if="showNewBranchModal"
:primary-button-label="__('Create new branch')" :primary-button-label="__('Create new branch')"
...@@ -108,28 +113,16 @@ export default { ...@@ -108,28 +113,16 @@ export default {
@toggle="showNewBranchModal = false" @toggle="showNewBranchModal = false"
@submit="makeCommit(true)" @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 <commit-files-list
title="Staged" title="Staged"
:file-list="changedFiles" :file-list="changedFiles"
:collapsed="collapsed" :collapsed="rightPanelCollapsed"
@toggleCollapsed="toggleCollapsed" @toggleCollapsed="toggleCollapsed"
/> />
<form <form
class="form-horizontal multi-file-commit-form" class="form-horizontal multi-file-commit-form"
@submit.prevent="tryCommit" @submit.prevent="tryCommit"
v-if="!collapsed" v-if="!rightPanelCollapsed"
> >
<div class="multi-file-commit-fieldset"> <div class="multi-file-commit-fieldset">
<textarea <textarea
......
<script> <script>
/* global monaco */ /* global monaco */
import { mapGetters, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '../../flash'; import flash from '../../flash';
import monacoLoader from '../monaco_loader'; import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor'; import Editor from '../lib/editor';
...@@ -24,6 +24,9 @@ export default { ...@@ -24,6 +24,9 @@ export default {
...mapActions([ ...mapActions([
'getRawFileData', 'getRawFileData',
'changeFileContent', 'changeFileContent',
'setFileLanguage',
'setEditorPosition',
'setFileEOL',
]), ]),
initMonaco() { initMonaco() {
if (this.shouldHideEditor) return; if (this.shouldHideEditor) return;
...@@ -43,12 +46,36 @@ export default { ...@@ -43,12 +46,36 @@ export default {
const model = this.editor.createModel(this.activeFile); const model = this.editor.createModel(this.activeFile);
this.editor.attachModel(model); this.editor.attachModel(model);
model.onChange((m) => { model.onChange((m) => {
this.changeFileContent({ this.changeFileContent({
file: this.activeFile, file: this.activeFile,
content: m.getValue(), 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: { watch: {
...@@ -57,12 +84,22 @@ export default { ...@@ -57,12 +84,22 @@ export default {
this.initMonaco(); this.initMonaco();
} }
}, },
leftPanelCollapsed() {
this.editor.updateDimensions();
},
rightPanelCollapsed() {
this.editor.updateDimensions();
},
}, },
computed: { computed: {
...mapGetters([ ...mapGetters([
'activeFile', 'activeFile',
'activeFileExtension', 'activeFileExtension',
]), ]),
...mapState([
'leftPanelCollapsed',
'rightPanelCollapsed',
]),
shouldHideEditor() { shouldHideEditor() {
return this.activeFile.binary && !this.activeFile.raw; return this.activeFile.binary && !this.activeFile.raw;
}, },
...@@ -76,13 +113,14 @@ export default { ...@@ -76,13 +113,14 @@ export default {
class="blob-viewer-container blob-editor-container" class="blob-viewer-container blob-editor-container"
> >
<div <div
v-show="shouldHideEditor" v-if="shouldHideEditor"
v-html="activeFile.html" v-html="activeFile.html"
> >
</div> </div>
<div <div
v-show="!shouldHideEditor" v-show="!shouldHideEditor"
ref="editor" ref="editor"
class="multi-file-editor-holder"
> >
</div> </div>
</div> </div>
......
<script> <script>
import { mapActions, mapGetters } from 'vuex'; import { mapState } from 'vuex';
import timeAgoMixin from '../../vue_shared/mixins/timeago'; import timeAgoMixin from '../../vue_shared/mixins/timeago';
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue'; import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
import newDropdown from './new_dropdown/index.vue';
export default { export default {
mixins: [ mixins: [
...@@ -9,20 +10,22 @@ ...@@ -9,20 +10,22 @@
], ],
components: { components: {
skeletonLoadingContainer, skeletonLoadingContainer,
newDropdown,
}, },
props: { props: {
file: { file: {
type: Object, type: Object,
required: true, required: true,
}, },
showExtraColumns: {
type: Boolean,
default: false,
},
}, },
computed: { computed: {
...mapGetters([ ...mapState([
'isCollapsed', 'leftPanelCollapsed',
]), ]),
isSubmodule() {
return this.file.type === 'submodule';
},
fileIcon() { fileIcon() {
return { return {
'fa-spinner fa-spin': this.file.loading, 'fa-spinner fa-spin': this.file.loading,
...@@ -30,6 +33,12 @@ ...@@ -30,6 +33,12 @@
'fa-folder-open': !this.file.loading && this.file.opened, 'fa-folder-open': !this.file.loading && this.file.opened,
}; };
}, },
isSubmodule() {
return this.file.type === 'submodule';
},
isTree() {
return this.file.type === 'tree';
},
levelIndentation() { levelIndentation() {
return { return {
marginLeft: `${this.file.level * 16}px`, marginLeft: `${this.file.level * 16}px`,
...@@ -39,13 +48,39 @@ ...@@ -39,13 +48,39 @@
return this.file.id.substr(0, 8); return this.file.id.substr(0, 8);
}, },
submoduleColSpan() { 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: { methods: {
...mapActions([ clickFile(row) {
'clickedTreeRow', // 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> </script>
...@@ -53,7 +88,8 @@ ...@@ -53,7 +88,8 @@
<template> <template>
<tr <tr
class="file" class="file"
@click.prevent="clickedTreeRow(file)"> :class="fileClass"
@click="clickFile(file)">
<td <td
class="multi-file-table-name" class="multi-file-table-name"
:colspan="submoduleColSpan" :colspan="submoduleColSpan"
...@@ -66,11 +102,23 @@ ...@@ -66,11 +102,23 @@
> >
</i> </i>
<a <a
:href="file.url"
class="repo-file-name" class="repo-file-name"
> >
{{ file.name }} {{ file.name }}
</a> </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"> <template v-if="isSubmodule && file.id">
@ @
<span class="commit-sha"> <span class="commit-sha">
...@@ -84,7 +132,7 @@ ...@@ -84,7 +132,7 @@
</template> </template>
</td> </td>
<template v-if="!isCollapsed && !isSubmodule"> <template v-if="showExtraColumns && !isSubmodule">
<td class="multi-file-table-col-commit-message hidden-sm hidden-xs"> <td class="multi-file-table-col-commit-message hidden-sm hidden-xs">
<a <a
v-if="file.lastCommit.message" v-if="file.lastCommit.message"
......
<script> <script>
import { mapGetters } from 'vuex'; import { mapState } from 'vuex';
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue'; import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
export default { export default {
...@@ -7,8 +7,8 @@ ...@@ -7,8 +7,8 @@
skeletonLoadingContainer, skeletonLoadingContainer,
}, },
computed: { computed: {
...mapGetters([ ...mapState([
'isCollapsed', 'leftPanelCollapsed',
]), ]),
}, },
}; };
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
:small="true" :small="true"
/> />
</td> </td>
<template v-if="!isCollapsed"> <template v-if="!leftPanelCollapsed">
<td <td
class="hidden-sm hidden-xs"> class="hidden-sm hidden-xs">
<skeleton-loading-container <skeleton-loading-container
......
<script> <script>
import { mapGetters, mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
export default { export default {
computed: { computed: {
...mapState([ ...mapState([
'parentTreeUrl', 'parentTreeUrl',
]), 'leftPanelCollapsed',
...mapGetters([
'isCollapsed',
]), ]),
colSpanCondition() { colSpanCondition() {
return this.isCollapsed ? undefined : 3; return this.leftPanelCollapsed ? undefined : 3;
}, },
}, },
methods: { methods: {
......
...@@ -27,16 +27,18 @@ export default { ...@@ -27,16 +27,18 @@ export default {
methods: { methods: {
...mapActions([ ...mapActions([
'setFileActive',
'closeFile', 'closeFile',
]), ]),
clickFile(tab) {
this.$router.push(`/project${tab.url}`);
},
}, },
}; };
</script> </script>
<template> <template>
<li <li
@click="setFileActive(tab)" @click="clickFile(tab)"
> >
<button <button
type="button" type="button"
......
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;
import Vue from 'vue'; import Vue from 'vue';
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import { convertPermissionToBoolean } from '../lib/utils/common_utils'; import { convertPermissionToBoolean } from '../lib/utils/common_utils';
import Repo from './components/repo.vue'; import ide from './components/ide.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 store from './stores';
import router from './ide_router';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
import ContextualSidebar from '../contextual_sidebar';
function initRepo(el) { function initIde(el) {
if (!el) return null; if (!el) return null;
return new Vue({ return new Vue({
el, el,
store, store,
router,
components: { components: {
repo: Repo, ide,
}, },
methods: { methods: {
...mapActions([ ...mapActions([
...@@ -26,11 +27,6 @@ function initRepo(el) { ...@@ -26,11 +27,6 @@ function initRepo(el) {
const data = el.dataset; const data = el.dataset;
this.setInitialData({ this.setInitialData({
project: {
id: data.projectId,
name: data.projectName,
url: data.projectUrl,
},
endpoints: { endpoints: {
rootEndpoint: data.url, rootEndpoint: data.url,
newMergeRequestUrl: data.newMergeRequestUrl, newMergeRequestUrl: data.newMergeRequestUrl,
...@@ -38,69 +34,22 @@ function initRepo(el) { ...@@ -38,69 +34,22 @@ function initRepo(el) {
}, },
canCommit: convertPermissionToBoolean(data.canCommit), canCommit: convertPermissionToBoolean(data.canCommit),
onTopOfBranch: convertPermissionToBoolean(data.onTopOfBranch), onTopOfBranch: convertPermissionToBoolean(data.onTopOfBranch),
currentRef: data.ref,
path: data.currentPath, path: data.currentPath,
currentBranch: data.currentBranch,
isRoot: convertPermissionToBoolean(data.root), isRoot: convertPermissionToBoolean(data.root),
isInitialRoot: convertPermissionToBoolean(data.root), isInitialRoot: convertPermissionToBoolean(data.root),
}); });
}, },
render(createElement) { render(createElement) {
return createElement('repo'); return createElement('ide');
},
});
}
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 ideElement = document.getElementById('ide');
const editButton = document.querySelector('.editable-mode');
const newDropdownHolder = document.querySelector('.js-new-dropdown');
Vue.use(Translate); Vue.use(Translate);
initRepo(repo); initIde(ideElement);
initRepoEditButton(editButton);
initNewBranchForm(); const contextualSidebar = new ContextualSidebar();
initNewDropdown(newDropdownHolder); contextualSidebar.bindEvents();
...@@ -28,6 +28,14 @@ export default class Model { ...@@ -28,6 +28,14 @@ export default class Model {
return this.model.uri.toString(); return this.model.uri.toString();
} }
get language() {
return this.model.getModeId();
}
get eol() {
return this.model.getEOL() === '\n' ? 'LF' : 'CRLF';
}
get path() { get path() {
return this.file.path; return this.file.path;
} }
......
...@@ -22,6 +22,11 @@ export default class Editor { ...@@ -22,6 +22,11 @@ export default class Editor {
this.modelManager = new ModelManager(this.monaco), this.modelManager = new ModelManager(this.monaco),
this.decorationsController = new DecorationsController(this), this.decorationsController = new DecorationsController(this),
); );
this.debouncedUpdate = _.debounce(() => {
this.updateDimensions();
}, 200);
window.addEventListener('resize', this.debouncedUpdate, false);
} }
createInstance(domElement) { createInstance(domElement) {
...@@ -32,6 +37,9 @@ export default class Editor { ...@@ -32,6 +37,9 @@ export default class Editor {
readOnly: false, readOnly: false,
contextmenu: true, contextmenu: true,
scrollBeyondLastLine: false, scrollBeyondLastLine: false,
minimap: {
enabled: false,
},
}), }),
this.dirtyDiffController = new DirtyDiffController( this.dirtyDiffController = new DirtyDiffController(
this.modelManager, this.decorationsController, this.modelManager, this.decorationsController,
...@@ -70,10 +78,32 @@ export default class Editor { ...@@ -70,10 +78,32 @@ export default class Editor {
dispose() { dispose() {
this.disposable.dispose(); this.disposable.dispose();
window.removeEventListener('resize', this.debouncedUpdate);
// dispose main monaco instance // dispose main monaco instance
if (this.instance) { if (this.instance) {
this.instance = null; 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 { ...@@ -23,8 +23,11 @@ export default {
return Vue.http.get(file.rawPath, { params: { format: 'json' } }) return Vue.http.get(file.rawPath, { params: { format: 'json' } })
.then(res => res.text()); .then(res => res.text());
}, },
getBranchData(projectId, currentBranch) { getProjectData(namespace, project) {
return Api.branchSingle(projectId, currentBranch); return Api.project(`${namespace}/${project}`);
},
getBranchData(projectId, currentBranchId) {
return Api.branchSingle(projectId, currentBranchId);
}, },
createBranch(projectId, payload) { createBranch(projectId, payload) {
const url = Api.buildUrl(Api.createBranchPath).replace(':id', projectId); const url = Api.buildUrl(Api.createBranchPath).replace(':id', projectId);
......
...@@ -6,9 +6,11 @@ import * as types from './mutation_types'; ...@@ -6,9 +6,11 @@ import * as types from './mutation_types';
export const redirectToUrl = (_, url) => visitUrl(url); export const redirectToUrl = (_, url) => visitUrl(url);
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data); export const setInitialData = ({ commit }, data) =>
commit(types.SET_INITIAL_DATA, data);
export const closeDiscardPopup = ({ commit }) => commit(types.TOGGLE_DISCARD_POPUP, false); export const closeDiscardPopup = ({ commit }) =>
commit(types.TOGGLE_DISCARD_POPUP, false);
export const discardAllChanges = ({ commit, getters, dispatch }) => { export const discardAllChanges = ({ commit, getters, dispatch }) => {
const changedFiles = getters.changedFiles; const changedFiles = getters.changedFiles;
...@@ -26,7 +28,10 @@ export const closeAllFiles = ({ state, dispatch }) => { ...@@ -26,7 +28,10 @@ export const closeAllFiles = ({ state, dispatch }) => {
state.openFiles.forEach(file => dispatch('closeFile', { file })); state.openFiles.forEach(file => dispatch('closeFile', { file }));
}; };
export const toggleEditMode = ({ state, commit, getters, dispatch }, force = false) => { export const toggleEditMode = (
{ state, commit, getters, dispatch },
force = false,
) => {
const changedFiles = getters.changedFiles; const changedFiles = getters.changedFiles;
if (changedFiles.length && !force) { if (changedFiles.length && !force) {
...@@ -50,67 +55,105 @@ export const toggleBlobView = ({ commit, state }) => { ...@@ -50,67 +55,105 @@ export const toggleBlobView = ({ commit, state }) => {
} }
}; };
export const checkCommitStatus = ({ state }) => service.getBranchData( export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
state.project.id, if (side === 'left') {
state.currentBranch, commit(types.SET_LEFT_PANEL_COLLAPSED, collapsed);
) } else {
.then((data) => { commit(types.SET_RIGHT_PANEL_COLLAPSED, collapsed);
const { id } = data.commit; }
};
if (state.currentRef !== id) {
return true;
}
return false; export const checkCommitStatus = ({ state }) =>
}) service
.catch(() => flash('Error checking branch data. Please try again.')); .getBranchData(state.currentProjectId, state.currentBranchId)
.then((data) => {
export const commitChanges = ({ commit, state, dispatch, getters }, { payload, newMr }) => const { id } = data.commit;
service.commit(state.project.id, payload) const selectedBranch =
.then((data) => { state.projects[state.currentProjectId].branches[state.currentBranchId];
const { branch } = payload;
if (!data.short_id) { if (selectedBranch.workingReference !== id) {
flash(data.message); return true;
return; }
}
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,
});
const lastCommit = { getters.changedFiles.forEach((entry) => {
commit_path: `${state.project.url}/commit/${data.id}`, commit(types.SET_LAST_COMMIT_DATA, {
commit: { entry,
message: data.message, lastCommit,
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('discardAllChanges');
dispatch('closeAllFiles'); dispatch('closeAllFiles');
dispatch('toggleEditMode');
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
}) })
.catch(() => flash('Error committing changes. Please try again.')); .catch(() => flash('Error committing changes. Please try again.'));
export const createTempEntry = ({ state, dispatch }, { name, type, content = '', base64 = false }) => { export const createTempEntry = (
{ state, dispatch },
{ projectId, branchId, parent, name, type, content = '', base64 = false },
) => {
const selectedParent = parent || state.trees[`${projectId}/${branchId}`];
if (type === 'tree') { if (type === 'tree') {
dispatch('createTempTree', name); dispatch('createTempTree', {
projectId,
branchId,
parent: selectedParent,
name,
});
} else if (type === 'blob') { } else if (type === 'blob') {
dispatch('createTempFile', { dispatch('createTempFile', {
tree: state, projectId,
branchId,
parent: selectedParent,
name, name,
base64, base64,
content, content,
...@@ -118,17 +161,6 @@ export const createTempEntry = ({ state, dispatch }, { name, type, content = '', ...@@ -118,17 +161,6 @@ export const createTempEntry = ({ state, dispatch }, { name, type, 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 = () => { export const scrollToTab = () => {
Vue.nextTick(() => { Vue.nextTick(() => {
const tabs = document.getElementById('tabs'); const tabs = document.getElementById('tabs');
...@@ -143,4 +175,5 @@ export const scrollToTab = () => { ...@@ -143,4 +175,5 @@ export const scrollToTab = () => {
export * from './actions/tree'; export * from './actions/tree';
export * from './actions/file'; export * from './actions/file';
export * from './actions/project';
export * from './actions/branch'; export * from './actions/branch';
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'; ...@@ -2,9 +2,9 @@ import { normalizeHeaders } from '../../../lib/utils/common_utils';
import flash from '../../../flash'; import flash from '../../../flash';
import service from '../../services'; import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import router from '../../ide_router';
import { import {
findEntry, findEntry,
pushState,
setPageTitle, setPageTitle,
createTemp, createTemp,
findIndexOfFile, findIndexOfFile,
...@@ -25,7 +25,7 @@ export const closeFile = ({ commit, state, dispatch }, { file, force = false }) ...@@ -25,7 +25,7 @@ export const closeFile = ({ commit, state, dispatch }, { file, force = false })
dispatch('setFileActive', nextFileToOpen); dispatch('setFileActive', nextFileToOpen);
} else if (!state.openFiles.length) { } else if (!state.openFiles.length) {
pushState(file.parentTreeUrl); router.push(`/project/${file.projectId}/tree/${file.branchId}/`);
} }
dispatch('getLastCommitData'); dispatch('getLastCommitData');
...@@ -45,6 +45,9 @@ export const setFileActive = ({ commit, state, getters, dispatch }, file) => { ...@@ -45,6 +45,9 @@ export const setFileActive = ({ commit, state, getters, dispatch }, file) => {
// reset hash for line highlighting // reset hash for line highlighting
location.hash = ''; location.hash = '';
commit(types.SET_CURRENT_PROJECT, file.projectId);
commit(types.SET_CURRENT_BRANCH, file.branchId);
}; };
export const getFileData = ({ state, commit, dispatch }, file) => { export const getFileData = ({ state, commit, dispatch }, file) => {
...@@ -63,8 +66,6 @@ export const getFileData = ({ state, commit, dispatch }, file) => { ...@@ -63,8 +66,6 @@ export const getFileData = ({ state, commit, dispatch }, file) => {
commit(types.TOGGLE_FILE_OPEN, file); commit(types.TOGGLE_FILE_OPEN, file);
dispatch('setFileActive', file); dispatch('setFileActive', file);
commit(types.TOGGLE_LOADING, file); commit(types.TOGGLE_LOADING, file);
pushState(file.url);
}) })
.catch(() => { .catch(() => {
commit(types.TOGGLE_LOADING, file); commit(types.TOGGLE_LOADING, file);
...@@ -82,21 +83,39 @@ export const changeFileContent = ({ commit }, { file, content }) => { ...@@ -82,21 +83,39 @@ export const changeFileContent = ({ commit }, { file, content }) => {
commit(types.UPDATE_FILE_CONTENT, { 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({ const file = createTemp({
name: name.replace(`${state.path}/`, ''), projectId,
path: tree.path, branchId,
name: name.replace(`${path}/`, ''),
path,
type: 'blob', type: 'blob',
level: tree.level !== undefined ? tree.level + 1 : 0, level: parent.level !== undefined ? parent.level + 1 : 0,
changed: true, changed: true,
content, content,
base64, 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, { commit(types.CREATE_TMP_FILE, {
parent: tree, parent,
file, file,
}); });
commit(types.TOGGLE_FILE_OPEN, file); commit(types.TOGGLE_FILE_OPEN, file);
...@@ -106,5 +125,7 @@ export const createTempFile = ({ state, commit, dispatch }, { tree, name, conten ...@@ -106,5 +125,7 @@ export const createTempFile = ({ state, commit, dispatch }, { tree, name, conten
dispatch('toggleEditMode', true); dispatch('toggleEditMode', true);
} }
router.push(`/project${file.url}`);
return Promise.resolve(file); return Promise.resolve(file);
}; };
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}`]);
}
});
...@@ -3,8 +3,8 @@ import { normalizeHeaders } from '../../../lib/utils/common_utils'; ...@@ -3,8 +3,8 @@ import { normalizeHeaders } from '../../../lib/utils/common_utils';
import flash from '../../../flash'; import flash from '../../../flash';
import service from '../../services'; import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import router from '../../ide_router';
import { import {
pushState,
setPageTitle, setPageTitle,
findEntry, findEntry,
createTemp, createTemp,
...@@ -13,59 +13,69 @@ import { ...@@ -13,59 +13,69 @@ import {
export const getTreeData = ( export const getTreeData = (
{ commit, state, dispatch }, { commit, state, dispatch },
{ endpoint = state.endpoints.rootEndpoint, tree = state } = {}, { endpoint, tree = null, projectId, branch, force = false } = {},
) => { ) => new Promise((resolve, reject) => {
commit(types.TOGGLE_LOADING, tree); // We already have the base tree so we resolve immediately
if (!tree && state.trees[`${projectId}/${branch}`] && !force) {
service.getTreeData(endpoint) resolve();
.then((res) => { } else {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']); if (tree) commit(types.TOGGLE_LOADING, tree);
const selectedProject = state.projects[projectId];
setPageTitle(pageTitle); // 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
return res.json(); const completeEndpoint = selectedProject.web_url + (endpoint).replace(projectId, '');
}) if (completeEndpoint && (!tree || !tree.tempFile)) {
.then((data) => { service.getTreeData(completeEndpoint)
const prevLastCommitPath = tree.lastCommitPath; .then((res) => {
if (!state.isInitialRoot) { const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
commit(types.SET_ROOT, data.path === '/');
} setPageTitle(pageTitle);
return res.json();
})
.then((data) => {
if (!state.isInitialRoot) {
commit(types.SET_ROOT, data.path === '/');
}
dispatch('updateDirectoryData', { data, tree }); dispatch('updateDirectoryData', { data, tree, projectId, branch });
commit(types.SET_PARENT_TREE_URL, data.parent_tree_url); const selectedTree = tree || state.trees[`${projectId}/${branch}`];
commit(types.SET_LAST_COMMIT_URL, { tree, url: data.last_commit_path });
commit(types.TOGGLE_LOADING, tree);
if (prevLastCommitPath !== null) { commit(types.SET_PARENT_TREE_URL, data.parent_tree_url);
dispatch('getLastCommitData', tree); commit(types.SET_LAST_COMMIT_URL, { tree: selectedTree, url: data.last_commit_path });
} if (tree) commit(types.TOGGLE_LOADING, selectedTree);
pushState(endpoint); const prevLastCommitPath = selectedTree.lastCommitPath;
}) if (prevLastCommitPath !== null) {
.catch(() => { dispatch('getLastCommitData', selectedTree);
flash('Error loading tree data. Please try again.'); }
commit(types.TOGGLE_LOADING, tree); 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 }) => { export const toggleTreeOpen = ({ commit, dispatch }, { endpoint, tree }) => {
if (tree.opened) { if (tree.opened) {
// send empty data to clear the tree // send empty data to clear the tree
const data = { trees: [], blobs: [], submodules: [] }; const data = { trees: [], blobs: [], submodules: [] };
pushState(tree.parentTreeUrl); dispatch('updateDirectoryData', { data, tree, projectId: tree.projectId, branchId: tree.branchId });
commit(types.SET_PREVIOUS_URL, tree.parentTreeUrl);
dispatch('updateDirectoryData', { data, tree });
} else { } else {
commit(types.SET_PREVIOUS_URL, endpoint); dispatch('getTreeData', { endpoint, tree, projectId: tree.projectId, branch: tree.branchId });
dispatch('getTreeData', { endpoint, tree });
} }
commit(types.TOGGLE_TREE_OPEN, tree); commit(types.TOGGLE_TREE_OPEN, tree);
}; };
export const clickedTreeRow = ({ commit, dispatch }, row) => { export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
if (row.type === 'tree') { if (row.type === 'tree') {
dispatch('toggleTreeOpen', { dispatch('toggleTreeOpen', {
endpoint: row.url, endpoint: row.url,
...@@ -73,7 +83,6 @@ export const clickedTreeRow = ({ commit, dispatch }, row) => { ...@@ -73,7 +83,6 @@ export const clickedTreeRow = ({ commit, dispatch }, row) => {
}); });
} else if (row.type === 'submodule') { } else if (row.type === 'submodule') {
commit(types.TOGGLE_LOADING, row); commit(types.TOGGLE_LOADING, row);
visitUrl(row.url); visitUrl(row.url);
} else if (row.type === 'blob' && row.opened) { } else if (row.type === 'blob' && row.opened) {
dispatch('setFileActive', row); dispatch('setFileActive', row);
...@@ -82,43 +91,46 @@ export const clickedTreeRow = ({ commit, dispatch }, row) => { ...@@ -82,43 +91,46 @@ export const clickedTreeRow = ({ commit, dispatch }, row) => {
} }
}; };
export const createTempTree = ({ state, commit, dispatch }, name) => { export const createTempTree = (
let tree = state; { state, commit, dispatch },
{ projectId, branchId, parent, name },
) => {
let selectedTree = parent;
const dirNames = name.replace(new RegExp(`^${state.path}/`), '').split('/'); const dirNames = name.replace(new RegExp(`^${state.path}/`), '').split('/');
dirNames.forEach((dirName) => { dirNames.forEach((dirName) => {
const foundEntry = findEntry(tree, 'tree', dirName); const foundEntry = findEntry(selectedTree.tree, 'tree', dirName);
if (!foundEntry) { if (!foundEntry) {
const path = selectedTree.path !== undefined ? selectedTree.path : '';
const tmpEntry = createTemp({ const tmpEntry = createTemp({
projectId,
branchId,
name: dirName, name: dirName,
path: tree.path, path,
type: 'tree', type: 'tree',
level: tree.level !== undefined ? tree.level + 1 : 0, level: selectedTree.level !== undefined ? selectedTree.level + 1 : 0,
tree: [],
url: `/${projectId}/blob/${branchId}/${path}${path ? '/' : ''}${dirName}`,
}); });
commit(types.CREATE_TMP_TREE, { commit(types.CREATE_TMP_TREE, {
parent: tree, parent: selectedTree,
tmpEntry, tmpEntry,
}); });
commit(types.TOGGLE_TREE_OPEN, tmpEntry); commit(types.TOGGLE_TREE_OPEN, tmpEntry);
tree = tmpEntry; router.push(`/project${tmpEntry.url}`);
selectedTree = tmpEntry;
} else { } else {
tree = foundEntry; selectedTree = foundEntry;
} }
}); });
if (tree.tempFile) {
dispatch('createTempFile', {
tree,
name: '.gitkeep',
});
}
}; };
export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => { export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
if (tree.lastCommitPath === null || getters.isCollapsed) return; if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
service.getTreeLastCommit(tree.lastCommitPath) service.getTreeLastCommit(tree.lastCommitPath)
.then((res) => { .then((res) => {
...@@ -130,7 +142,7 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s ...@@ -130,7 +142,7 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
}) })
.then((data) => { .then((data) => {
data.forEach((lastCommit) => { data.forEach((lastCommit) => {
const entry = findEntry(tree, lastCommit.type, lastCommit.file_name); const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name);
if (entry) { if (entry) {
commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit }); commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit });
...@@ -142,11 +154,24 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s ...@@ -142,11 +154,24 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
.catch(() => flash('Error fetching log data.')); .catch(() => flash('Error fetching log data.'));
}; };
export const updateDirectoryData = ({ commit, state }, { data, tree }) => { export const updateDirectoryData = (
const level = tree.level !== undefined ? tree.level + 1 : 0; { 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 parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl;
const createEntry = (entry, type) => createOrMergeEntry({ const createEntry = (entry, type) => createOrMergeEntry({
tree, tree: selectedTree,
projectId: `${projectId}`,
branchId: branch,
entry, entry,
level, level,
type, type,
...@@ -159,5 +184,5 @@ export const updateDirectoryData = ({ commit, state }, { data, tree }) => { ...@@ -159,5 +184,5 @@ export const updateDirectoryData = ({ commit, state }, { data, tree }) => {
...data.blobs.map(b => createEntry(b, 'blob')), ...data.blobs.map(b => createEntry(b, 'blob')),
]; ];
commit(types.SET_DIRECTORY_DATA, { tree, data: formattedData }); commit(types.SET_DIRECTORY_DATA, { tree: selectedTree, data: formattedData });
}; };
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 changedFiles = state => state.openFiles.filter(file => file.changed);
export const activeFile = state => state.openFiles.find(file => file.active); export const activeFile = state => state.openFiles.find(file => file.active) || null;
export const activeFileExtension = (state) => { export const activeFileExtension = (state) => {
const file = activeFile(state); const file = activeFile(state);
return file ? `.${file.path.split('.').pop()}` : ''; return file ? `.${file.path.split('.').pop()}` : '';
}; };
export const isCollapsed = state => !!state.openFiles.length;
export const canEditFile = (state) => { export const canEditFile = (state) => {
const currentActiveFile = activeFile(state); const currentActiveFile = activeFile(state);
const openedFiles = state.openFiles;
return state.canCommit && return state.canCommit &&
state.onTopOfBranch && (currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary);
openedFiles.length &&
(currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary);
}; };
export const addedFiles = state => changedFiles(state).filter(f => f.tempFile); export const addedFiles = state => changedFiles(state).filter(f => f.tempFile);
......
export const SET_INITIAL_DATA = 'SET_INITIAL_DATA'; export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
export const TOGGLE_LOADING = 'TOGGLE_LOADING'; 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_PARENT_TREE_URL = 'SET_PARENT_TREE_URL';
export const SET_ROOT = 'SET_ROOT'; 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_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 // Tree mutation types
export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA'; export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN'; export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN';
export const CREATE_TMP_TREE = 'CREATE_TMP_TREE'; export const CREATE_TMP_TREE = 'CREATE_TMP_TREE';
export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL'; export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL';
export const CREATE_TREE = 'CREATE_TREE';
// File mutation types // File mutation types
export const SET_FILE_DATA = 'SET_FILE_DATA'; export const SET_FILE_DATA = 'SET_FILE_DATA';
...@@ -18,6 +29,9 @@ export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN'; ...@@ -18,6 +29,9 @@ export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE'; export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA'; export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT'; 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 DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
export const CREATE_TMP_FILE = 'CREATE_TMP_FILE'; export const CREATE_TMP_FILE = 'CREATE_TMP_FILE';
...@@ -28,3 +42,4 @@ export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE'; ...@@ -28,3 +42,4 @@ export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE';
export const TOGGLE_DISCARD_POPUP = 'TOGGLE_DISCARD_POPUP'; export const TOGGLE_DISCARD_POPUP = 'TOGGLE_DISCARD_POPUP';
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH'; export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
import * as types from './mutation_types'; import * as types from './mutation_types';
import projectMutations from './mutations/project';
import fileMutations from './mutations/file'; import fileMutations from './mutations/file';
import treeMutations from './mutations/tree'; import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch'; import branchMutations from './mutations/branch';
...@@ -32,29 +33,32 @@ export default { ...@@ -32,29 +33,32 @@ export default {
discardPopupOpen, discardPopupOpen,
}); });
}, },
[types.SET_COMMIT_REF](state, ref) {
Object.assign(state, {
currentRef: ref,
});
},
[types.SET_ROOT](state, isRoot) { [types.SET_ROOT](state, isRoot) {
Object.assign(state, { Object.assign(state, {
isRoot, isRoot,
isInitialRoot: isRoot, isInitialRoot: isRoot,
}); });
}, },
[types.SET_PREVIOUS_URL](state, previousUrl) { [types.SET_LEFT_PANEL_COLLAPSED](state, collapsed) {
Object.assign(state, {
leftPanelCollapsed: collapsed,
});
},
[types.SET_RIGHT_PANEL_COLLAPSED](state, collapsed) {
Object.assign(state, { Object.assign(state, {
previousUrl, rightPanelCollapsed: collapsed,
}); });
}, },
[types.SET_LAST_COMMIT_DATA](state, { entry, lastCommit }) { [types.SET_LAST_COMMIT_DATA](state, { entry, lastCommit }) {
Object.assign(entry.lastCommit, { Object.assign(entry.lastCommit, {
id: lastCommit.commit.id,
url: lastCommit.commit_path, url: lastCommit.commit_path,
message: lastCommit.commit.message, message: lastCommit.commit.message,
author: lastCommit.commit.author_name,
updatedAt: lastCommit.commit.authored_date, updatedAt: lastCommit.commit.authored_date,
}); });
}, },
...projectMutations,
...fileMutations, ...fileMutations,
...treeMutations, ...treeMutations,
...branchMutations, ...branchMutations,
......
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 { ...@@ -6,6 +6,10 @@ export default {
Object.assign(file, { Object.assign(file, {
active, active,
}); });
Object.assign(state, {
selectedFile: file,
});
}, },
[types.TOGGLE_FILE_OPEN](state, file) { [types.TOGGLE_FILE_OPEN](state, file) {
Object.assign(file, { Object.assign(file, {
...@@ -42,6 +46,22 @@ export default { ...@@ -42,6 +46,22 @@ export default {
changed, 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) { [types.DISCARD_FILE_CHANGES](state, file) {
Object.assign(file, { Object.assign(file, {
content: '', content: '',
......
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 { ...@@ -6,6 +6,15 @@ export default {
opened: !tree.opened, 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 }) { [types.SET_DIRECTORY_DATA](state, { data, tree }) {
Object.assign(tree, { Object.assign(tree, {
tree: data, tree: data,
......
export default () => ({ export default () => ({
canCommit: false, canCommit: false,
currentBranch: '', currentProjectId: '',
currentBlobView: 'repo-preview', currentBranchId: '',
currentRef: '', currentBlobView: 'repo-editor',
discardPopupOpen: false, discardPopupOpen: false,
editMode: false, editMode: true,
endpoints: {}, endpoints: {},
isRoot: false, isRoot: false,
isInitialRoot: false, isInitialRoot: false,
...@@ -12,13 +12,11 @@ export default () => ({ ...@@ -12,13 +12,11 @@ export default () => ({
loading: false, loading: false,
onTopOfBranch: false, onTopOfBranch: false,
openFiles: [], openFiles: [],
selectedFile: null,
path: '', path: '',
project: {
id: 0,
name: '',
url: '',
},
parentTreeUrl: '', parentTreeUrl: '',
previousUrl: '', trees: {},
tree: [], projects: {},
leftPanelCollapsed: false,
rightPanelCollapsed: true,
}); });
...@@ -2,6 +2,8 @@ export const dataStructure = () => ({ ...@@ -2,6 +2,8 @@ export const dataStructure = () => ({
id: '', id: '',
key: '', key: '',
type: '', type: '',
projectId: '',
branchId: '',
name: '', name: '',
url: '', url: '',
path: '', path: '',
...@@ -15,9 +17,11 @@ export const dataStructure = () => ({ ...@@ -15,9 +17,11 @@ export const dataStructure = () => ({
changed: false, changed: false,
lastCommitPath: '', lastCommitPath: '',
lastCommit: { lastCommit: {
id: '',
url: '', url: '',
message: '', message: '',
updatedAt: '', updatedAt: '',
author: '',
}, },
tree_url: '', tree_url: '',
blamePath: '', blamePath: '',
...@@ -31,11 +35,17 @@ export const dataStructure = () => ({ ...@@ -31,11 +35,17 @@ export const dataStructure = () => ({
parentTreeUrl: '', parentTreeUrl: '',
renderError: false, renderError: false,
base64: false, base64: false,
editorRow: 1,
editorColumn: 1,
fileLanguage: '',
eol: '',
}); });
export const decorateData = (entity) => { export const decorateData = (entity) => {
const { const {
id, id,
projectId,
branchId,
type, type,
url, url,
name, name,
...@@ -56,6 +66,8 @@ export const decorateData = (entity) => { ...@@ -56,6 +66,8 @@ export const decorateData = (entity) => {
return { return {
...dataStructure(), ...dataStructure(),
id, id,
projectId,
branchId,
key: `${name}-${type}-${id}`, key: `${name}-${type}-${id}`,
type, type,
name, name,
...@@ -75,24 +87,51 @@ export const decorateData = (entity) => { ...@@ -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, f => f.type === type && f.name === name,
); );
export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path); export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path);
export const setPageTitle = (title) => { export const setPageTitle = (title) => {
document.title = title; document.title = title;
}; };
export const pushState = (url) => { export const createTemp = ({
history.pushState({ url }, '', url); projectId, branchId, name, path, type, level, changed, content, base64, url,
}; }) => {
export const createTemp = ({ name, path, type, level, changed, content, base64 }) => {
const treePath = path ? `${path}/${name}` : name; const treePath = path ? `${path}/${name}` : name;
return decorateData({ return decorateData({
id: new Date().getTime().toString(), id: new Date().getTime().toString(),
projectId,
branchId,
name, name,
type, type,
tempFile: true, tempFile: true,
...@@ -104,11 +143,18 @@ export const createTemp = ({ name, path, type, level, changed, content, base64 } ...@@ -104,11 +143,18 @@ export const createTemp = ({ name, path, type, level, changed, content, base64 }
level, level,
base64, base64,
renderError: base64, renderError: base64,
url,
}); });
}; };
export const createOrMergeEntry = ({ tree, entry, type, parentTreeUrl, level }) => { export const createOrMergeEntry = ({ tree,
const found = findEntry(tree, type, entry.name); projectId,
branchId,
entry,
type,
parentTreeUrl,
level }) => {
const found = findEntry(tree.tree || tree, type, entry.name);
if (found) { if (found) {
return Object.assign({}, found, { return Object.assign({}, found, {
...@@ -120,6 +166,8 @@ export const createOrMergeEntry = ({ tree, entry, type, parentTreeUrl, level }) ...@@ -120,6 +166,8 @@ export const createOrMergeEntry = ({ tree, entry, type, parentTreeUrl, level })
return decorateData({ return decorateData({
...entry, ...entry,
projectId,
branchId,
type, type,
parentTreeUrl, parentTreeUrl,
level, level,
......
...@@ -6,11 +6,12 @@ export default class NewCommitForm { ...@@ -6,11 +6,12 @@ export default class NewCommitForm {
this.branchName = form.find('.js-branch-name'); this.branchName = form.find('.js-branch-name');
this.originalBranch = form.find('.js-original-branch'); this.originalBranch = form.find('.js-original-branch');
this.createMergeRequest = form.find('.js-create-merge-request'); 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.branchName.keyup(this.renderDestination);
this.renderDestination(); this.renderDestination();
} }
renderDestination() { renderDestination() {
var different; var different;
different = this.branchName.val() !== this.originalBranch.val(); different = this.branchName.val() !== this.originalBranch.val();
...@@ -23,6 +24,6 @@ export default class NewCommitForm { ...@@ -23,6 +24,6 @@ export default class NewCommitForm {
this.createMergeRequestContainer.hide(); this.createMergeRequestContainer.hide();
this.createMergeRequest.prop('checked', false); this.createMergeRequest.prop('checked', false);
} }
return this.wasDifferent = different; return (this.wasDifferent = different);
} }
} }
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);
});
import * as types from '../mutation_types';
export default {
[types.SET_CURRENT_BRANCH](state, currentBranch) {
Object.assign(state, {
currentBranch,
});
},
};
<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 @@ ...@@ -23,7 +23,6 @@
.context-header { .context-header {
position: relative; position: relative;
margin-right: 2px; margin-right: 2px;
width: $contextual-sidebar-width;
a { a {
transition: padding $sidebar-transition-duration; transition: padding $sidebar-transition-duration;
......
...@@ -219,6 +219,7 @@ $gl-input-padding: 10px; ...@@ -219,6 +219,7 @@ $gl-input-padding: 10px;
$gl-vert-padding: 6px; $gl-vert-padding: 6px;
$gl-padding-top: 10px; $gl-padding-top: 10px;
$gl-sidebar-padding: 22px; $gl-sidebar-padding: 22px;
$gl-bar-padding: 3px;
/* /*
* Misc * Misc
......
...@@ -22,9 +22,10 @@ ...@@ -22,9 +22,10 @@
} }
} }
.multi-file { .ide-view {
display: flex; display: flex;
height: calc(100vh - 145px); height: calc(100vh - #{$header-height});
color: $almost-black;
border-top: 1px solid $white-dark; border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark; border-bottom: 1px solid $white-dark;
...@@ -35,12 +36,47 @@ ...@@ -35,12 +36,47 @@
} }
} }
.with-performance-bar .ide-view {
height: calc(100vh - #{$header-height});
}
.ide-file-list { .ide-file-list {
flex: 1; flex: 1;
overflow: scroll;
.file { .file {
cursor: pointer; 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 { a {
...@@ -55,10 +91,9 @@ ...@@ -55,10 +91,9 @@
.multi-file-table-name, .multi-file-table-name,
.multi-file-table-col-commit-message { .multi-file-table-col-commit-message {
white-space: nowrap; overflow: visible;
overflow: hidden;
text-overflow: ellipsis;
max-width: 0; max-width: 0;
padding: 6px 12px;
} }
.multi-file-table-name { .multi-file-table-name {
...@@ -66,6 +101,7 @@ ...@@ -66,6 +101,7 @@
} }
.multi-file-table-col-commit-message { .multi-file-table-col-commit-message {
white-space: nowrap;
width: 50%; width: 50%;
} }
...@@ -79,7 +115,7 @@ ...@@ -79,7 +115,7 @@
.multi-file-tabs { .multi-file-tabs {
display: flex; display: flex;
overflow: scroll; overflow-x: auto;
background-color: $white-normal; background-color: $white-normal;
box-shadow: inset 0 -1px $white-dark; box-shadow: inset 0 -1px $white-dark;
...@@ -128,9 +164,38 @@ ...@@ -128,9 +164,38 @@
height: 0; 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 { .multi-file-editor-btn-group {
padding: $grid-size; padding: $gl-bar-padding $gl-padding;
border-top: 1px solid $white-dark; 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 // Not great, but this is to deal with our current output
...@@ -138,10 +203,6 @@ ...@@ -138,10 +203,6 @@
height: 100%; height: 100%;
overflow: scroll; overflow: scroll;
.blob-viewer {
height: 100%;
}
.file-content.code { .file-content.code {
display: flex; display: flex;
...@@ -162,18 +223,101 @@ ...@@ -162,18 +223,101 @@
} }
} }
.file-content.blob-no-preview {
a {
margin-left: auto;
margin-right: auto;
}
}
.multi-file-commit-panel { .multi-file-commit-panel {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
width: 290px; width: 290px;
padding: $gl-padding; padding: 0;
background-color: $gray-light; background-color: $gray-light;
border-left: 1px solid $white-dark; 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 { &.is-collapsed {
width: 60px; 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 @@ ...@@ -186,9 +330,9 @@
.multi-file-commit-panel-header { .multi-file-commit-panel-header {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 0 12px;
margin-bottom: 12px; margin-bottom: 12px;
border-bottom: 1px solid $white-dark; border-bottom: 1px solid $white-dark;
padding: $gl-btn-padding 0;
&.is-collapsed { &.is-collapsed {
border-bottom: 1px solid $white-dark; border-bottom: 1px solid $white-dark;
...@@ -197,23 +341,33 @@ ...@@ -197,23 +341,33 @@
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
} }
.multi-file-commit-panel-collapse-btn {
margin-right: auto;
margin-left: auto;
border-left: 0;
}
} }
} }
.multi-file-commit-panel-collapse-btn { .multi-file-commit-panel-header-title {
padding-top: 0; display: flex;
padding-bottom: 0; flex: 1;
margin-left: auto; padding: $gl-btn-padding;
font-size: 20px;
&.is-collapsed { svg {
margin-right: auto; margin-right: $gl-btn-padding;
} }
} }
.multi-file-commit-panel-collapse-btn {
border-left: 1px solid $white-dark;
}
.multi-file-commit-list { .multi-file-commit-list {
flex: 1; flex: 1;
overflow: scroll; overflow: auto;
padding: $gl-padding;
} }
.multi-file-commit-list-item { .multi-file-commit-list-item {
...@@ -244,7 +398,7 @@ ...@@ -244,7 +398,7 @@
} }
.multi-file-commit-form { .multi-file-commit-form {
padding-top: 12px; padding: $gl-padding;
border-top: 1px solid $white-dark; border-top: 1px solid $white-dark;
} }
...@@ -295,3 +449,40 @@ ...@@ -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;
}
class IdeController < ApplicationController
layout 'nav_only'
def index
end
end
...@@ -306,7 +306,7 @@ module ApplicationHelper ...@@ -306,7 +306,7 @@ module ApplicationHelper
cookies["sidebar_collapsed"] == "true" cookies["sidebar_collapsed"] == "true"
end end
def show_new_repo? def show_new_ide?
cookies["new_repo"] == "true" && body_data_page != 'projects:show' cookies["new_repo"] == "true" && body_data_page != 'projects:show'
end end
......
...@@ -8,7 +8,7 @@ module BlobHelper ...@@ -8,7 +8,7 @@ module BlobHelper
%w(credits changelog news copying copyright license authors) %w(credits changelog news copying copyright license authors)
end 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, project_edit_blob_path(project,
tree_join(ref, path), tree_join(ref, path),
options[:link_opts]) options[:link_opts])
...@@ -26,10 +26,10 @@ module BlobHelper ...@@ -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' } 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 # This condition applies to anonymous or users who can edit directly
elsif !current_user || (current_user && can_modify_blob?(blob, project, ref)) 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) elsif current_user && can?(current_user, :fork_project, project)
continue_params = { continue_params = {
to: edit_path(project, ref, path, options), to: edit_blob_path(project, ref, path, options),
notice: edit_in_new_fork_notice, notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now notice_now: edit_in_new_fork_notice_now
} }
...@@ -41,6 +41,43 @@ module BlobHelper ...@@ -41,6 +41,43 @@ module BlobHelper
end end
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:) def modify_file_link(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:)
return unless current_user return unless current_user
......
- 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 ...')
!!! 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 @@ ...@@ -7,7 +7,7 @@
.nav-block .nav-block
= render 'projects/tree/tree_header', tree: @tree = 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 'shared/commit_well', commit: commit, ref: ref, project: project
= render 'projects/tree/tree_content', tree: @tree, content_url: content_url = render 'projects/tree/tree_content', tree: @tree, content_url: content_url
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
.btn-group{ role: "group" }< .btn-group{ role: "group" }<
= edit_blob_link = edit_blob_link
= ide_blob_link
- if current_user - if current_user
= replace_blob_link = replace_blob_link
= delete_blob_link = delete_blob_link
......
...@@ -6,21 +6,14 @@ ...@@ -6,21 +6,14 @@
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag 'blob' = webpack_bundle_tag 'blob'
- if show_new_repo?
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'repo'
= render 'projects/last_push' = render 'projects/last_push'
%div{ class: container_class } %div{ class: container_class }
- if show_new_repo? #tree-holder.tree-holder
= render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_blob_path(@project, @id) = render 'blob', blob: @blob
- else
#tree-holder.tree-holder
= render 'blob', blob: @blob
- if can_modify_blob?(@blob) - if can_modify_blob?(@blob)
= render 'projects/blob/remove' = render 'projects/blob/remove'
- title = "Replace #{@blob.name}" - 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 = render 'projects/blob/upload', title: title, placeholder: title, button_title: 'Replace file', form_path: project_update_blob_path(@project, @id), method: :put
.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'
- 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') }
- content_url = local_assigns.fetch(:content_url, nil) .tree-content-holder.js-tree-content{ 'data-logs-path': @logs_path }
- if show_new_repo? .table-holder
= render 'shared/repo/repo', project: @project, content_url: content_url %table.table#tree-slider{ class: "table_#{@hex_path} tree-table" }
- else %thead
= render 'projects/tree/old_tree_content', tree: tree %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 @@ ...@@ -2,16 +2,78 @@
.tree-ref-holder .tree-ref-holder
= render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true = render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true
- if show_new_repo? && can_push_branch?(@project, @ref) - if on_top_of_branch?
.js-new-dropdown - addtotree_toggle_attributes = { href: '#', 'data-toggle': 'dropdown', 'data-target': '.add-to-tree-dropdown' }
- else - else
= render 'projects/tree/old_tree_header' - 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 .tree-controls
- if show_new_repo? - if show_new_ide?
.editable-mode = succeed " " do
- else = link_to ide_edit_path(@project, @id), class: 'btn btn-default' do
= link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn' = ide_edit_text
= link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn'
= render 'projects/find_file_link' = render 'projects/find_file_link'
......
...@@ -6,11 +6,6 @@ ...@@ -6,11 +6,6 @@
= content_for :meta_tags do = content_for :meta_tags do
= auto_discovery_link_tag(:atom, project_commits_url(@project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits") = auto_discovery_link_tag(:atom, project_commits_url(@project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits")
- if show_new_repo? %div{ class: [(container_class), ("limit-container-width" unless fluid_layout)] }
- 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)] }
= render 'projects/last_push' = render 'projects/last_push'
= render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id) = render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id)
- show_create = local_assigns.fetch(:show_create, false) - 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 - dropdown_toggle_text = @ref || @project.default_branch
= form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do = form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do
= hidden_field_tag :destination, destination = hidden_field_tag :destination, destination
......
- @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 } }
---
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 ...@@ -43,6 +43,8 @@ Rails.application.routes.draw do
get 'liveness' => 'health#liveness' get 'liveness' => 'health#liveness'
get 'readiness' => 'health#readiness' get 'readiness' => 'health#readiness'
post 'storage_check' => 'health#storage_check' post 'storage_check' => 'health#storage_check'
get 'ide' => 'ide#index'
get 'ide/*vueroute' => 'ide#index', format: false
resources :metrics, only: [:index] resources :metrics, only: [:index]
mount Peek::Railtie => '/peek' mount Peek::Railtie => '/peek'
......
...@@ -70,7 +70,7 @@ var config = { ...@@ -70,7 +70,7 @@ var config = {
protected_branches: './protected_branches', protected_branches: './protected_branches',
protected_tags: './protected_tags', protected_tags: './protected_tags',
registry_list: './registry/index.js', registry_list: './registry/index.js',
repo: './repo/index.js', ide: './ide/index.js',
sidebar: './sidebar/sidebar_bundle.js', sidebar: './sidebar/sidebar_bundle.js',
schedule_form: './pipeline_schedules/pipeline_schedule_form_bundle.js', schedule_form: './pipeline_schedules/pipeline_schedule_form_bundle.js',
schedules_index: './pipeline_schedules/pipeline_schedules_index_bundle.js', schedules_index: './pipeline_schedules/pipeline_schedules_index_bundle.js',
...@@ -204,7 +204,7 @@ var config = { ...@@ -204,7 +204,7 @@ var config = {
'pipelines', 'pipelines',
'pipelines_details', 'pipelines_details',
'registry_list', 'registry_list',
'repo', 'ide',
'schedule_form', 'schedule_form',
'schedules_index', 'schedules_index',
'sidebar', 'sidebar',
......
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 ...@@ -13,6 +13,14 @@ feature 'Multi-file editor new directory', :js do
visit project_tree_path(project, :master) visit project_tree_path(project, :master)
wait_for_requests wait_for_requests
click_link('Multi Edit')
wait_for_requests
end
after do
set_cookie('new_repo', 'false')
end end
it 'creates directory in current directory' do it 'creates directory in current directory' do
...@@ -21,17 +29,29 @@ feature 'Multi-file editor new directory', :js do ...@@ -21,17 +29,29 @@ feature 'Multi-file editor new directory', :js do
click_link('New directory') click_link('New directory')
page.within('.modal') do page.within('.modal') do
find('.form-control').set('foldername') find('.form-control').set('folder name')
click_button('Create directory') click_button('Create directory')
end 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 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') click_button('Commit')
expect(page).to have_selector('td', text: 'commit message') expect(page).to have_content('folder name')
end end
end end
...@@ -13,6 +13,14 @@ feature 'Multi-file editor new file', :js do ...@@ -13,6 +13,14 @@ feature 'Multi-file editor new file', :js do
visit project_tree_path(project, :master) visit project_tree_path(project, :master)
wait_for_requests wait_for_requests
click_link('Multi Edit')
wait_for_requests
end
after do
set_cookie('new_repo', 'false')
end end
it 'creates file in current directory' do it 'creates file in current directory' do
...@@ -21,17 +29,19 @@ feature 'Multi-file editor new file', :js do ...@@ -21,17 +29,19 @@ feature 'Multi-file editor new file', :js do
click_link('New file') click_link('New file')
page.within('.modal') do page.within('.modal') do
find('.form-control').set('filename') find('.form-control').set('file name')
click_button('Create file') click_button('Create file')
end end
wait_for_requests
find('.multi-file-commit-panel-collapse-btn').click 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') click_button('Commit')
expect(page).to have_selector('td', text: 'commit message') expect(page).to have_content('file name')
end end
end end
...@@ -15,6 +15,14 @@ feature 'Multi-file editor upload file', :js do ...@@ -15,6 +15,14 @@ feature 'Multi-file editor upload file', :js do
visit project_tree_path(project, :master) visit project_tree_path(project, :master)
wait_for_requests wait_for_requests
click_link('Multi Edit')
wait_for_requests
end
after do
set_cookie('new_repo', 'false')
end end
it 'uploads text file' do it 'uploads text file' do
...@@ -41,6 +49,5 @@ feature 'Multi-file editor upload file', :js 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).to have_selector('.multi-file-tab', text: 'dk.png')
expect(page).not_to have_selector('.monaco-editor') 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
end end
import Vue from 'vue'; import Vue from 'vue';
import store from '~/repo/stores'; import store from '~/ide/stores';
import listCollapsed from '~/repo/components/commit_sidebar/list_collapsed.vue'; import listCollapsed from '~/ide/components/commit_sidebar/list_collapsed.vue';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
import { file } from '../../helpers'; import { file } from '../../helpers';
......
import Vue from 'vue'; 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 mountComponent from '../../../helpers/vue_mount_component_helper';
import { file } from '../../helpers'; import { file } from '../../helpers';
......
import Vue from 'vue'; import Vue from 'vue';
import store from '~/repo/stores'; import store from '~/ide/stores';
import commitSidebarList from '~/repo/components/commit_sidebar/list.vue'; import commitSidebarList from '~/ide/components/commit_sidebar/list.vue';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
import { file } from '../../helpers'; import { file } from '../../helpers';
...@@ -13,8 +13,11 @@ describe('Multi-file editor commit sidebar list', () => { ...@@ -13,8 +13,11 @@ describe('Multi-file editor commit sidebar list', () => {
vm = createComponentWithStore(Component, store, { vm = createComponentWithStore(Component, store, {
title: 'Staged', title: 'Staged',
fileList: [], fileList: [],
collapsed: false, });
}).$mount();
vm.$store.state.rightPanelCollapsed = false;
vm.$mount();
}); });
afterEach(() => { afterEach(() => {
...@@ -43,30 +46,14 @@ describe('Multi-file editor commit sidebar list', () => { ...@@ -43,30 +46,14 @@ describe('Multi-file editor commit sidebar list', () => {
describe('collapsed', () => { describe('collapsed', () => {
beforeEach((done) => { beforeEach((done) => {
vm.collapsed = true; vm.$store.state.rightPanelCollapsed = true;
Vue.nextTick(done); Vue.nextTick(done);
}); });
it('adds collapsed class', () => {
expect(vm.$el.querySelector('.is-collapsed')).not.toBeNull();
});
it('hides list', () => { it('hides list', () => {
expect(vm.$el.querySelector('.list-unstyled')).toBeNull(); expect(vm.$el.querySelector('.list-unstyled')).toBeNull();
expect(vm.$el.querySelector('.help-block')).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');
}); });
}); });
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,
});
});
});
import Vue from 'vue'; import Vue from 'vue';
import store from '~/repo/stores'; import store from '~/ide/stores';
import repoSidebar from '~/repo/components/repo_sidebar.vue'; import ideRepoTree from '~/ide/components/ide_repo_tree.vue';
import { file, resetStore } from '../helpers'; import { file, resetStore } from '../helpers';
describe('RepoSidebar', () => { describe('IdeRepoTree', () => {
let vm; let vm;
beforeEach(() => { beforeEach(() => {
const RepoSidebar = Vue.extend(repoSidebar); const IdeRepoTree = Vue.extend(ideRepoTree);
vm = new RepoSidebar({ vm = new IdeRepoTree({
store, store,
propsData: {
treeId: 'abcproject/mybranch',
},
}); });
vm.$store.state.currentBranch = 'master';
vm.$store.state.isRoot = true; vm.$store.state.isRoot = true;
vm.$store.state.tree.push(file()); vm.$store.state.trees['abcproject/mybranch'] = {
tree: [file()],
};
vm.$mount(); vm.$mount();
}); });
...@@ -26,13 +32,9 @@ describe('RepoSidebar', () => { ...@@ -26,13 +32,9 @@ describe('RepoSidebar', () => {
}); });
it('renders a sidebar', () => { it('renders a sidebar', () => {
const thead = vm.$el.querySelector('thead');
const tbody = vm.$el.querySelector('tbody'); const tbody = vm.$el.querySelector('tbody');
expect(vm.$el.classList.contains('sidebar-mini')).toBeFalsy(); 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('.repo-file-options')).toBeFalsy();
expect(tbody.querySelector('.prev-directory')).toBeFalsy(); expect(tbody.querySelector('.prev-directory')).toBeFalsy();
expect(tbody.querySelector('.loading-file')).toBeFalsy(); expect(tbody.querySelector('.loading-file')).toBeFalsy();
...@@ -40,7 +42,6 @@ describe('RepoSidebar', () => { ...@@ -40,7 +42,6 @@ describe('RepoSidebar', () => {
}); });
it('renders 5 loading files if tree is loading', (done) => { it('renders 5 loading files if tree is loading', (done) => {
vm.$store.state.tree = [];
vm.$store.state.loading = true; vm.$store.state.loading = true;
Vue.nextTick(() => { Vue.nextTick(() => {
......
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');
});
});
});
import Vue from 'vue'; import Vue from 'vue';
import store from '~/repo/stores'; import store from '~/ide/stores';
import repo from '~/repo/components/repo.vue'; import ide from '~/ide/components/ide.vue';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
import { file, resetStore } from '../helpers'; import { file, resetStore } from '../helpers';
describe('repo component', () => { describe('ide component', () => {
let vm; let vm;
beforeEach(() => { beforeEach(() => {
const Component = Vue.extend(repo); const Component = Vue.extend(ide);
vm = createComponentWithStore(Component, store).$mount(); vm = createComponentWithStore(Component, store).$mount();
}); });
...@@ -24,7 +24,9 @@ describe('repo component', () => { ...@@ -24,7 +24,9 @@ describe('repo component', () => {
}); });
it('renders panel right when files are open', (done) => { it('renders panel right when files are open', (done) => {
vm.$store.state.tree.push(file()); vm.$store.state.trees['abcproject/mybranch'] = {
tree: [file()],
};
Vue.nextTick(() => { Vue.nextTick(() => {
expect(vm.$el.querySelector('.panel-right')).toBeNull(); expect(vm.$el.querySelector('.panel-right')).toBeNull();
......
import Vue from 'vue'; import Vue from 'vue';
import store from '~/repo/stores'; import store from '~/ide/stores';
import newBranchForm from '~/repo/components/new_branch_form.vue'; import newBranchForm from '~/ide/components/new_branch_form.vue';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
import { resetStore } from '../helpers'; import { resetStore } from '../helpers';
......
import Vue from 'vue'; import Vue from 'vue';
import store from '~/repo/stores'; import store from '~/ide/stores';
import newDropdown from '~/repo/components/new_dropdown/index.vue'; import newDropdown from '~/ide/components/new_dropdown/index.vue';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
import { resetStore } from '../../helpers'; import { resetStore } from '../../helpers';
...@@ -10,8 +10,12 @@ describe('new dropdown component', () => { ...@@ -10,8 +10,12 @@ describe('new dropdown component', () => {
beforeEach(() => { beforeEach(() => {
const component = Vue.extend(newDropdown); const component = Vue.extend(newDropdown);
vm = createComponentWithStore(component, store); vm = createComponentWithStore(component, store, {
branch: 'master',
path: '',
});
vm.$store.state.currentProjectId = 'abcproject';
vm.$store.state.path = ''; vm.$store.state.path = '';
vm.$mount(); vm.$mount();
...@@ -23,9 +27,10 @@ describe('new dropdown component', () => { ...@@ -23,9 +27,10 @@ describe('new dropdown component', () => {
resetStore(vm.$store); resetStore(vm.$store);
}); });
it('renders new file and new directory links', () => { it('renders new file, upload and new directory links', () => {
expect(vm.$el.querySelectorAll('a')[0].textContent.trim()).toBe('New file'); expect(vm.$el.querySelectorAll('a')[0].textContent.trim()).toBe('New file');
expect(vm.$el.querySelectorAll('a')[1].textContent.trim()).toBe('New directory'); expect(vm.$el.querySelectorAll('a')[1].textContent.trim()).toBe('Upload file');
expect(vm.$el.querySelectorAll('a')[2].textContent.trim()).toBe('New directory');
}); });
describe('createNewItem', () => { describe('createNewItem', () => {
...@@ -36,7 +41,7 @@ describe('new dropdown component', () => { ...@@ -36,7 +41,7 @@ describe('new dropdown component', () => {
}); });
it('sets modalType to tree when new directory is clicked', () => { it('sets modalType to tree when new directory is clicked', () => {
vm.$el.querySelectorAll('a')[1].click(); vm.$el.querySelectorAll('a')[2].click();
expect(vm.modalType).toBe('tree'); expect(vm.modalType).toBe('tree');
}); });
......
import Vue from 'vue'; import Vue from 'vue';
import store from '~/repo/stores'; import store from '~/ide/stores';
import modal from '~/repo/components/new_dropdown/modal.vue'; import service from '~/ide/services';
import modal from '~/ide/components/new_dropdown/modal.vue';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
import { file, resetStore } from '../../helpers'; import { file, resetStore } from '../../helpers';
describe('new file modal component', () => { describe('new file modal component', () => {
const Component = Vue.extend(modal); const Component = Vue.extend(modal);
let vm; let vm;
let projectTree;
beforeEach(() => {
spyOn(service, 'getProjectData').and.returnValue(Promise.resolve({
data: {
id: '123',
},
}));
spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
commit: {
id: '123branch',
},
}));
spyOn(service, 'getTreeData').and.returnValue(Promise.resolve({
headers: {
'page-title': 'test',
},
json: () => Promise.resolve({
last_commit_path: 'last_commit_path',
parent_tree_url: 'parent_tree_url',
path: '/',
trees: [{ name: 'tree' }],
blobs: [{ name: 'blob' }],
submodules: [{ name: 'submodule' }],
}),
}));
});
afterEach(() => { afterEach(() => {
vm.$destroy(); vm.$destroy();
...@@ -17,12 +47,26 @@ describe('new file modal component', () => { ...@@ -17,12 +47,26 @@ describe('new file modal component', () => {
['tree', 'blob'].forEach((type) => { ['tree', 'blob'].forEach((type) => {
describe(type, () => { describe(type, () => {
beforeEach(() => { beforeEach(() => {
store.state.projects.abcproject = {
web_url: '',
};
store.state.trees = [];
store.state.trees['abcproject/mybranch'] = {
tree: [],
};
projectTree = store.state.trees['abcproject/mybranch'];
store.state.currentProjectId = 'abcproject';
vm = createComponentWithStore(Component, store, { vm = createComponentWithStore(Component, store, {
type, type,
branchId: 'master',
path: '', path: '',
}).$mount(); parent: projectTree,
});
vm.entryName = 'testing'; vm.entryName = 'testing';
vm.$mount();
}); });
it(`sets modal title as ${type}`, () => { it(`sets modal title as ${type}`, () => {
...@@ -50,6 +94,9 @@ describe('new file modal component', () => { ...@@ -50,6 +94,9 @@ describe('new file modal component', () => {
vm.createEntryInStore(); vm.createEntryInStore();
expect(vm.createTempEntry).toHaveBeenCalledWith({ expect(vm.createTempEntry).toHaveBeenCalledWith({
projectId: 'abcproject',
branchId: 'master',
parent: projectTree,
name: 'testing', name: 'testing',
type, type,
}); });
...@@ -76,31 +123,18 @@ describe('new file modal component', () => { ...@@ -76,31 +123,18 @@ describe('new file modal component', () => {
}); });
it('opens newly created file', (done) => { it('opens newly created file', (done) => {
vm.createEntryInStore(); if (type === 'blob') {
vm.createEntryInStore();
setTimeout(() => {
expect(vm.$store.state.openFiles.length).toBe(1);
expect(vm.$store.state.openFiles[0].name).toBe(type === 'blob' ? 'testing' : '.gitkeep');
done();
});
});
it(`creates ${type} in the current stores path`, (done) => {
vm.$store.state.path = 'app';
vm.createEntryInStore();
setTimeout(() => {
expect(vm.$store.state.tree[0].path).toBe('app/testing');
expect(vm.$store.state.tree[0].name).toBe('testing');
if (type === 'tree') { setTimeout(() => {
expect(vm.$store.state.tree[0].tree.length).toBe(1); expect(vm.$store.state.openFiles.length).toBe(1);
} expect(vm.$store.state.openFiles[0].name).toBe(type === 'blob' ? 'testing' : '.gitkeep');
done();
});
} else {
done(); done();
}); }
}); });
if (type === 'blob') { if (type === 'blob') {
...@@ -108,25 +142,27 @@ describe('new file modal component', () => { ...@@ -108,25 +142,27 @@ describe('new file modal component', () => {
vm.createEntryInStore(); vm.createEntryInStore();
setTimeout(() => { setTimeout(() => {
expect(vm.$store.state.tree.length).toBe(1); const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
expect(vm.$store.state.tree[0].name).toBe('testing'); expect(baseTree.length).toBe(1);
expect(vm.$store.state.tree[0].type).toBe('blob'); expect(baseTree[0].name).toBe('testing');
expect(vm.$store.state.tree[0].tempFile).toBeTruthy(); expect(baseTree[0].type).toBe('blob');
expect(baseTree[0].tempFile).toBeTruthy();
done(); done();
}); });
}); });
it('does not create temp file when file already exists', (done) => { it('does not create temp file when file already exists', (done) => {
vm.$store.state.tree.push(file('testing', '1', type)); const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
baseTree.push(file('testing', '1', type));
vm.createEntryInStore(); vm.createEntryInStore();
setTimeout(() => { setTimeout(() => {
expect(vm.$store.state.tree.length).toBe(1); expect(baseTree.length).toBe(1);
expect(vm.$store.state.tree[0].name).toBe('testing'); expect(baseTree[0].name).toBe('testing');
expect(vm.$store.state.tree[0].type).toBe('blob'); expect(baseTree[0].type).toBe('blob');
expect(vm.$store.state.tree[0].tempFile).toBeFalsy(); expect(baseTree[0].tempFile).toBeFalsy();
done(); done();
}); });
...@@ -135,48 +171,47 @@ describe('new file modal component', () => { ...@@ -135,48 +171,47 @@ describe('new file modal component', () => {
it('creates new tree', () => { it('creates new tree', () => {
vm.createEntryInStore(); vm.createEntryInStore();
expect(vm.$store.state.tree.length).toBe(1); const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
expect(vm.$store.state.tree[0].name).toBe('testing'); expect(baseTree.length).toBe(1);
expect(vm.$store.state.tree[0].type).toBe('tree'); expect(baseTree[0].name).toBe('testing');
expect(vm.$store.state.tree[0].tempFile).toBeTruthy(); expect(baseTree[0].type).toBe('tree');
expect(vm.$store.state.tree[0].tree.length).toBe(1); expect(baseTree[0].tempFile).toBeTruthy();
expect(vm.$store.state.tree[0].tree[0].name).toBe('.gitkeep');
}); });
it('creates multiple trees when entryName has slashes', () => { it('creates multiple trees when entryName has slashes', () => {
vm.entryName = 'app/test'; vm.entryName = 'app/test';
vm.createEntryInStore(); vm.createEntryInStore();
expect(vm.$store.state.tree.length).toBe(1); const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
expect(vm.$store.state.tree[0].name).toBe('app'); expect(baseTree.length).toBe(1);
expect(vm.$store.state.tree[0].tree[0].name).toBe('test'); expect(baseTree[0].name).toBe('app');
expect(vm.$store.state.tree[0].tree[0].tree[0].name).toBe('.gitkeep');
}); });
it('creates tree in existing tree', () => { it('creates tree in existing tree', () => {
vm.$store.state.tree.push(file('app', '1', 'tree')); const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
baseTree.push(file('app', '1', 'tree'));
vm.entryName = 'app/test'; vm.entryName = 'app/test';
vm.createEntryInStore(); vm.createEntryInStore();
expect(vm.$store.state.tree.length).toBe(1); expect(baseTree.length).toBe(1);
expect(vm.$store.state.tree[0].name).toBe('app'); expect(baseTree[0].name).toBe('app');
expect(vm.$store.state.tree[0].tempFile).toBeFalsy(); expect(baseTree[0].tempFile).toBeFalsy();
expect(vm.$store.state.tree[0].tree[0].tempFile).toBeTruthy(); expect(baseTree[0].tree[0].tempFile).toBeTruthy();
expect(vm.$store.state.tree[0].tree[0].name).toBe('test'); expect(baseTree[0].tree[0].name).toBe('test');
expect(vm.$store.state.tree[0].tree[0].tree[0].name).toBe('.gitkeep');
}); });
it('does not create new tree when already exists', () => { it('does not create new tree when already exists', () => {
vm.$store.state.tree.push(file('app', '1', 'tree')); const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
baseTree.push(file('app', '1', 'tree'));
vm.entryName = 'app'; vm.entryName = 'app';
vm.createEntryInStore(); vm.createEntryInStore();
expect(vm.$store.state.tree.length).toBe(1); expect(baseTree.length).toBe(1);
expect(vm.$store.state.tree[0].name).toBe('app'); expect(baseTree[0].name).toBe('app');
expect(vm.$store.state.tree[0].tempFile).toBeFalsy(); expect(baseTree[0].tempFile).toBeFalsy();
expect(vm.$store.state.tree[0].tree.length).toBe(0); expect(baseTree[0].tree.length).toBe(0);
}); });
} }
}); });
...@@ -188,6 +223,8 @@ describe('new file modal component', () => { ...@@ -188,6 +223,8 @@ describe('new file modal component', () => {
vm = createComponentWithStore(Component, store, { vm = createComponentWithStore(Component, store, {
type: 'tree', type: 'tree',
projectId: 'abcproject',
branchId: 'master',
path: '', path: '',
}).$mount('.js-test'); }).$mount('.js-test');
......
import Vue from 'vue'; import Vue from 'vue';
import upload from '~/repo/components/new_dropdown/upload.vue'; import upload from '~/ide/components/new_dropdown/upload.vue';
import store from '~/repo/stores'; import store from '~/ide/stores';
import service from '~/ide/services';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
import { resetStore } from '../../helpers'; import { resetStore } from '../../helpers';
describe('new dropdown upload', () => { describe('new dropdown upload', () => {
let vm; let vm;
let projectTree;
beforeEach(() => { beforeEach(() => {
spyOn(service, 'getProjectData').and.returnValue(Promise.resolve({
data: {
id: '123',
},
}));
spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
commit: {
id: '123branch',
},
}));
spyOn(service, 'getTreeData').and.returnValue(Promise.resolve({
headers: {
'page-title': 'test',
},
json: () => Promise.resolve({
last_commit_path: 'last_commit_path',
parent_tree_url: 'parent_tree_url',
path: '/',
trees: [{ name: 'tree' }],
blobs: [{ name: 'blob' }],
submodules: [{ name: 'submodule' }],
}),
}));
const Component = Vue.extend(upload); const Component = Vue.extend(upload);
store.state.projects.abcproject = {
web_url: '',
};
store.state.currentProjectId = 'abcproject';
store.state.trees = [];
store.state.trees['abcproject/mybranch'] = {
tree: [],
};
projectTree = store.state.trees['abcproject/mybranch'];
vm = createComponentWithStore(Component, store, { vm = createComponentWithStore(Component, store, {
branchId: 'master',
path: '', path: '',
parent: projectTree,
}); });
vm.entryName = 'testing';
vm.$mount(); vm.$mount();
}); });
...@@ -65,23 +107,33 @@ describe('new dropdown upload', () => { ...@@ -65,23 +107,33 @@ describe('new dropdown upload', () => {
vm.createFile(target, file, true); vm.createFile(target, file, true);
vm.$nextTick(() => { vm.$nextTick(() => {
expect(vm.$store.state.tree.length).toBe(1); const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
expect(vm.$store.state.tree[0].name).toBe(file.name); expect(baseTree.length).toBe(1);
expect(vm.$store.state.tree[0].content).toBe(target.result); expect(baseTree[0].name).toBe(file.name);
expect(baseTree[0].content).toBe(target.result);
done(); done();
}); });
}); });
it('creates new file in path', (done) => { it('creates new file in path', (done) => {
vm.$store.state.path = 'testing'; const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
const tree = {
type: 'tree',
name: 'testing',
path: 'testing',
tree: [],
};
baseTree.push(tree);
vm.parent = tree;
vm.createFile(target, file, true); vm.createFile(target, file, true);
vm.$nextTick(() => { vm.$nextTick(() => {
expect(vm.$store.state.tree.length).toBe(1); expect(baseTree.length).toBe(1);
expect(vm.$store.state.tree[0].name).toBe(file.name); expect(baseTree[0].tree[0].name).toBe(file.name);
expect(vm.$store.state.tree[0].content).toBe(target.result); expect(baseTree[0].tree[0].content).toBe(target.result);
expect(vm.$store.state.tree[0].path).toBe(`testing/${file.name}`); expect(baseTree[0].tree[0].path).toBe(`testing/${file.name}`);
done(); done();
}); });
...@@ -91,10 +143,11 @@ describe('new dropdown upload', () => { ...@@ -91,10 +143,11 @@ describe('new dropdown upload', () => {
vm.createFile(binaryTarget, file, false); vm.createFile(binaryTarget, file, false);
vm.$nextTick(() => { vm.$nextTick(() => {
expect(vm.$store.state.tree.length).toBe(1); const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
expect(vm.$store.state.tree[0].name).toBe(file.name); expect(baseTree.length).toBe(1);
expect(vm.$store.state.tree[0].content).toBe(binaryTarget.result.split('base64,')[1]); expect(baseTree[0].name).toBe(file.name);
expect(vm.$store.state.tree[0].base64).toBe(true); expect(baseTree[0].content).toBe(binaryTarget.result.split('base64,')[1]);
expect(baseTree[0].base64).toBe(true);
done(); done();
}); });
......
import Vue from 'vue'; import Vue from 'vue';
import * as urlUtils from '~/lib/utils/url_utility'; import * as urlUtils from '~/lib/utils/url_utility';
import store from '~/repo/stores'; import store from '~/ide/stores';
import service from '~/repo/services'; import service from '~/ide/services';
import repoCommitSection from '~/repo/components/repo_commit_section.vue'; import repoCommitSection from '~/ide/components/repo_commit_section.vue';
import getSetTimeoutPromise from '../../helpers/set_timeout_promise_helper'; import getSetTimeoutPromise from '../../helpers/set_timeout_promise_helper';
import { file, resetStore } from '../helpers'; import { file, resetStore } from '../helpers';
...@@ -16,6 +16,18 @@ describe('RepoCommitSection', () => { ...@@ -16,6 +16,18 @@ describe('RepoCommitSection', () => {
store, store,
}).$mount(); }).$mount();
comp.$store.state.currentProjectId = 'abcproject';
comp.$store.state.currentBranchId = 'master';
comp.$store.state.projects.abcproject = {
web_url: '',
branches: {
master: {
workingReference: '1',
},
},
};
comp.$store.state.rightPanelCollapsed = false;
comp.$store.state.currentBranch = 'master'; comp.$store.state.currentBranch = 'master';
comp.$store.state.openFiles = [file(), file()]; comp.$store.state.openFiles = [file(), file()];
comp.$store.state.openFiles.forEach(f => Object.assign(f, { comp.$store.state.openFiles.forEach(f => Object.assign(f, {
...@@ -29,7 +41,19 @@ describe('RepoCommitSection', () => { ...@@ -29,7 +41,19 @@ describe('RepoCommitSection', () => {
beforeEach((done) => { beforeEach((done) => {
vm = createComponent(); vm = createComponent();
vm.collapsed = false; spyOn(service, 'getTreeData').and.returnValue(Promise.resolve({
headers: {
'page-title': 'test',
},
json: () => Promise.resolve({
last_commit_path: 'last_commit_path',
parent_tree_url: 'parent_tree_url',
path: '/',
trees: [{ name: 'tree' }],
blobs: [{ name: 'blob' }],
submodules: [{ name: 'submodule' }],
}),
}));
Vue.nextTick(done); Vue.nextTick(done);
}); });
...@@ -45,7 +69,6 @@ describe('RepoCommitSection', () => { ...@@ -45,7 +69,6 @@ describe('RepoCommitSection', () => {
const submitCommit = vm.$el.querySelector('form .btn'); const submitCommit = vm.$el.querySelector('form .btn');
expect(vm.$el.querySelector('.multi-file-commit-form')).not.toBeNull(); expect(vm.$el.querySelector('.multi-file-commit-form')).not.toBeNull();
expect(vm.$el.querySelector('.multi-file-commit-panel-section header').textContent.trim()).toEqual('Staged');
expect(changedFileElements.length).toEqual(2); expect(changedFileElements.length).toEqual(2);
changedFileElements.forEach((changedFile, i) => { changedFileElements.forEach((changedFile, i) => {
......
import Vue from 'vue'; import Vue from 'vue';
import store from '~/repo/stores'; import store from '~/ide/stores';
import repoEditButton from '~/repo/components/repo_edit_button.vue'; import repoEditButton from '~/ide/components/repo_edit_button.vue';
import { file, resetStore } from '../helpers'; import { file, resetStore } from '../helpers';
describe('RepoEditButton', () => { describe('RepoEditButton', () => {
...@@ -32,7 +32,7 @@ describe('RepoEditButton', () => { ...@@ -32,7 +32,7 @@ describe('RepoEditButton', () => {
vm.$mount(); vm.$mount();
expect(vm.$el.querySelector('.btn')).not.toBeNull(); expect(vm.$el.querySelector('.btn')).not.toBeNull();
expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Edit'); expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Cancel edit');
}); });
it('renders edit button with cancel text', () => { it('renders edit button with cancel text', () => {
...@@ -50,7 +50,7 @@ describe('RepoEditButton', () => { ...@@ -50,7 +50,7 @@ describe('RepoEditButton', () => {
vm.$el.querySelector('.btn').click(); vm.$el.querySelector('.btn').click();
vm.$nextTick(() => { vm.$nextTick(() => {
expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Cancel edit'); expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Edit');
done(); done();
}); });
......
import Vue from 'vue'; import Vue from 'vue';
import store from '~/repo/stores'; import store from '~/ide/stores';
import repoEditor from '~/repo/components/repo_editor.vue'; import repoEditor from '~/ide/components/repo_editor.vue';
import monacoLoader from '~/repo/monaco_loader'; import monacoLoader from '~/ide/monaco_loader';
import { file, resetStore } from '../helpers'; import { file, resetStore } from '../helpers';
describe('RepoEditor', () => { describe('RepoEditor', () => {
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment