diff options
author | Filipa Lacerda <filipa@gitlab.com> | 2018-05-07 09:24:30 +0000 |
---|---|---|
committer | Filipa Lacerda <filipa@gitlab.com> | 2018-05-07 09:24:30 +0000 |
commit | eaeeac2ff42232f139a74d629d6dcf39c1819ee2 (patch) | |
tree | 51f73287ea8144a2253476b7aa0140a4bcc76d85 | |
parent | 9f7a6742466931f219cb83ff63e6debcec5db221 (diff) | |
parent | 398ee68457fd5bdcac14b5298f5c3f35b008d808 (diff) | |
download | gitlab-ce-eaeeac2ff42232f139a74d629d6dcf39c1819ee2.tar.gz |
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
67 files changed, 1851 insertions, 1406 deletions
diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue new file mode 100644 index 00000000000..05dbc1410de --- /dev/null +++ b/app/assets/javascripts/ide/components/activity_bar.vue @@ -0,0 +1,106 @@ +<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> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue index 45321df191c..6a5790c9dff 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue @@ -1,5 +1,5 @@ <script> -import { mapState } from 'vuex'; +import { mapActions, mapState } from 'vuex'; import { sprintf, __ } from '~/locale'; import * as consts from '../../stores/modules/commit/constants'; import RadioGroup from './radio_group.vue'; @@ -9,7 +9,7 @@ export default { RadioGroup, }, computed: { - ...mapState(['currentBranchId']), + ...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']), commitToCurrentBranchText() { return sprintf( __('Commit to %{branchName} branch'), @@ -17,6 +17,17 @@ export default { false, ); }, + disableMergeRequestRadio() { + return this.changedFiles.length > 0 && this.stagedFiles.length > 0; + }, + }, + mounted() { + if (this.disableMergeRequestRadio) { + this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH); + } + }, + methods: { + ...mapActions('commit', ['updateCommitAction']), }, commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH, commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH, @@ -44,6 +55,7 @@ export default { :value="$options.commitToNewBranchMR" :label="__('Create a new branch and merge request')" :show-input="true" + :disabled="disableMergeRequestRadio" /> </div> </template> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue b/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue index 1f6bbca13b5..d0a60d647e5 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue @@ -1,27 +1,9 @@ <script> -import { mapActions, mapState, mapGetters } from 'vuex'; -import Icon from '~/vue_shared/components/icon.vue'; -import tooltip from '~/vue_shared/directives/tooltip'; +import { mapState } from 'vuex'; export default { - components: { - Icon, - }, - directives: { - tooltip, - }, - props: { - noChangesStateSvgPath: { - type: String, - required: true, - }, - }, computed: { - ...mapState(['lastCommitMsg', 'rightPanelCollapsed', 'changedFiles', 'stagedFiles']), - ...mapGetters(['collapseButtonIcon', 'collapseButtonTooltip']), - }, - methods: { - ...mapActions(['toggleRightPanelCollapsed']), + ...mapState(['lastCommitMsg', 'noChangesStateSvgPath']), }, }; </script> @@ -31,31 +13,8 @@ export default { v-if="!lastCommitMsg" class="multi-file-commit-panel-section ide-commit-empty-state js-empty-state" > - <header - class="multi-file-commit-panel-header" - :class="{ - 'is-collapsed': rightPanelCollapsed, - }" - > - <button - v-tooltip - :title="collapseButtonTooltip" - data-container="body" - data-placement="left" - type="button" - class="btn btn-transparent multi-file-commit-panel-collapse-btn" - :aria-label="__('Toggle sidebar')" - @click.stop="toggleRightPanelCollapsed" - > - <icon - :name="collapseButtonIcon" - :size="18" - /> - </button> - </header> <div class="ide-commit-empty-state-container" - v-if="!rightPanelCollapsed" > <div class="svg-content svg-80"> <img :src="noChangesStateSvgPath" /> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue new file mode 100644 index 00000000000..4a645c827ad --- /dev/null +++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue @@ -0,0 +1,171 @@ +<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> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue index ff05ee8682a..c3ac18bfb83 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue @@ -1,16 +1,14 @@ <script> -import { mapActions, mapState, mapGetters } from 'vuex'; +import { mapActions } from 'vuex'; import { __, sprintf } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; import tooltip from '~/vue_shared/directives/tooltip'; import ListItem from './list_item.vue'; -import ListCollapsed from './list_collapsed.vue'; export default { components: { Icon, ListItem, - ListCollapsed, }, directives: { tooltip, @@ -24,11 +22,6 @@ export default { type: Array, required: true, }, - showToggle: { - type: Boolean, - required: false, - default: true, - }, iconName: { type: String, required: true, @@ -51,9 +44,12 @@ export default { default: false, }, }, + data() { + return { + showActionButton: false, + }; + }, computed: { - ...mapState(['rightPanelCollapsed']), - ...mapGetters(['collapseButtonIcon', 'collapseButtonTooltip']), titleText() { return sprintf(__('%{title} changes'), { title: this.title, @@ -61,10 +57,13 @@ export default { }, }, methods: { - ...mapActions(['toggleRightPanelCollapsed', 'stageAllChanges', 'unstageAllChanges']), + ...mapActions(['stageAllChanges', 'unstageAllChanges']), actionBtnClicked() { this[this.action](); }, + setShowActionButton(show) { + this.showActionButton = show; + }, }, }; </script> @@ -72,19 +71,14 @@ export default { <template> <div class="ide-commit-list-container" - :class="{ - 'is-collapsed': rightPanelCollapsed, - }" > <header class="multi-file-commit-panel-header" + @mouseenter="setShowActionButton(true)" + @mouseleave="setShowActionButton(false)" > <div - v-if="!rightPanelCollapsed" class="multi-file-commit-panel-header-title" - :class="{ - 'append-right-10': showToggle, - }" > <icon v-once @@ -92,7 +86,14 @@ export default { :size="18" /> {{ titleText }} + <span + v-show="!showActionButton" + class="ide-commit-file-count" + > + {{ fileList.length }} + </span> <button + v-show="showActionButton" type="button" class="btn btn-blank btn-link ide-staged-action-btn" @click="actionBtnClicked" @@ -100,52 +101,28 @@ export default { {{ actionBtnText }} </button> </div> - <button - v-if="showToggle" - v-tooltip - :title="collapseButtonTooltip" - data-container="body" - data-placement="left" - type="button" - class="btn btn-transparent multi-file-commit-panel-collapse-btn" - :aria-label="__('Toggle sidebar')" - @click.stop="toggleRightPanelCollapsed" - > - <icon - :name="collapseButtonIcon" - :size="18" - /> - </button> </header> - <list-collapsed - v-if="rightPanelCollapsed" - :files="fileList" - :icon-name="iconName" - :title="title" - /> - <template v-else> - <ul - v-if="fileList.length" - class="multi-file-commit-list list-unstyled append-bottom-0" - > - <li - v-for="file in fileList" - :key="file.key" - > - <list-item - :file="file" - :action-component="itemActionComponent" - :key-prefix="title" - :staged-list="stagedList" - /> - </li> - </ul> - <p - v-else - class="multi-file-commit-list help-block" + <ul + v-if="fileList.length" + class="multi-file-commit-list list-unstyled append-bottom-0" + > + <li + v-for="file in fileList" + :key="file.key" > - {{ __('No changes') }} - </p> - </template> + <list-item + :file="file" + :action-component="itemActionComponent" + :key-prefix="title" + :staged-list="stagedList" + /> + </li> + </ul> + <p + v-else + class="multi-file-commit-list help-block" + > + {{ __('No changes') }} + </p> </div> </template> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue index 872302840e2..03f3e4de83c 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue @@ -3,6 +3,7 @@ import { mapActions } from 'vuex'; import Icon from '~/vue_shared/components/icon.vue'; import StageButton from './stage_button.vue'; import UnstageButton from './unstage_button.vue'; +import { viewerTypes } from '../../constants'; export default { components: { @@ -53,7 +54,7 @@ export default { keyPrefix: this.keyPrefix.toLowerCase(), }).then(changeViewer => { if (changeViewer) { - this.updateViewer('diff'); + this.updateViewer(viewerTypes.diff); } }); }, diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue index b660a2961cb..00f2312ae51 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue @@ -1,5 +1,6 @@ <script> import { mapActions, mapState, mapGetters } from 'vuex'; +import { __ } from '~/locale'; import tooltip from '~/vue_shared/directives/tooltip'; export default { @@ -26,10 +27,20 @@ export default { required: false, default: false, }, + disabled: { + type: Boolean, + required: false, + default: false, + }, }, computed: { ...mapState('commit', ['commitAction']), ...mapGetters('commit', ['newBranchName']), + tooltipTitle() { + return this.disabled + ? __('This option is disabled while you still have unstaged changes') + : ''; + }, }, methods: { ...mapActions('commit', ['updateCommitAction', 'updateBranchName']), @@ -39,19 +50,28 @@ export default { <template> <fieldset> - <label> + <label + v-tooltip + :title="tooltipTitle" + :class="{ + 'is-disabled': disabled + }" + > <input type="radio" name="commit-action" :value="value" @change="updateCommitAction($event.target.value)" - :checked="checked" - v-once + :checked="commitAction === value" + :disabled="disabled" /> <span class="prepend-left-10"> - <template v-if="label"> + <span + v-if="label" + class="ide-radio-label" + > {{ label }} - </template> + </span> <slot v-else></slot> </span> </label> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue b/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue index 628a17eddca..a6df91b79c2 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue @@ -2,14 +2,8 @@ import { mapState } from 'vuex'; export default { - props: { - committedStateSvgPath: { - type: String, - required: true, - }, - }, computed: { - ...mapState(['lastCommitMsg']), + ...mapState(['lastCommitMsg', 'committedStateSvgPath']), }, }; </script> diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue index 0c44a755f56..b9af4d27145 100644 --- a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue +++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue @@ -1,28 +1,15 @@ <script> -import Icon from '~/vue_shared/components/icon.vue'; import { __, sprintf } from '~/locale'; +import { viewerTypes } from '../constants'; export default { - components: { - Icon, - }, props: { - hasChanges: { - type: Boolean, - required: false, - default: false, - }, - mergeRequestId: { - type: String, - required: false, - default: '', - }, viewer: { type: String, required: true, }, - showShadow: { - type: Boolean, + mergeRequestId: { + type: Number, required: true, }, }, @@ -38,84 +25,45 @@ export default { this.$emit('click', mode); }, }, + viewerTypes, }; </script> <template> <div class="dropdown" - :class="{ - shadow: showShadow, - }" > <button type="button" - class="btn btn-primary btn-sm" - :class="{ - 'btn-inverted': hasChanges, - }" + class="btn btn-link" data-toggle="dropdown" > - <template v-if="viewer === 'mrdiff' && mergeRequestId"> - {{ mergeReviewLine }} - </template> - <template v-else-if="viewer === 'editor'"> - {{ __('Editing') }} - </template> - <template v-else> - {{ __('Reviewing') }} - </template> - <icon - name="angle-down" - :size="12" - css-classes="caret-down" - /> + {{ __('Edit') }} </button> <div class="dropdown-menu dropdown-menu-selectable dropdown-open-left"> <ul> - <template v-if="mergeRequestId"> - <li> - <a - href="#" - @click.prevent="changeMode('mrdiff')" - :class="{ - 'is-active': viewer === 'mrdiff', - }" - > - <strong class="dropdown-menu-inner-title"> - {{ mergeReviewLine }} - </strong> - <span class="dropdown-menu-inner-content"> - {{ __('Compare changes with the merge request target branch') }} - </span> - </a> - </li> - <li - role="separator" - class="divider" - > - </li> - </template> <li> <a href="#" - @click.prevent="changeMode('editor')" + @click.prevent="changeMode($options.viewerTypes.mr)" :class="{ - 'is-active': viewer === 'editor', + 'is-active': viewer === $options.viewerTypes.mr, }" > - <strong class="dropdown-menu-inner-title">{{ __('Editing') }}</strong> + <strong class="dropdown-menu-inner-title"> + {{ mergeReviewLine }} + </strong> <span class="dropdown-menu-inner-content"> - {{ __('View and edit lines') }} + {{ __('Compare changes with the merge request target branch') }} </span> </a> </li> <li> <a href="#" - @click.prevent="changeMode('diff')" + @click.prevent="changeMode($options.viewerTypes.diff)" :class="{ - 'is-active': viewer === 'diff', + 'is-active': viewer === $options.viewerTypes.diff, }" > <strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong> diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 0274fc7d299..77b35078e56 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -1,82 +1,67 @@ <script> - import { mapActions, mapState, mapGetters } from 'vuex'; - import Mousetrap from 'mousetrap'; - import ideSidebar from './ide_side_bar.vue'; - import ideContextbar from './ide_context_bar.vue'; - import repoTabs from './repo_tabs.vue'; - import ideStatusBar from './ide_status_bar.vue'; - import repoEditor from './repo_editor.vue'; - import FindFile from './file_finder/index.vue'; +import Mousetrap from 'mousetrap'; +import { mapActions, mapState, mapGetters } from 'vuex'; +import IdeSidebar from './ide_side_bar.vue'; +import RepoTabs from './repo_tabs.vue'; +import IdeStatusBar from './ide_status_bar.vue'; +import RepoEditor from './repo_editor.vue'; +import FindFile from './file_finder/index.vue'; - const originalStopCallback = Mousetrap.stopCallback; +const originalStopCallback = Mousetrap.stopCallback; - export default { - components: { - ideSidebar, - ideContextbar, - repoTabs, - ideStatusBar, - repoEditor, - FindFile, - }, - props: { - emptyStateSvgPath: { - type: String, - required: true, - }, - noChangesStateSvgPath: { - type: String, - required: true, - }, - committedStateSvgPath: { - type: String, - required: true, - }, - }, - computed: { - ...mapState([ - 'changedFiles', - 'openFiles', - 'viewer', - 'currentMergeRequestId', - 'fileFindVisible', - ]), - ...mapGetters(['activeFile', 'hasChanges']), - }, - mounted() { - const returnValue = 'Are you sure you want to lose unsaved changes?'; - window.onbeforeunload = e => { - if (!this.changedFiles.length) return undefined; +export default { + components: { + IdeSidebar, + RepoTabs, + IdeStatusBar, + RepoEditor, + FindFile, + }, + computed: { + ...mapState([ + 'changedFiles', + 'openFiles', + 'viewer', + 'currentMergeRequestId', + 'fileFindVisible', + 'emptyStateSvgPath', + ]), + ...mapGetters(['activeFile', 'hasChanges']), + }, + mounted() { + const returnValue = 'Are you sure you want to lose unsaved changes?'; + window.onbeforeunload = e => { + if (!this.changedFiles.length) return undefined; - Object.assign(e, { - returnValue, - }); - return returnValue; - }; + Object.assign(e, { + returnValue, + }); + return returnValue; + }; - Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => { - if (e.preventDefault) { - e.preventDefault(); - } + Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => { + if (e.preventDefault) { + e.preventDefault(); + } - this.toggleFileFinder(!this.fileFindVisible); - }); + this.toggleFileFinder(!this.fileFindVisible); + }); - Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo); - }, - methods: { - ...mapActions(['toggleFileFinder']), - mousetrapStopCallback(e, el, combo) { - if (combo === 't' && el.classList.contains('dropdown-input-field')) { - return true; - } else if (combo === 'command+p' || combo === 'ctrl+p') { - return false; - } + Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo); + }, + methods: { + ...mapActions(['toggleFileFinder']), + mousetrapStopCallback(e, el, combo) { + if (combo === 't' && el.classList.contains('dropdown-input-field')) { + return true; + } else if (combo === 'command+p' || combo === 'ctrl+p') { + return false; + } - return originalStopCallback(e, el, combo); - }, + return originalStopCallback(e, el, combo); }, - }; + }, +}; </script> <template> @@ -136,9 +121,5 @@ </div> </template> </div> - <ide-contextbar - :no-changes-state-svg-path="noChangesStateSvgPath" - :committed-state-svg-path="committedStateSvgPath" - /> </div> </template> diff --git a/app/assets/javascripts/ide/components/ide_context_bar.vue b/app/assets/javascripts/ide/components/ide_context_bar.vue deleted file mode 100644 index 627fbeb9adf..00000000000 --- a/app/assets/javascripts/ide/components/ide_context_bar.vue +++ /dev/null @@ -1,42 +0,0 @@ -<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> diff --git a/app/assets/javascripts/ide/components/ide_external_links.vue b/app/assets/javascripts/ide/components/ide_external_links.vue deleted file mode 100644 index c6f6e0d2348..00000000000 --- a/app/assets/javascripts/ide/components/ide_external_links.vue +++ /dev/null @@ -1,43 +0,0 @@ -<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> diff --git a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue deleted file mode 100644 index eb2749e6151..00000000000 --- a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue +++ /dev/null @@ -1,47 +0,0 @@ -<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> diff --git a/app/assets/javascripts/ide/components/ide_project_tree.vue b/app/assets/javascripts/ide/components/ide_project_tree.vue deleted file mode 100644 index a6f40286ac1..00000000000 --- a/app/assets/javascripts/ide/components/ide_project_tree.vue +++ /dev/null @@ -1,65 +0,0 @@ -<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> diff --git a/app/assets/javascripts/ide/components/ide_repo_tree.vue b/app/assets/javascripts/ide/components/ide_repo_tree.vue deleted file mode 100644 index e6af88e04bc..00000000000 --- a/app/assets/javascripts/ide/components/ide_repo_tree.vue +++ /dev/null @@ -1,41 +0,0 @@ -<script> -import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; -import RepoFile from './repo_file.vue'; - -export default { - components: { - RepoFile, - SkeletonLoadingContainer, - }, - props: { - tree: { - type: Object, - required: true, - }, - }, -}; -</script> - -<template> - <div - class="ide-file-list" - > - <template v-if="tree.loading"> - <div - class="multi-file-loading-container" - v-for="n in 3" - :key="n" - > - <skeleton-loading-container /> - </div> - </template> - <template v-else> - <repo-file - v-for="file in tree.tree" - :key="file.key" - :file="file" - :level="0" - /> - </template> - </div> -</template> diff --git a/app/assets/javascripts/ide/components/ide_review.vue b/app/assets/javascripts/ide/components/ide_review.vue new file mode 100644 index 00000000000..0c9ec3b00f0 --- /dev/null +++ b/app/assets/javascripts/ide/components/ide_review.vue @@ -0,0 +1,62 @@ +<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> diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue index 8cf1ccb4fce..3f980203911 100644 --- a/app/assets/javascripts/ide/components/ide_side_bar.vue +++ b/app/assets/javascripts/ide/components/ide_side_bar.vue @@ -1,36 +1,82 @@ <script> - import { mapState, mapGetters } from 'vuex'; - import icon from '~/vue_shared/components/icon.vue'; - import panelResizer from '~/vue_shared/components/panel_resizer.vue'; - import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; - import projectTree from './ide_project_tree.vue'; - import ResizablePanel from './resizable_panel.vue'; +import { mapState, mapGetters } from 'vuex'; +import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue'; +import Icon from '~/vue_shared/components/icon.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; +import PanelResizer from '~/vue_shared/components/panel_resizer.vue'; +import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; +import Identicon from '../../vue_shared/components/identicon.vue'; +import IdeTree from './ide_tree.vue'; +import ResizablePanel from './resizable_panel.vue'; +import ActivityBar from './activity_bar.vue'; +import CommitSection from './repo_commit_section.vue'; +import CommitForm from './commit_sidebar/form.vue'; +import IdeReview from './ide_review.vue'; +import SuccessMessage from './commit_sidebar/success_message.vue'; +import { activityBarViews } from '../constants'; - export default { - components: { - projectTree, - icon, - panelResizer, - skeletonLoadingContainer, - ResizablePanel, +export default { + directives: { + tooltip, + }, + components: { + Icon, + PanelResizer, + SkeletonLoadingContainer, + ResizablePanel, + ActivityBar, + ProjectAvatarImage, + Identicon, + CommitSection, + IdeTree, + CommitForm, + IdeReview, + SuccessMessage, + }, + data() { + return { + showTooltip: false, + }; + }, + computed: { + ...mapState([ + 'loading', + 'currentBranchId', + 'currentActivityView', + 'changedFiles', + 'stagedFiles', + 'lastCommitMsg', + ]), + ...mapGetters(['currentProject', 'someUncommitedChanges']), + showSuccessMessage() { + return ( + this.currentActivityView === activityBarViews.edit && + (this.lastCommitMsg && !this.someUncommitedChanges) + ); }, - computed: { - ...mapState([ - 'loading', - ]), - ...mapGetters([ - 'projectsWithTrees', - ]), + branchTooltipTitle() { + return this.showTooltip ? this.currentBranchId : undefined; }, - }; + }, + watch: { + currentBranchId() { + this.$nextTick(() => { + this.showTooltip = this.$refs.branchId.scrollWidth > this.$refs.branchId.offsetWidth; + }); + }, + }, +}; </script> <template> <resizable-panel :collapsible="false" - :initial-width="290" + :initial-width="340" side="left" > + <activity-bar + v-if="!loading" + /> <div class="multi-file-commit-panel-inner"> <template v-if="loading"> <div @@ -41,11 +87,54 @@ <skeleton-loading-container /> </div> </template> - <project-tree - v-for="project in projectsWithTrees" - :key="project.id" - :project="project" - /> + <template v-else> + <div class="context-header ide-context-header"> + <a + :href="currentProject.web_url" + > + <div + v-if="currentProject.avatar_url" + class="avatar-container s40 project-avatar" + > + <project-avatar-image + class="avatar-container project-avatar" + :link-href="currentProject.path" + :img-src="currentProject.avatar_url" + :img-alt="currentProject.name" + :img-size="40" + /> + </div> + <identicon + v-else + size-class="s40" + :entity-id="currentProject.id" + :entity-name="currentProject.name" + /> + <div class="ide-sidebar-project-title"> + <div class="sidebar-context-title"> + {{ currentProject.name }} + </div> + <div + class="sidebar-context-title ide-sidebar-branch-title" + ref="branchId" + v-tooltip + :title="branchTooltipTitle" + > + <icon + name="branch" + css-classes="append-right-5" + />{{ currentBranchId }} + </div> + </div> + </a> + </div> + <div class="multi-file-commit-panel-inner-scroll"> + <component + :is="currentActivityView" + /> + </div> + <commit-form /> + </template> </div> </resizable-panel> </template> diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue new file mode 100644 index 00000000000..8fc4ebe6ca6 --- /dev/null +++ b/app/assets/javascripts/ide/components/ide_tree.vue @@ -0,0 +1,42 @@ +<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> diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue new file mode 100644 index 00000000000..e64a09fcc90 --- /dev/null +++ b/app/assets/javascripts/ide/components/ide_tree_list.vue @@ -0,0 +1,76 @@ +<script> +import { mapActions, mapGetters, mapState } from 'vuex'; +import Icon from '~/vue_shared/components/icon.vue'; +import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; +import RepoFile from './repo_file.vue'; +import NewDropdown from './new_dropdown/index.vue'; + +export default { + components: { + Icon, + RepoFile, + SkeletonLoadingContainer, + NewDropdown, + }, + props: { + viewerType: { + type: String, + required: true, + }, + headerClass: { + type: String, + required: false, + default: null, + }, + disableActionDropdown: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + ...mapState(['currentBranchId']), + ...mapGetters(['currentProject', 'currentTree']), + showLoading() { + return !this.currentTree || this.currentTree.loading; + }, + }, + mounted() { + this.updateViewer(this.viewerType); + }, + methods: { + ...mapActions(['updateViewer']), + }, +}; +</script> + +<template> + <div + class="ide-file-list" + > + <template v-if="showLoading"> + <div + class="multi-file-loading-container" + v-for="n in 3" + :key="n" + > + <skeleton-loading-container /> + </div> + </template> + <template v-else> + <header + class="ide-tree-header" + :class="headerClass" + > + <slot name="header"></slot> + </header> + <repo-file + v-for="file in currentTree.tree" + :key="file.key" + :file="file" + :level="0" + :disable-action-dropdown="disableActionDropdown" + /> + </template> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue index b1b5c0d4a28..a0ce1c9dac7 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue @@ -17,7 +17,8 @@ export default { }, path: { type: String, - required: true, + required: false, + default: '', }, }, data() { diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index fa929381744..c5092d8e04d 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -3,13 +3,10 @@ import { mapState, mapActions, mapGetters } from 'vuex'; import tooltip from '~/vue_shared/directives/tooltip'; import Icon from '~/vue_shared/components/icon.vue'; import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; -import LoadingButton from '~/vue_shared/components/loading_button.vue'; import CommitFilesList from './commit_sidebar/list.vue'; import EmptyState from './commit_sidebar/empty_state.vue'; -import CommitMessageField from './commit_sidebar/message_field.vue'; -import SuccessMessage from './commit_sidebar/success_message.vue'; import * as consts from '../stores/modules/commit/constants'; -import Actions from './commit_sidebar/actions.vue'; +import { activityBarViews } from '../constants'; export default { components: { @@ -17,42 +14,50 @@ export default { Icon, CommitFilesList, EmptyState, - SuccessMessage, - Actions, - LoadingButton, - CommitMessageField, }, directives: { tooltip, }, - props: { - noChangesStateSvgPath: { - type: String, - required: true, - }, - committedStateSvgPath: { - type: String, - required: true, - }, - }, computed: { + ...mapState([ + 'changedFiles', + 'stagedFiles', + 'rightPanelCollapsed', + 'lastCommitMsg', + 'unusedSeal', + ]), + ...mapState('commit', ['commitMessage', 'submitCommitLoading']), + ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges']), + ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']), showStageUnstageArea() { return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal); }, - someUncommitedChanges() { - return !!(this.changedFiles.length || this.stagedFiles.length); + }, + watch: { + hasChanges() { + if (!this.hasChanges) { + this.updateActivityBarView(activityBarViews.edit); + } }, - ...mapState(['changedFiles', 'stagedFiles', 'rightPanelCollapsed', 'lastCommitMsg', 'unusedSeal']), - ...mapState('commit', ['commitMessage', 'submitCommitLoading']), - ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']), + }, + mounted() { + if (this.lastOpenedFile) { + this.openPendingTab({ + file: this.lastOpenedFile, + }) + .then(changeViewer => { + if (changeViewer) { + this.updateViewer('diff'); + } + }) + .catch(e => { + throw e; + }); + } }, methods: { - ...mapActions('commit', [ - 'updateCommitMessage', - 'discardDraft', - 'commitChanges', - 'updateCommitAction', - ]), + ...mapActions(['openPendingTab', 'updateViewer', 'updateActivityBarView']), + ...mapActions('commit', ['commitChanges', 'updateCommitAction']), forceCreateNewBranch() { return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges()); }, @@ -80,6 +85,7 @@ export default { v-if="showStageUnstageArea" > <commit-files-list + class="is-first" icon-name="unstaged" :title="__('Unstaged')" :file-list="changedFiles" @@ -94,49 +100,11 @@ export default { action="unstageAllChanges" :action-btn-text="__('Unstage all')" item-action-component="unstage-button" - :show-toggle="false" :staged-list="true" /> </template> <empty-state v-if="unusedSeal" - :no-changes-state-svg-path="noChangesStateSvgPath" /> - <div - class="multi-file-commit-panel-bottom" - > - <form - class="form-horizontal multi-file-commit-form" - @submit.prevent.stop="commitChanges" - v-if="!rightPanelCollapsed" - > - <success-message - v-if="lastCommitMsg && !someUncommitedChanges" - :committed-state-svg-path="committedStateSvgPath" - /> - <commit-message-field - :text="commitMessage" - @input="updateCommitMessage" - /> - <div class="clearfix prepend-top-15"> - <actions /> - <loading-button - :loading="submitCommitLoading" - :disabled="commitButtonDisabled" - container-class="btn btn-success btn-sm pull-left" - :label="__('Commit')" - @click="commitChanges" - /> - <button - v-if="!discardDraftButtonDisabled" - type="button" - class="btn btn-default btn-sm pull-right" - @click="discardDraft" - > - {{ __('Discard draft') }} - </button> - </div> - </form> - </div> </div> </template> diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index 3a04cdd8e46..ff7e546fb9c 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -3,6 +3,7 @@ import { mapState, mapGetters, mapActions } from 'vuex'; import flash from '~/flash'; import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue'; +import { activityBarViews, viewerTypes } from '../constants'; import monacoLoader from '../monaco_loader'; import Editor from '../lib/editor'; import IdeFileButtons from './ide_file_buttons.vue'; @@ -19,8 +20,14 @@ export default { }, }, computed: { - ...mapState(['rightPanelCollapsed', 'viewer', 'delayViewerUpdated', 'panelResizing']), - ...mapGetters(['currentMergeRequest', 'getStagedFile']), + ...mapState(['rightPanelCollapsed', 'viewer', 'panelResizing', 'currentActivityView']), + ...mapGetters([ + 'currentMergeRequest', + 'getStagedFile', + 'isEditModeActive', + 'isCommitModeActive', + 'isReviewModeActive', + ]), shouldHideEditor() { return this.file && this.file.binary && !this.file.content; }, @@ -40,6 +47,21 @@ export default { // Compare key to allow for files opened in review mode to be cached differently if (newVal.key !== this.file.key) { this.initMonaco(); + + if (this.currentActivityView !== activityBarViews.edit) { + this.setFileViewMode({ + file: this.file, + viewMode: 'edit', + }); + } + } + }, + currentActivityView() { + if (this.currentActivityView !== activityBarViews.edit) { + this.setFileViewMode({ + file: this.file, + viewMode: 'edit', + }); } }, rightPanelCollapsed() { @@ -77,7 +99,6 @@ export default { 'setFileViewMode', 'setFileEOL', 'updateViewer', - 'updateDelayViewerUpdated', ]), initMonaco() { if (this.shouldHideEditor) return; @@ -89,14 +110,6 @@ export default { baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '', }) .then(() => { - const viewerPromise = this.delayViewerUpdated - ? this.updateViewer(this.file.pending ? 'diff' : 'editor') - : Promise.resolve(); - - return viewerPromise; - }) - .then(() => { - this.updateDelayViewerUpdated(false); this.createEditorInstance(); }) .catch(err => { @@ -108,10 +121,10 @@ export default { this.editor.dispose(); this.$nextTick(() => { - if (this.viewer === 'editor') { + if (this.viewer === viewerTypes.edit) { this.editor.createInstance(this.$refs.editor); } else { - this.editor.createDiffInstance(this.$refs.editor); + this.editor.createDiffInstance(this.$refs.editor, !this.isReviewModeActive); } this.setupEditor(); @@ -127,7 +140,7 @@ export default { this.file.staged && this.file.key.indexOf('unstaged-') === 0 ? head : null, ); - if (this.viewer === 'mrdiff') { + if (this.viewer === viewerTypes.mr) { this.editor.attachMergeRequestModel(this.model); } else { this.editor.attachModel(this.model); @@ -168,6 +181,7 @@ export default { }); }, }, + viewerTypes, }; </script> @@ -176,16 +190,17 @@ export default { id="ide" class="blob-viewer-container blob-editor-container" > - <div class="ide-mode-tabs clearfix"> + <div class="ide-mode-tabs clearfix" > <ul class="nav-links pull-left" - v-if="!shouldHideEditor"> + v-if="!shouldHideEditor && isEditModeActive" + > <li :class="editTabCSS"> <a href="javascript:void(0);" role="button" @click.prevent="setFileViewMode({ file, viewMode: 'edit' })"> - <template v-if="viewer === 'editor'"> + <template v-if="viewer === $options.viewerTypes.edit"> {{ __('Edit') }} </template> <template v-else> @@ -212,6 +227,9 @@ export default { v-show="!shouldHideEditor && file.viewMode === 'edit'" ref="editor" class="multi-file-editor-holder" + :class="{ + 'is-readonly': isCommitModeActive, + }" > </div> <content-viewer diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue index 89c5ce70dd3..14946f8c9fa 100644 --- a/app/assets/javascripts/ide/components/repo_file.vue +++ b/app/assets/javascripts/ide/components/repo_file.vue @@ -34,6 +34,11 @@ export default { type: Number, required: true, }, + disableActionDropdown: { + type: Boolean, + required: false, + default: false, + }, }, computed: { ...mapGetters([ @@ -99,16 +104,14 @@ export default { } }, methods: { - ...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']), + ...mapActions(['toggleTreeOpen']), clickFile() { // Manual Action if a tree is selected/opened if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) { this.toggleTreeOpen(this.file.path); } - return this.updateDelayViewerUpdated(true).then(() => { - router.push(`/project${this.file.url}`); - }); + router.push(`/project${this.file.url}`); }, }, }; @@ -170,7 +173,7 @@ export default { /> </span> <new-dropdown - v-if="isTree" + v-if="isTree && !disableActionDropdown" :project-id="file.projectId" :branch="file.branchId" :path="file.path" diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue index a3ee3184c19..fb26b973236 100644 --- a/app/assets/javascripts/ide/components/repo_tab.vue +++ b/app/assets/javascripts/ide/components/repo_tab.vue @@ -32,6 +32,8 @@ export default { return `Close ${this.tab.name}`; }, showChangedIcon() { + if (this.tab.pending) return true; + return this.fileHasChanged ? !this.tabMouseOver : false; }, fileHasChanged() { @@ -66,15 +68,32 @@ export default { <template> <li + :class="{ + active: tab.active + }" @click="clickFile(tab)" @mouseover="mouseOverTab" @mouseout="mouseOutTab" > + <div + class="multi-file-tab" + :title="tab.url" + > + <file-icon + :file-name="tab.name" + :size="16" + /> + {{ tab.name }} + <file-status-icon + :file="tab" + /> + </div> <button type="button" class="multi-file-tab-close" @click.stop.prevent="closeFile(tab)" :aria-label="closeLabel" + :disabled="tab.pending" > <icon v-if="!showChangedIcon" @@ -87,22 +106,5 @@ export default { :force-modified-icon="true" /> </button> - - <div - class="multi-file-tab" - :class="{ - active: tab.active - }" - :title="tab.url" - > - <file-icon - :file-name="tab.name" - :size="16" - /> - {{ tab.name }} - <file-status-icon - :file="tab" - /> - </div> </li> </template> diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue index 7bd646ba9b0..99e51097e12 100644 --- a/app/assets/javascripts/ide/components/repo_tabs.vue +++ b/app/assets/javascripts/ide/components/repo_tabs.vue @@ -32,16 +32,6 @@ export default { default: '', }, }, - data() { - return { - showShadow: false, - }; - }, - updated() { - if (!this.$refs.tabsScroller) return; - - this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth; - }, methods: { ...mapActions(['updateViewer', 'removePendingTab']), openFileViewer(viewer) { @@ -71,12 +61,5 @@ export default { :tab="tab" /> </ul> - <editor-mode - :viewer="viewer" - :show-shadow="showShadow" - :has-changes="hasChanges" - :merge-request-id="mergeRequestId" - @click="openFileViewer" - /> </div> </template> diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index b06da9f95d1..48d4cc43198 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -3,6 +3,22 @@ export const MAX_FILE_FINDER_RESULTS = 40; export const FILE_FINDER_ROW_HEIGHT = 55; export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33; +export const MAX_WINDOW_HEIGHT_COMPACT = 750; + +export const COMMIT_ITEM_PADDING = 32; + // Commit message textarea export const MAX_TITLE_LENGTH = 50; export const MAX_BODY_LENGTH = 72; + +export const activityBarViews = { + edit: 'ide-tree', + commit: 'commit-section', + review: 'ide-review', +}; + +export const viewerTypes = { + mr: 'mrdiff', + edit: 'editor', + diff: 'diff', +}; diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js index 4a0a303d5a6..adca85dc65b 100644 --- a/app/assets/javascripts/ide/ide_router.js +++ b/app/assets/javascripts/ide/ide_router.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import VueRouter from 'vue-router'; import flash from '~/flash'; import store from './stores'; +import { activityBarViews } from './constants'; Vue.use(VueRouter); @@ -63,6 +64,8 @@ router.beforeEach((to, from, next) => { const fullProjectId = `${to.params.namespace}/${to.params.project}`; if (to.params.branch) { + store.dispatch('setCurrentBranchId', to.params.branch); + store.dispatch('getBranchData', { projectId: fullProjectId, branchId: to.params.branch, @@ -99,14 +102,14 @@ router.beforeEach((to, from, next) => { throw e; }); } else if (to.params.mrid) { - store.dispatch('updateViewer', 'mrdiff'); - store .dispatch('getMergeRequestData', { projectId: fullProjectId, mergeRequestId: to.params.mrid, }) .then(mr => { + store.dispatch('updateActivityBarView', activityBarViews.review); + store.dispatch('getBranchData', { projectId: fullProjectId, branchId: mr.source_branch, diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js index cbfb3dc54f2..e5c47d09886 100644 --- a/app/assets/javascripts/ide/index.js +++ b/app/assets/javascripts/ide/index.js @@ -14,15 +14,16 @@ function initIde(el) { components: { ide, }, - render(createElement) { - return createElement('ide', { - props: { - emptyStateSvgPath: el.dataset.emptyStateSvgPath, - noChangesStateSvgPath: el.dataset.noChangesStateSvgPath, - committedStateSvgPath: el.dataset.committedStateSvgPath, - }, + created() { + this.$store.dispatch('setEmptyStateSvgs', { + emptyStateSvgPath: el.dataset.emptyStateSvgPath, + noChangesStateSvgPath: el.dataset.noChangesStateSvgPath, + committedStateSvgPath: el.dataset.committedStateSvgPath, }); }, + render(createElement) { + return createElement('ide'); + }, }); } diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js index b65d9c68a0b..9c3bb9cc17d 100644 --- a/app/assets/javascripts/ide/lib/editor.js +++ b/app/assets/javascripts/ide/lib/editor.js @@ -61,19 +61,19 @@ export default class Editor { } } - createDiffInstance(domElement) { + createDiffInstance(domElement, readOnly = true) { if (!this.instance) { clearDomElement(domElement); this.disposable.add( (this.instance = this.monaco.editor.createDiffEditor(domElement, { ...defaultEditorOptions, - readOnly: true, quickSuggestions: false, occurrencesHighlight: false, - renderLineHighlight: 'none', - hideCursorInOverviewRuler: true, renderSideBySide: Editor.renderSideBySide(domElement), + readOnly, + renderLineHighlight: readOnly ? 'all' : 'none', + hideCursorInOverviewRuler: !readOnly, })), ); diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index 7358dd9ef92..1a98b42761e 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -123,6 +123,8 @@ export const scrollToTab = () => { }; export const stageAllChanges = ({ state, commit }) => { + commit(types.SET_LAST_COMMIT_MSG, ''); + state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path)); }; @@ -138,6 +140,18 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => { commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay); }; +export const updateActivityBarView = ({ commit }, view) => { + commit(types.UPDATE_ACTIVITY_BAR_VIEW, view); +}; + +export const setEmptyStateSvgs = ({ commit }, svgs) => { + commit(types.SET_EMPTY_STATE_SVGS, svgs); +}; + +export const setCurrentBranchId = ({ commit }, currentBranchId) => { + commit(types.SET_CURRENT_BRANCH, currentBranchId); +}; + export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, tempFile }) => { commit(types.UPDATE_TEMP_FLAG, { path: file.path, tempFile }); diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index 861830badee..3ac9b9222ca 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -5,6 +5,7 @@ import service from '../../services'; import * as types from '../mutation_types'; import router from '../../ide_router'; import { setPageTitle } from '../utils'; +import { viewerTypes } from '../../constants'; export const closeFile = ({ commit, state, dispatch }, file) => { const path = file.path; @@ -23,13 +24,12 @@ export const closeFile = ({ commit, state, dispatch }, file) => { const nextFileToOpen = state.openFiles[nextIndexToOpen]; if (nextFileToOpen.pending) { - dispatch('updateViewer', 'diff'); + dispatch('updateViewer', viewerTypes.diff); dispatch('openPendingTab', { file: nextFileToOpen, keyPrefix: nextFileToOpen.staged ? 'staged' : 'unstaged', }); } else { - dispatch('updateDelayViewerUpdated', true); router.push(`/project${nextFileToOpen.url}`); } } else if (!state.openFiles.length) { @@ -184,6 +184,7 @@ export const stageChange = ({ commit, state }, path) => { const stagedFile = state.stagedFiles.find(f => f.path === path); commit(types.STAGE_CHANGE, path); + commit(types.SET_LAST_COMMIT_MSG, ''); if (stagedFile) { eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content); @@ -195,9 +196,7 @@ export const unstageChange = ({ commit }, path) => { }; export const openPendingTab = ({ commit, getters, dispatch, state }, { file, keyPrefix }) => { - if (getters.activeFile && getters.activeFile === file && state.viewer === 'diff') { - return false; - } + state.openFiles.forEach(f => eventHub.$emit(`editor.update.model.dispose.${f.key}`)); commit(types.ADD_PENDING_TAB, { file, keyPrefix }); diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index 4eb23b2ee0f..971a15a4036 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -55,7 +55,6 @@ export const getBranchData = ( branch: data, }); commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id }); - commit(types.SET_CURRENT_BRANCH, branchId); resolve(data); }) .catch(() => { diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js index a93d29fd865..75286ed0cd2 100644 --- a/app/assets/javascripts/ide/stores/getters.js +++ b/app/assets/javascripts/ide/stores/getters.js @@ -1,5 +1,5 @@ -import { __ } from '~/locale'; import { getChangesCountForFiles, filePathMatches } from './utils'; +import { activityBarViews } from '../constants'; export const activeFile = state => state.openFiles.find(file => file.active) || null; @@ -31,15 +31,12 @@ export const currentMergeRequest = state => { return null; }; -// eslint-disable-next-line no-confusing-arrow -export const collapseButtonIcon = state => - state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right'; +export const currentProject = state => state.projects[state.currentProjectId]; -export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length; +export const currentTree = state => + state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; -// eslint-disable-next-line no-confusing-arrow -export const collapseButtonTooltip = state => - state.rightPanelCollapsed ? __('Expand sidebar') : __('Collapse sidebar'); +export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length; export const hasMergeRequest = state => !!state.currentMergeRequestId; @@ -59,6 +56,16 @@ export const allBlobs = state => export const getChangedFile = state => path => state.changedFiles.find(f => f.path === path); export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path); +export const lastOpenedFile = state => + [...state.changedFiles, ...state.stagedFiles].sort((a, b) => b.lastOpenedAt - a.lastOpenedAt)[0]; + +export const isEditModeActive = state => state.currentActivityView === activityBarViews.edit; +export const isCommitModeActive = state => state.currentActivityView === activityBarViews.commit; +export const isReviewModeActive = state => state.currentActivityView === activityBarViews.review; + +export const someUncommitedChanges = state => + !!(state.changedFiles.length || state.stagedFiles.length); + export const getChangesInFolder = state => path => { const changedFilesCount = state.changedFiles.filter(f => filePathMatches(f, path)).length; const stagedFilesCount = state.stagedFiles.filter( diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index 4fbc97d053e..ab00d12089d 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -8,6 +8,7 @@ import router from '../../../ide_router'; import service from '../../../services'; import * as types from './mutation_types'; import * as consts from './constants'; +import { activityBarViews } from '../../../constants'; import eventHub from '../../../eventhub'; export const updateCommitMessage = ({ commit }, message) => { @@ -75,7 +76,7 @@ export const checkCommitStatus = ({ rootState }) => export const updateFilesAfterCommit = ( { commit, dispatch, state, rootState, rootGetters }, - { data, branch }, + { data }, ) => { const selectedProject = rootState.projects[rootState.currentProjectId]; const lastCommit = { @@ -126,15 +127,9 @@ export const updateFilesAfterCommit = ( changed: !!changedFile, }); }); - - if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH && rootGetters.activeFile) { - router.push( - `/project/${rootState.currentProjectId}/blob/${branch}/${rootGetters.activeFile.path}`, - ); - } }; -export const commitChanges = ({ commit, state, getters, dispatch, rootState }) => { +export const commitChanges = ({ commit, state, getters, dispatch, rootState, rootGetters }) => { const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH; const payload = createCommitPayload(getters.branchName, newBranch, state, rootState); const getCommitStatus = newBranch ? Promise.resolve(false) : dispatch('checkCommitStatus'); @@ -187,6 +182,34 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) = commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true }); }, 5000); }) + .then(() => { + if (rootGetters.lastOpenedFile) { + dispatch( + 'openPendingTab', + { + file: rootGetters.lastOpenedFile, + }, + { root: true }, + ) + .then(changeViewer => { + if (changeViewer) { + dispatch('updateViewer', 'diff', { root: true }); + } + }) + .catch(e => { + throw e; + }); + } else { + dispatch('updateActivityBarView', activityBarViews.edit, { root: true }); + dispatch('updateViewer', 'editor', { root: true }); + + router.push( + `/project/${rootState.currentProjectId}/blob/${getters.branchName}/${ + rootGetters.activeFile.path + }`, + ); + } + }) .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH)); }) .catch(err => { diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index 87b39379338..08e22e5ad7e 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -5,6 +5,7 @@ export const SET_LAST_COMMIT_MSG = 'SET_LAST_COMMIT_MSG'; export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED'; export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED'; export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS'; +export const SET_EMPTY_STATE_SVGS = 'SET_EMPTY_STATE_SVGS'; // Project Mutation Types export const SET_PROJECT = 'SET_PROJECT'; @@ -59,6 +60,7 @@ export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT'; export const ADD_PENDING_TAB = 'ADD_PENDING_TAB'; export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB'; +export const UPDATE_ACTIVITY_BAR_VIEW = 'UPDATE_ACTIVITY_BAR_VIEW'; export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG'; export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER'; export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index 539a07116b3..a257e2ef025 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -107,6 +107,21 @@ export default { delayViewerUpdated, }); }, + [types.UPDATE_ACTIVITY_BAR_VIEW](state, currentActivityView) { + Object.assign(state, { + currentActivityView, + }); + }, + [types.SET_EMPTY_STATE_SVGS]( + state, + { emptyStateSvgPath, noChangesStateSvgPath, committedStateSvgPath }, + ) { + Object.assign(state, { + emptyStateSvgPath, + noChangesStateSvgPath, + committedStateSvgPath, + }); + }, [types.TOGGLE_FILE_FINDER](state, fileFindVisible) { Object.assign(state, { fileFindVisible, diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js index c3041c77199..13f123b6630 100644 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ b/app/assets/javascripts/ide/stores/mutations/file.js @@ -1,3 +1,4 @@ +/* eslint-disable no-param-reassign */ import * as types from '../mutation_types'; export default { @@ -169,32 +170,24 @@ export default { }); }, [types.ADD_PENDING_TAB](state, { file, keyPrefix = 'pending' }) { - const key = `${keyPrefix}-${file.key}`; - const pendingTab = state.openFiles.find(f => f.key === key && f.pending); - let openFiles = state.openFiles.map(f => Object.assign(f, { active: false, opened: false })); - - if (!pendingTab) { - const openFile = openFiles.find(f => f.path === file.path); - - openFiles = openFiles.concat(openFile ? null : file).reduce((acc, f) => { - if (!f) return acc; - - if (f.path === file.path) { - return acc.concat({ - ...f, - content: file.content, - active: true, - pending: true, - opened: true, - key, - }); - } - - return acc.concat(f); - }, []); - } - - Object.assign(state, { openFiles }); + state.entries[file.path].opened = false; + state.entries[file.path].active = false; + state.entries[file.path].lastOpenedAt = new Date().getTime(); + state.openFiles.forEach(f => + Object.assign(f, { + opened: false, + active: false, + }), + ); + state.openFiles = [ + { + ...file, + key: `${keyPrefix}-${file.key}`, + pending: true, + opened: true, + active: true, + }, + ]; }, [types.REMOVE_PENDING_TAB](state, file) { Object.assign(state, { diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js index 0976d278559..e7411f16a4f 100644 --- a/app/assets/javascripts/ide/stores/state.js +++ b/app/assets/javascripts/ide/stores/state.js @@ -1,3 +1,5 @@ +import { activityBarViews, viewerTypes } from '../constants'; + export default () => ({ currentProjectId: '', currentBranchId: '', @@ -16,8 +18,9 @@ export default () => ({ rightPanelCollapsed: false, panelResizing: false, entries: {}, - viewer: 'editor', + viewer: viewerTypes.edit, delayViewerUpdated: false, + currentActivityView: activityBarViews.edit, unusedSeal: true, fileFindVisible: false, }); diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss index 05cb0196ced..0bbd6eb27c1 100644 --- a/app/assets/stylesheets/framework/gitlab_theme.scss +++ b/app/assets/stylesheets/framework/gitlab_theme.scss @@ -177,25 +177,6 @@ } } - // Web IDE - .ide-sidebar-link { - color: $color-200; - background-color: $color-700; - - &:hover, - &:focus { - background-color: $color-500; - } - - &:active { - background: $color-800; - } - } - - .branch-container { - border-left-color: $color-700; - } - .branch-header-title { color: $color-700; } @@ -203,6 +184,13 @@ .ide-file-list .file.file-active { color: $color-700; } + + .ide-sidebar-link { + &.active { + color: $color-700; + box-shadow: inset 3px 0 $color-700; + } + } } body { @@ -343,9 +331,5 @@ body { .sidebar-top-level-items > li.active .badge { color: $theme-gray-900; } - - .ide-sidebar-link { - color: $white-light; - } } } diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 888757c12d8..7f1f0c1f5f1 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -121,14 +121,6 @@ .multi-file-loading-container { margin-top: 10px; padding: 10px; - - .animation-container { - background: $gray-light; - - div { - background: $gray-light; - } - } } .multi-file-table-col-commit-message { @@ -155,69 +147,56 @@ } li { - position: relative; - } - - .dropdown { display: flex; - margin-left: auto; - margin-bottom: 1px; - padding: 0 $grid-size; - border-left: 1px solid $white-dark; - background-color: $white-light; - - &.shadow { - box-shadow: 0 0 10px $dropdown-shadow-color; - } + align-items: center; + padding: $grid-size $gl-padding; + background-color: $gray-normal; + border-right: 1px solid $white-dark; + border-bottom: 1px solid $white-dark; - .btn { - margin-top: auto; - margin-bottom: auto; + &.active { + background-color: $white-light; + border-bottom-color: $white-light; } } } .multi-file-tab { - @include str-truncated(150px); - padding: ($gl-padding / 2) ($gl-padding + 12) ($gl-padding / 2) $gl-padding; - background-color: $gray-normal; - border-right: 1px solid $white-dark; - border-bottom: 1px solid $white-dark; + @include str-truncated(141px); cursor: pointer; svg { vertical-align: middle; } - - &.active { - background-color: $white-light; - border-bottom-color: $white-light; - } } .multi-file-tab-close { - position: absolute; - right: 8px; - top: 50%; width: 16px; height: 16px; padding: 0; + margin-left: $grid-size; background: none; border: 0; border-radius: $border-radius-default; color: $theme-gray-900; - transform: translateY(-50%); svg { position: relative; top: -1px; } - &:hover { + .ide-file-changed-icon { + display: block; + position: relative; + top: 1px; + right: -2px; + } + + &:not([disabled]):hover { background-color: $theme-gray-200; } - &:focus { + &:not([disabled]):focus { background-color: $blue-500; color: $white-light; outline: 0; @@ -248,6 +227,17 @@ display: none; } + .is-readonly, + .editor.original { + .view-lines { + cursor: default; + } + + .cursors-layer { + display: none; + } + } + .monaco-diff-editor.vs { .editor.modified { box-shadow: none; @@ -306,15 +296,12 @@ .margin-view-overlays .delete-sign { opacity: 0.4; } - - .cursors-layer { - display: none; - } } } .multi-file-editor-holder { height: 100%; + min-height: 0; } .preview-container { @@ -380,6 +367,7 @@ .ide-btn-group { padding: $gl-padding-4 $gl-vert-padding; + line-height: 24px; } .ide-status-bar { @@ -433,28 +421,35 @@ .multi-file-commit-panel { display: flex; position: relative; - flex-direction: column; width: 340px; padding: 0; background-color: $gray-light; - padding-right: 3px; + padding-right: 1px; + + .context-header { + width: auto; + margin-right: 0; + + a:hover, + a:focus { + text-decoration: none; + } + } .projects-sidebar { min-height: 0; display: flex; flex-direction: column; flex: 1; - - .context-header { - width: auto; - margin-right: 0; - } } .multi-file-commit-panel-inner { + position: relative; display: flex; flex-direction: column; height: 100%; + min-width: 0; + width: 100%; } .multi-file-commit-panel-inner-scroll { @@ -462,68 +457,10 @@ flex: 1; flex-direction: column; overflow: auto; - } - - &.is-collapsed { - width: 60px; - - .multi-file-commit-list { - padding-top: $gl-padding; - overflow: hidden; - } - - .multi-file-context-bar-icon { - align-items: center; - - svg { - float: none; - margin: 0; - } - } - } - - .branch-container { - border-left: 4px solid; - margin-bottom: $gl-bar-padding; - } - - .branch-header { - background: $white-dark; - display: flex; - } - - .branch-header-title { - flex: 1; - padding: $grid-size $gl-padding; - font-weight: $gl-font-weight-bold; - - svg { - vertical-align: middle; - } - } - - .branch-header-btns { - padding: $gl-vert-padding $gl-padding; - } - - .left-collapse-btn { - display: none; - background: $gray-light; - text-align: left; + background-color: $white-light; + border-left: 1px solid $white-dark; border-top: 1px solid $white-dark; - - svg { - vertical-align: middle; - } - } -} - -.multi-file-context-bar-icon { - padding: 10px; - - svg { - margin-right: 10px; - float: left; + border-top-left-radius: $border-radius-small; } } @@ -549,14 +486,13 @@ align-items: center; margin-bottom: 0; border-bottom: 1px solid $white-dark; - padding: $gl-btn-padding 0; - min-height: 56px; + padding: $gl-btn-padding $gl-padding; } .multi-file-commit-panel-header-title { display: flex; flex: 1; - padding-left: $grid-size; + align-items: center; svg { margin-right: $gl-btn-padding; @@ -572,7 +508,7 @@ .multi-file-commit-list { flex: 1; overflow: auto; - padding: $gl-padding 0; + padding: $gl-padding; min-height: 60px; } @@ -667,30 +603,24 @@ } .multi-file-commit-form { + position: relative; padding: $gl-padding; + background-color: $white-light; border-top: 1px solid $white-dark; + border-left: 1px solid $white-dark; + transition: all 0.3s ease; .btn { font-size: $gl-font-size; } + + .multi-file-commit-panel-success-message { + top: 0; + } } .multi-file-commit-panel-bottom { position: relative; - - .multi-file-commit-panel-success-message { - position: absolute; - top: 1px; - left: 3px; - bottom: 0; - right: 0; - z-index: 10; - background: $gray-light; - overflow: auto; - display: flex; - flex-direction: column; - justify-content: center; - } } .dirty-diff { @@ -826,7 +756,7 @@ position: absolute; top: 0; bottom: 0; - width: 3px; + width: 1px; background-color: $white-dark; &.dragright { @@ -840,42 +770,40 @@ .ide-commit-list-container { display: flex; + flex: 1; flex-direction: column; width: 100%; - padding: 0 16px; + min-height: 140px; - &:not(.is-collapsed) { - flex: 1; - min-height: 140px; - } - - &.is-collapsed { - .multi-file-commit-panel-header { - margin-left: -$gl-padding; - margin-right: -$gl-padding; - - svg { - margin-left: auto; - margin-right: auto; - } - - .multi-file-commit-panel-collapse-btn { - margin-right: auto; - margin-left: auto; - border-left: 0; - } - } + &.is-first { + border-bottom: 1px solid $white-dark; } } .ide-staged-action-btn { margin-left: auto; - color: $gl-link-color; + line-height: 22px; +} + +.ide-commit-file-count { + min-width: 22px; + margin-left: auto; + background-color: $gray-light; + border-radius: $border-radius-default; + border: 1px solid $white-dark; + line-height: 20px; + text-align: center; } .ide-commit-radios { label { font-weight: normal; + + &.is-disabled { + .ide-radio-label { + text-decoration: line-through; + } + } } .help-block { @@ -888,17 +816,58 @@ margin-left: 25px; } -.ide-external-links { - p { - margin: 0; - } -} - .ide-sidebar-link { - padding: $gl-padding-8 $gl-padding; display: flex; align-items: center; - font-weight: $gl-font-weight-bold; + position: relative; + height: 60px; + width: 100%; + padding: 0 $gl-padding; + color: $gl-text-color-secondary; + background-color: transparent; + border: 0; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + outline: 0; + + svg { + margin: 0 auto; + } + + &:hover { + color: $gl-text-color; + background-color: $theme-gray-100; + } + + &:focus { + color: $gl-text-color; + background-color: $theme-gray-200; + } + + &.active { + // extend width over border of sidebar section + width: calc(100% + 1px); + padding-right: $gl-padding + 1px; + background-color: $white-light; + border-top-color: $white-dark; + border-bottom-color: $white-dark; + + &::after { + content: ''; + position: absolute; + right: -1px; + top: 0; + bottom: 0; + width: 1px; + background: $white-light; + } + } +} + +.ide-activity-bar { + position: relative; + flex: 0 0 60px; + z-index: 1; } .ide-file-finder-overlay { @@ -992,6 +961,80 @@ resize: none; } +.ide-tree-header { + display: flex; + align-items: center; + padding: 10px 0; + margin-left: 10px; + margin-right: 10px; + border-bottom: 1px solid $white-dark; + + .ide-new-btn { + margin-left: auto; + } +} + +.ide-sidebar-branch-title { + font-weight: $gl-font-weight-normal; + + svg { + position: relative; + top: 3px; + margin-top: -1px; + } +} + +.commit-form-compact { + .btn { + margin-bottom: 8px; + } + + p { + margin-bottom: 0; + } +} + +.commit-form-slide-up-enter-active, +.commit-form-slide-up-leave-active { + position: absolute; + top: 16px; + left: 16px; + right: 16px; + transition: all 0.3s ease; +} + +.is-full .commit-form-slide-up-enter, +.is-compact .commit-form-slide-up-leave-to { + transform: translateY(100%); +} + +.is-full .commit-form-slide-up-enter-to, +.is-compact .commit-form-slide-up-leave { + transform: translateY(0); +} + +.commit-form-slide-up-enter, +.commit-form-slide-up-leave-to { + opacity: 0; +} + +.ide-review-header { + flex-direction: column; + align-items: flex-start; + + .dropdown { + margin-left: auto; + } + + a { + color: $gl-link-color; + } +} + +.ide-review-sub-header { + color: $gl-text-color-secondary; +} + .ide-tree-changes { display: flex; align-items: center; @@ -1001,3 +1044,37 @@ .ide-new-modal-label { line-height: 34px; } + +.multi-file-commit-panel-success-message { + position: absolute; + top: 61px; + left: 1px; + bottom: 0; + right: 0; + z-index: 10; + background: $white-light; + overflow: auto; + display: flex; + flex-direction: column; + justify-content: center; +} + +.ide-review-button-holder { + display: flex; + width: 100%; + align-items: center; +} + +.ide-context-header { + .avatar { + flex: 0 0 40px; + } +} + +.ide-sidebar-project-title { + min-width: 0; + + .sidebar-context-title { + white-space: nowrap; + } +} diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb index b242e41df1c..3017048e506 100644 --- a/spec/features/projects/tree/create_directory_spec.rb +++ b/spec/features/projects/tree/create_directory_spec.rb @@ -44,12 +44,17 @@ feature 'Multi-file editor new directory', :js do wait_for_requests - click_button 'Stage all' + find('.js-ide-commit-mode').click + + find('.multi-file-commit-list-item').hover + first('.multi-file-discard-btn .btn').click fill_in('commit-message', with: 'commit message ide') click_button('Commit') + find('.js-ide-edit-mode').click + expect(page).to have_content('folder name') end end diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb index 7d65456e049..56471c8e7aa 100644 --- a/spec/features/projects/tree/create_file_spec.rb +++ b/spec/features/projects/tree/create_file_spec.rb @@ -34,7 +34,10 @@ feature 'Multi-file editor new file', :js do wait_for_requests - click_button 'Stage all' + find('.js-ide-commit-mode').click + + find('.multi-file-commit-list-item').hover + first('.multi-file-discard-btn .btn').click fill_in('commit-message', with: 'commit message ide') diff --git a/spec/javascripts/ide/components/activity_bar_spec.js b/spec/javascripts/ide/components/activity_bar_spec.js new file mode 100644 index 00000000000..946c7e8e9c8 --- /dev/null +++ b/spec/javascripts/ide/components/activity_bar_spec.js @@ -0,0 +1,92 @@ +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(); + }); + }); + }); +}); diff --git a/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js b/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js index 53275b78da5..16d0b354a30 100644 --- a/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js @@ -10,10 +10,9 @@ describe('IDE commit panel empty state', () => { beforeEach(() => { const Component = Vue.extend(emptyState); - vm = createComponentWithStore(Component, store, { - noChangesStateSvgPath: 'no-changes', - committedStateSvgPath: 'committed-state', - }); + Vue.set(store.state, 'noChangesStateSvgPath', 'no-changes'); + + vm = createComponentWithStore(Component, store); vm.$mount(); }); @@ -27,37 +26,4 @@ describe('IDE commit panel empty state', () => { it('renders no changes text when last commit message is empty', () => { expect(vm.$el.textContent).toContain('No changes'); }); - - describe('toggle button', () => { - it('calls store action', () => { - spyOn(vm, 'toggleRightPanelCollapsed'); - - vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click(); - - expect(vm.toggleRightPanelCollapsed).toHaveBeenCalled(); - }); - - it('renders collapsed class', done => { - vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click(); - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.is-collapsed')).not.toBeNull(); - - done(); - }); - }); - }); - - describe('collapsed state', () => { - beforeEach(done => { - vm.$store.state.rightPanelCollapsed = true; - - Vue.nextTick(done); - }); - - it('does not render text & svg', () => { - expect(vm.$el.querySelector('img')).toBeNull(); - expect(vm.$el.textContent).not.toContain('No changes'); - }); - }); }); diff --git a/spec/javascripts/ide/components/commit_sidebar/form_spec.js b/spec/javascripts/ide/components/commit_sidebar/form_spec.js new file mode 100644 index 00000000000..ce7c134bc97 --- /dev/null +++ b/spec/javascripts/ide/components/commit_sidebar/form_spec.js @@ -0,0 +1,146 @@ +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); + }); + }); + }); +}); diff --git a/spec/javascripts/ide/components/commit_sidebar/list_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_spec.js index 62fc3f90ad1..54625ef90f8 100644 --- a/spec/javascripts/ide/components/commit_sidebar/list_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/list_spec.js @@ -49,45 +49,4 @@ describe('Multi-file editor commit sidebar list', () => { expect(vm.$el.textContent).toContain('No changes'); }); }); - - describe('collapsed', () => { - beforeEach(done => { - vm.$store.state.rightPanelCollapsed = true; - - Vue.nextTick(done); - }); - - it('hides list', () => { - expect(vm.$el.querySelector('.list-unstyled')).toBeNull(); - expect(vm.$el.querySelector('.help-block')).toBeNull(); - }); - }); - - describe('with toggle', () => { - beforeEach(done => { - spyOn(vm, 'toggleRightPanelCollapsed'); - - vm.showToggle = true; - - Vue.nextTick(done); - }); - - it('calls setPanelCollapsedStatus when clickin toggle', () => { - vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click(); - - expect(vm.toggleRightPanelCollapsed).toHaveBeenCalled(); - }); - }); - - describe('action button', () => { - beforeEach(() => { - spyOn(vm, 'stageAllChanges'); - }); - - it('calls store action when clicked', () => { - vm.$el.querySelector('.ide-staged-action-btn').click(); - - expect(vm.stageAllChanges).toHaveBeenCalled(); - }); - }); }); diff --git a/spec/javascripts/ide/components/ide_context_bar_spec.js b/spec/javascripts/ide/components/ide_context_bar_spec.js deleted file mode 100644 index e17b051f137..00000000000 --- a/spec/javascripts/ide/components/ide_context_bar_spec.js +++ /dev/null @@ -1,37 +0,0 @@ -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(); - }); - }); -}); diff --git a/spec/javascripts/ide/components/ide_external_links_spec.js b/spec/javascripts/ide/components/ide_external_links_spec.js deleted file mode 100644 index 9f6cb459f3b..00000000000 --- a/spec/javascripts/ide/components/ide_external_links_spec.js +++ /dev/null @@ -1,43 +0,0 @@ -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); - }); - }); -}); diff --git a/spec/javascripts/ide/components/ide_project_tree_spec.js b/spec/javascripts/ide/components/ide_project_tree_spec.js deleted file mode 100644 index 657682cb39c..00000000000 --- a/spec/javascripts/ide/components/ide_project_tree_spec.js +++ /dev/null @@ -1,39 +0,0 @@ -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(); - }); - }); -}); diff --git a/spec/javascripts/ide/components/ide_repo_tree_spec.js b/spec/javascripts/ide/components/ide_repo_tree_spec.js deleted file mode 100644 index e0fbc90ca61..00000000000 --- a/spec/javascripts/ide/components/ide_repo_tree_spec.js +++ /dev/null @@ -1,43 +0,0 @@ -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(); - }); - }); -}); diff --git a/spec/javascripts/ide/components/ide_review_spec.js b/spec/javascripts/ide/components/ide_review_spec.js new file mode 100644 index 00000000000..b9ee22b7c1a --- /dev/null +++ b/spec/javascripts/ide/components/ide_review_spec.js @@ -0,0 +1,69 @@ +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(); + }); + }); + }); +}); diff --git a/spec/javascripts/ide/components/ide_side_bar_spec.js b/spec/javascripts/ide/components/ide_side_bar_spec.js index 699dae1ce2f..20ee20bc1d7 100644 --- a/spec/javascripts/ide/components/ide_side_bar_spec.js +++ b/spec/javascripts/ide/components/ide_side_bar_spec.js @@ -1,8 +1,10 @@ import Vue from 'vue'; import store from '~/ide/stores'; import ideSidebar from '~/ide/components/ide_side_bar.vue'; +import { activityBarViews } from '~/ide/constants'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { resetStore } from '../helpers'; +import { projectData } from '../mock_data'; describe('IdeSidebar', () => { let vm; @@ -10,6 +12,9 @@ describe('IdeSidebar', () => { beforeEach(() => { const Component = Vue.extend(ideSidebar); + store.state.currentProjectId = 'abcproject'; + store.state.projects.abcproject = projectData; + vm = createComponentWithStore(Component, store).$mount(); }); @@ -20,23 +25,33 @@ describe('IdeSidebar', () => { }); it('renders a sidebar', () => { - expect( - vm.$el.querySelector('.multi-file-commit-panel-inner'), - ).not.toBeNull(); + expect(vm.$el.querySelector('.multi-file-commit-panel-inner')).not.toBeNull(); }); it('renders loading icon component', done => { vm.$store.state.loading = true; vm.$nextTick(() => { - expect( - vm.$el.querySelector('.multi-file-loading-container'), - ).not.toBeNull(); - expect( - vm.$el.querySelectorAll('.multi-file-loading-container').length, - ).toBe(3); + expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull(); + expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3); done(); }); }); + + describe('activityBarComponent', () => { + it('renders tree component', () => { + expect(vm.$el.querySelector('.ide-file-list')).not.toBeNull(); + }); + + it('renders commit component', done => { + vm.$store.state.currentActivityView = activityBarViews.commit; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.multi-file-commit-panel-section')).not.toBeNull(); + + done(); + }); + }); + }); }); diff --git a/spec/javascripts/ide/components/ide_spec.js b/spec/javascripts/ide/components/ide_spec.js index 7bfcfc90572..6f580e1f7af 100644 --- a/spec/javascripts/ide/components/ide_spec.js +++ b/spec/javascripts/ide/components/ide_spec.js @@ -4,6 +4,7 @@ import store from '~/ide/stores'; import ide from '~/ide/components/ide.vue'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { file, resetStore } from '../helpers'; +import { projectData } from '../mock_data'; describe('ide component', () => { let vm; @@ -11,6 +12,10 @@ describe('ide component', () => { beforeEach(() => { const Component = Vue.extend(ide); + store.state.currentProjectId = 'abcproject'; + store.state.currentBranchId = 'master'; + store.state.projects.abcproject = Object.assign({}, projectData); + vm = createComponentWithStore(Component, store, { emptyStateSvgPath: 'svg', noChangesStateSvgPath: 'svg', @@ -24,11 +29,11 @@ describe('ide component', () => { resetStore(vm.$store); }); - it('does not render panel right when no files open', () => { + it('does not render right right when no files open', () => { expect(vm.$el.querySelector('.panel-right')).toBeNull(); }); - it('renders panel right when files are open', done => { + it('renders right panel when files are open', done => { vm.$store.state.trees['abcproject/mybranch'] = { tree: [file()], }; diff --git a/spec/javascripts/ide/components/ide_tree_list_spec.js b/spec/javascripts/ide/components/ide_tree_list_spec.js new file mode 100644 index 00000000000..4ecbdb8a55e --- /dev/null +++ b/spec/javascripts/ide/components/ide_tree_list_spec.js @@ -0,0 +1,54 @@ +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'); + }); +}); diff --git a/spec/javascripts/ide/components/ide_tree_spec.js b/spec/javascripts/ide/components/ide_tree_spec.js new file mode 100644 index 00000000000..97a0a2432f1 --- /dev/null +++ b/spec/javascripts/ide/components/ide_tree_spec.js @@ -0,0 +1,34 @@ +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'); + }); +}); diff --git a/spec/javascripts/ide/components/repo_commit_section_spec.js b/spec/javascripts/ide/components/repo_commit_section_spec.js index 768f6e99bf1..5e3e00a180b 100644 --- a/spec/javascripts/ide/components/repo_commit_section_spec.js +++ b/spec/javascripts/ide/components/repo_commit_section_spec.js @@ -1,9 +1,9 @@ import Vue from 'vue'; import store from '~/ide/stores'; import service from '~/ide/services'; +import router from '~/ide/ide_router'; import repoCommitSection from '~/ide/components/repo_commit_section.vue'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper'; import { file, resetStore } from '../helpers'; describe('RepoCommitSection', () => { @@ -12,10 +12,10 @@ describe('RepoCommitSection', () => { function createComponent() { const Component = Vue.extend(repoCommitSection); - vm = createComponentWithStore(Component, store, { - noChangesStateSvgPath: 'svg', - committedStateSvgPath: 'commitsvg', - }); + store.state.noChangesStateSvgPath = 'svg'; + store.state.committedStateSvgPath = 'commitsvg'; + + vm = createComponentWithStore(Component, store); vm.$store.state.currentProjectId = 'abcproject'; vm.$store.state.currentBranchId = 'master'; @@ -60,6 +60,8 @@ describe('RepoCommitSection', () => { } beforeEach(done => { + spyOn(router, 'push'); + vm = createComponent(); spyOn(service, 'getTreeData').and.returnValue( @@ -93,61 +95,49 @@ describe('RepoCommitSection', () => { resetStore(vm.$store); const Component = Vue.extend(repoCommitSection); - vm = createComponentWithStore(Component, store, { - noChangesStateSvgPath: 'nochangessvg', - committedStateSvgPath: 'svg', - }).$mount(); + store.state.noChangesStateSvgPath = 'nochangessvg'; + store.state.committedStateSvgPath = 'svg'; - expect( - vm.$el.querySelector('.js-empty-state').textContent.trim(), - ).toContain('No changes'); - expect( - vm.$el.querySelector('.js-empty-state img').getAttribute('src'), - ).toBe('nochangessvg'); + vm = createComponentWithStore(Component, store).$mount(); + + expect(vm.$el.querySelector('.js-empty-state').textContent.trim()).toContain('No changes'); + expect(vm.$el.querySelector('.js-empty-state img').getAttribute('src')).toBe('nochangessvg'); }); }); it('renders a commit section', () => { - const changedFileElements = [ - ...vm.$el.querySelectorAll('.multi-file-commit-list li'), - ]; - const submitCommit = vm.$el.querySelector('form .btn'); - const allFiles = vm.$store.state.changedFiles.concat( - vm.$store.state.stagedFiles, - ); + const changedFileElements = [...vm.$el.querySelectorAll('.multi-file-commit-list li')]; + const allFiles = vm.$store.state.changedFiles.concat(vm.$store.state.stagedFiles); - expect(vm.$el.querySelector('.multi-file-commit-form')).not.toBeNull(); expect(changedFileElements.length).toEqual(4); changedFileElements.forEach((changedFile, i) => { expect(changedFile.textContent.trim()).toContain(allFiles[i].path); }); - - expect(submitCommit.disabled).toBeTruthy(); - expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeNull(); }); it('adds changed files into staged files', done => { - vm.$el.querySelector('.ide-staged-action-btn').click(); - - Vue.nextTick(() => { - expect( - vm.$el.querySelector('.ide-commit-list-container').textContent, - ).toContain('No changes'); - - done(); - }); + vm.$el.querySelector('.multi-file-discard-btn .btn').click(); + vm + .$nextTick() + .then(() => vm.$el.querySelector('.multi-file-discard-btn .btn').click()) + .then(vm.$nextTick) + .then(() => { + expect(vm.$el.querySelector('.ide-commit-list-container').textContent).toContain( + 'No changes', + ); + }) + .then(done) + .catch(done.fail); }); it('stages a single file', done => { vm.$el.querySelector('.multi-file-discard-btn .btn').click(); Vue.nextTick(() => { - expect( - vm.$el - .querySelector('.ide-commit-list-container') - .querySelectorAll('li').length, - ).toBe(1); + expect(vm.$el.querySelector('.ide-commit-list-container').querySelectorAll('li').length).toBe( + 1, + ); done(); }); @@ -157,26 +147,10 @@ describe('RepoCommitSection', () => { vm.$el.querySelectorAll('.multi-file-discard-btn .btn')[1].click(); Vue.nextTick(() => { - expect( - vm.$el.querySelector('.ide-commit-list-container').textContent, - ).not.toContain('file1'); - expect( - vm.$el - .querySelector('.ide-commit-list-container') - .querySelectorAll('li').length, - ).toBe(1); - - done(); - }); - }); - - it('removes all staged files', done => { - vm.$el.querySelectorAll('.ide-staged-action-btn')[1].click(); - - Vue.nextTick(() => { - expect( - vm.$el.querySelectorAll('.ide-commit-list-container')[1].textContent, - ).toContain('No changes'); + expect(vm.$el.querySelector('.ide-commit-list-container').textContent).not.toContain('file1'); + expect(vm.$el.querySelector('.ide-commit-list-container').querySelectorAll('li').length).toBe( + 1, + ); done(); }); @@ -190,75 +164,17 @@ describe('RepoCommitSection', () => { Vue.nextTick(() => { expect( - vm.$el - .querySelectorAll('.ide-commit-list-container')[1] - .querySelectorAll('li').length, + vm.$el.querySelectorAll('.ide-commit-list-container')[1].querySelectorAll('li').length, ).toBe(1); done(); }); }); - it('updates commitMessage in store on input', done => { - const textarea = vm.$el.querySelector('textarea'); - - textarea.value = 'testing commit message'; - - textarea.dispatchEvent(new Event('input')); - - getSetTimeoutPromise() - .then(() => { - expect(vm.$store.state.commit.commitMessage).toBe( - 'testing commit message', - ); - }) - .then(done) - .catch(done.fail); - }); - - describe('discard draft button', () => { - it('hidden when commitMessage is empty', () => { - expect( - vm.$el.querySelector('.multi-file-commit-form .btn-default'), - ).toBeNull(); - }); - - it('resets commitMessage when clicking discard button', done => { - vm.$store.state.commit.commitMessage = 'testing commit message'; - - getSetTimeoutPromise() - .then(() => { - vm.$el.querySelector('.multi-file-commit-form .btn-default').click(); - }) - .then(Vue.nextTick) - .then(() => { - expect(vm.$store.state.commit.commitMessage).not.toBe( - 'testing commit message', - ); - }) - .then(done) - .catch(done.fail); - }); - }); - - describe('when submitting', () => { - beforeEach(() => { - spyOn(vm, 'commitChanges'); - }); - - it('calls commitChanges', done => { - vm.$store.state.commit.commitMessage = 'testing commit message'; - - getSetTimeoutPromise() - .then(() => { - vm.$el.querySelector('.multi-file-commit-form .btn-success').click(); - }) - .then(Vue.nextTick) - .then(() => { - expect(vm.commitChanges).toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); + describe('mounted', () => { + it('opens last opened file', () => { + expect(store.state.openFiles.length).toBe(1); + expect(store.state.openFiles[0].pending).toBe(true); }); }); }); diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js index b06a6c62a1c..360b6d4dc15 100644 --- a/spec/javascripts/ide/components/repo_editor_spec.js +++ b/spec/javascripts/ide/components/repo_editor_spec.js @@ -5,6 +5,7 @@ import store from '~/ide/stores'; import repoEditor from '~/ide/components/repo_editor.vue'; import monacoLoader from '~/ide/monaco_loader'; import Editor from '~/ide/lib/editor'; +import { activityBarViews } from '~/ide/constants'; import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; import setTimeoutPromise from '../../helpers/set_timeout_promise_helper'; import { file, resetStore } from '../helpers'; @@ -295,4 +296,30 @@ describe('RepoEditor', () => { }); }); }); + + describe('show tabs', () => { + it('shows tabs in edit mode', () => { + expect(vm.$el.querySelector('.nav-links')).not.toBe(null); + }); + + it('hides tabs in review mode', done => { + vm.$store.state.currentActivityView = activityBarViews.review; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.nav-links')).toBe(null); + + done(); + }); + }); + + it('hides tabs in commit mode', done => { + vm.$store.state.currentActivityView = activityBarViews.commit; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.nav-links')).toBe(null); + + done(); + }); + }); + }); }); diff --git a/spec/javascripts/ide/components/repo_file_spec.js b/spec/javascripts/ide/components/repo_file_spec.js index 28ff06e1f80..156233653ab 100644 --- a/spec/javascripts/ide/components/repo_file_spec.js +++ b/spec/javascripts/ide/components/repo_file_spec.js @@ -73,6 +73,43 @@ describe('RepoFile', () => { expect(treeChangesEl).not.toBeNull(); expect(treeChangesEl.textContent).toContain('1'); }); + + it('renders action dropdown', done => { + createComponent({ + file: { + ...file('t4'), + type: 'tree', + branchId: 'master', + projectId: 'project', + }, + level: 0, + }); + + setTimeout(() => { + expect(vm.$el.querySelector('.ide-new-btn')).not.toBeNull(); + + done(); + }); + }); + + it('disables action dropdown', done => { + createComponent({ + file: { + ...file('t4'), + type: 'tree', + branchId: 'master', + projectId: 'project', + }, + level: 0, + disableActionDropdown: true, + }); + + setTimeout(() => { + expect(vm.$el.querySelector('.ide-new-btn')).toBeNull(); + + done(); + }); + }); }); describe('locked file', () => { diff --git a/spec/javascripts/ide/components/repo_tabs_spec.js b/spec/javascripts/ide/components/repo_tabs_spec.js index cb785ba2cd3..583f71e6121 100644 --- a/spec/javascripts/ide/components/repo_tabs_spec.js +++ b/spec/javascripts/ide/components/repo_tabs_spec.js @@ -26,60 +26,10 @@ describe('RepoTabs', () => { const tabs = [...vm.$el.querySelectorAll('.multi-file-tab')]; expect(tabs.length).toEqual(2); - expect(tabs[0].classList.contains('active')).toEqual(true); - expect(tabs[1].classList.contains('active')).toEqual(false); + expect(tabs[0].parentNode.classList.contains('active')).toEqual(true); + expect(tabs[1].parentNode.classList.contains('active')).toEqual(false); done(); }); }); - - describe('updated', () => { - it('sets showShadow as true when scroll width is larger than width', done => { - const el = document.createElement('div'); - el.innerHTML = '<div id="test-app"></div>'; - document.body.appendChild(el); - - const style = document.createElement('style'); - style.innerText = ` - .multi-file-tabs { - width: 100px; - } - - .multi-file-tabs .list-unstyled { - display: flex; - overflow-x: auto; - } - `; - document.head.appendChild(style); - - vm = createComponent( - RepoTabs, - { - files: [], - viewer: 'editor', - hasChanges: false, - activeFile: file('activeFile'), - hasMergeRequest: false, - }, - '#test-app', - ); - - vm - .$nextTick() - .then(() => { - expect(vm.showShadow).toEqual(false); - - vm.files = openedFiles; - }) - .then(vm.$nextTick) - .then(() => { - expect(vm.showShadow).toEqual(true); - - style.remove(); - el.remove(); - }) - .then(done) - .catch(done.fail); - }); - }); }); diff --git a/spec/javascripts/ide/lib/editor_spec.js b/spec/javascripts/ide/lib/editor_spec.js index 530bdfa2759..b88a12264ca 100644 --- a/spec/javascripts/ide/lib/editor_spec.js +++ b/spec/javascripts/ide/lib/editor_spec.js @@ -74,10 +74,10 @@ describe('Multi-file editor library', () => { scrollBeyondLastLine: false, quickSuggestions: false, occurrencesHighlight: false, - renderLineHighlight: 'none', - hideCursorInOverviewRuler: true, wordWrap: 'on', renderSideBySide: true, + renderLineHighlight: 'all', + hideCursorInOverviewRuler: false, }); }); }); diff --git a/spec/javascripts/ide/mock_data.js b/spec/javascripts/ide/mock_data.js new file mode 100644 index 00000000000..3c6d75ab5e4 --- /dev/null +++ b/spec/javascripts/ide/mock_data.js @@ -0,0 +1,15 @@ +// 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: {}, +}; diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js index 3ee11bd2f03..3ef5a859001 100644 --- a/spec/javascripts/ide/stores/actions/file_spec.js +++ b/spec/javascripts/ide/stores/actions/file_spec.js @@ -511,7 +511,10 @@ describe('IDE store file actions', () => { actions.stageChange, 'path', store.state, - [{ type: types.STAGE_CHANGE, payload: 'path' }], + [ + { type: types.STAGE_CHANGE, payload: 'path' }, + { type: types.SET_LAST_COMMIT_MSG, payload: '' }, + ], [], done, ); @@ -524,7 +527,10 @@ describe('IDE store file actions', () => { actions.unstageChange, 'path', store.state, - [{ type: types.UNSTAGE_CHANGE, payload: 'path' }], + [ + { type: types.UNSTAGE_CHANGE, payload: 'path' }, + { type: types.SET_LAST_COMMIT_MSG, payload: '' }, + ], [], done, ); @@ -589,20 +595,6 @@ describe('IDE store file actions', () => { .then(done) .catch(done.fail); }); - - it('returns false when passed in file is active & viewer is diff', done => { - f.active = true; - store.state.openFiles.push(f); - store.state.viewer = 'diff'; - - store - .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' }) - .then(added => { - expect(added).toBe(false); - }) - .then(done) - .catch(done.fail); - }); }); describe('removePendingTab', () => { diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js index a64af5b941b..062c3497623 100644 --- a/spec/javascripts/ide/stores/actions_spec.js +++ b/spec/javascripts/ide/stores/actions_spec.js @@ -2,6 +2,9 @@ import actions, { stageAllChanges, unstageAllChanges, toggleFileFinder, + setCurrentBranchId, + setEmptyStateSvgs, + updateActivityBarView, updateTempFlagForEntry, } from '~/ide/stores/actions'; import store from '~/ide/stores'; @@ -306,6 +309,7 @@ describe('Multi-file store actions', () => { null, store.state, [ + { type: types.SET_LAST_COMMIT_MSG, payload: '' }, { type: types.STAGE_CHANGE, payload: store.state.changedFiles[0].path }, { type: types.STAGE_CHANGE, payload: store.state.changedFiles[1].path }, ], @@ -345,6 +349,32 @@ describe('Multi-file store actions', () => { }); }); + describe('updateActivityBarView', () => { + it('commits UPDATE_ACTIVITY_BAR_VIEW', done => { + testAction( + updateActivityBarView, + 'test', + {}, + [{ type: 'UPDATE_ACTIVITY_BAR_VIEW', payload: 'test' }], + [], + done, + ); + }); + }); + + describe('setEmptyStateSvgs', () => { + it('commits setEmptyStateSvgs', done => { + testAction( + setEmptyStateSvgs, + 'svg', + {}, + [{ type: 'SET_EMPTY_STATE_SVGS', payload: 'svg' }], + [], + done, + ); + }); + }); + describe('updateTempFlagForEntry', () => { it('commits UPDATE_TEMP_FLAG', done => { const f = { @@ -388,6 +418,19 @@ describe('Multi-file store actions', () => { }); }); + describe('setCurrentBranchId', () => { + it('commits setCurrentBranchId', done => { + testAction( + setCurrentBranchId, + 'branchId', + {}, + [{ type: 'SET_CURRENT_BRANCH', payload: 'branchId' }], + [], + done, + ); + }); + }); + describe('toggleFileFinder', () => { it('commits TOGGLE_FILE_FINDER', done => { testAction( diff --git a/spec/javascripts/ide/stores/getters_spec.js b/spec/javascripts/ide/stores/getters_spec.js index bd834443730..67a848e8edd 100644 --- a/spec/javascripts/ide/stores/getters_spec.js +++ b/spec/javascripts/ide/stores/getters_spec.js @@ -37,12 +37,6 @@ describe('IDE store getters', () => { expect(modifiedFiles.length).toBe(1); expect(modifiedFiles[0].name).toBe('changed'); }); - - it('returns angle left when collapsed', () => { - localState.rightPanelCollapsed = true; - - expect(getters.collapseButtonIcon(localState)).toBe('angle-double-left'); - }); }); describe('currentMergeRequest', () => { diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js index b2b4b85ca42..a2869ff378b 100644 --- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js @@ -289,21 +289,6 @@ describe('IDE commit module actions', () => { .then(done) .catch(done.fail); }); - - it('pushes route to new branch if commitAction is new branch', done => { - store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH; - - store - .dispatch('commit/updateFilesAfterCommit', { - data, - branch, - }) - .then(() => { - expect(router.push).toHaveBeenCalledWith(`/project/abcproject/blob/master/${f.path}`); - }) - .then(done) - .catch(done.fail); - }); }); describe('commitChanges', () => { @@ -391,21 +376,6 @@ describe('IDE commit module actions', () => { .catch(done.fail); }); - it('pushes router to new route', done => { - store - .dispatch('commit/commitChanges') - .then(() => { - expect(router.push).toHaveBeenCalledWith( - `/project/${store.state.currentProjectId}/blob/${ - store.getters['commit/newBranchName'] - }/changed`, - ); - - done(); - }) - .catch(done.fail); - }); - it('sets last Commit Msg', done => { store .dispatch('commit/commitChanges') diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js index 6fba934810d..e83961fcedc 100644 --- a/spec/javascripts/ide/stores/mutations/file_spec.js +++ b/spec/javascripts/ide/stores/mutations/file_spec.js @@ -267,41 +267,23 @@ describe('IDE store file mutations', () => { it('adds file into openFiles as pending', () => { mutations.ADD_PENDING_TAB(localState, { file: localFile }); - expect(localState.openFiles.length).toBe(2); - expect(localState.openFiles[1].pending).toBe(true); - expect(localState.openFiles[1].key).toBe(`pending-${localFile.key}`); - }); - - it('updates open file to pending', () => { - mutations.ADD_PENDING_TAB(localState, { file: localState.openFiles[0] }); - expect(localState.openFiles.length).toBe(1); + expect(localState.openFiles[0].pending).toBe(true); + expect(localState.openFiles[0].key).toBe(`pending-${localFile.key}`); }); - it('updates pending open file to active', () => { - localState.openFiles.push({ - ...localFile, - pending: true, - }); + it('only allows 1 open pending file', () => { + const newFile = file('test'); + localState.entries[newFile.path] = newFile; mutations.ADD_PENDING_TAB(localState, { file: localFile }); - expect(localState.openFiles[1].pending).toBe(true); - expect(localState.openFiles[1].active).toBe(true); - }); - - it('sets all openFiles to not active', () => { - mutations.ADD_PENDING_TAB(localState, { file: localFile }); + expect(localState.openFiles.length).toBe(1); - expect(localState.openFiles.length).toBe(2); + mutations.ADD_PENDING_TAB(localState, { file: file('test') }); - localState.openFiles.forEach(f => { - if (f.pending) { - expect(f.active).toBe(true); - } else { - expect(f.active).toBe(false); - } - }); + expect(localState.openFiles.length).toBe(1); + expect(localState.openFiles[0].name).toBe('test'); }); }); diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js index 61efb6372c9..972713c5ad2 100644 --- a/spec/javascripts/ide/stores/mutations_spec.js +++ b/spec/javascripts/ide/stores/mutations_spec.js @@ -87,6 +87,28 @@ describe('Multi-file store mutations', () => { }); }); + describe('UPDATE_ACTIVITY_BAR_VIEW', () => { + it('updates currentActivityBar', () => { + mutations.UPDATE_ACTIVITY_BAR_VIEW(localState, 'test'); + + expect(localState.currentActivityView).toBe('test'); + }); + }); + + describe('SET_EMPTY_STATE_SVGS', () => { + it('updates empty state SVGs', () => { + mutations.SET_EMPTY_STATE_SVGS(localState, { + emptyStateSvgPath: 'emptyState', + noChangesStateSvgPath: 'noChanges', + committedStateSvgPath: 'commited', + }); + + expect(localState.emptyStateSvgPath).toBe('emptyState'); + expect(localState.noChangesStateSvgPath).toBe('noChanges'); + expect(localState.committedStateSvgPath).toBe('commited'); + }); + }); + describe('UPDATE_TEMP_FLAG', () => { beforeEach(() => { localState.entries.test = { |