BigW Consortium Gitlab

Commit eaeeac2f by Filipa Lacerda

Merge branch '44846-improve-web-ide-left-panel-and-modes' into 'master'

Resolve "Improve Web IDE left panel and modes" Closes #44846 See merge request gitlab-org/gitlab-ce!18581
parents 9f7a6742 398ee684
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { activityBarViews } from '../constants';
export default {
components: {
Icon,
},
directives: {
tooltip,
},
computed: {
...mapGetters(['currentProject', 'hasChanges']),
...mapState(['currentActivityView']),
goBackUrl() {
return document.referrer || this.currentProject.web_url;
},
},
methods: {
...mapActions(['updateActivityBarView']),
},
activityBarViews,
};
</script>
<template>
<nav class="ide-activity-bar">
<ul class="list-unstyled">
<li v-once>
<a
v-tooltip
data-container="body"
data-placement="right"
:href="goBackUrl"
class="ide-sidebar-link"
:title="s__('IDE|Go back')"
:aria-label="s__('IDE|Go back')"
>
<icon
:size="16"
name="go-back"
/>
</a>
</li>
<li>
<button
v-tooltip
data-container="body"
data-placement="right"
type="button"
class="ide-sidebar-link js-ide-edit-mode"
:class="{
active: currentActivityView === $options.activityBarViews.edit
}"
@click.prevent="updateActivityBarView($options.activityBarViews.edit)"
:title="s__('IDE|Edit')"
:aria-label="s__('IDE|Edit')"
>
<icon
name="code"
/>
</button>
</li>
<li>
<button
v-tooltip
data-container="body"
data-placement="right"
type="button"
class="ide-sidebar-link js-ide-review-mode"
:class="{
active: currentActivityView === $options.activityBarViews.review
}"
@click.prevent="updateActivityBarView($options.activityBarViews.review)"
:title="s__('IDE|Review')"
:aria-label="s__('IDE|Review')"
>
<icon
name="file-modified"
/>
</button>
</li>
<li v-show="hasChanges">
<button
v-tooltip
data-container="body"
data-placement="right"
type="button"
class="ide-sidebar-link js-ide-commit-mode"
:class="{
active: currentActivityView === $options.activityBarViews.commit
}"
@click.prevent="updateActivityBarView($options.activityBarViews.commit)"
:title="s__('IDE|Commit')"
:aria-label="s__('IDE|Commit')"
>
<icon
name="commit"
/>
</button>
</li>
</ul>
</nav>
</template>
<script>
import { mapState } from 'vuex';
import { mapActions, mapState } from 'vuex';
import { sprintf, __ } from '~/locale';
import * as consts from '../../stores/modules/commit/constants';
import RadioGroup from './radio_group.vue';
......@@ -9,7 +9,7 @@ export default {
RadioGroup,
},
computed: {
...mapState(['currentBranchId']),
...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']),
commitToCurrentBranchText() {
return sprintf(
__('Commit to %{branchName} branch'),
......@@ -17,6 +17,17 @@ export default {
false,
);
},
disableMergeRequestRadio() {
return this.changedFiles.length > 0 && this.stagedFiles.length > 0;
},
},
mounted() {
if (this.disableMergeRequestRadio) {
this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH);
}
},
methods: {
...mapActions('commit', ['updateCommitAction']),
},
commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
......@@ -44,6 +55,7 @@ export default {
:value="$options.commitToNewBranchMR"
:label="__('Create a new branch and merge request')"
:show-input="true"
:disabled="disableMergeRequestRadio"
/>
</div>
</template>
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { mapState } from 'vuex';
export default {
components: {
Icon,
},
directives: {
tooltip,
},
props: {
noChangesStateSvgPath: {
type: String,
required: true,
},
},
computed: {
...mapState(['lastCommitMsg', 'rightPanelCollapsed', 'changedFiles', 'stagedFiles']),
...mapGetters(['collapseButtonIcon', 'collapseButtonTooltip']),
},
methods: {
...mapActions(['toggleRightPanelCollapsed']),
...mapState(['lastCommitMsg', 'noChangesStateSvgPath']),
},
};
</script>
......@@ -31,31 +13,8 @@ export default {
v-if="!lastCommitMsg"
class="multi-file-commit-panel-section ide-commit-empty-state js-empty-state"
>
<header
class="multi-file-commit-panel-header"
:class="{
'is-collapsed': rightPanelCollapsed,
}"
>
<button
v-tooltip
:title="collapseButtonTooltip"
data-container="body"
data-placement="left"
type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
:aria-label="__('Toggle sidebar')"
@click.stop="toggleRightPanelCollapsed"
>
<icon
:name="collapseButtonIcon"
:size="18"
/>
</button>
</header>
<div
class="ide-commit-empty-state-container"
v-if="!rightPanelCollapsed"
>
<div class="svg-content svg-80">
<img :src="noChangesStateSvgPath" />
......
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import { sprintf, __ } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import CommitMessageField from './message_field.vue';
import Actions from './actions.vue';
import SuccessMessage from './success_message.vue';
import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT, COMMIT_ITEM_PADDING } from '../../constants';
export default {
components: {
Actions,
LoadingButton,
CommitMessageField,
SuccessMessage,
},
data() {
return {
isCompact: true,
componentHeight: null,
};
},
computed: {
...mapState(['changedFiles', 'stagedFiles', 'currentActivityView', 'lastCommitMsg']),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters(['hasChanges']),
...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
overviewText() {
return sprintf(
__(
'<strong>%{changedFilesLength} unstaged</strong> and <strong>%{stagedFilesLength} staged</strong> changes',
),
{
stagedFilesLength: this.stagedFiles.length,
changedFilesLength: this.changedFiles.length,
},
);
},
},
watch: {
currentActivityView() {
if (this.lastCommitMsg) {
this.isCompact = false;
} else {
this.isCompact = !(
this.currentActivityView === activityBarViews.commit &&
window.innerHeight >= MAX_WINDOW_HEIGHT_COMPACT
);
}
},
lastCommitMsg() {
this.isCompact =
this.currentActivityView !== activityBarViews.commit && this.lastCommitMsg === '';
},
},
methods: {
...mapActions(['updateActivityBarView']),
...mapActions('commit', ['updateCommitMessage', 'discardDraft', 'commitChanges']),
toggleIsSmall() {
this.updateActivityBarView(activityBarViews.commit)
.then(() => {
this.isCompact = !this.isCompact;
})
.catch(e => {
throw e;
});
},
beforeEnterTransition() {
const elHeight = this.isCompact
? this.$refs.formEl && this.$refs.formEl.offsetHeight
: this.$refs.compactEl && this.$refs.compactEl.offsetHeight;
this.componentHeight = elHeight + COMMIT_ITEM_PADDING;
},
enterTransition() {
this.$nextTick(() => {
const elHeight = this.isCompact
? this.$refs.compactEl && this.$refs.compactEl.offsetHeight
: this.$refs.formEl && this.$refs.formEl.offsetHeight;
this.componentHeight = elHeight + COMMIT_ITEM_PADDING;
});
},
afterEndTransition() {
this.componentHeight = null;
},
},
activityBarViews,
};
</script>
<template>
<div
class="multi-file-commit-form"
:class="{
'is-compact': isCompact,
'is-full': !isCompact
}"
:style="{
height: componentHeight ? `${componentHeight}px` : null,
}"
>
<transition
name="commit-form-slide-up"
@before-enter="beforeEnterTransition"
@enter="enterTransition"
@after-enter="afterEndTransition"
>
<div
v-if="isCompact"
class="commit-form-compact"
ref="compactEl"
>
<button
type="button"
:disabled="!hasChanges"
class="btn btn-primary btn-sm btn-block"
@click="toggleIsSmall"
>
{{ __('Commit') }}
</button>
<p
class="text-center"
v-html="overviewText"
></p>
</div>
<form
v-if="!isCompact"
class="form-horizontal"
@submit.prevent.stop="commitChanges"
ref="formEl"
>
<transition name="fade">
<success-message
v-show="lastCommitMsg"
/>
</transition>
<commit-message-field
:text="commitMessage"
@input="updateCommitMessage"
/>
<div class="clearfix prepend-top-15">
<actions />
<loading-button
:loading="submitCommitLoading"
:disabled="commitButtonDisabled"
container-class="btn btn-success btn-sm pull-left"
:label="__('Commit')"
@click="commitChanges"
/>
<button
v-if="!discardDraftButtonDisabled"
type="button"
class="btn btn-default btn-sm pull-right"
@click="discardDraft"
>
{{ __('Discard draft') }}
</button>
<button
v-else
type="button"
class="btn btn-default btn-sm pull-right"
@click="toggleIsSmall"
>
{{ __('Collapse') }}
</button>
</div>
</form>
</transition>
</div>
</template>
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import { mapActions } from 'vuex';
import { __, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import ListItem from './list_item.vue';
import ListCollapsed from './list_collapsed.vue';
export default {
components: {
Icon,
ListItem,
ListCollapsed,
},
directives: {
tooltip,
......@@ -24,11 +22,6 @@ export default {
type: Array,
required: true,
},
showToggle: {
type: Boolean,
required: false,
default: true,
},
iconName: {
type: String,
required: true,
......@@ -51,9 +44,12 @@ export default {
default: false,
},
},
data() {
return {
showActionButton: false,
};
},
computed: {
...mapState(['rightPanelCollapsed']),
...mapGetters(['collapseButtonIcon', 'collapseButtonTooltip']),
titleText() {
return sprintf(__('%{title} changes'), {
title: this.title,
......@@ -61,10 +57,13 @@ export default {
},
},
methods: {
...mapActions(['toggleRightPanelCollapsed', 'stageAllChanges', 'unstageAllChanges']),
...mapActions(['stageAllChanges', 'unstageAllChanges']),
actionBtnClicked() {
this[this.action]();
},
setShowActionButton(show) {
this.showActionButton = show;
},
},
};
</script>
......@@ -72,19 +71,14 @@ export default {
<template>
<div
class="ide-commit-list-container"
:class="{
'is-collapsed': rightPanelCollapsed,
}"
>
<header
class="multi-file-commit-panel-header"
@mouseenter="setShowActionButton(true)"
@mouseleave="setShowActionButton(false)"
>
<div
v-if="!rightPanelCollapsed"
class="multi-file-commit-panel-header-title"
:class="{
'append-right-10': showToggle,
}"
>
<icon
v-once
......@@ -92,7 +86,14 @@ export default {
:size="18"
/>
{{ titleText }}
<span
v-show="!showActionButton"
class="ide-commit-file-count"
>
{{ fileList.length }}
</span>
<button
v-show="showActionButton"
type="button"
class="btn btn-blank btn-link ide-staged-action-btn"
@click="actionBtnClicked"
......@@ -100,52 +101,28 @@ export default {
{{ actionBtnText }}
</button>
</div>
<button
v-if="showToggle"
v-tooltip
:title="collapseButtonTooltip"
data-container="body"
data-placement="left"
type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
:aria-label="__('Toggle sidebar')"
@click.stop="toggleRightPanelCollapsed"
>
<icon
:name="collapseButtonIcon"
:size="18"
/>
</button>
</header>
<list-collapsed
v-if="rightPanelCollapsed"
:files="fileList"
:icon-name="iconName"
:title="title"
/>
<template v-else>
<ul
v-if="fileList.length"
class="multi-file-commit-list list-unstyled append-bottom-0"
>
<li
v-for="file in fileList"
:key="file.key"
>
<list-item
:file="file"
:action-component="itemActionComponent"
:key-prefix="title"
:staged-list="stagedList"
/>
</li>
</ul>
<p
v-else
class="multi-file-commit-list help-block"
<ul
v-if="fileList.length"
class="multi-file-commit-list list-unstyled append-bottom-0"
>
<li
v-for="file in fileList"
:key="file.key"
>
{{ __('No changes') }}
</p>
</template>
<list-item
:file="file"
:action-component="itemActionComponent"
:key-prefix="title"
:staged-list="stagedList"
/>
</li>
</ul>
<p
v-else
class="multi-file-commit-list help-block"
>
{{ __('No changes') }}
</p>
</div>
</template>
......@@ -3,6 +3,7 @@ import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import StageButton from './stage_button.vue';
import UnstageButton from './unstage_button.vue';
import { viewerTypes } from '../../constants';
export default {
components: {
......@@ -53,7 +54,7 @@ export default {
keyPrefix: this.keyPrefix.toLowerCase(),
}).then(changeViewer => {
if (changeViewer) {
this.updateViewer('diff');
this.updateViewer(viewerTypes.diff);
}
});
},
......
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
......@@ -26,10 +27,20 @@ export default {
required: false,
default: false,
},
disabled: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapState('commit', ['commitAction']),
...mapGetters('commit', ['newBranchName']),
tooltipTitle() {
return this.disabled
? __('This option is disabled while you still have unstaged changes')
: '';
},
},
methods: {
...mapActions('commit', ['updateCommitAction', 'updateBranchName']),
......@@ -39,19 +50,28 @@ export default {
<template>
<fieldset>
<label>
<label
v-tooltip
:title="tooltipTitle"
:class="{
'is-disabled': disabled
}"
>
<input
type="radio"
name="commit-action"
:value="value"
@change="updateCommitAction($event.target.value)"
:checked="checked"
v-once
:checked="commitAction === value"
:disabled="disabled"
/>
<span class="prepend-left-10">
<template v-if="label">
<span
v-if="label"
class="ide-radio-label"
>
{{ label }}
</template>
</span>
<slot v-else></slot>
</span>
</label>
......
......@@ -2,14 +2,8 @@
import { mapState } from 'vuex';
export default {
props: {
committedStateSvgPath: {
type: String,
required: true,
},
},
computed: {
...mapState(['lastCommitMsg']),
...mapState(['lastCommitMsg', 'committedStateSvgPath']),
},
};
</script>
......
<script>
import Icon from '~/vue_shared/components/icon.vue';
import { __, sprintf } from '~/locale';
import { viewerTypes } from '../constants';
export default {
components: {
Icon,
},
props: {
hasChanges: {
type: Boolean,
required: false,
default: false,
},
mergeRequestId: {
type: String,
required: false,
default: '',
},
viewer: {
type: String,
required: true,
},
showShadow: {
type: Boolean,
mergeRequestId: {
type: Number,
required: true,
},
},
......@@ -38,84 +25,45 @@ export default {
this.$emit('click', mode);
},
},
viewerTypes,
};
</script>
<template>
<div
class="dropdown"
:class="{
shadow: showShadow,
}"
>
<button
type="button"
class="btn btn-primary btn-sm"
:class="{
'btn-inverted': hasChanges,
}"
class="btn btn-link"
data-toggle="dropdown"
>
<template v-if="viewer === 'mrdiff' && mergeRequestId">
{{ mergeReviewLine }}
</template>
<template v-else-if="viewer === 'editor'">
{{ __('Editing') }}
</template>
<template v-else>
{{ __('Reviewing') }}
</template>
<icon
name="angle-down"
:size="12"
css-classes="caret-down"
/>
{{ __('Edit') }}
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
<ul>
<template v-if="mergeRequestId">
<li>
<a
href="#"
@click.prevent="changeMode('mrdiff')"
:class="{
'is-active': viewer === 'mrdiff',
}"
>
<strong class="dropdown-menu-inner-title">
{{ mergeReviewLine }}
</strong>
<span class="dropdown-menu-inner-content">
{{ __('Compare changes with the merge request target branch') }}
</span>
</a>
</li>
<li
role="separator"
class="divider"
>
</li>
</template>
<li>
<a
href="#"
@click.prevent="changeMode('editor')"
@click.prevent="changeMode($options.viewerTypes.mr)"
:class="{
'is-active': viewer === 'editor',
'is-active': viewer === $options.viewerTypes.mr,
}"
>
<strong class="dropdown-menu-inner-title">{{ __('Editing') }}</strong>
<strong class="dropdown-menu-inner-title">
{{ mergeReviewLine }}
</strong>
<span class="dropdown-menu-inner-content">
{{ __('View and edit lines') }}
{{ __('Compare changes with the merge request target branch') }}
</span>
</a>
</li>
<li>
<a
href="#"
@click.prevent="changeMode('diff')"
@click.prevent="changeMode($options.viewerTypes.diff)"
:class="{
'is-active': viewer === 'diff',
'is-active': viewer === $options.viewerTypes.diff,
}"
>
<strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong>
......
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import Mousetrap from 'mousetrap';
import ideSidebar from './ide_side_bar.vue';
import ideContextbar from './ide_context_bar.vue';
import repoTabs from './repo_tabs.vue';
import ideStatusBar from './ide_status_bar.vue';
import repoEditor from './repo_editor.vue';
import FindFile from './file_finder/index.vue';
import Mousetrap from 'mousetrap';
import { mapActions, mapState, mapGetters } from 'vuex';
import IdeSidebar from './ide_side_bar.vue';
import RepoTabs from './repo_tabs.vue';
import IdeStatusBar from './ide_status_bar.vue';
import RepoEditor from './repo_editor.vue';
import FindFile from './file_finder/index.vue';
const originalStopCallback = Mousetrap.stopCallback;
const originalStopCallback = Mousetrap.stopCallback;
export default {
components: {
ideSidebar,
ideContextbar,
repoTabs,
ideStatusBar,
repoEditor,
FindFile,
},
props: {
emptyStateSvgPath: {
type: String,
required: true,
},
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
},
computed: {
...mapState([
'changedFiles',
'openFiles',
'viewer',
'currentMergeRequestId',
'fileFindVisible',
]),
...mapGetters(['activeFile', 'hasChanges']),
},
mounted() {
const returnValue = 'Are you sure you want to lose unsaved changes?';
window.onbeforeunload = e => {
if (!this.changedFiles.length) return undefined;
export default {
components: {
IdeSidebar,
RepoTabs,
IdeStatusBar,
RepoEditor,
FindFile,
},
computed: {
...mapState([
'changedFiles',
'openFiles',
'viewer',
'currentMergeRequestId',
'fileFindVisible',
'emptyStateSvgPath',
]),
...mapGetters(['activeFile', 'hasChanges']),
},
mounted() {
const returnValue = 'Are you sure you want to lose unsaved changes?';
window.onbeforeunload = e => {
if (!this.changedFiles.length) return undefined;
Object.assign(e, {
returnValue,
});
return returnValue;
};
Object.assign(e, {
returnValue,
});
return returnValue;
};
Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
if (e.preventDefault) {
e.preventDefault();
}
Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
if (e.preventDefault) {
e.preventDefault();
}
this.toggleFileFinder(!this.fileFindVisible);
});
this.toggleFileFinder(!this.fileFindVisible);
});
Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
},
methods: {
...mapActions(['toggleFileFinder']),
mousetrapStopCallback(e, el, combo) {
if (combo === 't' && el.classList.contains('dropdown-input-field')) {
return true;
} else if (combo === 'command+p' || combo === 'ctrl+p') {
return false;
}
Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
},
methods: {
...mapActions(['toggleFileFinder']),
mousetrapStopCallback(e, el, combo) {
if (combo === 't' && el.classList.contains('dropdown-input-field')) {
return true;
} else if (combo === 'command+p' || combo === 'ctrl+p') {
return false;
}
return originalStopCallback(e, el, combo);
},
return originalStopCallback(e, el, combo);
},
};
},
};
</script>
<template>
......@@ -136,9 +121,5 @@
</div>
</template>
</div>
<ide-contextbar
:no-changes-state-svg-path="noChangesStateSvgPath"
:committed-state-svg-path="committedStateSvgPath"
/>
</div>
</template>
<script>
import icon from '~/vue_shared/components/icon.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import repoCommitSection from './repo_commit_section.vue';
import ResizablePanel from './resizable_panel.vue';
export default {
components: {
repoCommitSection,
icon,
panelResizer,
ResizablePanel,
},
props: {
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
},
};
</script>
<template>
<resizable-panel
:collapsible="true"
:initial-width="340"
side="right"
>
<div
class="multi-file-commit-panel-section"
>
<repo-commit-section
:no-changes-state-svg-path="noChangesStateSvgPath"
:committed-state-svg-path="committedStateSvgPath"
/>
</div>
</resizable-panel>
</template>
<script>
import icon from '~/vue_shared/components/icon.vue';
export default {
components: {
icon,
},
props: {
projectUrl: {
type: String,
required: true,
},
},
computed: {
goBackUrl() {
return document.referrer || this.projectUrl;
},
},
};
</script>
<template>
<nav
class="ide-external-links"
v-once
>
<p>
<a
:href="goBackUrl"
class="ide-sidebar-link"
>
<icon
:size="16"
class="append-right-8"
name="go-back"
/>
<span class="ide-external-links-text">
{{ s__('Go back') }}
</span>
</a>
</p>
</nav>
</template>
<script>
import icon from '~/vue_shared/components/icon.vue';
import repoTree from './ide_repo_tree.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 str-truncated ref-name">
<icon
name="branch"
:size="12"
/>
{{ branch.name }}
</div>
<div class="branch-header-btns">
<new-dropdown
:project-id="projectId"
:branch="branch.name"
path=""
/>
</div>
</div>
<repo-tree
:tree="branch.tree"
/>
</div>
</template>
<script>
import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
import Identicon from '../../vue_shared/components/identicon.vue';
import BranchesTree from './ide_project_branches_tree.vue';
import ExternalLinks from './ide_external_links.vue';
export default {
components: {
BranchesTree,
ExternalLinks,
ProjectAvatarImage,
Identicon,
},
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
v-if="project.avatar_url"
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>
<identicon
v-else
size-class="s40"
:entity-id="project.id"
:entity-name="project.name"
/>
<div class="sidebar-context-title">
{{ project.name }}
</div>
</a>
</div>
<external-links
:project-url="project.web_url"
/>
<div class="multi-file-commit-panel-inner-scroll">
<branches-tree
v-for="branch in project.branches"
:key="branch.name"
:project-id="project.path_with_namespace"
:branch="branch"
/>
</div>
</div>
</template>
<script>
import { mapGetters, mapState, mapActions } from 'vuex';
import IdeTreeList from './ide_tree_list.vue';
import EditorModeDropdown from './editor_mode_dropdown.vue';
import { viewerTypes } from '../constants';
export default {
components: {
IdeTreeList,
EditorModeDropdown,
},
computed: {
...mapGetters(['currentMergeRequest']),
...mapState(['viewer']),
showLatestChangesText() {
return !this.currentMergeRequest || this.viewer === viewerTypes.diff;
},
showMergeRequestText() {
return this.currentMergeRequest && this.viewer === viewerTypes.mr;
},
},
mounted() {
this.$nextTick(() => {
this.updateViewer(this.currentMergeRequest ? viewerTypes.mr : viewerTypes.diff);
});
},
methods: {
...mapActions(['updateViewer']),
},
};
</script>
<template>
<ide-tree-list
:viewer-type="viewer"
header-class="ide-review-header"
:disable-action-dropdown="true"
>
<template
slot="header"
>
<div class="ide-review-button-holder">
{{ __('Review') }}
<editor-mode-dropdown
v-if="currentMergeRequest"
:viewer="viewer"
:merge-request-id="currentMergeRequest.iid"
@click="updateViewer"
/>
</div>
<div class="prepend-top-5 ide-review-sub-header">
<template v-if="showLatestChangesText">
{{ __('Latest changes') }}
</template>
<template v-else-if="showMergeRequestText">
{{ __('Merge request') }}
(<a :href="currentMergeRequest.web_url">!{{ currentMergeRequest.iid }}</a>)
</template>
</div>
</template>
</ide-tree-list>
</template>
<script>
import { mapState, mapGetters } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import projectTree from './ide_project_tree.vue';
import ResizablePanel from './resizable_panel.vue';
import { mapState, mapGetters } from 'vuex';
import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import Identicon from '../../vue_shared/components/identicon.vue';
import IdeTree from './ide_tree.vue';
import ResizablePanel from './resizable_panel.vue';
import ActivityBar from './activity_bar.vue';
import CommitSection from './repo_commit_section.vue';
import CommitForm from './commit_sidebar/form.vue';
import IdeReview from './ide_review.vue';
import SuccessMessage from './commit_sidebar/success_message.vue';
import { activityBarViews } from '../constants';
export default {
components: {
projectTree,
icon,
panelResizer,
skeletonLoadingContainer,
ResizablePanel,
export default {
directives: {
tooltip,
},
components: {
Icon,
PanelResizer,
SkeletonLoadingContainer,
ResizablePanel,
ActivityBar,
ProjectAvatarImage,
Identicon,
CommitSection,
IdeTree,
CommitForm,
IdeReview,
SuccessMessage,
},
data() {
return {
showTooltip: false,
};
},
computed: {
...mapState([
'loading',
'currentBranchId',
'currentActivityView',
'changedFiles',
'stagedFiles',
'lastCommitMsg',
]),
...mapGetters(['currentProject', 'someUncommitedChanges']),
showSuccessMessage() {
return (
this.currentActivityView === activityBarViews.edit &&
(this.lastCommitMsg && !this.someUncommitedChanges)
);
},
computed: {
...mapState([
'loading',
]),
...mapGetters([
'projectsWithTrees',
]),
branchTooltipTitle() {
return this.showTooltip ? this.currentBranchId : undefined;
},
};
},
watch: {
currentBranchId() {
this.$nextTick(() => {
this.showTooltip = this.$refs.branchId.scrollWidth > this.$refs.branchId.offsetWidth;
});
},
},
};
</script>
<template>
<resizable-panel
:collapsible="false"
:initial-width="290"
:initial-width="340"
side="left"
>
<activity-bar
v-if="!loading"
/>
<div class="multi-file-commit-panel-inner">
<template v-if="loading">
<div
......@@ -41,11 +87,54 @@
<skeleton-loading-container />
</div>
</template>
<project-tree
v-for="project in projectsWithTrees"
:key="project.id"
:project="project"
/>
<template v-else>
<div class="context-header ide-context-header">
<a
:href="currentProject.web_url"
>
<div
v-if="currentProject.avatar_url"
class="avatar-container s40 project-avatar"
>
<project-avatar-image
class="avatar-container project-avatar"
:link-href="currentProject.path"
:img-src="currentProject.avatar_url"
:img-alt="currentProject.name"
:img-size="40"
/>
</div>
<identicon
v-else
size-class="s40"
:entity-id="currentProject.id"
:entity-name="currentProject.name"
/>
<div class="ide-sidebar-project-title">
<div class="sidebar-context-title">
{{ currentProject.name }}
</div>
<div
class="sidebar-context-title ide-sidebar-branch-title"
ref="branchId"
v-tooltip
:title="branchTooltipTitle"
>
<icon
name="branch"
css-classes="append-right-5"
/>{{ currentBranchId }}
</div>
</div>
</a>
</div>
<div class="multi-file-commit-panel-inner-scroll">
<component
:is="currentActivityView"
/>
</div>
<commit-form />
</template>
</div>
</resizable-panel>
</template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import NewDropdown from './new_dropdown/index.vue';
import IdeTreeList from './ide_tree_list.vue';
export default {
components: {
NewDropdown,
IdeTreeList,
},
computed: {
...mapState(['currentBranchId']),
...mapGetters(['currentProject', 'currentTree', 'activeFile']),
},
mounted() {
if (this.activeFile && this.activeFile.pending) {
this.$router.push(`/project${this.activeFile.url}`, () => {
this.updateViewer('editor');
});
}
},
methods: {
...mapActions(['updateViewer']),
},
};
</script>
<template>
<ide-tree-list
viewer-type="editor"
>
<template
slot="header"
>
{{ __('Edit') }}
<new-dropdown
:project-id="currentProject.name_with_namespace"
:branch="currentBranchId"
/>
</template>
</ide-tree-list>
</template>
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import RepoFile from './repo_file.vue';
import NewDropdown from './new_dropdown/index.vue';
export default {
components: {
Icon,
RepoFile,
SkeletonLoadingContainer,
NewDropdown,
},
props: {
tree: {
type: Object,
viewerType: {
type: String,
required: true,
},
headerClass: {
type: String,
required: false,
default: null,
},
disableActionDropdown: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapState(['currentBranchId']),
...mapGetters(['currentProject', 'currentTree']),
showLoading() {
return !this.currentTree || this.currentTree.loading;
},
},
mounted() {
this.updateViewer(this.viewerType);
},
methods: {
...mapActions(['updateViewer']),
},
};
</script>
......@@ -20,7 +48,7 @@ export default {
<div
class="ide-file-list"
>
<template v-if="tree.loading">
<template v-if="showLoading">
<div
class="multi-file-loading-container"
v-for="n in 3"
......@@ -30,11 +58,18 @@ export default {
</div>
</template>
<template v-else>
<header
class="ide-tree-header"
:class="headerClass"
>
<slot name="header"></slot>
</header>
<repo-file
v-for="file in tree.tree"
v-for="file in currentTree.tree"
:key="file.key"
:file="file"
:level="0"
:disable-action-dropdown="disableActionDropdown"
/>
</template>
</div>
......
......@@ -17,7 +17,8 @@ export default {
},
path: {
type: String,
required: true,
required: false,
default: '',
},
},
data() {
......
......@@ -3,13 +3,10 @@ import { mapState, mapActions, mapGetters } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import CommitFilesList from './commit_sidebar/list.vue';
import EmptyState from './commit_sidebar/empty_state.vue';
import CommitMessageField from './commit_sidebar/message_field.vue';
import SuccessMessage from './commit_sidebar/success_message.vue';
import * as consts from '../stores/modules/commit/constants';
import Actions from './commit_sidebar/actions.vue';
import { activityBarViews } from '../constants';
export default {
components: {
......@@ -17,42 +14,50 @@ export default {
Icon,
CommitFilesList,
EmptyState,
SuccessMessage,
Actions,
LoadingButton,
CommitMessageField,
},
directives: {
tooltip,
},
props: {
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
},
computed: {
...mapState([
'changedFiles',
'stagedFiles',
'rightPanelCollapsed',
'lastCommitMsg',
'unusedSeal',
]),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges']),
...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
showStageUnstageArea() {
return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal);
},
someUncommitedChanges() {
return !!(this.changedFiles.length || this.stagedFiles.length);
},
watch: {
hasChanges() {
if (!this.hasChanges) {
this.updateActivityBarView(activityBarViews.edit);
}
},
...mapState(['changedFiles', 'stagedFiles', 'rightPanelCollapsed', 'lastCommitMsg', 'unusedSeal']),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
},
mounted() {
if (this.lastOpenedFile) {
this.openPendingTab({
file: this.lastOpenedFile,
})
.then(changeViewer => {
if (changeViewer) {
this.updateViewer('diff');
}
})
.catch(e => {
throw e;
});
}
},
methods: {
...mapActions('commit', [
'updateCommitMessage',
'discardDraft',
'commitChanges',
'updateCommitAction',
]),
...mapActions(['openPendingTab', 'updateViewer', 'updateActivityBarView']),
...mapActions('commit', ['commitChanges', 'updateCommitAction']),
forceCreateNewBranch() {
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges());
},
......@@ -80,6 +85,7 @@ export default {
v-if="showStageUnstageArea"
>
<commit-files-list
class="is-first"
icon-name="unstaged"
:title="__('Unstaged')"
:file-list="changedFiles"
......@@ -94,49 +100,11 @@ export default {
action="unstageAllChanges"
:action-btn-text="__('Unstage all')"
item-action-component="unstage-button"
:show-toggle="false"
:staged-list="true"
/>
</template>
<empty-state
v-if="unusedSeal"
:no-changes-state-svg-path="noChangesStateSvgPath"
/>
<div
class="multi-file-commit-panel-bottom"
>
<form
class="form-horizontal multi-file-commit-form"
@submit.prevent.stop="commitChanges"
v-if="!rightPanelCollapsed"
>
<success-message
v-if="lastCommitMsg && !someUncommitedChanges"
:committed-state-svg-path="committedStateSvgPath"
/>
<commit-message-field
:text="commitMessage"
@input="updateCommitMessage"
/>
<div class="clearfix prepend-top-15">
<actions />
<loading-button
:loading="submitCommitLoading"
:disabled="commitButtonDisabled"
container-class="btn btn-success btn-sm pull-left"
:label="__('Commit')"
@click="commitChanges"
/>
<button
v-if="!discardDraftButtonDisabled"
type="button"
class="btn btn-default btn-sm pull-right"
@click="discardDraft"
>
{{ __('Discard draft') }}
</button>
</div>
</form>
</div>
</div>
</template>
......@@ -3,6 +3,7 @@
import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import { activityBarViews, viewerTypes } from '../constants';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
import IdeFileButtons from './ide_file_buttons.vue';
......@@ -19,8 +20,14 @@ export default {
},
},
computed: {
...mapState(['rightPanelCollapsed', 'viewer', 'delayViewerUpdated', 'panelResizing']),
...mapGetters(['currentMergeRequest', 'getStagedFile']),
...mapState(['rightPanelCollapsed', 'viewer', 'panelResizing', 'currentActivityView']),
...mapGetters([
'currentMergeRequest',
'getStagedFile',
'isEditModeActive',
'isCommitModeActive',
'isReviewModeActive',
]),
shouldHideEditor() {
return this.file && this.file.binary && !this.file.content;
},
......@@ -40,6 +47,21 @@ export default {
// Compare key to allow for files opened in review mode to be cached differently
if (newVal.key !== this.file.key) {
this.initMonaco();
if (this.currentActivityView !== activityBarViews.edit) {
this.setFileViewMode({
file: this.file,
viewMode: 'edit',
});
}
}
},
currentActivityView() {
if (this.currentActivityView !== activityBarViews.edit) {
this.setFileViewMode({
file: this.file,
viewMode: 'edit',
});
}
},
rightPanelCollapsed() {
......@@ -77,7 +99,6 @@ export default {
'setFileViewMode',
'setFileEOL',
'updateViewer',
'updateDelayViewerUpdated',
]),
initMonaco() {
if (this.shouldHideEditor) return;
......@@ -89,14 +110,6 @@ export default {
baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
})
.then(() => {
const viewerPromise = this.delayViewerUpdated
? this.updateViewer(this.file.pending ? 'diff' : 'editor')
: Promise.resolve();
return viewerPromise;
})
.then(() => {
this.updateDelayViewerUpdated(false);
this.createEditorInstance();
})
.catch(err => {
......@@ -108,10 +121,10 @@ export default {
this.editor.dispose();
this.$nextTick(() => {
if (this.viewer === 'editor') {
if (this.viewer === viewerTypes.edit) {
this.editor.createInstance(this.$refs.editor);
} else {
this.editor.createDiffInstance(this.$refs.editor);
this.editor.createDiffInstance(this.$refs.editor, !this.isReviewModeActive);
}
this.setupEditor();
......@@ -127,7 +140,7 @@ export default {
this.file.staged && this.file.key.indexOf('unstaged-') === 0 ? head : null,
);
if (this.viewer === 'mrdiff') {
if (this.viewer === viewerTypes.mr) {
this.editor.attachMergeRequestModel(this.model);
} else {
this.editor.attachModel(this.model);
......@@ -168,6 +181,7 @@ export default {
});
},
},
viewerTypes,
};
</script>
......@@ -176,16 +190,17 @@ export default {
id="ide"
class="blob-viewer-container blob-editor-container"
>
<div class="ide-mode-tabs clearfix">
<div class="ide-mode-tabs clearfix" >
<ul
class="nav-links pull-left"
v-if="!shouldHideEditor">
v-if="!shouldHideEditor && isEditModeActive"
>
<li :class="editTabCSS">
<a
href="javascript:void(0);"
role="button"
@click.prevent="setFileViewMode({ file, viewMode: 'edit' })">
<template v-if="viewer === 'editor'">
<template v-if="viewer === $options.viewerTypes.edit">
{{ __('Edit') }}
</template>
<template v-else>
......@@ -212,6 +227,9 @@ export default {
v-show="!shouldHideEditor && file.viewMode === 'edit'"
ref="editor"
class="multi-file-editor-holder"
:class="{
'is-readonly': isCommitModeActive,
}"
>
</div>
<content-viewer
......
......@@ -34,6 +34,11 @@ export default {
type: Number,
required: true,
},
disableActionDropdown: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapGetters([
......@@ -99,16 +104,14 @@ export default {
}
},
methods: {
...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
...mapActions(['toggleTreeOpen']),
clickFile() {
// Manual Action if a tree is selected/opened
if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
this.toggleTreeOpen(this.file.path);
}
return this.updateDelayViewerUpdated(true).then(() => {
router.push(`/project${this.file.url}`);
});
router.push(`/project${this.file.url}`);
},
},
};
......@@ -170,7 +173,7 @@ export default {
/>
</span>
<new-dropdown
v-if="isTree"
v-if="isTree && !disableActionDropdown"
:project-id="file.projectId"
:branch="file.branchId"
:path="file.path"
......
......@@ -32,6 +32,8 @@ export default {
return `Close ${this.tab.name}`;
},
showChangedIcon() {
if (this.tab.pending) return true;
return this.fileHasChanged ? !this.tabMouseOver : false;
},
fileHasChanged() {
......@@ -66,15 +68,32 @@ export default {
<template>
<li
:class="{
active: tab.active
}"
@click="clickFile(tab)"
@mouseover="mouseOverTab"
@mouseout="mouseOutTab"
>
<div
class="multi-file-tab"
:title="tab.url"
>
<file-icon
:file-name="tab.name"
:size="16"
/>
{{ tab.name }}
<file-status-icon
:file="tab"
/>
</div>
<button
type="button"
class="multi-file-tab-close"
@click.stop.prevent="closeFile(tab)"
:aria-label="closeLabel"
:disabled="tab.pending"
>
<icon
v-if="!showChangedIcon"
......@@ -87,22 +106,5 @@ export default {
:force-modified-icon="true"
/>
</button>
<div
class="multi-file-tab"
:class="{
active: tab.active
}"
:title="tab.url"
>
<file-icon
:file-name="tab.name"
:size="16"
/>
{{ tab.name }}
<file-status-icon
:file="tab"
/>
</div>
</li>
</template>
......@@ -32,16 +32,6 @@ export default {
default: '',
},
},
data() {
return {
showShadow: false,
};
},
updated() {
if (!this.$refs.tabsScroller) return;
this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
},
methods: {
...mapActions(['updateViewer', 'removePendingTab']),
openFileViewer(viewer) {
......@@ -71,12 +61,5 @@ export default {
:tab="tab"
/>
</ul>
<editor-mode
:viewer="viewer"
:show-shadow="showShadow"
:has-changes="hasChanges"
:merge-request-id="mergeRequestId"
@click="openFileViewer"
/>
</div>
</template>
......@@ -3,6 +3,22 @@ export const MAX_FILE_FINDER_RESULTS = 40;
export const FILE_FINDER_ROW_HEIGHT = 55;
export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
export const MAX_WINDOW_HEIGHT_COMPACT = 750;
export const COMMIT_ITEM_PADDING = 32;
// Commit message textarea
export const MAX_TITLE_LENGTH = 50;
export const MAX_BODY_LENGTH = 72;
export const activityBarViews = {
edit: 'ide-tree',
commit: 'commit-section',
review: 'ide-review',
};
export const viewerTypes = {
mr: 'mrdiff',
edit: 'editor',
diff: 'diff',
};
......@@ -2,6 +2,7 @@ import Vue from 'vue';
import VueRouter from 'vue-router';
import flash from '~/flash';
import store from './stores';
import { activityBarViews } from './constants';
Vue.use(VueRouter);
......@@ -63,6 +64,8 @@ router.beforeEach((to, from, next) => {
const fullProjectId = `${to.params.namespace}/${to.params.project}`;
if (to.params.branch) {
store.dispatch('setCurrentBranchId', to.params.branch);
store.dispatch('getBranchData', {
projectId: fullProjectId,
branchId: to.params.branch,
......@@ -99,14 +102,14 @@ router.beforeEach((to, from, next) => {
throw e;
});
} else if (to.params.mrid) {
store.dispatch('updateViewer', 'mrdiff');
store
.dispatch('getMergeRequestData', {
projectId: fullProjectId,
mergeRequestId: to.params.mrid,
})
.then(mr => {
store.dispatch('updateActivityBarView', activityBarViews.review);
store.dispatch('getBranchData', {
projectId: fullProjectId,
branchId: mr.source_branch,
......
......@@ -14,15 +14,16 @@ function initIde(el) {
components: {
ide,
},
render(createElement) {
return createElement('ide', {
props: {
emptyStateSvgPath: el.dataset.emptyStateSvgPath,
noChangesStateSvgPath: el.dataset.noChangesStateSvgPath,
committedStateSvgPath: el.dataset.committedStateSvgPath,
},
created() {
this.$store.dispatch('setEmptyStateSvgs', {
emptyStateSvgPath: el.dataset.emptyStateSvgPath,
noChangesStateSvgPath: el.dataset.noChangesStateSvgPath,
committedStateSvgPath: el.dataset.committedStateSvgPath,
});
},
render(createElement) {
return createElement('ide');
},
});
}
......
......@@ -61,19 +61,19 @@ export default class Editor {
}
}
createDiffInstance(domElement) {
createDiffInstance(domElement, readOnly = true) {
if (!this.instance) {
clearDomElement(domElement);
this.disposable.add(
(this.instance = this.monaco.editor.createDiffEditor(domElement, {
...defaultEditorOptions,
readOnly: true,
quickSuggestions: false,
occurrencesHighlight: false,
renderLineHighlight: 'none',
hideCursorInOverviewRuler: true,
renderSideBySide: Editor.renderSideBySide(domElement),
readOnly,
renderLineHighlight: readOnly ? 'all' : 'none',
hideCursorInOverviewRuler: !readOnly,
})),
);
......
......@@ -123,6 +123,8 @@ export const scrollToTab = () => {
};
export const stageAllChanges = ({ state, commit }) => {
commit(types.SET_LAST_COMMIT_MSG, '');
state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path));
};
......@@ -138,6 +140,18 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay);
};
export const updateActivityBarView = ({ commit }, view) => {
commit(types.UPDATE_ACTIVITY_BAR_VIEW, view);
};
export const setEmptyStateSvgs = ({ commit }, svgs) => {
commit(types.SET_EMPTY_STATE_SVGS, svgs);
};
export const setCurrentBranchId = ({ commit }, currentBranchId) => {
commit(types.SET_CURRENT_BRANCH, currentBranchId);
};
export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, tempFile }) => {
commit(types.UPDATE_TEMP_FLAG, { path: file.path, tempFile });
......
......@@ -5,6 +5,7 @@ import service from '../../services';
import * as types from '../mutation_types';
import router from '../../ide_router';
import { setPageTitle } from '../utils';
import { viewerTypes } from '../../constants';
export const closeFile = ({ commit, state, dispatch }, file) => {
const path = file.path;
......@@ -23,13 +24,12 @@ export const closeFile = ({ commit, state, dispatch }, file) => {
const nextFileToOpen = state.openFiles[nextIndexToOpen];
if (nextFileToOpen.pending) {
dispatch('updateViewer', 'diff');
dispatch('updateViewer', viewerTypes.diff);
dispatch('openPendingTab', {
file: nextFileToOpen,
keyPrefix: nextFileToOpen.staged ? 'staged' : 'unstaged',
});
} else {
dispatch('updateDelayViewerUpdated', true);
router.push(`/project${nextFileToOpen.url}`);
}
} else if (!state.openFiles.length) {
......@@ -184,6 +184,7 @@ export const stageChange = ({ commit, state }, path) => {
const stagedFile = state.stagedFiles.find(f => f.path === path);
commit(types.STAGE_CHANGE, path);
commit(types.SET_LAST_COMMIT_MSG, '');
if (stagedFile) {
eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content);
......@@ -195,9 +196,7 @@ export const unstageChange = ({ commit }, path) => {
};
export const openPendingTab = ({ commit, getters, dispatch, state }, { file, keyPrefix }) => {
if (getters.activeFile && getters.activeFile === file && state.viewer === 'diff') {
return false;
}
state.openFiles.forEach(f => eventHub.$emit(`editor.update.model.dispose.${f.key}`));
commit(types.ADD_PENDING_TAB, { file, keyPrefix });
......
......@@ -55,7 +55,6 @@ export const getBranchData = (
branch: data,
});
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
commit(types.SET_CURRENT_BRANCH, branchId);
resolve(data);
})
.catch(() => {
......
import { __ } from '~/locale';
import { getChangesCountForFiles, filePathMatches } from './utils';
import { activityBarViews } from '../constants';
export const activeFile = state => state.openFiles.find(file => file.active) || null;
......@@ -31,15 +31,12 @@ export const currentMergeRequest = state => {
return null;
};
// eslint-disable-next-line no-confusing-arrow
export const collapseButtonIcon = state =>
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
export const currentProject = state => state.projects[state.currentProjectId];
export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length;
export const currentTree = state =>
state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
// eslint-disable-next-line no-confusing-arrow
export const collapseButtonTooltip = state =>
state.rightPanelCollapsed ? __('Expand sidebar') : __('Collapse sidebar');
export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length;
export const hasMergeRequest = state => !!state.currentMergeRequestId;
......@@ -59,6 +56,16 @@ export const allBlobs = state =>
export const getChangedFile = state => path => state.changedFiles.find(f => f.path === path);
export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path);
export const lastOpenedFile = state =>
[...state.changedFiles, ...state.stagedFiles].sort((a, b) => b.lastOpenedAt - a.lastOpenedAt)[0];
export const isEditModeActive = state => state.currentActivityView === activityBarViews.edit;
export const isCommitModeActive = state => state.currentActivityView === activityBarViews.commit;
export const isReviewModeActive = state => state.currentActivityView === activityBarViews.review;
export const someUncommitedChanges = state =>
!!(state.changedFiles.length || state.stagedFiles.length);
export const getChangesInFolder = state => path => {
const changedFilesCount = state.changedFiles.filter(f => filePathMatches(f, path)).length;
const stagedFilesCount = state.stagedFiles.filter(
......
......@@ -8,6 +8,7 @@ import router from '../../../ide_router';
import service from '../../../services';
import * as types from './mutation_types';
import * as consts from './constants';
import { activityBarViews } from '../../../constants';
import eventHub from '../../../eventhub';
export const updateCommitMessage = ({ commit }, message) => {
......@@ -75,7 +76,7 @@ export const checkCommitStatus = ({ rootState }) =>
export const updateFilesAfterCommit = (
{ commit, dispatch, state, rootState, rootGetters },
{ data, branch },
{ data },
) => {
const selectedProject = rootState.projects[rootState.currentProjectId];
const lastCommit = {
......@@ -126,15 +127,9 @@ export const updateFilesAfterCommit = (
changed: !!changedFile,
});
});
if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH && rootGetters.activeFile) {
router.push(
`/project/${rootState.currentProjectId}/blob/${branch}/${rootGetters.activeFile.path}`,
);
}
};
export const commitChanges = ({ commit, state, getters, dispatch, rootState }) => {
export const commitChanges = ({ commit, state, getters, dispatch, rootState, rootGetters }) => {
const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
const payload = createCommitPayload(getters.branchName, newBranch, state, rootState);
const getCommitStatus = newBranch ? Promise.resolve(false) : dispatch('checkCommitStatus');
......@@ -187,6 +182,34 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) =
commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true });
}, 5000);
})
.then(() => {
if (rootGetters.lastOpenedFile) {
dispatch(
'openPendingTab',
{
file: rootGetters.lastOpenedFile,
},
{ root: true },
)
.then(changeViewer => {
if (changeViewer) {
dispatch('updateViewer', 'diff', { root: true });
}
})
.catch(e => {
throw e;
});
} else {
dispatch('updateActivityBarView', activityBarViews.edit, { root: true });
dispatch('updateViewer', 'editor', { root: true });
router.push(
`/project/${rootState.currentProjectId}/blob/${getters.branchName}/${
rootGetters.activeFile.path
}`,
);
}
})
.then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH));
})
.catch(err => {
......
......@@ -5,6 +5,7 @@ export const SET_LAST_COMMIT_MSG = 'SET_LAST_COMMIT_MSG';
export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS';
export const SET_EMPTY_STATE_SVGS = 'SET_EMPTY_STATE_SVGS';
// Project Mutation Types
export const SET_PROJECT = 'SET_PROJECT';
......@@ -59,6 +60,7 @@ export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT';
export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
export const UPDATE_ACTIVITY_BAR_VIEW = 'UPDATE_ACTIVITY_BAR_VIEW';
export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL';
......@@ -107,6 +107,21 @@ export default {
delayViewerUpdated,
});
},
[types.UPDATE_ACTIVITY_BAR_VIEW](state, currentActivityView) {
Object.assign(state, {
currentActivityView,
});
},
[types.SET_EMPTY_STATE_SVGS](
state,
{ emptyStateSvgPath, noChangesStateSvgPath, committedStateSvgPath },
) {
Object.assign(state, {
emptyStateSvgPath,
noChangesStateSvgPath,
committedStateSvgPath,
});
},
[types.TOGGLE_FILE_FINDER](state, fileFindVisible) {
Object.assign(state, {
fileFindVisible,
......
/* eslint-disable no-param-reassign */
import * as types from '../mutation_types';
export default {
......@@ -169,32 +170,24 @@ export default {
});
},
[types.ADD_PENDING_TAB](state, { file, keyPrefix = 'pending' }) {
const key = `${keyPrefix}-${file.key}`;
const pendingTab = state.openFiles.find(f => f.key === key && f.pending);
let openFiles = state.openFiles.map(f => Object.assign(f, { active: false, opened: false }));
if (!pendingTab) {
const openFile = openFiles.find(f => f.path === file.path);
openFiles = openFiles.concat(openFile ? null : file).reduce((acc, f) => {
if (!f) return acc;
if (f.path === file.path) {
return acc.concat({
...f,
content: file.content,
active: true,
pending: true,
opened: true,
key,
});
}
return acc.concat(f);
}, []);
}
Object.assign(state, { openFiles });
state.entries[file.path].opened = false;
state.entries[file.path].active = false;
state.entries[file.path].lastOpenedAt = new Date().getTime();
state.openFiles.forEach(f =>
Object.assign(f, {
opened: false,
active: false,
}),
);
state.openFiles = [
{
...file,
key: `${keyPrefix}-${file.key}`,
pending: true,
opened: true,
active: true,
},
];
},
[types.REMOVE_PENDING_TAB](state, file) {
Object.assign(state, {
......
import { activityBarViews, viewerTypes } from '../constants';
export default () => ({
currentProjectId: '',
currentBranchId: '',
......@@ -16,8 +18,9 @@ export default () => ({
rightPanelCollapsed: false,
panelResizing: false,
entries: {},
viewer: 'editor',
viewer: viewerTypes.edit,
delayViewerUpdated: false,
currentActivityView: activityBarViews.edit,
unusedSeal: true,
fileFindVisible: false,
});
......@@ -177,25 +177,6 @@
}
}
// Web IDE
.ide-sidebar-link {
color: $color-200;
background-color: $color-700;
&:hover,
&:focus {
background-color: $color-500;
}
&:active {
background: $color-800;
}
}
.branch-container {
border-left-color: $color-700;
}
.branch-header-title {
color: $color-700;
}
......@@ -203,6 +184,13 @@
.ide-file-list .file.file-active {
color: $color-700;
}
.ide-sidebar-link {
&.active {
color: $color-700;
box-shadow: inset 3px 0 $color-700;
}
}
}
body {
......@@ -343,9 +331,5 @@ body {
.sidebar-top-level-items > li.active .badge {
color: $theme-gray-900;
}
.ide-sidebar-link {
color: $white-light;
}
}
}
......@@ -44,12 +44,17 @@ feature 'Multi-file editor new directory', :js do
wait_for_requests
click_button 'Stage all'
find('.js-ide-commit-mode').click
find('.multi-file-commit-list-item').hover
first('.multi-file-discard-btn .btn').click
fill_in('commit-message', with: 'commit message ide')
click_button('Commit')
find('.js-ide-edit-mode').click
expect(page).to have_content('folder name')
end
end
......@@ -34,7 +34,10 @@ feature 'Multi-file editor new file', :js do
wait_for_requests
click_button 'Stage all'
find('.js-ide-commit-mode').click
find('.multi-file-commit-list-item').hover
first('.multi-file-discard-btn .btn').click
fill_in('commit-message', with: 'commit message ide')
......
import Vue from 'vue';
import store from '~/ide/stores';
import { activityBarViews } from '~/ide/constants';
import ActivityBar from '~/ide/components/activity_bar.vue';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
import { resetStore } from '../helpers';
describe('IDE activity bar', () => {
const Component = Vue.extend(ActivityBar);
let vm;
beforeEach(() => {
Vue.set(store.state.projects, 'abcproject', {
web_url: 'testing',
});
Vue.set(store.state, 'currentProjectId', 'abcproject');
vm = createComponentWithStore(Component, store);
});
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
describe('goBackUrl', () => {
it('renders the Go Back link with the referrer when present', () => {
const fakeReferrer = '/example/README.md';
spyOnProperty(document, 'referrer').and.returnValue(fakeReferrer);
vm.$mount();
expect(vm.goBackUrl).toEqual(fakeReferrer);
});
it('renders the Go Back link with the project url when referrer is not present', () => {
const fakeReferrer = '';
spyOnProperty(document, 'referrer').and.returnValue(fakeReferrer);
vm.$mount();
expect(vm.goBackUrl).toEqual('testing');
});
});
describe('updateActivityBarView', () => {
beforeEach(() => {
spyOn(vm, 'updateActivityBarView');
vm.$mount();
});
it('calls updateActivityBarView with edit value on click', () => {
vm.$el.querySelector('.js-ide-edit-mode').click();
expect(vm.updateActivityBarView).toHaveBeenCalledWith(activityBarViews.edit);
});
it('calls updateActivityBarView with commit value on click', () => {
vm.$el.querySelector('.js-ide-commit-mode').click();
expect(vm.updateActivityBarView).toHaveBeenCalledWith(activityBarViews.commit);
});
it('calls updateActivityBarView with review value on click', () => {
vm.$el.querySelector('.js-ide-review-mode').click();
expect(vm.updateActivityBarView).toHaveBeenCalledWith(activityBarViews.review);
});
});
describe('active item', () => {
beforeEach(() => {
vm.$mount();
});
it('sets edit item active', () => {
expect(vm.$el.querySelector('.js-ide-edit-mode').classList).toContain('active');
});
it('sets commit item active', done => {
vm.$store.state.currentActivityView = activityBarViews.commit;
vm.$nextTick(() => {
expect(vm.$el.querySelector('.js-ide-commit-mode').classList).toContain('active');
done();
});
});
});
});
......@@ -10,10 +10,9 @@ describe('IDE commit panel empty state', () => {
beforeEach(() => {
const Component = Vue.extend(emptyState);
vm = createComponentWithStore(Component, store, {
noChangesStateSvgPath: 'no-changes',
committedStateSvgPath: 'committed-state',
});
Vue.set(store.state, 'noChangesStateSvgPath', 'no-changes');
vm = createComponentWithStore(Component, store);
vm.$mount();
});
......@@ -27,37 +26,4 @@ describe('IDE commit panel empty state', () => {
it('renders no changes text when last commit message is empty', () => {
expect(vm.$el.textContent).toContain('No changes');
});
describe('toggle button', () => {
it('calls store action', () => {
spyOn(vm, 'toggleRightPanelCollapsed');
vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click();
expect(vm.toggleRightPanelCollapsed).toHaveBeenCalled();
});
it('renders collapsed class', done => {
vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click();
Vue.nextTick(() => {
expect(vm.$el.querySelector('.is-collapsed')).not.toBeNull();
done();
});
});
});
describe('collapsed state', () => {
beforeEach(done => {
vm.$store.state.rightPanelCollapsed = true;
Vue.nextTick(done);
});
it('does not render text & svg', () => {
expect(vm.$el.querySelector('img')).toBeNull();
expect(vm.$el.textContent).not.toContain('No changes');
});
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import CommitForm from '~/ide/components/commit_sidebar/form.vue';
import { activityBarViews } from '~/ide/constants';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
import { resetStore } from '../../helpers';
describe('IDE commit form', () => {
const Component = Vue.extend(CommitForm);
let vm;
beforeEach(() => {
spyOnProperty(window, 'innerHeight').and.returnValue(800);
store.state.changedFiles.push('test');
vm = createComponentWithStore(Component, store).$mount();
});
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
it('enables button when has changes', () => {
expect(vm.$el.querySelector('[disabled]')).toBe(null);
});
describe('compact', () => {
it('renders commit button in compact mode', () => {
expect(vm.$el.querySelector('.btn-primary')).not.toBeNull();
expect(vm.$el.querySelector('.btn-primary').textContent).toContain('Commit');
});
it('does not render form', () => {
expect(vm.$el.querySelector('form')).toBeNull();
});
it('renders overview text', done => {
vm.$store.state.stagedFiles.push('test');
vm.$nextTick(() => {
expect(vm.$el.querySelector('p').textContent).toContain('1 unstaged and 1 staged changes');
done();
});
});
it('shows form when clicking commit button', done => {
vm.$el.querySelector('.btn-primary').click();
vm.$nextTick(() => {
expect(vm.$el.querySelector('form')).not.toBeNull();
done();
});
});
it('toggles activity bar vie when clicking commit button', done => {
vm.$el.querySelector('.btn-primary').click();
vm.$nextTick(() => {
expect(store.state.currentActivityView).toBe(activityBarViews.commit);
done();
});
});
});
describe('full', () => {
beforeEach(done => {
vm.isCompact = false;
vm.$nextTick(done);
});
it('updates commitMessage in store on input', done => {
const textarea = vm.$el.querySelector('textarea');
textarea.value = 'testing commit message';
textarea.dispatchEvent(new Event('input'));
getSetTimeoutPromise()
.then(() => {
expect(vm.$store.state.commit.commitMessage).toBe('testing commit message');
})
.then(done)
.catch(done.fail);
});
it('updating currentActivityView not to commit view sets compact mode', done => {
store.state.currentActivityView = 'a';
vm.$nextTick(() => {
expect(vm.isCompact).toBe(true);
done();
});
});
describe('discard draft button', () => {
it('hidden when commitMessage is empty', () => {
expect(vm.$el.querySelector('.btn-default').textContent).toContain('Collapse');
});
it('resets commitMessage when clicking discard button', done => {
vm.$store.state.commit.commitMessage = 'testing commit message';
getSetTimeoutPromise()
.then(() => {
vm.$el.querySelector('.btn-default').click();
})
.then(Vue.nextTick)
.then(() => {
expect(vm.$store.state.commit.commitMessage).not.toBe('testing commit message');
})
.then(done)
.catch(done.fail);
});
});
describe('when submitting', () => {
beforeEach(() => {
spyOn(vm, 'commitChanges');
vm.$store.state.stagedFiles.push('test');
});
it('calls commitChanges', done => {
vm.$store.state.commit.commitMessage = 'testing commit message';
getSetTimeoutPromise()
.then(() => {
vm.$el.querySelector('.btn-success').click();
})
.then(Vue.nextTick)
.then(() => {
expect(vm.commitChanges).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
});
});
});
......@@ -49,45 +49,4 @@ describe('Multi-file editor commit sidebar list', () => {
expect(vm.$el.textContent).toContain('No changes');
});
});
describe('collapsed', () => {
beforeEach(done => {
vm.$store.state.rightPanelCollapsed = true;
Vue.nextTick(done);
});
it('hides list', () => {
expect(vm.$el.querySelector('.list-unstyled')).toBeNull();
expect(vm.$el.querySelector('.help-block')).toBeNull();
});
});
describe('with toggle', () => {
beforeEach(done => {
spyOn(vm, 'toggleRightPanelCollapsed');
vm.showToggle = true;
Vue.nextTick(done);
});
it('calls setPanelCollapsedStatus when clickin toggle', () => {
vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click();
expect(vm.toggleRightPanelCollapsed).toHaveBeenCalled();
});
});
describe('action button', () => {
beforeEach(() => {
spyOn(vm, 'stageAllChanges');
});
it('calls store action when clicked', () => {
vm.$el.querySelector('.ide-staged-action-btn').click();
expect(vm.stageAllChanges).toHaveBeenCalled();
});
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import ideContextBar from '~/ide/components/ide_context_bar.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
describe('Multi-file editor right context bar', () => {
let vm;
beforeEach(() => {
const Component = Vue.extend(ideContextBar);
vm = createComponentWithStore(Component, store, {
noChangesStateSvgPath: 'svg',
committedStateSvgPath: 'svg',
});
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();
});
});
});
import Vue from 'vue';
import ideExternalLinks from '~/ide/components/ide_external_links.vue';
import createComponent from 'spec/helpers/vue_mount_component_helper';
describe('ide external links component', () => {
let vm;
let fakeReferrer;
let Component;
const fakeProjectUrl = '/project/';
beforeEach(() => {
Component = Vue.extend(ideExternalLinks);
});
afterEach(() => {
vm.$destroy();
});
describe('goBackUrl', () => {
it('renders the Go Back link with the referrer when present', () => {
fakeReferrer = '/example/README.md';
spyOnProperty(document, 'referrer').and.returnValue(fakeReferrer);
vm = createComponent(Component, {
projectUrl: fakeProjectUrl,
}).$mount();
expect(vm.goBackUrl).toEqual(fakeReferrer);
});
it('renders the Go Back link with the project url when referrer is not present', () => {
fakeReferrer = '';
spyOnProperty(document, 'referrer').and.returnValue(fakeReferrer);
vm = createComponent(Component, {
projectUrl: fakeProjectUrl,
}).$mount();
expect(vm.goBackUrl).toEqual(fakeProjectUrl);
});
});
});
import Vue from 'vue';
import ProjectTree from '~/ide/components/ide_project_tree.vue';
import createComponent from 'spec/helpers/vue_mount_component_helper';
describe('IDE project tree', () => {
const Component = Vue.extend(ProjectTree);
let vm;
beforeEach(() => {
vm = createComponent(Component, {
project: {
id: 1,
name: 'test',
web_url: gl.TEST_HOST,
avatar_url: '',
branches: [],
},
});
});
afterEach(() => {
vm.$destroy();
});
it('renders identicon when projct has no avatar', () => {
expect(vm.$el.querySelector('.identicon')).not.toBeNull();
});
it('renders avatar image if project has avatar', done => {
vm.project.avatar_url = gl.TEST_HOST;
vm.$nextTick(() => {
expect(vm.$el.querySelector('.identicon')).toBeNull();
expect(vm.$el.querySelector('img.avatar')).not.toBeNull();
done();
});
});
});
import Vue from 'vue';
import ideRepoTree from '~/ide/components/ide_repo_tree.vue';
import createComponent from '../../helpers/vue_mount_component_helper';
import { file } from '../helpers';
describe('IdeRepoTree', () => {
let vm;
let tree;
beforeEach(() => {
const IdeRepoTree = Vue.extend(ideRepoTree);
tree = {
tree: [file()],
loading: false,
};
vm = createComponent(IdeRepoTree, {
tree,
});
});
afterEach(() => {
vm.$destroy();
});
it('renders a sidebar', () => {
expect(vm.$el.querySelector('.loading-file')).toBeNull();
expect(vm.$el.querySelector('.file')).not.toBeNull();
});
it('renders 3 loading files if tree is loading', done => {
tree.loading = true;
vm.$nextTick(() => {
expect(
vm.$el.querySelectorAll('.multi-file-loading-container').length,
).toEqual(3);
done();
});
});
});
import Vue from 'vue';
import IdeReview from '~/ide/components/ide_review.vue';
import store from '~/ide/stores';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
import { trimText } from '../../helpers/vue_component_helper';
import { resetStore, file } from '../helpers';
import { projectData } from '../mock_data';
describe('IDE review mode', () => {
const Component = Vue.extend(IdeReview);
let vm;
beforeEach(() => {
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
store.state.projects.abcproject = Object.assign({}, projectData);
Vue.set(store.state.trees, 'abcproject/master', {
tree: [file('fileName')],
loading: false,
});
vm = createComponentWithStore(Component, store).$mount();
});
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
it('renders list of files', () => {
expect(vm.$el.textContent).toContain('fileName');
});
describe('merge request', () => {
beforeEach(done => {
store.state.currentMergeRequestId = '1';
store.state.projects.abcproject.mergeRequests['1'] = {
iid: 123,
web_url: 'testing123',
};
vm.$nextTick(done);
});
it('renders edit dropdown', () => {
expect(vm.$el.querySelector('.btn')).not.toBe(null);
});
it('renders merge request link & IID', () => {
const link = vm.$el.querySelector('.ide-review-sub-header');
expect(link.querySelector('a').getAttribute('href')).toBe('testing123');
expect(trimText(link.textContent)).toBe('Merge request (!123)');
});
it('changes text to latest changes when viewer is not mrdiff', done => {
store.state.viewer = 'diff';
vm.$nextTick(() => {
expect(trimText(vm.$el.querySelector('.ide-review-sub-header').textContent)).toBe(
'Latest changes',
);
done();
});
});
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import ideSidebar from '~/ide/components/ide_side_bar.vue';
import { activityBarViews } from '~/ide/constants';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../helpers';
import { projectData } from '../mock_data';
describe('IdeSidebar', () => {
let vm;
......@@ -10,6 +12,9 @@ describe('IdeSidebar', () => {
beforeEach(() => {
const Component = Vue.extend(ideSidebar);
store.state.currentProjectId = 'abcproject';
store.state.projects.abcproject = projectData;
vm = createComponentWithStore(Component, store).$mount();
});
......@@ -20,23 +25,33 @@ describe('IdeSidebar', () => {
});
it('renders a sidebar', () => {
expect(
vm.$el.querySelector('.multi-file-commit-panel-inner'),
).not.toBeNull();
expect(vm.$el.querySelector('.multi-file-commit-panel-inner')).not.toBeNull();
});
it('renders loading icon component', done => {
vm.$store.state.loading = true;
vm.$nextTick(() => {
expect(
vm.$el.querySelector('.multi-file-loading-container'),
).not.toBeNull();
expect(
vm.$el.querySelectorAll('.multi-file-loading-container').length,
).toBe(3);
expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull();
expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3);
done();
});
});
describe('activityBarComponent', () => {
it('renders tree component', () => {
expect(vm.$el.querySelector('.ide-file-list')).not.toBeNull();
});
it('renders commit component', done => {
vm.$store.state.currentActivityView = activityBarViews.commit;
vm.$nextTick(() => {
expect(vm.$el.querySelector('.multi-file-commit-panel-section')).not.toBeNull();
done();
});
});
});
});
......@@ -4,6 +4,7 @@ import store from '~/ide/stores';
import ide from '~/ide/components/ide.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { file, resetStore } from '../helpers';
import { projectData } from '../mock_data';
describe('ide component', () => {
let vm;
......@@ -11,6 +12,10 @@ describe('ide component', () => {
beforeEach(() => {
const Component = Vue.extend(ide);
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
store.state.projects.abcproject = Object.assign({}, projectData);
vm = createComponentWithStore(Component, store, {
emptyStateSvgPath: 'svg',
noChangesStateSvgPath: 'svg',
......@@ -24,11 +29,11 @@ describe('ide component', () => {
resetStore(vm.$store);
});
it('does not render panel right when no files open', () => {
it('does not render right right when no files open', () => {
expect(vm.$el.querySelector('.panel-right')).toBeNull();
});
it('renders panel right when files are open', done => {
it('renders right panel when files are open', done => {
vm.$store.state.trees['abcproject/mybranch'] = {
tree: [file()],
};
......
import Vue from 'vue';
import IdeTreeList from '~/ide/components/ide_tree_list.vue';
import store from '~/ide/stores';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
import { resetStore, file } from '../helpers';
import { projectData } from '../mock_data';
describe('IDE tree list', () => {
const Component = Vue.extend(IdeTreeList);
let vm;
beforeEach(() => {
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
store.state.projects.abcproject = Object.assign({}, projectData);
Vue.set(store.state.trees, 'abcproject/master', {
tree: [file('fileName')],
loading: false,
});
vm = createComponentWithStore(Component, store, {
viewerType: 'edit',
});
spyOn(vm, 'updateViewer').and.callThrough();
vm.$mount();
});
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
it('updates viewer on mount', () => {
expect(vm.updateViewer).toHaveBeenCalledWith('edit');
});
it('renders loading indicator', done => {
store.state.trees['abcproject/master'].loading = true;
vm.$nextTick(() => {
expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull();
expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3);
done();
});
});
it('renders list of files', () => {
expect(vm.$el.textContent).toContain('fileName');
});
});
import Vue from 'vue';
import IdeTree from '~/ide/components/ide_tree.vue';
import store from '~/ide/stores';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
import { resetStore, file } from '../helpers';
import { projectData } from '../mock_data';
describe('IdeRepoTree', () => {
let vm;
beforeEach(() => {
const IdeRepoTree = Vue.extend(IdeTree);
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
store.state.projects.abcproject = Object.assign({}, projectData);
Vue.set(store.state.trees, 'abcproject/master', {
tree: [file('fileName')],
loading: false,
});
vm = createComponentWithStore(IdeRepoTree, store).$mount();
});
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
it('renders list of files', () => {
expect(vm.$el.textContent).toContain('fileName');
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import service from '~/ide/services';
import router from '~/ide/ide_router';
import repoCommitSection from '~/ide/components/repo_commit_section.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
import { file, resetStore } from '../helpers';
describe('RepoCommitSection', () => {
......@@ -12,10 +12,10 @@ describe('RepoCommitSection', () => {
function createComponent() {
const Component = Vue.extend(repoCommitSection);
vm = createComponentWithStore(Component, store, {
noChangesStateSvgPath: 'svg',
committedStateSvgPath: 'commitsvg',
});
store.state.noChangesStateSvgPath = 'svg';
store.state.committedStateSvgPath = 'commitsvg';
vm = createComponentWithStore(Component, store);
vm.$store.state.currentProjectId = 'abcproject';
vm.$store.state.currentBranchId = 'master';
......@@ -60,6 +60,8 @@ describe('RepoCommitSection', () => {
}
beforeEach(done => {
spyOn(router, 'push');
vm = createComponent();
spyOn(service, 'getTreeData').and.returnValue(
......@@ -93,61 +95,49 @@ describe('RepoCommitSection', () => {
resetStore(vm.$store);
const Component = Vue.extend(repoCommitSection);
vm = createComponentWithStore(Component, store, {
noChangesStateSvgPath: 'nochangessvg',
committedStateSvgPath: 'svg',
}).$mount();
store.state.noChangesStateSvgPath = 'nochangessvg';
store.state.committedStateSvgPath = 'svg';
expect(
vm.$el.querySelector('.js-empty-state').textContent.trim(),
).toContain('No changes');
expect(
vm.$el.querySelector('.js-empty-state img').getAttribute('src'),
).toBe('nochangessvg');
vm = createComponentWithStore(Component, store).$mount();
expect(vm.$el.querySelector('.js-empty-state').textContent.trim()).toContain('No changes');
expect(vm.$el.querySelector('.js-empty-state img').getAttribute('src')).toBe('nochangessvg');
});
});
it('renders a commit section', () => {
const changedFileElements = [
...vm.$el.querySelectorAll('.multi-file-commit-list li'),
];
const submitCommit = vm.$el.querySelector('form .btn');
const allFiles = vm.$store.state.changedFiles.concat(
vm.$store.state.stagedFiles,
);
const changedFileElements = [...vm.$el.querySelectorAll('.multi-file-commit-list li')];
const allFiles = vm.$store.state.changedFiles.concat(vm.$store.state.stagedFiles);
expect(vm.$el.querySelector('.multi-file-commit-form')).not.toBeNull();
expect(changedFileElements.length).toEqual(4);
changedFileElements.forEach((changedFile, i) => {
expect(changedFile.textContent.trim()).toContain(allFiles[i].path);
});
expect(submitCommit.disabled).toBeTruthy();
expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeNull();
});
it('adds changed files into staged files', done => {
vm.$el.querySelector('.ide-staged-action-btn').click();
Vue.nextTick(() => {
expect(
vm.$el.querySelector('.ide-commit-list-container').textContent,
).toContain('No changes');
done();
});
vm.$el.querySelector('.multi-file-discard-btn .btn').click();
vm
.$nextTick()
.then(() => vm.$el.querySelector('.multi-file-discard-btn .btn').click())
.then(vm.$nextTick)
.then(() => {
expect(vm.$el.querySelector('.ide-commit-list-container').textContent).toContain(
'No changes',
);
})
.then(done)
.catch(done.fail);
});
it('stages a single file', done => {
vm.$el.querySelector('.multi-file-discard-btn .btn').click();
Vue.nextTick(() => {
expect(
vm.$el
.querySelector('.ide-commit-list-container')
.querySelectorAll('li').length,
).toBe(1);
expect(vm.$el.querySelector('.ide-commit-list-container').querySelectorAll('li').length).toBe(
1,
);
done();
});
......@@ -157,26 +147,10 @@ describe('RepoCommitSection', () => {
vm.$el.querySelectorAll('.multi-file-discard-btn .btn')[1].click();
Vue.nextTick(() => {
expect(
vm.$el.querySelector('.ide-commit-list-container').textContent,
).not.toContain('file1');
expect(
vm.$el
.querySelector('.ide-commit-list-container')
.querySelectorAll('li').length,
).toBe(1);
done();
});
});
it('removes all staged files', done => {
vm.$el.querySelectorAll('.ide-staged-action-btn')[1].click();
Vue.nextTick(() => {
expect(
vm.$el.querySelectorAll('.ide-commit-list-container')[1].textContent,
).toContain('No changes');
expect(vm.$el.querySelector('.ide-commit-list-container').textContent).not.toContain('file1');
expect(vm.$el.querySelector('.ide-commit-list-container').querySelectorAll('li').length).toBe(
1,
);
done();
});
......@@ -190,75 +164,17 @@ describe('RepoCommitSection', () => {
Vue.nextTick(() => {
expect(
vm.$el
.querySelectorAll('.ide-commit-list-container')[1]
.querySelectorAll('li').length,
vm.$el.querySelectorAll('.ide-commit-list-container')[1].querySelectorAll('li').length,
).toBe(1);
done();
});
});
it('updates commitMessage in store on input', done => {
const textarea = vm.$el.querySelector('textarea');
textarea.value = 'testing commit message';
textarea.dispatchEvent(new Event('input'));
getSetTimeoutPromise()
.then(() => {
expect(vm.$store.state.commit.commitMessage).toBe(
'testing commit message',
);
})
.then(done)
.catch(done.fail);
});
describe('discard draft button', () => {
it('hidden when commitMessage is empty', () => {
expect(
vm.$el.querySelector('.multi-file-commit-form .btn-default'),
).toBeNull();
});
it('resets commitMessage when clicking discard button', done => {
vm.$store.state.commit.commitMessage = 'testing commit message';
getSetTimeoutPromise()
.then(() => {
vm.$el.querySelector('.multi-file-commit-form .btn-default').click();
})
.then(Vue.nextTick)
.then(() => {
expect(vm.$store.state.commit.commitMessage).not.toBe(
'testing commit message',
);
})
.then(done)
.catch(done.fail);
});
});
describe('when submitting', () => {
beforeEach(() => {
spyOn(vm, 'commitChanges');
});
it('calls commitChanges', done => {
vm.$store.state.commit.commitMessage = 'testing commit message';
getSetTimeoutPromise()
.then(() => {
vm.$el.querySelector('.multi-file-commit-form .btn-success').click();
})
.then(Vue.nextTick)
.then(() => {
expect(vm.commitChanges).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
describe('mounted', () => {
it('opens last opened file', () => {
expect(store.state.openFiles.length).toBe(1);
expect(store.state.openFiles[0].pending).toBe(true);
});
});
});
......@@ -5,6 +5,7 @@ import store from '~/ide/stores';
import repoEditor from '~/ide/components/repo_editor.vue';
import monacoLoader from '~/ide/monaco_loader';
import Editor from '~/ide/lib/editor';
import { activityBarViews } from '~/ide/constants';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
import setTimeoutPromise from '../../helpers/set_timeout_promise_helper';
import { file, resetStore } from '../helpers';
......@@ -295,4 +296,30 @@ describe('RepoEditor', () => {
});
});
});
describe('show tabs', () => {
it('shows tabs in edit mode', () => {
expect(vm.$el.querySelector('.nav-links')).not.toBe(null);
});
it('hides tabs in review mode', done => {
vm.$store.state.currentActivityView = activityBarViews.review;
vm.$nextTick(() => {
expect(vm.$el.querySelector('.nav-links')).toBe(null);
done();
});
});
it('hides tabs in commit mode', done => {
vm.$store.state.currentActivityView = activityBarViews.commit;
vm.$nextTick(() => {
expect(vm.$el.querySelector('.nav-links')).toBe(null);
done();
});
});
});
});
......@@ -73,6 +73,43 @@ describe('RepoFile', () => {
expect(treeChangesEl).not.toBeNull();
expect(treeChangesEl.textContent).toContain('1');
});
it('renders action dropdown', done => {
createComponent({
file: {
...file('t4'),
type: 'tree',
branchId: 'master',
projectId: 'project',
},
level: 0,
});
setTimeout(() => {
expect(vm.$el.querySelector('.ide-new-btn')).not.toBeNull();
done();
});
});
it('disables action dropdown', done => {
createComponent({
file: {
...file('t4'),
type: 'tree',
branchId: 'master',
projectId: 'project',
},
level: 0,
disableActionDropdown: true,
});
setTimeout(() => {
expect(vm.$el.querySelector('.ide-new-btn')).toBeNull();
done();
});
});
});
describe('locked file', () => {
......
......@@ -26,60 +26,10 @@ describe('RepoTabs', () => {
const tabs = [...vm.$el.querySelectorAll('.multi-file-tab')];
expect(tabs.length).toEqual(2);
expect(tabs[0].classList.contains('active')).toEqual(true);
expect(tabs[1].classList.contains('active')).toEqual(false);
expect(tabs[0].parentNode.classList.contains('active')).toEqual(true);
expect(tabs[1].parentNode.classList.contains('active')).toEqual(false);
done();
});
});
describe('updated', () => {
it('sets showShadow as true when scroll width is larger than width', done => {
const el = document.createElement('div');
el.innerHTML = '<div id="test-app"></div>';
document.body.appendChild(el);
const style = document.createElement('style');
style.innerText = `
.multi-file-tabs {
width: 100px;
}
.multi-file-tabs .list-unstyled {
display: flex;
overflow-x: auto;
}
`;
document.head.appendChild(style);
vm = createComponent(
RepoTabs,
{
files: [],
viewer: 'editor',
hasChanges: false,
activeFile: file('activeFile'),
hasMergeRequest: false,
},
'#test-app',
);
vm
.$nextTick()
.then(() => {
expect(vm.showShadow).toEqual(false);
vm.files = openedFiles;
})
.then(vm.$nextTick)
.then(() => {
expect(vm.showShadow).toEqual(true);
style.remove();
el.remove();
})
.then(done)
.catch(done.fail);
});
});
});
......@@ -74,10 +74,10 @@ describe('Multi-file editor library', () => {
scrollBeyondLastLine: false,
quickSuggestions: false,
occurrencesHighlight: false,
renderLineHighlight: 'none',
hideCursorInOverviewRuler: true,
wordWrap: 'on',
renderSideBySide: true,
renderLineHighlight: 'all',
hideCursorInOverviewRuler: false,
});
});
});
......
// eslint-disable-next-line import/prefer-default-export
export const projectData = {
id: 1,
name: 'abcproject',
web_url: '',
avatar_url: '',
path: '',
name_with_namespace: 'namespace/abcproject',
branches: {
master: {
treeId: 'abcproject/master',
},
},
mergeRequests: {},
};
......@@ -511,7 +511,10 @@ describe('IDE store file actions', () => {
actions.stageChange,
'path',
store.state,
[{ type: types.STAGE_CHANGE, payload: 'path' }],
[
{ type: types.STAGE_CHANGE, payload: 'path' },
{ type: types.SET_LAST_COMMIT_MSG, payload: '' },
],
[],
done,
);
......@@ -524,7 +527,10 @@ describe('IDE store file actions', () => {
actions.unstageChange,
'path',
store.state,
[{ type: types.UNSTAGE_CHANGE, payload: 'path' }],
[
{ type: types.UNSTAGE_CHANGE, payload: 'path' },
{ type: types.SET_LAST_COMMIT_MSG, payload: '' },
],
[],
done,
);
......@@ -589,20 +595,6 @@ describe('IDE store file actions', () => {
.then(done)
.catch(done.fail);
});
it('returns false when passed in file is active & viewer is diff', done => {
f.active = true;
store.state.openFiles.push(f);
store.state.viewer = 'diff';
store
.dispatch('openPendingTab', { file: f, keyPrefix: 'pending' })
.then(added => {
expect(added).toBe(false);
})
.then(done)
.catch(done.fail);
});
});
describe('removePendingTab', () => {
......
......@@ -2,6 +2,9 @@ import actions, {
stageAllChanges,
unstageAllChanges,
toggleFileFinder,
setCurrentBranchId,
setEmptyStateSvgs,
updateActivityBarView,
updateTempFlagForEntry,
} from '~/ide/stores/actions';
import store from '~/ide/stores';
......@@ -306,6 +309,7 @@ describe('Multi-file store actions', () => {
null,
store.state,
[
{ type: types.SET_LAST_COMMIT_MSG, payload: '' },
{ type: types.STAGE_CHANGE, payload: store.state.changedFiles[0].path },
{ type: types.STAGE_CHANGE, payload: store.state.changedFiles[1].path },
],
......@@ -345,6 +349,32 @@ describe('Multi-file store actions', () => {
});
});
describe('updateActivityBarView', () => {
it('commits UPDATE_ACTIVITY_BAR_VIEW', done => {
testAction(
updateActivityBarView,
'test',
{},
[{ type: 'UPDATE_ACTIVITY_BAR_VIEW', payload: 'test' }],
[],
done,
);
});
});
describe('setEmptyStateSvgs', () => {
it('commits setEmptyStateSvgs', done => {
testAction(
setEmptyStateSvgs,
'svg',
{},
[{ type: 'SET_EMPTY_STATE_SVGS', payload: 'svg' }],
[],
done,
);
});
});
describe('updateTempFlagForEntry', () => {
it('commits UPDATE_TEMP_FLAG', done => {
const f = {
......@@ -388,6 +418,19 @@ describe('Multi-file store actions', () => {
});
});
describe('setCurrentBranchId', () => {
it('commits setCurrentBranchId', done => {
testAction(
setCurrentBranchId,
'branchId',
{},
[{ type: 'SET_CURRENT_BRANCH', payload: 'branchId' }],
[],
done,
);
});
});
describe('toggleFileFinder', () => {
it('commits TOGGLE_FILE_FINDER', done => {
testAction(
......
......@@ -37,12 +37,6 @@ describe('IDE store getters', () => {
expect(modifiedFiles.length).toBe(1);
expect(modifiedFiles[0].name).toBe('changed');
});
it('returns angle left when collapsed', () => {
localState.rightPanelCollapsed = true;
expect(getters.collapseButtonIcon(localState)).toBe('angle-double-left');
});
});
describe('currentMergeRequest', () => {
......
......@@ -289,21 +289,6 @@ describe('IDE commit module actions', () => {
.then(done)
.catch(done.fail);
});
it('pushes route to new branch if commitAction is new branch', done => {
store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
store
.dispatch('commit/updateFilesAfterCommit', {
data,
branch,
})
.then(() => {
expect(router.push).toHaveBeenCalledWith(`/project/abcproject/blob/master/${f.path}`);
})
.then(done)
.catch(done.fail);
});
});
describe('commitChanges', () => {
......@@ -391,21 +376,6 @@ describe('IDE commit module actions', () => {
.catch(done.fail);
});
it('pushes router to new route', done => {
store
.dispatch('commit/commitChanges')
.then(() => {
expect(router.push).toHaveBeenCalledWith(
`/project/${store.state.currentProjectId}/blob/${
store.getters['commit/newBranchName']
}/changed`,
);
done();
})
.catch(done.fail);
});
it('sets last Commit Msg', done => {
store
.dispatch('commit/commitChanges')
......
......@@ -267,41 +267,23 @@ describe('IDE store file mutations', () => {
it('adds file into openFiles as pending', () => {
mutations.ADD_PENDING_TAB(localState, { file: localFile });
expect(localState.openFiles.length).toBe(2);
expect(localState.openFiles[1].pending).toBe(true);
expect(localState.openFiles[1].key).toBe(`pending-${localFile.key}`);
});
it('updates open file to pending', () => {
mutations.ADD_PENDING_TAB(localState, { file: localState.openFiles[0] });
expect(localState.openFiles.length).toBe(1);
expect(localState.openFiles[0].pending).toBe(true);
expect(localState.openFiles[0].key).toBe(`pending-${localFile.key}`);
});
it('updates pending open file to active', () => {
localState.openFiles.push({
...localFile,
pending: true,
});
it('only allows 1 open pending file', () => {
const newFile = file('test');
localState.entries[newFile.path] = newFile;
mutations.ADD_PENDING_TAB(localState, { file: localFile });
expect(localState.openFiles[1].pending).toBe(true);
expect(localState.openFiles[1].active).toBe(true);
});
it('sets all openFiles to not active', () => {
mutations.ADD_PENDING_TAB(localState, { file: localFile });
expect(localState.openFiles.length).toBe(1);
expect(localState.openFiles.length).toBe(2);
mutations.ADD_PENDING_TAB(localState, { file: file('test') });
localState.openFiles.forEach(f => {
if (f.pending) {
expect(f.active).toBe(true);
} else {
expect(f.active).toBe(false);
}
});
expect(localState.openFiles.length).toBe(1);
expect(localState.openFiles[0].name).toBe('test');
});
});
......
......@@ -87,6 +87,28 @@ describe('Multi-file store mutations', () => {
});
});
describe('UPDATE_ACTIVITY_BAR_VIEW', () => {
it('updates currentActivityBar', () => {
mutations.UPDATE_ACTIVITY_BAR_VIEW(localState, 'test');
expect(localState.currentActivityView).toBe('test');
});
});
describe('SET_EMPTY_STATE_SVGS', () => {
it('updates empty state SVGs', () => {
mutations.SET_EMPTY_STATE_SVGS(localState, {
emptyStateSvgPath: 'emptyState',
noChangesStateSvgPath: 'noChanges',
committedStateSvgPath: 'commited',
});
expect(localState.emptyStateSvgPath).toBe('emptyState');
expect(localState.noChangesStateSvgPath).toBe('noChanges');
expect(localState.committedStateSvgPath).toBe('commited');
});
});
describe('UPDATE_TEMP_FLAG', () => {
beforeEach(() => {
localState.entries.test = {
......
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