Commit eaeeac2f authored by Filipa Lacerda's avatar 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> <script>
import { mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import { sprintf, __ } from '~/locale'; import { sprintf, __ } from '~/locale';
import * as consts from '../../stores/modules/commit/constants'; import * as consts from '../../stores/modules/commit/constants';
import RadioGroup from './radio_group.vue'; import RadioGroup from './radio_group.vue';
...@@ -9,7 +9,7 @@ export default { ...@@ -9,7 +9,7 @@ export default {
RadioGroup, RadioGroup,
}, },
computed: { computed: {
...mapState(['currentBranchId']), ...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']),
commitToCurrentBranchText() { commitToCurrentBranchText() {
return sprintf( return sprintf(
__('Commit to %{branchName} branch'), __('Commit to %{branchName} branch'),
...@@ -17,6 +17,17 @@ export default { ...@@ -17,6 +17,17 @@ export default {
false, 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, commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH, commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
...@@ -44,6 +55,7 @@ export default { ...@@ -44,6 +55,7 @@ export default {
:value="$options.commitToNewBranchMR" :value="$options.commitToNewBranchMR"
:label="__('Create a new branch and merge request')" :label="__('Create a new branch and merge request')"
:show-input="true" :show-input="true"
:disabled="disableMergeRequestRadio"
/> />
</div> </div>
</template> </template>
<script> <script>
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
components: {
Icon,
},
directives: {
tooltip,
},
props: {
noChangesStateSvgPath: {
type: String,
required: true,
},
},
computed: { computed: {
...mapState(['lastCommitMsg', 'rightPanelCollapsed', 'changedFiles', 'stagedFiles']), ...mapState(['lastCommitMsg', 'noChangesStateSvgPath']),
...mapGetters(['collapseButtonIcon', 'collapseButtonTooltip']),
},
methods: {
...mapActions(['toggleRightPanelCollapsed']),
}, },
}; };
</script> </script>
...@@ -31,31 +13,8 @@ export default { ...@@ -31,31 +13,8 @@ export default {
v-if="!lastCommitMsg" v-if="!lastCommitMsg"
class="multi-file-commit-panel-section ide-commit-empty-state js-empty-state" 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 <div
class="ide-commit-empty-state-container" class="ide-commit-empty-state-container"
v-if="!rightPanelCollapsed"
> >
<div class="svg-content svg-80"> <div class="svg-content svg-80">
<img :src="noChangesStateSvgPath" /> <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> <script>
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions } from 'vuex';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import ListItem from './list_item.vue'; import ListItem from './list_item.vue';
import ListCollapsed from './list_collapsed.vue';
export default { export default {
components: { components: {
Icon, Icon,
ListItem, ListItem,
ListCollapsed,
}, },
directives: { directives: {
tooltip, tooltip,
...@@ -24,11 +22,6 @@ export default { ...@@ -24,11 +22,6 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
showToggle: {
type: Boolean,
required: false,
default: true,
},
iconName: { iconName: {
type: String, type: String,
required: true, required: true,
...@@ -51,9 +44,12 @@ export default { ...@@ -51,9 +44,12 @@ export default {
default: false, default: false,
}, },
}, },
data() {
return {
showActionButton: false,
};
},
computed: { computed: {
...mapState(['rightPanelCollapsed']),
...mapGetters(['collapseButtonIcon', 'collapseButtonTooltip']),
titleText() { titleText() {
return sprintf(__('%{title} changes'), { return sprintf(__('%{title} changes'), {
title: this.title, title: this.title,
...@@ -61,10 +57,13 @@ export default { ...@@ -61,10 +57,13 @@ export default {
}, },
}, },
methods: { methods: {
...mapActions(['toggleRightPanelCollapsed', 'stageAllChanges', 'unstageAllChanges']), ...mapActions(['stageAllChanges', 'unstageAllChanges']),
actionBtnClicked() { actionBtnClicked() {
this[this.action](); this[this.action]();
}, },
setShowActionButton(show) {
this.showActionButton = show;
},
}, },
}; };
</script> </script>
...@@ -72,19 +71,14 @@ export default { ...@@ -72,19 +71,14 @@ export default {
<template> <template>
<div <div
class="ide-commit-list-container" class="ide-commit-list-container"
:class="{
'is-collapsed': rightPanelCollapsed,
}"
> >
<header <header
class="multi-file-commit-panel-header" class="multi-file-commit-panel-header"
@mouseenter="setShowActionButton(true)"
@mouseleave="setShowActionButton(false)"
> >
<div <div
v-if="!rightPanelCollapsed"
class="multi-file-commit-panel-header-title" class="multi-file-commit-panel-header-title"
:class="{
'append-right-10': showToggle,
}"
> >
<icon <icon
v-once v-once
...@@ -92,7 +86,14 @@ export default { ...@@ -92,7 +86,14 @@ export default {
:size="18" :size="18"
/> />
{{ titleText }} {{ titleText }}
<span
v-show="!showActionButton"
class="ide-commit-file-count"
>
{{ fileList.length }}
</span>
<button <button
v-show="showActionButton"
type="button" type="button"
class="btn btn-blank btn-link ide-staged-action-btn" class="btn btn-blank btn-link ide-staged-action-btn"
@click="actionBtnClicked" @click="actionBtnClicked"
...@@ -100,30 +101,7 @@ export default { ...@@ -100,30 +101,7 @@ export default {
{{ actionBtnText }} {{ actionBtnText }}
</button> </button>
</div> </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> </header>
<list-collapsed
v-if="rightPanelCollapsed"
:files="fileList"
:icon-name="iconName"
:title="title"
/>
<template v-else>
<ul <ul
v-if="fileList.length" v-if="fileList.length"
class="multi-file-commit-list list-unstyled append-bottom-0" class="multi-file-commit-list list-unstyled append-bottom-0"
...@@ -146,6 +124,5 @@ export default { ...@@ -146,6 +124,5 @@ export default {
> >
{{ __('No changes') }} {{ __('No changes') }}
</p> </p>
</template>
</div> </div>
</template> </template>
...@@ -3,6 +3,7 @@ import { mapActions } from 'vuex'; ...@@ -3,6 +3,7 @@ import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import StageButton from './stage_button.vue'; import StageButton from './stage_button.vue';
import UnstageButton from './unstage_button.vue'; import UnstageButton from './unstage_button.vue';
import { viewerTypes } from '../../constants';
export default { export default {
components: { components: {
...@@ -53,7 +54,7 @@ export default { ...@@ -53,7 +54,7 @@ export default {
keyPrefix: this.keyPrefix.toLowerCase(), keyPrefix: this.keyPrefix.toLowerCase(),
}).then(changeViewer => { }).then(changeViewer => {
if (changeViewer) { if (changeViewer) {
this.updateViewer('diff'); this.updateViewer(viewerTypes.diff);
} }
}); });
}, },
......
<script> <script>
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
...@@ -26,10 +27,20 @@ export default { ...@@ -26,10 +27,20 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
disabled: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
...mapState('commit', ['commitAction']), ...mapState('commit', ['commitAction']),
...mapGetters('commit', ['newBranchName']), ...mapGetters('commit', ['newBranchName']),
tooltipTitle() {
return this.disabled
? __('This option is disabled while you still have unstaged changes')
: '';
},
}, },
methods: { methods: {
...mapActions('commit', ['updateCommitAction', 'updateBranchName']), ...mapActions('commit', ['updateCommitAction', 'updateBranchName']),
...@@ -39,19 +50,28 @@ export default { ...@@ -39,19 +50,28 @@ export default {
<template> <template>
<fieldset> <fieldset>
<label> <label
v-tooltip
:title="tooltipTitle"
:class="{
'is-disabled': disabled
}"
>
<input <input
type="radio" type="radio"
name="commit-action" name="commit-action"
:value="value" :value="value"
@change="updateCommitAction($event.target.value)" @change="updateCommitAction($event.target.value)"
:checked="checked" :checked="commitAction === value"
v-once :disabled="disabled"
/> />
<span class="prepend-left-10"> <span class="prepend-left-10">
<template v-if="label"> <span
v-if="label"
class="ide-radio-label"
>
{{ label }} {{ label }}
</template> </span>
<slot v-else></slot> <slot v-else></slot>
</span> </span>
</label> </label>
......
...@@ -2,14 +2,8 @@ ...@@ -2,14 +2,8 @@
import { mapState } from 'vuex'; import { mapState } from 'vuex';
export default { export default {
props: {
committedStateSvgPath: {
type: String,
required: true,
},
},
computed: { computed: {
...mapState(['lastCommitMsg']), ...mapState(['lastCommitMsg', 'committedStateSvgPath']),
}, },
}; };
</script> </script>
......
<script> <script>
import Icon from '~/vue_shared/components/icon.vue';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import { viewerTypes } from '../constants';
export default { export default {
components: {
Icon,
},
props: { props: {
hasChanges: {
type: Boolean,
required: false,
default: false,
},
mergeRequestId: {
type: String,
required: false,
default: '',
},
viewer: { viewer: {
type: String, type: String,
required: true, required: true,
}, },
showShadow: { mergeRequestId: {
type: Boolean, type: Number,
required: true, required: true,
}, },
}, },
...@@ -38,48 +25,29 @@ export default { ...@@ -38,48 +25,29 @@ export default {
this.$emit('click', mode); this.$emit('click', mode);
}, },
}, },
viewerTypes,
}; };
</script> </script>
<template> <template>
<div <div
class="dropdown" class="dropdown"
:class="{
shadow: showShadow,
}"
> >
<button <button
type="button" type="button"
class="btn btn-primary btn-sm" class="btn btn-link"
:class="{
'btn-inverted': hasChanges,
}"
data-toggle="dropdown" data-toggle="dropdown"
> >
<template v-if="viewer === 'mrdiff' && mergeRequestId"> {{ __('Edit') }}
{{ 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"
/>
</button> </button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left"> <div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
<ul> <ul>
<template v-if="mergeRequestId">
<li> <li>
<a <a
href="#" href="#"
@click.prevent="changeMode('mrdiff')" @click.prevent="changeMode($options.viewerTypes.mr)"
:class="{ :class="{
'is-active': viewer === 'mrdiff', 'is-active': viewer === $options.viewerTypes.mr,
}" }"
> >
<strong class="dropdown-menu-inner-title"> <strong class="dropdown-menu-inner-title">
...@@ -90,32 +58,12 @@ export default { ...@@ -90,32 +58,12 @@ export default {
</span> </span>
</a> </a>
</li> </li>
<li
role="separator"
class="divider"
>
</li>
</template>
<li>
<a
href="#"
@click.prevent="changeMode('editor')"
:class="{
'is-active': viewer === 'editor',
}"
>
<strong class="dropdown-menu-inner-title">{{ __('Editing') }}</strong>
<span class="dropdown-menu-inner-content">
{{ __('View and edit lines') }}
</span>
</a>
</li>
<li> <li>
<a <a
href="#" href="#"
@click.prevent="changeMode('diff')" @click.prevent="changeMode($options.viewerTypes.diff)"
:class="{ :class="{
'is-active': viewer === 'diff', 'is-active': viewer === $options.viewerTypes.diff,
}" }"
> >
<strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong> <strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong>
......
<script> <script>
import { mapActions, mapState, mapGetters } from 'vuex'; import Mousetrap from 'mousetrap';
import Mousetrap from 'mousetrap'; import { mapActions, mapState, mapGetters } from 'vuex';
import ideSidebar from './ide_side_bar.vue'; import IdeSidebar from './ide_side_bar.vue';
import ideContextbar from './ide_context_bar.vue'; import RepoTabs from './repo_tabs.vue';
import repoTabs from './repo_tabs.vue'; import IdeStatusBar from './ide_status_bar.vue';
import ideStatusBar from './ide_status_bar.vue'; import RepoEditor from './repo_editor.vue';
import repoEditor from './repo_editor.vue'; import FindFile from './file_finder/index.vue';
import FindFile from './file_finder/index.vue';
const originalStopCallback = Mousetrap.stopCallback; const originalStopCallback = Mousetrap.stopCallback;
export default { export default {
components: { components: {
ideSidebar, IdeSidebar,
ideContextbar, RepoTabs,
repoTabs, IdeStatusBar,
ideStatusBar, RepoEditor,
repoEditor,
FindFile, FindFile,
}, },
props: {
emptyStateSvgPath: {
type: String,
required: true,
},
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
},
computed: { computed: {
...mapState([ ...mapState([
'changedFiles', 'changedFiles',
...@@ -40,6 +24,7 @@ ...@@ -40,6 +24,7 @@
'viewer', 'viewer',
'currentMergeRequestId', 'currentMergeRequestId',
'fileFindVisible', 'fileFindVisible',
'emptyStateSvgPath',
]), ]),
...mapGetters(['activeFile', 'hasChanges']), ...mapGetters(['activeFile', 'hasChanges']),
}, },
...@@ -76,7 +61,7 @@ ...@@ -76,7 +61,7 @@
return originalStopCallback(e, el, combo); return originalStopCallback(e, el, combo);
}, },
}, },
}; };
</script> </script>
<template> <template>
...@@ -136,9 +121,5 @@ ...@@ -136,9 +121,5 @@
</div> </div>
</template> </template>
</div> </div>
<ide-contextbar
:no-changes-state-svg-path="noChangesStateSvgPath"
:committed-state-svg-path="committedStateSvgPath"
/>
</div> </div>
</template> </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> <script>
import { mapState, mapGetters } from 'vuex'; import { mapState, mapGetters } from 'vuex';
import icon from '~/vue_shared/components/icon.vue'; import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue'; import Icon from '~/vue_shared/components/icon.vue';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; import tooltip from '~/vue_shared/directives/tooltip';
import projectTree from './ide_project_tree.vue'; import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
import ResizablePanel from './resizable_panel.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 { export default {
directives: {
tooltip,
},
components: { components: {
projectTree, Icon,
icon, PanelResizer,
panelResizer, SkeletonLoadingContainer,
skeletonLoadingContainer,
ResizablePanel, ResizablePanel,
ActivityBar,
ProjectAvatarImage,
Identicon,
CommitSection,
IdeTree,
CommitForm,
IdeReview,
SuccessMessage,
},
data() {
return {
showTooltip: false,
};
}, },
computed: { computed: {
...mapState([ ...mapState([
'loading', 'loading',
'currentBranchId',
'currentActivityView',
'changedFiles',
'stagedFiles',
'lastCommitMsg',
]), ]),
...mapGetters([ ...mapGetters(['currentProject', 'someUncommitedChanges']),
'projectsWithTrees', showSuccessMessage() {
]), return (
this.currentActivityView === activityBarViews.edit &&
(this.lastCommitMsg && !this.someUncommitedChanges)
);
}, },
}; branchTooltipTitle() {
return this.showTooltip ? this.currentBranchId : undefined;
},
},
watch: {
currentBranchId() {
this.$nextTick(() => {
this.showTooltip = this.$refs.branchId.scrollWidth > this.$refs.branchId.offsetWidth;
});
},
},
};
</script> </script>
<template> <template>
<resizable-panel <resizable-panel
:collapsible="false" :collapsible="false"
:initial-width="290" :initial-width="340"
side="left" side="left"
> >
<activity-bar
v-if="!loading"
/>
<div class="multi-file-commit-panel-inner"> <div class="multi-file-commit-panel-inner">
<template v-if="loading"> <template v-if="loading">
<div <div
...@@ -41,11 +87,54 @@ ...@@ -41,11 +87,54 @@
<skeleton-loading-container /> <skeleton-loading-container />
</div> </div>
</template> </template>
<project-tree <template v-else>
v-for="project in projectsWithTrees" <div class="context-header ide-context-header">
:key="project.id" <a
:project="project" :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> </div>
<commit-form />
</template>
</div>
</resizable-panel> </resizable-panel>
</template> </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> <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 SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import RepoFile from './repo_file.vue'; import RepoFile from './repo_file.vue';
import NewDropdown from './new_dropdown/index.vue';
export default { export default {
components: { components: {
Icon,
RepoFile, RepoFile,
SkeletonLoadingContainer, SkeletonLoadingContainer,
NewDropdown,
}, },
props: { props: {
tree: { viewerType: {
type: Object, type: String,
required: true, 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> </script>
...@@ -20,7 +48,7 @@ export default { ...@@ -20,7 +48,7 @@ export default {
<div <div
class="ide-file-list" class="ide-file-list"
> >
<template v-if="tree.loading"> <template v-if="showLoading">
<div <div
class="multi-file-loading-container" class="multi-file-loading-container"
v-for="n in 3" v-for="n in 3"
...@@ -30,11 +58,18 @@ export default { ...@@ -30,11 +58,18 @@ export default {
</div> </div>
</template> </template>
<template v-else> <template v-else>
<header
class="ide-tree-header"
:class="headerClass"
>
<slot name="header"></slot>
</header>
<repo-file <repo-file
v-for="file in tree.tree" v-for="file in currentTree.tree"
:key="file.key" :key="file.key"
:file="file" :file="file"
:level="0" :level="0"
:disable-action-dropdown="disableActionDropdown"
/> />
</template> </template>
</div> </div>
......
...@@ -17,7 +17,8 @@ export default { ...@@ -17,7 +17,8 @@ export default {
}, },
path: { path: {
type: String, type: String,
required: true, required: false,
default: '',
}, },
}, },
data() { data() {
......
...@@ -3,13 +3,10 @@ import { mapState, mapActions, mapGetters } from 'vuex'; ...@@ -3,13 +3,10 @@ import { mapState, mapActions, mapGetters } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.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 CommitFilesList from './commit_sidebar/list.vue';
import EmptyState from './commit_sidebar/empty_state.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 * as consts from '../stores/modules/commit/constants';
import Actions from './commit_sidebar/actions.vue'; import { activityBarViews } from '../constants';
export default { export default {
components: { components: {
...@@ -17,42 +14,50 @@ export default { ...@@ -17,42 +14,50 @@ export default {
Icon, Icon,
CommitFilesList, CommitFilesList,
EmptyState, EmptyState,
SuccessMessage,
Actions,
LoadingButton,
CommitMessageField,
}, },
directives: { directives: {
tooltip, tooltip,
}, },
props: {
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
},
computed: { computed: {
...mapState([
'changedFiles',
'stagedFiles',
'rightPanelCollapsed',
'lastCommitMsg',
'unusedSeal',
]),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges']),
...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
showStageUnstageArea() { showStageUnstageArea() {
return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal); return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal);
}, },
someUncommitedChanges() {
return !!(this.changedFiles.length || this.stagedFiles.length);
}, },
...mapState(['changedFiles', 'stagedFiles', 'rightPanelCollapsed', 'lastCommitMsg', 'unusedSeal']), watch: {
...mapState('commit', ['commitMessage', 'submitCommitLoading']), hasChanges() {
...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']), if (!this.hasChanges) {
this.updateActivityBarView(activityBarViews.edit);
}
},
},
mounted() {
if (this.lastOpenedFile) {
this.openPendingTab({
file: this.lastOpenedFile,
})
.then(changeViewer => {
if (changeViewer) {
this.updateViewer('diff');
}
})
.catch(e => {
throw e;
});
}
}, },
methods: { methods: {
...mapActions('commit', [ ...mapActions(['openPendingTab', 'updateViewer', 'updateActivityBarView']),
'updateCommitMessage', ...mapActions('commit', ['commitChanges', 'updateCommitAction']),
'discardDraft',
'commitChanges',
'updateCommitAction',
]),
forceCreateNewBranch() { forceCreateNewBranch() {
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges()); return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges());
}, },
...@@ -80,6 +85,7 @@ export default { ...@@ -80,6 +85,7 @@ export default {
v-if="showStageUnstageArea" v-if="showStageUnstageArea"
> >
<commit-files-list <commit-files-list
class="is-first"
icon-name="unstaged" icon-name="unstaged"
:title="__('Unstaged')" :title="__('Unstaged')"
:file-list="changedFiles" :file-list="changedFiles"
...@@ -94,49 +100,11 @@ export default { ...@@ -94,49 +100,11 @@ export default {
action="unstageAllChanges" action="unstageAllChanges"
:action-btn-text="__('Unstage all')" :action-btn-text="__('Unstage all')"
item-action-component="unstage-button" item-action-component="unstage-button"
:show-toggle="false"
:staged-list="true" :staged-list="true"
/> />
</template> </template>
<empty-state <empty-state
v-if="unusedSeal" 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> </div>
</template> </template>
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import { mapState, mapGetters, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash'; import flash from '~/flash';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue'; import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import { activityBarViews, viewerTypes } from '../constants';
import monacoLoader from '../monaco_loader'; import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor'; import Editor from '../lib/editor';
import IdeFileButtons from './ide_file_buttons.vue'; import IdeFileButtons from './ide_file_buttons.vue';
...@@ -19,8 +20,14 @@ export default { ...@@ -19,8 +20,14 @@ export default {
}, },
}, },
computed: { computed: {
...mapState(['rightPanelCollapsed', 'viewer', 'delayViewerUpdated', 'panelResizing']), ...mapState(['rightPanelCollapsed', 'viewer', 'panelResizing', 'currentActivityView']),
...mapGetters(['currentMergeRequest', 'getStagedFile']), ...mapGetters([
'currentMergeRequest',
'getStagedFile',
'isEditModeActive',
'isCommitModeActive',
'isReviewModeActive',
]),
shouldHideEditor() { shouldHideEditor() {
return this.file && this.file.binary && !this.file.content; return this.file && this.file.binary && !this.file.content;
}, },
...@@ -40,6 +47,21 @@ export default { ...@@ -40,6 +47,21 @@ export default {
// Compare key to allow for files opened in review mode to be cached differently // Compare key to allow for files opened in review mode to be cached differently
if (newVal.key !== this.file.key) { if (newVal.key !== this.file.key) {
this.initMonaco(); 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() { rightPanelCollapsed() {
...@@ -77,7 +99,6 @@ export default { ...@@ -77,7 +99,6 @@ export default {
'setFileViewMode', 'setFileViewMode',
'setFileEOL', 'setFileEOL',
'updateViewer', 'updateViewer',
'updateDelayViewerUpdated',
]), ]),
initMonaco() { initMonaco() {
if (this.shouldHideEditor) return; if (this.shouldHideEditor) return;
...@@ -89,14 +110,6 @@ export default { ...@@ -89,14 +110,6 @@ export default {
baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '', baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
}) })
.then(() => { .then(() => {
const viewerPromise = this.delayViewerUpdated
? this.updateViewer(this.file.pending ? 'diff' : 'editor')
: Promise.resolve();
return viewerPromise;
})
.then(() => {
this.updateDelayViewerUpdated(false);
this.createEditorInstance(); this.createEditorInstance();
}) })
.catch(err => { .catch(err => {
...@@ -108,10 +121,10 @@ export default { ...@@ -108,10 +121,10 @@ export default {
this.editor.dispose(); this.editor.dispose();
this.$nextTick(() => { this.$nextTick(() => {
if (this.viewer === 'editor') { if (this.viewer === viewerTypes.edit) {
this.editor.createInstance(this.$refs.editor); this.editor.createInstance(this.$refs.editor);
} else { } else {
this.editor.createDiffInstance(this.$refs.editor); this.editor.createDiffInstance(this.$refs.editor, !this.isReviewModeActive);
} }
this.setupEditor(); this.setupEditor();
...@@ -127,7 +140,7 @@ export default { ...@@ -127,7 +140,7 @@ export default {
this.file.staged && this.file.key.indexOf('unstaged-') === 0 ? head : null, 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); this.editor.attachMergeRequestModel(this.model);
} else { } else {
this.editor.attachModel(this.model); this.editor.attachModel(this.model);
...@@ -168,6 +181,7 @@ export default { ...@@ -168,6 +181,7 @@ export default {
}); });
}, },
}, },
viewerTypes,
}; };
</script> </script>
...@@ -176,16 +190,17 @@ export default { ...@@ -176,16 +190,17 @@ export default {
id="ide" id="ide"
class="blob-viewer-container blob-editor-container" class="blob-viewer-container blob-editor-container"
> >
<div class="ide-mode-tabs clearfix"> <div class="ide-mode-tabs clearfix" >
<ul <ul
class="nav-links pull-left" class="nav-links pull-left"
v-if="!shouldHideEditor"> v-if="!shouldHideEditor && isEditModeActive"
>
<li :class="editTabCSS"> <li :class="editTabCSS">
<a <a
href="javascript:void(0);" href="javascript:void(0);"
role="button" role="button"
@click.prevent="setFileViewMode({ file, viewMode: 'edit' })"> @click.prevent="setFileViewMode({ file, viewMode: 'edit' })">
<template v-if="viewer === 'editor'"> <template v-if="viewer === $options.viewerTypes.edit">
{{ __('Edit') }} {{ __('Edit') }}
</template> </template>
<template v-else> <template v-else>
...@@ -212,6 +227,9 @@ export default { ...@@ -212,6 +227,9 @@ export default {
v-show="!shouldHideEditor && file.viewMode === 'edit'" v-show="!shouldHideEditor && file.viewMode === 'edit'"
ref="editor" ref="editor"
class="multi-file-editor-holder" class="multi-file-editor-holder"
:class="{
'is-readonly': isCommitModeActive,
}"
> >
</div> </div>
<content-viewer <content-viewer
......
...@@ -34,6 +34,11 @@ export default { ...@@ -34,6 +34,11 @@ export default {
type: Number, type: Number,
required: true, required: true,
}, },
disableActionDropdown: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
...mapGetters([ ...mapGetters([
...@@ -99,16 +104,14 @@ export default { ...@@ -99,16 +104,14 @@ export default {
} }
}, },
methods: { methods: {
...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']), ...mapActions(['toggleTreeOpen']),
clickFile() { clickFile() {
// Manual Action if a tree is selected/opened // Manual Action if a tree is selected/opened
if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) { if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
this.toggleTreeOpen(this.file.path); 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 { ...@@ -170,7 +173,7 @@ export default {
/> />
</span> </span>
<new-dropdown <new-dropdown
v-if="isTree" v-if="isTree && !disableActionDropdown"
:project-id="file.projectId" :project-id="file.projectId"
:branch="file.branchId" :branch="file.branchId"
:path="file.path" :path="file.path"
......
...@@ -32,6 +32,8 @@ export default { ...@@ -32,6 +32,8 @@ export default {
return `Close ${this.tab.name}`; return `Close ${this.tab.name}`;
}, },
showChangedIcon() { showChangedIcon() {
if (this.tab.pending) return true;
return this.fileHasChanged ? !this.tabMouseOver : false; return this.fileHasChanged ? !this.tabMouseOver : false;
}, },
fileHasChanged() { fileHasChanged() {
...@@ -66,15 +68,32 @@ export default { ...@@ -66,15 +68,32 @@ export default {
<template> <template>
<li <li
:class="{
active: tab.active
}"
@click="clickFile(tab)" @click="clickFile(tab)"
@mouseover="mouseOverTab" @mouseover="mouseOverTab"
@mouseout="mouseOutTab" @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 <button
type="button" type="button"
class="multi-file-tab-close" class="multi-file-tab-close"
@click.stop.prevent="closeFile(tab)" @click.stop.prevent="closeFile(tab)"
:aria-label="closeLabel" :aria-label="closeLabel"
:disabled="tab.pending"
> >
<icon <icon
v-if="!showChangedIcon" v-if="!showChangedIcon"
...@@ -87,22 +106,5 @@ export default { ...@@ -87,22 +106,5 @@ export default {
:force-modified-icon="true" :force-modified-icon="true"
/> />
</button> </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> </li>
</template> </template>
...@@ -32,16 +32,6 @@ export default { ...@@ -32,16 +32,6 @@ export default {
default: '', default: '',
}, },
}, },
data() {
return {
showShadow: false,
};
},
updated() {
if (!this.$refs.tabsScroller) return;
this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
},
methods: { methods: {
...mapActions(['updateViewer', 'removePendingTab']), ...mapActions(['updateViewer', 'removePendingTab']),
openFileViewer(viewer) { openFileViewer(viewer) {
...@@ -71,12 +61,5 @@ export default { ...@@ -71,12 +61,5 @@ export default {
:tab="tab" :tab="tab"
/> />
</ul> </ul>
<editor-mode
:viewer="viewer"
:show-shadow="showShadow"
:has-changes="hasChanges"
:merge-request-id="mergeRequestId"
@click="openFileViewer"
/>
</div> </div>
</template> </template>
...@@ -3,6 +3,22 @@ export const MAX_FILE_FINDER_RESULTS = 40; ...@@ -3,6 +3,22 @@ export const MAX_FILE_FINDER_RESULTS = 40;
export const FILE_FINDER_ROW_HEIGHT = 55; export const FILE_FINDER_ROW_HEIGHT = 55;
export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33; export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
export const MAX_WINDOW_HEIGHT_COMPACT = 750;
export const COMMIT_ITEM_PADDING = 32;
// Commit message textarea // Commit message textarea
export const MAX_TITLE_LENGTH = 50; export const MAX_TITLE_LENGTH = 50;
export const MAX_BODY_LENGTH = 72; 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'; ...@@ -2,6 +2,7 @@ import Vue from 'vue';
import VueRouter from 'vue-router'; import VueRouter from 'vue-router';
import flash from '~/flash'; import flash from '~/flash';
import store from './stores'; import store from './stores';
import { activityBarViews } from './constants';
Vue.use(VueRouter); Vue.use(VueRouter);
...@@ -63,6 +64,8 @@ router.beforeEach((to, from, next) => { ...@@ -63,6 +64,8 @@ router.beforeEach((to, from, next) => {
const fullProjectId = `${to.params.namespace}/${to.params.project}`; const fullProjectId = `${to.params.namespace}/${to.params.project}`;
if (to.params.branch) { if (to.params.branch) {
store.dispatch('setCurrentBranchId', to.params.branch);
store.dispatch('getBranchData', { store.dispatch('getBranchData', {
projectId: fullProjectId, projectId: fullProjectId,
branchId: to.params.branch, branchId: to.params.branch,
...@@ -99,14 +102,14 @@ router.beforeEach((to, from, next) => { ...@@ -99,14 +102,14 @@ router.beforeEach((to, from, next) => {
throw e; throw e;
}); });
} else if (to.params.mrid) { } else if (to.params.mrid) {
store.dispatch('updateViewer', 'mrdiff');
store store
.dispatch('getMergeRequestData', { .dispatch('getMergeRequestData', {
projectId: fullProjectId, projectId: fullProjectId,
mergeRequestId: to.params.mrid, mergeRequestId: to.params.mrid,
}) })
.then(mr => { .then(mr => {
store.dispatch('updateActivityBarView', activityBarViews.review);
store.dispatch('getBranchData', { store.dispatch('getBranchData', {
projectId: fullProjectId, projectId: fullProjectId,
branchId: mr.source_branch, branchId: mr.source_branch,
......
...@@ -14,15 +14,16 @@ function initIde(el) { ...@@ -14,15 +14,16 @@ function initIde(el) {
components: { components: {
ide, ide,
}, },
render(createElement) { created() {
return createElement('ide', { this.$store.dispatch('setEmptyStateSvgs', {
props: {
emptyStateSvgPath: el.dataset.emptyStateSvgPath, emptyStateSvgPath: el.dataset.emptyStateSvgPath,
noChangesStateSvgPath: el.dataset.noChangesStateSvgPath, noChangesStateSvgPath: el.dataset.noChangesStateSvgPath,
committedStateSvgPath: el.dataset.committedStateSvgPath, committedStateSvgPath: el.dataset.committedStateSvgPath,
},
}); });
}, },
render(createElement) {
return createElement('ide');
},
}); });
} }
......
...@@ -61,19 +61,19 @@ export default class Editor { ...@@ -61,19 +61,19 @@ export default class Editor {
} }
} }
createDiffInstance(domElement) { createDiffInstance(domElement, readOnly = true) {
if (!this.instance) { if (!this.instance) {
clearDomElement(domElement); clearDomElement(domElement);
this.disposable.add( this.disposable.add(
(this.instance = this.monaco.editor.createDiffEditor(domElement, { (this.instance = this.monaco.editor.createDiffEditor(domElement, {
...defaultEditorOptions, ...defaultEditorOptions,
readOnly: true,
quickSuggestions: false, quickSuggestions: false,
occurrencesHighlight: false, occurrencesHighlight: false,
renderLineHighlight: 'none',
hideCursorInOverviewRuler: true,
renderSideBySide: Editor.renderSideBySide(domElement), renderSideBySide: Editor.renderSideBySide(domElement),
readOnly,
renderLineHighlight: readOnly ? 'all' : 'none',
hideCursorInOverviewRuler: !readOnly,
})), })),
); );
......
...@@ -123,6 +123,8 @@ export const scrollToTab = () => { ...@@ -123,6 +123,8 @@ export const scrollToTab = () => {
}; };
export const stageAllChanges = ({ state, commit }) => { export const stageAllChanges = ({ state, commit }) => {
commit(types.SET_LAST_COMMIT_MSG, '');
state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path)); state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path));
}; };
...@@ -138,6 +140,18 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => { ...@@ -138,6 +140,18 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
commit(types.UPDATE_DELAY_VIEWER_CHANGE, 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 }) => { export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, tempFile }) => {
commit(types.UPDATE_TEMP_FLAG, { path: file.path, tempFile }); commit(types.UPDATE_TEMP_FLAG, { path: file.path, tempFile });
......
...@@ -5,6 +5,7 @@ import service from '../../services'; ...@@ -5,6 +5,7 @@ import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import router from '../../ide_router'; import router from '../../ide_router';
import { setPageTitle } from '../utils'; import { setPageTitle } from '../utils';
import { viewerTypes } from '../../constants';
export const closeFile = ({ commit, state, dispatch }, file) => { export const closeFile = ({ commit, state, dispatch }, file) => {
const path = file.path; const path = file.path;
...@@ -23,13 +24,12 @@ export const closeFile = ({ commit, state, dispatch }, file) => { ...@@ -23,13 +24,12 @@ export const closeFile = ({ commit, state, dispatch }, file) => {
const nextFileToOpen = state.openFiles[nextIndexToOpen]; const nextFileToOpen = state.openFiles[nextIndexToOpen];
if (nextFileToOpen.pending) { if (nextFileToOpen.pending) {
dispatch('updateViewer', 'diff'); dispatch('updateViewer', viewerTypes.diff);
dispatch('openPendingTab', { dispatch('openPendingTab', {
file: nextFileToOpen, file: nextFileToOpen,
keyPrefix: nextFileToOpen.staged ? 'staged' : 'unstaged', keyPrefix: nextFileToOpen.staged ? 'staged' : 'unstaged',
}); });
} else { } else {
dispatch('updateDelayViewerUpdated', true);
router.push(`/project${nextFileToOpen.url}`); router.push(`/project${nextFileToOpen.url}`);
} }
} else if (!state.openFiles.length) { } else if (!state.openFiles.length) {
...@@ -184,6 +184,7 @@ export const stageChange = ({ commit, state }, path) => { ...@@ -184,6 +184,7 @@ export const stageChange = ({ commit, state }, path) => {
const stagedFile = state.stagedFiles.find(f => f.path === path); const stagedFile = state.stagedFiles.find(f => f.path === path);
commit(types.STAGE_CHANGE, path); commit(types.STAGE_CHANGE, path);
commit(types.SET_LAST_COMMIT_MSG, '');
if (stagedFile) { if (stagedFile) {
eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content); eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content);
...@@ -195,9 +196,7 @@ export const unstageChange = ({ commit }, path) => { ...@@ -195,9 +196,7 @@ export const unstageChange = ({ commit }, path) => {
}; };
export const openPendingTab = ({ commit, getters, dispatch, state }, { file, keyPrefix }) => { export const openPendingTab = ({ commit, getters, dispatch, state }, { file, keyPrefix }) => {
if (getters.activeFile && getters.activeFile === file && state.viewer === 'diff') { state.openFiles.forEach(f => eventHub.$emit(`editor.update.model.dispose.${f.key}`));
return false;
}
commit(types.ADD_PENDING_TAB, { file, keyPrefix }); commit(types.ADD_PENDING_TAB, { file, keyPrefix });
......
...@@ -55,7 +55,6 @@ export const getBranchData = ( ...@@ -55,7 +55,6 @@ export const getBranchData = (
branch: data, branch: data,
}); });
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id }); commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
commit(types.SET_CURRENT_BRANCH, branchId);
resolve(data); resolve(data);
}) })
.catch(() => { .catch(() => {
......
import { __ } from '~/locale';
import { getChangesCountForFiles, filePathMatches } from './utils'; import { getChangesCountForFiles, filePathMatches } from './utils';
import { activityBarViews } from '../constants';
export const activeFile = state => state.openFiles.find(file => file.active) || null; export const activeFile = state => state.openFiles.find(file => file.active) || null;
...@@ -31,15 +31,12 @@ export const currentMergeRequest = state => { ...@@ -31,15 +31,12 @@ export const currentMergeRequest = state => {
return null; return null;
}; };
// eslint-disable-next-line no-confusing-arrow export const currentProject = state => state.projects[state.currentProjectId];
export const collapseButtonIcon = state =>
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
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 hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length;
export const collapseButtonTooltip = state =>
state.rightPanelCollapsed ? __('Expand sidebar') : __('Collapse sidebar');
export const hasMergeRequest = state => !!state.currentMergeRequestId; export const hasMergeRequest = state => !!state.currentMergeRequestId;
...@@ -59,6 +56,16 @@ export const allBlobs = state => ...@@ -59,6 +56,16 @@ export const allBlobs = state =>
export const getChangedFile = state => path => state.changedFiles.find(f => f.path === path); 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 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 => { export const getChangesInFolder = state => path => {
const changedFilesCount = state.changedFiles.filter(f => filePathMatches(f, path)).length; const changedFilesCount = state.changedFiles.filter(f => filePathMatches(f, path)).length;
const stagedFilesCount = state.stagedFiles.filter( const stagedFilesCount = state.stagedFiles.filter(
......
...@@ -8,6 +8,7 @@ import router from '../../../ide_router'; ...@@ -8,6 +8,7 @@ import router from '../../../ide_router';
import service from '../../../services'; import service from '../../../services';
import * as types from './mutation_types'; import * as types from './mutation_types';
import * as consts from './constants'; import * as consts from './constants';
import { activityBarViews } from '../../../constants';
import eventHub from '../../../eventhub'; import eventHub from '../../../eventhub';
export const updateCommitMessage = ({ commit }, message) => { export const updateCommitMessage = ({ commit }, message) => {
...@@ -75,7 +76,7 @@ export const checkCommitStatus = ({ rootState }) => ...@@ -75,7 +76,7 @@ export const checkCommitStatus = ({ rootState }) =>
export const updateFilesAfterCommit = ( export const updateFilesAfterCommit = (
{ commit, dispatch, state, rootState, rootGetters }, { commit, dispatch, state, rootState, rootGetters },
{ data, branch }, { data },
) => { ) => {
const selectedProject = rootState.projects[rootState.currentProjectId]; const selectedProject = rootState.projects[rootState.currentProjectId];
const lastCommit = { const lastCommit = {
...@@ -126,15 +127,9 @@ export const updateFilesAfterCommit = ( ...@@ -126,15 +127,9 @@ export const updateFilesAfterCommit = (
changed: !!changedFile, 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 newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
const payload = createCommitPayload(getters.branchName, newBranch, state, rootState); const payload = createCommitPayload(getters.branchName, newBranch, state, rootState);
const getCommitStatus = newBranch ? Promise.resolve(false) : dispatch('checkCommitStatus'); const getCommitStatus = newBranch ? Promise.resolve(false) : dispatch('checkCommitStatus');
...@@ -187,6 +182,34 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) = ...@@ -187,6 +182,34 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) =
commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true }); commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true });
}, 5000); }, 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)); .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH));
}) })
.catch(err => { .catch(err => {
......
...@@ -5,6 +5,7 @@ export const SET_LAST_COMMIT_MSG = 'SET_LAST_COMMIT_MSG'; ...@@ -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_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED'; export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS'; export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS';
export const SET_EMPTY_STATE_SVGS = 'SET_EMPTY_STATE_SVGS';
// Project Mutation Types // Project Mutation Types
export const SET_PROJECT = 'SET_PROJECT'; export const SET_PROJECT = 'SET_PROJECT';
...@@ -59,6 +60,7 @@ export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT'; ...@@ -59,6 +60,7 @@ export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT';
export const ADD_PENDING_TAB = 'ADD_PENDING_TAB'; export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
export const REMOVE_PENDING_TAB = 'REMOVE_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 UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER'; export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL'; export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL';
...@@ -107,6 +107,21 @@ export default { ...@@ -107,6 +107,21 @@ export default {
delayViewerUpdated, 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) { [types.TOGGLE_FILE_FINDER](state, fileFindVisible) {
Object.assign(state, { Object.assign(state, {
fileFindVisible, fileFindVisible,
......
/* eslint-disable no-param-reassign */
import * as types from '../mutation_types'; import * as types from '../mutation_types';
export default { export default {
...@@ -169,32 +170,24 @@ export default { ...@@ -169,32 +170,24 @@ export default {
}); });
}, },
[types.ADD_PENDING_TAB](state, { file, keyPrefix = 'pending' }) { [types.ADD_PENDING_TAB](state, { file, keyPrefix = 'pending' }) {
const key = `${keyPrefix}-${file.key}`; state.entries[file.path].opened = false;
const pendingTab = state.openFiles.find(f => f.key === key && f.pending); state.entries[file.path].active = false;
let openFiles = state.openFiles.map(f => Object.assign(f, { active: false, opened: false })); state.entries[file.path].lastOpenedAt = new Date().getTime();
state.openFiles.forEach(f =>
if (!pendingTab) { Object.assign(f, {
const openFile = openFiles.find(f => f.path === file.path); opened: false,
active: false,
openFiles = openFiles.concat(openFile ? null : file).reduce((acc, f) => { }),
if (!f) return acc; );
state.openFiles = [
if (f.path === file.path) { {
return acc.concat({ ...file,
...f, key: `${keyPrefix}-${file.key}`,
content: file.content,
active: true,
pending: true, pending: true,
opened: true, opened: true,
key, active: true,
}); },
} ];
return acc.concat(f);
}, []);
}
Object.assign(state, { openFiles });
}, },
[types.REMOVE_PENDING_TAB](state, file) { [types.REMOVE_PENDING_TAB](state, file) {
Object.assign(state, { Object.assign(state, {
......
import { activityBarViews, viewerTypes } from '../constants';
export default () => ({ export default () => ({
currentProjectId: '', currentProjectId: '',
currentBranchId: '', currentBranchId: '',
...@@ -16,8 +18,9 @@ export default () => ({ ...@@ -16,8 +18,9 @@ export default () => ({
rightPanelCollapsed: false, rightPanelCollapsed: false,
panelResizing: false, panelResizing: false,
entries: {}, entries: {},
viewer: 'editor', viewer: viewerTypes.edit,
delayViewerUpdated: false, delayViewerUpdated: false,
currentActivityView: activityBarViews.edit,
unusedSeal: true, unusedSeal: true,
fileFindVisible: false, fileFindVisible: false,
}); });
...@@ -177,25 +177,6 @@ ...@@ -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 { .branch-header-title {
color: $color-700; color: $color-700;
} }
...@@ -203,6 +184,13 @@ ...@@ -203,6 +184,13 @@
.ide-file-list .file.file-active { .ide-file-list .file.file-active {
color: $color-700; color: $color-700;
} }
.ide-sidebar-link {
&.active {
color: $color-700;
box-shadow: inset 3px 0 $color-700;
}
}
} }
body { body {
...@@ -343,9 +331,5 @@ body { ...@@ -343,9 +331,5 @@ body {
.sidebar-top-level-items > li.active .badge { .sidebar-top-level-items > li.active .badge {
color: $theme-gray-900; color: $theme-gray-900;
} }
.ide-sidebar-link {
color: $white-light;
}
} }
} }
This diff is collapsed.
...@@ -44,12 +44,17 @@ feature 'Multi-file editor new directory', :js do ...@@ -44,12 +44,17 @@ feature 'Multi-file editor new directory', :js do
wait_for_requests 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') fill_in('commit-message', with: 'commit message ide')
click_button('Commit') click_button('Commit')
find('.js-ide-edit-mode').click
expect(page).to have_content('folder name') expect(page).to have_content('folder name')
end end
end end
...@@ -34,7 +34,10 @@ feature 'Multi-file editor new file', :js do ...@@ -34,7 +34,10 @@ feature 'Multi-file editor new file', :js do
wait_for_requests 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') 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', () => { ...@@ -10,10 +10,9 @@ describe('IDE commit panel empty state', () => {
beforeEach(() => { beforeEach(() => {
const Component = Vue.extend(emptyState); const Component = Vue.extend(emptyState);
vm = createComponentWithStore(Component, store, { Vue.set(store.state, 'noChangesStateSvgPath', 'no-changes');
noChangesStateSvgPath: 'no-changes',
committedStateSvgPath: 'committed-state', vm = createComponentWithStore(Component, store);
});
vm.$mount(); vm.$mount();
}); });
...@@ -27,37 +26,4 @@ describe('IDE commit panel empty state', () => { ...@@ -27,37 +26,4 @@ describe('IDE commit panel empty state', () => {
it('renders no changes text when last commit message is empty', () => { it('renders no changes text when last commit message is empty', () => {
expect(vm.$el.textContent).toContain('No changes'); 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', () => { ...@@ -49,45 +49,4 @@ describe('Multi-file editor commit sidebar list', () => {
expect(vm.$el.textContent).toContain('No changes'); 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 Vue from 'vue';
import store from '~/ide/stores'; import store from '~/ide/stores';
import ideSidebar from '~/ide/components/ide_side_bar.vue'; import ideSidebar from '~/ide/components/ide_side_bar.vue';
import { activityBarViews } from '~/ide/constants';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../helpers'; import { resetStore } from '../helpers';
import { projectData } from '../mock_data';
describe('IdeSidebar', () => { describe('IdeSidebar', () => {
let vm; let vm;
...@@ -10,6 +12,9 @@ describe('IdeSidebar', () => { ...@@ -10,6 +12,9 @@ describe('IdeSidebar', () => {
beforeEach(() => { beforeEach(() => {
const Component = Vue.extend(ideSidebar); const Component = Vue.extend(ideSidebar);
store.state.currentProjectId = 'abcproject';
store.state.projects.abcproject = projectData;
vm = createComponentWithStore(Component, store).$mount(); vm = createComponentWithStore(Component, store).$mount();
}); });
...@@ -20,23 +25,33 @@ describe('IdeSidebar', () => { ...@@ -20,23 +25,33 @@ describe('IdeSidebar', () => {
}); });
it('renders a sidebar', () => { it('renders a sidebar', () => {
expect( expect(vm.$el.querySelector('.multi-file-commit-panel-inner')).not.toBeNull();
vm.$el.querySelector('.multi-file-commit-panel-inner'),
).not.toBeNull();
}); });
it('renders loading icon component', done => { it('renders loading icon component', done => {
vm.$store.state.loading = true; vm.$store.state.loading = true;
vm.$nextTick(() => { vm.$nextTick(() => {
expect( expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull();
vm.$el.querySelector('.multi-file-loading-container'), expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3);
).not.toBeNull();
expect(
vm.$el.querySelectorAll('.multi-file-loading-container').length,
).toBe(3);
done(); 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'; ...@@ -4,6 +4,7 @@ import store from '~/ide/stores';
import ide from '~/ide/components/ide.vue'; import ide from '~/ide/components/ide.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { file, resetStore } from '../helpers'; import { file, resetStore } from '../helpers';
import { projectData } from '../mock_data';
describe('ide component', () => { describe('ide component', () => {
let vm; let vm;
...@@ -11,6 +12,10 @@ describe('ide component', () => { ...@@ -11,6 +12,10 @@ describe('ide component', () => {
beforeEach(() => { beforeEach(() => {
const Component = Vue.extend(ide); const Component = Vue.extend(ide);
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
store.state.projects.abcproject = Object.assign({}, projectData);
vm = createComponentWithStore(Component, store, { vm = createComponentWithStore(Component, store, {
emptyStateSvgPath: 'svg', emptyStateSvgPath: 'svg',
noChangesStateSvgPath: 'svg', noChangesStateSvgPath: 'svg',
...@@ -24,11 +29,11 @@ describe('ide component', () => { ...@@ -24,11 +29,11 @@ describe('ide component', () => {
resetStore(vm.$store); 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(); 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'] = { vm.$store.state.trees['abcproject/mybranch'] = {
tree: [file()], 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 Vue from 'vue';
import store from '~/ide/stores'; import store from '~/ide/stores';
import service from '~/ide/services'; import service from '~/ide/services';
import router from '~/ide/ide_router';
import repoCommitSection from '~/ide/components/repo_commit_section.vue'; import repoCommitSection from '~/ide/components/repo_commit_section.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
import { file, resetStore } from '../helpers'; import { file, resetStore } from '../helpers';
describe('RepoCommitSection', () => { describe('RepoCommitSection', () => {
...@@ -12,10 +12,10 @@ describe('RepoCommitSection', () => { ...@@ -12,10 +12,10 @@ describe('RepoCommitSection', () => {
function createComponent() { function createComponent() {
const Component = Vue.extend(repoCommitSection); const Component = Vue.extend(repoCommitSection);
vm = createComponentWithStore(Component, store, { store.state.noChangesStateSvgPath = 'svg';
noChangesStateSvgPath: 'svg', store.state.committedStateSvgPath = 'commitsvg';
committedStateSvgPath: 'commitsvg',
}); vm = createComponentWithStore(Component, store);
vm.$store.state.currentProjectId = 'abcproject'; vm.$store.state.currentProjectId = 'abcproject';
vm.$store.state.currentBranchId = 'master'; vm.$store.state.currentBranchId = 'master';
...@@ -60,6 +60,8 @@ describe('RepoCommitSection', () => { ...@@ -60,6 +60,8 @@ describe('RepoCommitSection', () => {
} }
beforeEach(done => { beforeEach(done => {
spyOn(router, 'push');
vm = createComponent(); vm = createComponent();
spyOn(service, 'getTreeData').and.returnValue( spyOn(service, 'getTreeData').and.returnValue(
...@@ -93,61 +95,49 @@ describe('RepoCommitSection', () => { ...@@ -93,61 +95,49 @@ describe('RepoCommitSection', () => {
resetStore(vm.$store); resetStore(vm.$store);
const Component = Vue.extend(repoCommitSection); const Component = Vue.extend(repoCommitSection);
vm = createComponentWithStore(Component, store, { store.state.noChangesStateSvgPath = 'nochangessvg';
noChangesStateSvgPath: 'nochangessvg', store.state.committedStateSvgPath = 'svg';
committedStateSvgPath: 'svg',
}).$mount();
expect( vm = createComponentWithStore(Component, store).$mount();
vm.$el.querySelector('.js-empty-state').textContent.trim(),
).toContain('No changes'); expect(vm.$el.querySelector('.js-empty-state').textContent.trim()).toContain('No changes');
expect( expect(vm.$el.querySelector('.js-empty-state img').getAttribute('src')).toBe('nochangessvg');
vm.$el.querySelector('.js-empty-state img').getAttribute('src'),
).toBe('nochangessvg');
}); });
}); });
it('renders a commit section', () => { it('renders a commit section', () => {
const changedFileElements = [ const changedFileElements = [...vm.$el.querySelectorAll('.multi-file-commit-list li')];
...vm.$el.querySelectorAll('.multi-file-commit-list li'), const allFiles = vm.$store.state.changedFiles.concat(vm.$store.state.stagedFiles);
];
const submitCommit = vm.$el.querySelector('form .btn');
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); expect(changedFileElements.length).toEqual(4);
changedFileElements.forEach((changedFile, i) => { changedFileElements.forEach((changedFile, i) => {
expect(changedFile.textContent.trim()).toContain(allFiles[i].path); 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 => { it('adds changed files into staged files', done => {
vm.$el.querySelector('.ide-staged-action-btn').click(); vm.$el.querySelector('.multi-file-discard-btn .btn').click();
vm
Vue.nextTick(() => { .$nextTick()
expect( .then(() => vm.$el.querySelector('.multi-file-discard-btn .btn').click())
vm.$el.querySelector('.ide-commit-list-container').textContent, .then(vm.$nextTick)
).toContain('No changes'); .then(() => {
expect(vm.$el.querySelector('.ide-commit-list-container').textContent).toContain(
done(); 'No changes',
}); );
})
.then(done)
.catch(done.fail);
}); });
it('stages a single file', done => { it('stages a single file', done => {
vm.$el.querySelector('.multi-file-discard-btn .btn').click(); vm.$el.querySelector('.multi-file-discard-btn .btn').click();
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(vm.$el.querySelector('.ide-commit-list-container').querySelectorAll('li').length).toBe(
vm.$el 1,
.querySelector('.ide-commit-list-container') );
.querySelectorAll('li').length,
).toBe(1);
done(); done();
}); });
...@@ -157,26 +147,10 @@ describe('RepoCommitSection', () => { ...@@ -157,26 +147,10 @@ describe('RepoCommitSection', () => {
vm.$el.querySelectorAll('.multi-file-discard-btn .btn')[1].click(); vm.$el.querySelectorAll('.multi-file-discard-btn .btn')[1].click();
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(vm.$el.querySelector('.ide-commit-list-container').textContent).not.toContain('file1');
vm.$el.querySelector('.ide-commit-list-container').textContent, expect(vm.$el.querySelector('.ide-commit-list-container').querySelectorAll('li').length).toBe(
).not.toContain('file1'); 1,
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');
done(); done();
}); });
...@@ -190,75 +164,17 @@ describe('RepoCommitSection', () => { ...@@ -190,75 +164,17 @@ describe('RepoCommitSection', () => {
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(
vm.$el vm.$el.querySelectorAll('.ide-commit-list-container')[1].querySelectorAll('li').length,
.querySelectorAll('.ide-commit-list-container')[1]
.querySelectorAll('li').length,
).toBe(1); ).toBe(1);
done(); done();
}); });
}); });
it('updates commitMessage in store on input', done => { describe('mounted', () => {
const textarea = vm.$el.querySelector('textarea'); it('opens last opened file', () => {
expect(store.state.openFiles.length).toBe(1);
textarea.value = 'testing commit message'; expect(store.state.openFiles[0].pending).toBe(true);
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);
}); });
}); });
}); });
...@@ -5,6 +5,7 @@ import store from '~/ide/stores'; ...@@ -5,6 +5,7 @@ import store from '~/ide/stores';
import repoEditor from '~/ide/components/repo_editor.vue'; import repoEditor from '~/ide/components/repo_editor.vue';
import monacoLoader from '~/ide/monaco_loader'; import monacoLoader from '~/ide/monaco_loader';
import Editor from '~/ide/lib/editor'; import Editor from '~/ide/lib/editor';
import { activityBarViews } from '~/ide/constants';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
import setTimeoutPromise from '../../helpers/set_timeout_promise_helper'; import setTimeoutPromise from '../../helpers/set_timeout_promise_helper';
import { file, resetStore } from '../helpers'; import { file, resetStore } from '../helpers';
...@@ -295,4 +296,30 @@ describe('RepoEditor', () => { ...@@ -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', () => { ...@@ -73,6 +73,43 @@ describe('RepoFile', () => {
expect(treeChangesEl).not.toBeNull(); expect(treeChangesEl).not.toBeNull();
expect(treeChangesEl.textContent).toContain('1'); 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', () => { describe('locked file', () => {
......
...@@ -26,60 +26,10 @@ describe('RepoTabs', () => { ...@@ -26,60 +26,10 @@ describe('RepoTabs', () => {
const tabs = [...vm.$el.querySelectorAll('.multi-file-tab')]; const tabs = [...vm.$el.querySelectorAll('.multi-file-tab')];
expect(tabs.length).toEqual(2); expect(tabs.length).toEqual(2);
expect(tabs[0].classList.contains('active')).toEqual(true); expect(tabs[0].parentNode.classList.contains('active')).toEqual(true);
expect(tabs[1].classList.contains('active')).toEqual(false); expect(tabs[1].parentNode.classList.contains('active')).toEqual(false);
done(); 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', () => { ...@@ -74,10 +74,10 @@ describe('Multi-file editor library', () => {
scrollBeyondLastLine: false, scrollBeyondLastLine: false,
quickSuggestions: false, quickSuggestions: false,
occurrencesHighlight: false, occurrencesHighlight: false,
renderLineHighlight: 'none',
hideCursorInOverviewRuler: true,
wordWrap: 'on', wordWrap: 'on',
renderSideBySide: true, 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', () => { ...@@ -511,7 +511,10 @@ describe('IDE store file actions', () => {
actions.stageChange, actions.stageChange,
'path', 'path',
store.state, store.state,
[{ type: types.STAGE_CHANGE, payload: 'path' }], [
{ type: types.STAGE_CHANGE, payload: 'path' },
{ type: types.SET_LAST_COMMIT_MSG, payload: '' },
],
[], [],
done, done,
); );
...@@ -524,7 +527,10 @@ describe('IDE store file actions', () => { ...@@ -524,7 +527,10 @@ describe('IDE store file actions', () => {
actions.unstageChange, actions.unstageChange,
'path', 'path',
store.state, store.state,
[{ type: types.UNSTAGE_CHANGE, payload: 'path' }], [
{ type: types.UNSTAGE_CHANGE, payload: 'path' },
{ type: types.SET_LAST_COMMIT_MSG, payload: '' },
],
[], [],
done, done,
); );
...@@ -589,20 +595,6 @@ describe('IDE store file actions', () => { ...@@ -589,20 +595,6 @@ describe('IDE store file actions', () => {
.then(done) .then(done)
.catch(done.fail); .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', () => { describe('removePendingTab', () => {
......
...@@ -2,6 +2,9 @@ import actions, { ...@@ -2,6 +2,9 @@ import actions, {
stageAllChanges, stageAllChanges,
unstageAllChanges, unstageAllChanges,
toggleFileFinder, toggleFileFinder,
setCurrentBranchId,
setEmptyStateSvgs,
updateActivityBarView,
updateTempFlagForEntry, updateTempFlagForEntry,
} from '~/ide/stores/actions'; } from '~/ide/stores/actions';
import store from '~/ide/stores'; import store from '~/ide/stores';
...@@ -306,6 +309,7 @@ describe('Multi-file store actions', () => { ...@@ -306,6 +309,7 @@ describe('Multi-file store actions', () => {
null, null,
store.state, 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[0].path },
{ type: types.STAGE_CHANGE, payload: store.state.changedFiles[1].path }, { type: types.STAGE_CHANGE, payload: store.state.changedFiles[1].path },
], ],
...@@ -345,6 +349,32 @@ describe('Multi-file store actions', () => { ...@@ -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', () => { describe('updateTempFlagForEntry', () => {
it('commits UPDATE_TEMP_FLAG', done => { it('commits UPDATE_TEMP_FLAG', done => {
const f = { const f = {
...@@ -388,6 +418,19 @@ describe('Multi-file store actions', () => { ...@@ -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', () => { describe('toggleFileFinder', () => {
it('commits TOGGLE_FILE_FINDER', done => { it('commits TOGGLE_FILE_FINDER', done => {
testAction( testAction(
......
...@@ -37,12 +37,6 @@ describe('IDE store getters', () => { ...@@ -37,12 +37,6 @@ describe('IDE store getters', () => {
expect(modifiedFiles.length).toBe(1); expect(modifiedFiles.length).toBe(1);
expect(modifiedFiles[0].name).toBe('changed'); 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', () => { describe('currentMergeRequest', () => {
......
...@@ -289,21 +289,6 @@ describe('IDE commit module actions', () => { ...@@ -289,21 +289,6 @@ describe('IDE commit module actions', () => {
.then(done) .then(done)
.catch(done.fail); .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', () => { describe('commitChanges', () => {
...@@ -391,21 +376,6 @@ describe('IDE commit module actions', () => { ...@@ -391,21 +376,6 @@ describe('IDE commit module actions', () => {
.catch(done.fail); .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 => { it('sets last Commit Msg', done => {
store store
.dispatch('commit/commitChanges') .dispatch('commit/commitChanges')
......
...@@ -267,41 +267,23 @@ describe('IDE store file mutations', () => { ...@@ -267,41 +267,23 @@ describe('IDE store file mutations', () => {
it('adds file into openFiles as pending', () => { it('adds file into openFiles as pending', () => {
mutations.ADD_PENDING_TAB(localState, { file: localFile }); 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.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', () => { it('only allows 1 open pending file', () => {
localState.openFiles.push({ const newFile = file('test');
...localFile, localState.entries[newFile.path] = newFile;
pending: true,
});
mutations.ADD_PENDING_TAB(localState, { file: localFile }); mutations.ADD_PENDING_TAB(localState, { file: localFile });
expect(localState.openFiles[1].pending).toBe(true); expect(localState.openFiles.length).toBe(1);
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(2); mutations.ADD_PENDING_TAB(localState, { file: file('test') });
localState.openFiles.forEach(f => { expect(localState.openFiles.length).toBe(1);
if (f.pending) { expect(localState.openFiles[0].name).toBe('test');
expect(f.active).toBe(true);
} else {
expect(f.active).toBe(false);
}
});
}); });
}); });
......
...@@ -87,6 +87,28 @@ describe('Multi-file store mutations', () => { ...@@ -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', () => { describe('UPDATE_TEMP_FLAG', () => {
beforeEach(() => { beforeEach(() => {
localState.entries.test = { 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