Commit 51c64f3f authored by Phil Hughes's avatar Phil Hughes

Added staged files state to IDE

Closes https://gitlab.com/gitlab-org/gitlab-ee/issues/4541
parent 4718f22f
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
Icon,
},
props: {
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
},
computed: {
...mapState(['lastCommitMsg', 'rightPanelCollapsed']),
...mapGetters(['collapseButtonIcon']),
statusSvg() {
return this.lastCommitMsg
? this.committedStateSvgPath
: this.noChangesStateSvgPath;
},
},
methods: {
...mapActions(['toggleRightPanelCollapsed']),
},
};
</script>
<template>
<div
class="multi-file-commit-panel-section ide-commity-empty-state js-empty-state"
>
<header
class="multi-file-commit-panel-header"
:class="{
'is-collapsed': rightPanelCollapsed,
}"
>
<button
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="statusSvg" />
</div>
<div class="append-right-default prepend-left-default">
<div
class="text-content text-center"
v-if="!lastCommitMsg"
>
<h4>
{{ __('No changes') }}
</h4>
<p>
{{ __('Edit files in the editor and commit changes here') }}
</p>
</div>
<div
class="text-content text-center"
v-else
>
<h4>
{{ __('All changes are committed') }}
</h4>
<p v-html="lastCommitMsg"></p>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import { mapActions, mapState, mapGetters } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import listItem from './list_item.vue';
import listCollapsed from './list_collapsed.vue';
......@@ -19,20 +19,44 @@
type: Array,
required: true,
},
showToggle: {
type: Boolean,
required: false,
default: true,
},
icon: {
type: String,
required: true,
},
action: {
type: String,
required: true,
},
actionBtnText: {
type: String,
required: true,
},
itemActionComponent: {
type: String,
required: true,
},
},
computed: {
...mapState([
'currentProjectId',
'currentBranchId',
'rightPanelCollapsed',
]),
isCommitInfoShown() {
return this.rightPanelCollapsed || this.fileList.length;
},
...mapGetters([
'collapseButtonIcon',
]),
},
methods: {
toggleCollapsed() {
this.$emit('toggleCollapsed');
...mapActions([
'toggleRightPanelCollapsed',
'stageAllChanges',
'unstageAllChanges',
]),
actionBtnClicked() {
this[this.action]();
},
},
};
......@@ -40,17 +64,60 @@
<template>
<div
class="ide-commit-list-container"
:class="{
'multi-file-commit-list': isCommitInfoShown
'is-collapsed': rightPanelCollapsed,
}"
>
<header
class="multi-file-commit-panel-header"
:class="{
'is-collapsed': rightPanelCollapsed,
}"
>
<div
v-if="!rightPanelCollapsed"
class="multi-file-commit-panel-header-title"
:class="{
'append-right-10': showToggle,
}"
>
<icon
v-once
:name="icon"
:size="18"
/>
{{ title }}
<button
type="button"
class="btn btn-blank btn-link ide-staged-action-btn"
@click="actionBtnClicked"
>
{{ actionBtnText }}
</button>
</div>
<button
v-if="showToggle"
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="icon"
/>
<template v-else>
<ul
v-if="fileList.length"
class="list-unstyled append-bottom-0"
class="multi-file-commit-list list-unstyled append-bottom-0"
>
<li
v-for="file in fileList"
......@@ -58,9 +125,16 @@
>
<list-item
:file="file"
:action-component="itemActionComponent"
/>
</li>
</ul>
<p
v-else
class="multi-file-commit-list help-block"
>
{{ __('No changes') }}
</p>
</template>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import icon from '~/vue_shared/components/icon.vue';
export default {
components: {
icon,
export default {
components: {
icon,
},
props: {
files: {
type: Array,
required: true,
},
computed: {
...mapGetters([
'addedFiles',
'modifiedFiles',
]),
icon: {
type: String,
required: true,
},
};
},
computed: {
addedFilesLength() {
return this.files.filter(f => f.tempFile).length;
},
modifiedFilesLength() {
return this.files.filter(f => !f.tempFile).length;
},
addedFilesIconClass() {
return this.addedFilesLength ? 'multi-file-addition' : '';
},
modifiedFilesClass() {
return this.modifiedFilesLength ? 'multi-file-modified' : '';
},
},
};
</script>
<template>
<div
class="multi-file-commit-list-collapsed text-center"
>
<icon
v-once
:name="icon"
:size="18"
css-classes="append-bottom-15"
/>
<icon
name="file-addition"
:size="18"
css-classes="multi-file-addition append-bottom-10"
:css-classes="addedFilesIconClass + 'append-bottom-10'"
/>
{{ addedFiles.length }}
{{ addedFilesLength }}
<icon
name="file-modified"
:size="18"
css-classes="multi-file-modified prepend-top-10 append-bottom-10"
:css-classes="modifiedFilesClass + ' prepend-top-10 append-bottom-10'"
/>
{{ modifiedFiles.length }}
{{ modifiedFilesLength }}
</div>
</template>
<script>
import { mapActions } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import router from '../../ide_router';
import Icon from '~/vue_shared/components/icon.vue';
import StageButton from './stage_button.vue';
import UnstageButton from './unstage_button.vue';
import router from '../../ide_router';
export default {
components: {
icon,
export default {
components: {
Icon,
StageButton,
UnstageButton,
},
props: {
file: {
type: Object,
required: true,
},
props: {
file: {
type: Object,
required: true,
},
actionComponent: {
type: String,
required: true,
},
computed: {
iconName() {
return this.file.tempFile ? 'file-addition' : 'file-modified';
},
iconClass() {
return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
},
},
computed: {
iconName() {
return this.file.tempFile ? 'file-addition' : 'file-modified';
},
methods: {
...mapActions([
'discardFileChanges',
'updateViewer',
]),
openFileInEditor(file) {
this.updateViewer('diff');
iconClass() {
return `multi-file-${
this.file.tempFile ? 'addition' : 'modified'
} append-right-8`;
},
},
methods: {
...mapActions(['updateViewer']),
openFileInEditor(file) {
this.updateViewer('diff');
router.push(`/project${file.url}`);
},
router.push(`/project${file.url}`);
},
};
},
};
</script>
<template>
......@@ -49,12 +55,9 @@
/>{{ file.path }}
</span>
</button>
<button
type="button"
class="btn btn-blank multi-file-discard-btn"
@click="discardFileChanges(file.path)"
>
Discard
</button>
<component
:is="actionComponent"
:file="file"
/>
</div>
</template>
<script>
import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
Icon,
},
props: {
file: {
type: Object,
required: true,
},
},
methods: {
...mapActions(['stageChange', 'discardFileChanges']),
},
};
</script>
<template>
<div
v-once
class="multi-file-discard-btn"
>
<button
type="button"
class="btn btn-blank append-right-5"
:aria-label="__('Stage change')"
@click.stop="stageChange(file)"
>
<icon
name="mobile-issue-close"
:size="12"
/>
</button>
<button
type="button"
class="btn btn-blank"
:aria-label="__('Discard change')"
@click.stop="discardFileChanges(file)"
>
<icon
name="remove"
:size="12"
/>
</button>
</div>
</template>
<script>
import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
Icon,
},
props: {
file: {
type: Object,
required: true,
},
},
methods: {
...mapActions(['unstageChange']),
},
};
</script>
<template>
<div
v-once
class="multi-file-discard-btn"
>
<button
type="button"
class="btn btn-blank"
:aria-label="__('Unstage change')"
@click="unstageChange(file)"
>
<icon
name="history"
:size="12"
/>
</button>
</div>
</template>
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import repoCommitSection from './repo_commit_section.vue';
......@@ -22,13 +21,6 @@ export default {
required: true,
},
},
computed: {
...mapState(['changedFiles', 'rightPanelCollapsed']),
...mapGetters(['currentIcon']),
},
methods: {
...mapActions(['setPanelCollapsedStatus']),
},
};
</script>
......@@ -41,40 +33,6 @@ export default {
<div
class="multi-file-commit-panel-section"
>
<header
class="multi-file-commit-panel-header"
:class="{
'is-collapsed': rightPanelCollapsed,
}"
>
<div
class="multi-file-commit-panel-header-title"
v-if="!rightPanelCollapsed"
>
<div
v-if="changedFiles.length"
>
<icon
name="list-bulleted"
:size="18"
/>
Staged
</div>
</div>
<button
type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
@click.stop="setPanelCollapsedStatus({
side: 'right',
collapsed: !rightPanelCollapsed,
})"
>
<icon
:name="currentIcon"
:size="18"
/>
</button>
</header>
<repo-commit-section
:no-changes-state-svg-path="noChangesStateSvgPath"
:committed-state-svg-path="committedStateSvgPath"
......
......@@ -5,14 +5,16 @@ import icon from '~/vue_shared/components/icon.vue';
import modal from '~/vue_shared/components/modal.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import commitFilesList from './commit_sidebar/list.vue';
import * as consts from '../stores/modules/commit/constants';
import EmptyState from './commit_sidebar/empty_state.vue';
import Actions from './commit_sidebar/actions.vue';
import * as consts from '../stores/modules/commit/constants';
export default {
components: {
modal,
icon,
commitFilesList,
EmptyState,
Actions,
LoadingButton,
},
......@@ -30,45 +32,26 @@ export default {
},
},
computed: {
...mapState([
'currentProjectId',
'currentBranchId',
'rightPanelCollapsed',
'lastCommitMsg',
'changedFiles',
]),
...mapState('commit', [
'commitMessage',
'submitCommitLoading',
]),
...mapState(['stagedFiles', 'rightPanelCollapsed']),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters(['unstagedFiles']),
...mapGetters('commit', [
'commitButtonDisabled',
'discardDraftButtonDisabled',
'branchName',
]),
statusSvg() {
return this.lastCommitMsg ? this.committedStateSvgPath : this.noChangesStateSvgPath;
},
},
methods: {
...mapActions([
'setPanelCollapsedStatus',
]),
...mapActions('commit', [
'updateCommitMessage',
'discardDraft',
'commitChanges',
'updateCommitAction',
]),
toggleCollapsed() {
this.setPanelCollapsedStatus({
side: 'right',
collapsed: !this.rightPanelCollapsed,
});
},
forceCreateNewBranch() {
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH)
.then(() => this.commitChanges());
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() =>
this.commitChanges(),
);
},
},
};
......@@ -77,9 +60,6 @@ export default {
<template>
<div
class="multi-file-commit-panel-section"
:class="{
'multi-file-commit-empty-state-container': !changedFiles.length
}"
>
<modal
id="ide-create-branch-modal"
......@@ -93,15 +73,26 @@ export default {
Would you like to create a new branch?`) }}
</template>
</modal>
<commit-files-list
title="Staged"
:file-list="changedFiles"
:collapsed="rightPanelCollapsed"
@toggleCollapsed="toggleCollapsed"
/>
<template
v-if="changedFiles.length"
v-if="unstagedFiles.length || stagedFiles.length"
>
<commit-files-list
icon="unstaged"
:title="__('Unstaged')"
:file-list="unstagedFiles"
action="stageAllChanges"
:action-btn-text="__('Stage all')"
item-action-component="stage-button"
/>
<commit-files-list
icon="staged"
:title="__('Staged')"
:file-list="stagedFiles"
action="unstageAllChanges"
:action-btn-text="__('Unstage all')"
item-action-component="unstage-button"
:show-toggle="false"
/>
<form
class="form-horizontal multi-file-commit-form"
@submit.prevent.stop="commitChanges"
......@@ -137,38 +128,10 @@ export default {
</div>
</form>
</template>
<div
v-else-if="!rightPanelCollapsed"
class="row js-empty-state"
>
<div class="col-xs-10 col-xs-offset-1">
<div class="svg-content svg-80">
<img :src="statusSvg" />
</div>
</div>
<div class="col-xs-10 col-xs-offset-1">
<div
class="text-content text-center"
v-if="!lastCommitMsg"
>
<h4>
{{ __('No changes') }}
</h4>
<p>
{{ __('Edit files in the editor and commit changes here') }}
</p>
</div>
<div
class="text-content text-center"
v-else
>
<h4>
{{ __('All changes are committed') }}
</h4>
<p v-html="lastCommitMsg">
</p>
</div>
</div>
</div>
<empty-state
v-else
:no-changes-state-svg-path="noChangesStateSvgPath"
:committed-state-svg-path="committedStateSvgPath"
/>
</div>
</template>
......@@ -33,6 +33,13 @@ export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
}
};
export const toggleRightPanelCollapsed = ({ dispatch, state }) => {
dispatch('setPanelCollapsedStatus', {
side: 'right',
collapsed: !state.rightPanelCollapsed,
});
};
export const setResizingStatus = ({ commit }, resizing) => {
commit(types.SET_RESIZING_STATUS, resizing);
};
......@@ -108,6 +115,14 @@ export const scrollToTab = () => {
});
};
export const stageAllChanges = ({ state, commit }) => {
[...state.changedFiles].forEach(file => commit(types.STAGE_CHANGE, file));
};
export const unstageAllChanges = ({ state, commit }) => {
[...state.stagedFiles].forEach(file => commit(types.UNSTAGE_CHANGE, file));
};
export const updateViewer = ({ commit }, viewer) => {
commit(types.UPDATE_VIEWER, viewer);
};
......
......@@ -144,3 +144,11 @@ export const discardFileChanges = ({ state, commit }, path) => {
eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw);
};
export const stageChange = ({ commit }, file) => {
commit(types.STAGE_CHANGE, file);
};
export const unstageChange = ({ commit }, file) => {
commit(types.UNSTAGE_CHANGE, file);
};
......@@ -28,3 +28,5 @@ export const currentIcon = state =>
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
export const hasChanges = state => !!state.changedFiles.length;
export const unstagedFiles = state => state.changedFiles.filter(f => !f.staged);
......@@ -131,9 +131,10 @@ export const updateFilesAfterCommit = (
);
});
commit(rootTypes.REMOVE_ALL_CHANGES_FILES, null, { root: true });
if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH) {
if (
state.commitAction === consts.COMMIT_TO_NEW_BRANCH &&
rootGetters.activeFile
) {
router.push(
`/project/${rootState.currentProjectId}/blob/${branch}/${
rootGetters.activeFile.path
......@@ -186,7 +187,6 @@ export const commitChanges = ({
}
dispatch('setLastCommitMessage', data);
dispatch('updateCommitMessage', '');
if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR) {
dispatch(
......@@ -204,6 +204,10 @@ export const commitChanges = ({
branch: getters.branchName,
});
}
commit(rootTypes.CLEAR_STAGED_CHANGES, null, { root: true });
dispatch('discardDraft');
})
.catch(err => {
let errMsg = __('Error committing changes. Please try again.');
......
......@@ -41,3 +41,7 @@ export const SET_ENTRIES = 'SET_ENTRIES';
export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY';
export const UPDATE_VIEWER = 'UPDATE_VIEWER';
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
export const CLEAR_STAGED_CHANGES = 'CLEAR_STAGED_CHANGES';
export const STAGE_CHANGE = 'STAGE_CHANGE';
export const UNSTAGE_CHANGE = 'UNSTAGE_CHANGE';
......@@ -51,6 +51,11 @@ export default {
lastCommitMsg,
});
},
[types.CLEAR_STAGED_CHANGES](state) {
Object.assign(state, {
stagedFiles: [],
});
},
[types.SET_ENTRIES](state, entries) {
Object.assign(state, {
entries,
......
import * as types from '../mutation_types';
import { findIndexOfFile, findEntry } from '../utils';
export default {
[types.SET_FILE_ACTIVE](state, { path, active }) {
......@@ -75,6 +76,33 @@ export default {
changedFiles: state.changedFiles.filter(f => f.path !== path),
});
},
[types.STAGE_CHANGE](state, file) {
const stagedFile = findEntry(state.stagedFiles, 'blob', file.name);
Object.assign(file, {
staged: true,
});
if (stagedFile) {
Object.assign(stagedFile, {
...file,
});
} else {
state.stagedFiles.push({
...file,
});
}
},
[types.UNSTAGE_CHANGE](state, file) {
const indexOfStagedFile = findIndexOfFile(state.stagedFiles, file);
const changedFile = findEntry(state.changedFiles, 'blob', file.name);
state.stagedFiles.splice(indexOfStagedFile, 1);
Object.assign(changedFile, {
staged: false,
});
},
[types.TOGGLE_FILE_CHANGED](state, { file, changed }) {
Object.assign(state.entries[file.path], {
changed,
......
......@@ -2,6 +2,7 @@ export default () => ({
currentProjectId: '',
currentBranchId: '',
changedFiles: [],
stagedFiles: [],
endpoints: {},
lastCommitMsg: '',
lastCommitPath: '',
......
......@@ -13,6 +13,7 @@ export const dataStructure = () => ({
opened: false,
active: false,
changed: false,
staged: false,
lastCommitPath: '',
lastCommit: {
id: '',
......@@ -38,7 +39,7 @@ export const dataStructure = () => ({
eol: '',
});
export const decorateData = (entity) => {
export const decorateData = entity => {
const {
id,
projectId,
......@@ -57,7 +58,6 @@ export const decorateData = (entity) => {
base64 = false,
file_lock,
} = entity;
return {
......@@ -80,24 +80,23 @@ export const decorateData = (entity) => {
base64,
file_lock,
};
};
export const findEntry = (tree, type, name, prop = 'name') => tree.find(
f => f.type === type && f[prop] === name,
);
export const findEntry = (tree, type, name, prop = 'name') =>
tree.find(f => f.type === type && f[prop] === name);
export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path);
export const findIndexOfFile = (state, file) =>
state.findIndex(f => f.path === file.path);
export const setPageTitle = (title) => {
export const setPageTitle = title => {
document.title = title;
};
export const createCommitPayload = (branch, newBranch, state, rootState) => ({
branch,
commit_message: state.commitMessage,
actions: rootState.changedFiles.map(f => ({
actions: rootState.stagedFiles.map(f => ({
action: f.tempFile ? 'create' : 'update',
file_path: f.path,
content: f.content,
......@@ -120,6 +119,11 @@ const sortTreesByTypeAndName = (a, b) => {
return 0;
};
export const sortTree = sortedTree => sortedTree.map(entity => Object.assign(entity, {
tree: entity.tree.length ? sortTree(entity.tree) : [],
})).sort(sortTreesByTypeAndName);
export const sortTree = sortedTree =>
sortedTree
.map(entity =>
Object.assign(entity, {
tree: entity.tree.length ? sortTree(entity.tree) : [],
}),
)
.sort(sortTreesByTypeAndName);
......@@ -449,9 +449,13 @@
flex: 1;
}
.multi-file-commit-empty-state-container {
align-items: center;
justify-content: center;
.ide-commity-empty-state {
padding: 0 $gl-padding;
}
.ide-commit-empty-state-container {
margin-top: auto;
margin-bottom: auto;
}
.multi-file-commit-panel-header {
......@@ -462,7 +466,8 @@
padding: $gl-btn-padding 0;
&.is-collapsed {
border-bottom: 1px solid $white-dark;
margin-left: -$gl-padding;
margin-right: -$gl-padding;
svg {
margin-left: auto;
......@@ -480,7 +485,6 @@
.multi-file-commit-panel-header-title {
display: flex;
flex: 1;
padding: 0 $gl-btn-padding;
svg {
margin-right: $gl-btn-padding;
......@@ -489,6 +493,7 @@
.multi-file-commit-panel-collapse-btn {
border-left: 1px solid $white-dark;
margin-left: auto;
}
.multi-file-commit-list {
......@@ -502,12 +507,14 @@
display: flex;
padding: 0;
align-items: center;
border-radius: $border-radius-default;
.multi-file-discard-btn {
display: none;
margin-top: -2px;
margin-left: auto;
margin-right: $grid-size;
color: $gl-link-color;
padding: 0 2px;
&:focus,
&:hover {
......@@ -519,7 +526,7 @@
background: $white-normal;
.multi-file-discard-btn {
display: block;
display: flex;
}
}
}
......@@ -535,10 +542,12 @@
.multi-file-commit-list-collapsed {
display: flex;
flex-direction: column;
padding: $gl-padding 0;
> svg {
margin-left: auto;
margin-right: auto;
color: $theme-gray-700;
}
.file-status-icon {
......@@ -550,7 +559,7 @@
.multi-file-commit-list-path {
padding: $grid-size / 2;
padding-left: $gl-padding;
padding-left: $grid-size;
background: none;
border: 0;
text-align: left;
......@@ -740,6 +749,22 @@
}
}
.ide-commit-list-container {
display: flex;
flex-direction: column;
width: 100%;
padding: 0 16px;
&:not(.is-collapsed) {
flex: 1;
}
}
.ide-staged-action-btn {
margin-left: auto;
color: $gl-link-color;
}
.ide-commit-radios {
label {
font-weight: normal;
......
......@@ -44,6 +44,8 @@ feature 'Multi-file editor new directory', :js do
wait_for_requests
click_button 'Stage all'
fill_in('commit-message', with: 'commit message ide')
click_button('Commit')
......
......@@ -34,6 +34,8 @@ feature 'Multi-file editor new file', :js do
wait_for_requests
click_button 'Stage all'
fill_in('commit-message', with: 'commit message ide')
click_button('Commit')
......
import Vue from 'vue';
import store from '~/ide/stores';
import emptyState from '~/ide/components/commit_sidebar/empty_state.vue';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
import { resetStore } from '../../helpers';
describe('IDE commit panel empty state', () => {
let vm;
beforeEach(() => {
const Component = Vue.extend(emptyState);
vm = createComponentWithStore(Component, store, {
noChangesStateSvgPath: 'no-changes',
committedStateSvgPath: 'committed-state',
});
vm.$mount();
});
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
describe('statusSvg', () => {
it('uses noChangesStateSvgPath when commit message is empty', () => {
expect(vm.statusSvg).toBe('no-changes');
expect(vm.$el.querySelector('img').getAttribute('src')).toBe(
'no-changes',
);
});
it('uses committedStateSvgPath when commit message exists', done => {
vm.$store.state.lastCommitMsg = 'testing';
Vue.nextTick(() => {
expect(vm.statusSvg).toBe('committed-state');
expect(vm.$el.querySelector('img').getAttribute('src')).toBe(
'committed-state',
);
done();
});
});
});
it('renders no changes text when last commit message is empty', () => {
expect(vm.$el.textContent).toContain('No changes');
});
it('renders last commit message when it exists', done => {
vm.$store.state.lastCommitMsg = 'testing commit message';
Vue.nextTick(() => {
expect(vm.$el.textContent).toContain('testing commit message');
done();
});
});
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');
});
});
});
......@@ -10,10 +10,16 @@ describe('Multi-file editor commit sidebar list collapsed', () => {
beforeEach(() => {
const Component = Vue.extend(listCollapsed);
vm = createComponentWithStore(Component, store);
vm.$store.state.changedFiles.push(file('file1'), file('file2'));
vm.$store.state.changedFiles[0].tempFile = true;
vm = createComponentWithStore(Component, store, {
files: [
{
...file('file1'),
tempFile: true,
},
file('file2'),
],
icon: 'staged',
});
vm.$mount();
});
......@@ -25,4 +31,40 @@ describe('Multi-file editor commit sidebar list collapsed', () => {
it('renders added & modified files count', () => {
expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toBe('1 1');
});
describe('addedFilesLength', () => {
it('returns an length of temp files', () => {
expect(vm.addedFilesLength).toBe(1);
});
});
describe('modifiedFilesLength', () => {
it('returns an length of modified files', () => {
expect(vm.modifiedFilesLength).toBe(1);
});
});
describe('addedFilesIconClass', () => {
it('includes multi-file-addition when addedFiles is not empty', () => {
expect(vm.addedFilesIconClass).toContain('multi-file-addition');
});
it('excludes multi-file-addition when addedFiles is empty', () => {
vm.files = [];
expect(vm.addedFilesIconClass).not.toContain('multi-file-addition');
});
});
describe('modifiedFilesClass', () => {
it('includes multi-file-modified when addedFiles is not empty', () => {
expect(vm.modifiedFilesClass).toContain('multi-file-modified');
});
it('excludes multi-file-modified when addedFiles is empty', () => {
vm.files = [];
expect(vm.modifiedFilesClass).not.toContain('multi-file-modified');
});
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import listItem from '~/ide/components/commit_sidebar/list_item.vue';
import router from '~/ide/ide_router';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { file } from '../../helpers';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { file, resetStore } from '../../helpers';
describe('Multi-file editor commit sidebar list item', () => {
let vm;
let f;
beforeEach(() => {
beforeEach(done => {
const Component = Vue.extend(listItem);
f = file('test-file');
vm = mountComponent(Component, {
vm = createComponentWithStore(Component, store, {
file: f,
actionComponent: 'stage-button',
});
vm.$mount();
Vue.nextTick(done);
});
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
it('renders file path', () => {
......@@ -28,12 +36,8 @@ describe('Multi-file editor commit sidebar list item', () => {
).toBe(f.path);
});
it('calls discardFileChanges when clicking discard button', () => {
spyOn(vm, 'discardFileChanges');
vm.$el.querySelector('.multi-file-discard-btn').click();
expect(vm.discardFileChanges).toHaveBeenCalled();
it('renders actionn button', () => {
expect(vm.$el.querySelector('.multi-file-discard-btn')).not.toBeNull();
});
it('opens a closed file in the editor when clicking the file path', () => {
......
......@@ -2,7 +2,7 @@ import Vue from 'vue';
import store from '~/ide/stores';
import commitSidebarList from '~/ide/components/commit_sidebar/list.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { file } from '../../helpers';
import { file, resetStore } from '../../helpers';
describe('Multi-file editor commit sidebar list', () => {
let vm;
......@@ -13,6 +13,10 @@ describe('Multi-file editor commit sidebar list', () => {
vm = createComponentWithStore(Component, store, {
title: 'Staged',
fileList: [],
icon: 'staged',
action: 'stageAllChanges',
actionBtnText: 'stage all',
itemActionComponent: 'stage-button',
});
vm.$store.state.rightPanelCollapsed = false;
......@@ -22,6 +26,8 @@ describe('Multi-file editor commit sidebar list', () => {
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
describe('with a list of files', () => {
......@@ -38,6 +44,12 @@ describe('Multi-file editor commit sidebar list', () => {
});
});
describe('empty files array', () => {
it('renders no changes text when empty', () => {
expect(vm.$el.textContent).toContain('No changes');
});
});
describe('collapsed', () => {
beforeEach(done => {
vm.$store.state.rightPanelCollapsed = true;
......@@ -50,4 +62,32 @@ describe('Multi-file editor commit sidebar list', () => {
expect(vm.$el.querySelector('.help-block')).toBeNull();
});
});
describe('with toggle', () => {
beforeEach(done => {
spyOn(vm, 'toggleRightPanelCollapsed');
vm.showToggle = true;
Vue.nextTick(done);
});
it('calls setPanelCollapsedStatus when clickin toggle', () => {
vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click();
expect(vm.toggleRightPanelCollapsed).toHaveBeenCalled();
});
});
describe('action button', () => {
beforeEach(() => {
spyOn(vm, 'stageAllChanges');
});
it('calls store action when clicked', () => {
vm.$el.querySelector('.ide-staged-action-btn').click();
expect(vm.stageAllChanges).toHaveBeenCalled();
});
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import stageButton from '~/ide/components/commit_sidebar/stage_button.vue';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
import { file, resetStore } from '../../helpers';
describe('IDE stage file button', () => {
let vm;
let f;
beforeEach(() => {
const Component = Vue.extend(stageButton);
f = file();
vm = createComponentWithStore(Component, store, {
file: f,
});
spyOn(vm, 'stageChange');
spyOn(vm, 'discardFileChanges');
vm.$mount();
});
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
it('renders button to discard & stage', () => {
expect(vm.$el.querySelectorAll('.btn').length).toBe(2);
});
it('calls store with stage button', () => {
vm.$el.querySelectorAll('.btn')[0].click();
expect(vm.stageChange).toHaveBeenCalledWith(f);
});
it('calls store with discard button', () => {
vm.$el.querySelectorAll('.btn')[1].click();
expect(vm.discardFileChanges).toHaveBeenCalledWith(f);
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import unstageButton from '~/ide/components/commit_sidebar/unstage_button.vue';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
import { file, resetStore } from '../../helpers';
describe('IDE unstage file button', () => {
let vm;
let f;
beforeEach(() => {
const Component = Vue.extend(unstageButton);
f = file();
vm = createComponentWithStore(Component, store, {
file: f,
});
spyOn(vm, 'unstageChange');
vm.$mount();
});
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
it('renders button to unstage', () => {
expect(vm.$el.querySelectorAll('.btn').length).toBe(1);
});
it('calls store with unnstage button', () => {
vm.$el.querySelector('.btn').click();
expect(vm.unstageChange).toHaveBeenCalledWith(f);
});
});
......@@ -28,10 +28,24 @@ describe('RepoCommitSection', () => {
},
};
const files = [file('file1'), file('file2')].map(f =>
Object.assign(f, {
type: 'blob',
}),
);
vm.$store.state.rightPanelCollapsed = false;
vm.$store.state.currentBranch = 'master';
vm.$store.state.changedFiles = [file('file1'), file('file2')];
vm.$store.state.changedFiles = [...files];
vm.$store.state.changedFiles.forEach(f =>
Object.assign(f, {
changed: true,
content: 'changedFile testing',
}),
);
vm.$store.state.stagedFiles = [{ ...files[0] }, { ...files[1] }];
vm.$store.state.stagedFiles.forEach(f =>
Object.assign(f, {
changed: true,
content: 'testing',
......@@ -94,20 +108,93 @@ describe('RepoCommitSection', () => {
...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,
);
expect(vm.$el.querySelector('.multi-file-commit-form')).not.toBeNull();
expect(changedFileElements.length).toEqual(2);
expect(changedFileElements.length).toEqual(4);
changedFileElements.forEach((changedFile, i) => {
expect(changedFile.textContent.trim()).toContain(
vm.$store.state.changedFiles[i].path,
);
expect(changedFile.textContent.trim()).toContain(allFiles[i].path);
});
expect(submitCommit.disabled).toBeTruthy();
expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeNull();
});
it('adds changed files into staged files', done => {
vm.$el.querySelector('.ide-staged-action-btn').click();
Vue.nextTick(() => {
expect(
vm.$el.querySelector('.ide-commit-list-container').textContent,
).toContain('No changes');
done();
});
});
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);
done();
});
});
it('discards a single file', done => {
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');
done();
});
});
it('unstages a single file', done => {
vm.$el
.querySelectorAll('.multi-file-discard-btn')[2]
.querySelector('.btn')
.click();
Vue.nextTick(() => {
expect(
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');
......
......@@ -292,6 +292,84 @@ describe('Multi-file store actions', () => {
});
});
describe('stageAllChanges', () => {
it('adds all files from changedFiles to stagedFiles', done => {
const f = file();
store.state.changedFiles.push(f);
store.state.changedFiles.push(file('new'));
store
.dispatch('stageAllChanges')
.then(() => {
expect(store.state.stagedFiles.length).toBe(2);
expect(store.state.stagedFiles[0]).toEqual(f);
done();
})
.catch(done.fail);
});
it('sets all files from changedFiles as staged after adding to stagedFiles', done => {
store.state.changedFiles.push(file());
store.state.changedFiles.push(file('new'));
store
.dispatch('stageAllChanges')
.then(() => {
expect(store.state.changedFiles.length).toBe(2);
store.state.changedFiles.forEach(f => {
expect(f.staged).toBeTruthy();
});
done();
})
.catch(done.fail);
});
});
describe('unstageAllChanges', () => {
let f;
beforeEach(() => {
f = {
...file(),
type: 'blob',
staged: true,
};
store.state.changedFiles.push({
...f,
});
});
it('sets staged to false in changedFiles when unstaging', done => {
store.state.stagedFiles.push(f);
store
.dispatch('unstageAllChanges')
.then(() => {
expect(store.state.stagedFiles.length).toBe(0);
expect(store.state.changedFiles[0].staged).toBeFalsy();
done();
})
.catch(done.fail);
});
it('removes all files from stagedFiles after unstaging', done => {
store.state.stagedFiles.push(file());
store
.dispatch('unstageAllChanges')
.then(() => {
expect(store.state.stagedFiles.length).toBe(0);
done();
})
.catch(done.fail);
});
});
describe('updateViewer', () => {
it('updates viewer state', done => {
store
......
......@@ -37,19 +37,11 @@ describe('Multi-file store getters', () => {
expect(modifiedFiles.length).toBe(1);
expect(modifiedFiles[0].name).toBe('changed');
});
});
describe('addedFiles', () => {
it('returns a list of added files', () => {
localState.openFiles.push(file());
localState.changedFiles.push(file('added'));
localState.changedFiles[0].changed = true;
localState.changedFiles[0].tempFile = true;
it('returns angle left when collapsed', () => {
localState.rightPanelCollapsed = true;
const modifiedFiles = getters.addedFiles(localState);
expect(modifiedFiles.length).toBe(1);
expect(modifiedFiles[0].name).toBe('added');
expect(getters.collapseButtonIcon(localState)).toBe('angle-double-left');
});
});
});
......@@ -359,12 +359,22 @@ describe('IDE commit module actions', () => {
},
},
};
store.state.changedFiles.push(file('changed'));
store.state.changedFiles[0].active = true;
const f = {
...file('changed'),
type: 'blob',
active: true,
};
store.state.stagedFiles.push(f);
store.state.changedFiles = [
{
...f,
},
];
store.state.openFiles = store.state.changedFiles;
store.state.openFiles.forEach(f => {
store.state.entries[f.path] = f;
store.state.openFiles.forEach(localF => {
store.state.entries[localF.path] = localF;
});
store.state.commit.commitAction = '2';
......@@ -444,7 +454,7 @@ describe('IDE commit module actions', () => {
.catch(done.fail);
});
it('adds commit data to changed files', done => {
it('adds commit data to files', done => {
store
.dispatch('commit/commitChanges')
.then(() => {
......
......@@ -144,6 +144,72 @@ describe('Multi-file store file mutations', () => {
});
});
describe('STAGE_CHANGE', () => {
it('adds file into stagedFiles array', () => {
const f = file();
mutations.STAGE_CHANGE(localState, f);
expect(localState.stagedFiles.length).toBe(1);
expect(localState.stagedFiles[0]).toEqual(f);
});
it('updates changedFiles file to staged', () => {
const f = {
...file(),
type: 'blob',
staged: false,
};
localState.changedFiles.push(f);
mutations.STAGE_CHANGE(localState, f);
expect(localState.changedFiles[0].staged).toBeTruthy();
});
it('updates stagedFile if it is already staged', () => {
const f = file();
f.type = 'blob';
mutations.STAGE_CHANGE(localState, f);
f.raw = 'testing 123';
mutations.STAGE_CHANGE(localState, f);
expect(localState.stagedFiles.length).toBe(1);
expect(localState.stagedFiles[0].raw).toEqual('testing 123');
});
});
describe('UNSTAGE_CHANGE', () => {
let f;
beforeEach(() => {
f = {
...file(),
type: 'blob',
staged: true,
};
localState.stagedFiles.push(f);
localState.changedFiles.push(f);
});
it('removes from stagedFiles array', () => {
mutations.UNSTAGE_CHANGE(localState, f);
expect(localState.stagedFiles.length).toBe(0);
});
it('updates changedFiles array file to unstaged', () => {
mutations.UNSTAGE_CHANGE(localState, f);
expect(localState.changedFiles[0].staged).toBeFalsy();
});
});
describe('TOGGLE_FILE_CHANGED', () => {
it('updates file changed status', () => {
mutations.TOGGLE_FILE_CHANGED(localState, {
......
......@@ -69,6 +69,16 @@ describe('Multi-file store mutations', () => {
});
});
describe('CLEAR_STAGED_CHANGES', () => {
it('clears stagedFiles array', () => {
localState.stagedFiles.push('a');
mutations.CLEAR_STAGED_CHANGES(localState);
expect(localState.stagedFiles.length).toBe(0);
});
});
describe('UPDATE_VIEWER', () => {
it('sets viewer state', () => {
mutations.UPDATE_VIEWER(localState, 'diff');
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment