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