summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue87
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue94
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue53
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue73
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue49
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue38
-rw-r--r--app/assets/javascripts/ide/components/ide_context_bar.vue42
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue101
-rw-r--r--app/assets/javascripts/ide/stores/actions.js15
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js8
-rw-r--r--app/assets/javascripts/ide/stores/getters.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js12
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js4
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js5
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js28
-rw-r--r--app/assets/javascripts/ide/stores/state.js1
-rw-r--r--app/assets/javascripts/ide/stores/utils.js28
-rw-r--r--app/assets/stylesheets/pages/repo.scss41
-rw-r--r--spec/features/projects/tree/create_directory_spec.rb2
-rw-r--r--spec/features/projects/tree/create_file_spec.rb2
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js95
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js50
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/list_item_spec.js24
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/list_spec.js42
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js46
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js39
-rw-r--r--spec/javascripts/ide/components/repo_commit_section_spec.js97
-rw-r--r--spec/javascripts/ide/stores/actions_spec.js78
-rw-r--r--spec/javascripts/ide/stores/getters_spec.js14
-rw-r--r--spec/javascripts/ide/stores/modules/commit/actions_spec.js20
-rw-r--r--spec/javascripts/ide/stores/mutations/file_spec.js66
-rw-r--r--spec/javascripts/ide/stores/mutations_spec.js10
32 files changed, 1035 insertions, 231 deletions
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue b/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue
new file mode 100644
index 00000000000..e69535335d4
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue
@@ -0,0 +1,87 @@
+<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>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
index 453208f3f19..e3280bbb204 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -1,5 +1,5 @@
<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>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
index 15918ac9631..3ab454c2441 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
@@ -1,18 +1,35 @@
<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>
@@ -20,16 +37,22 @@
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>
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 18934af004a..93c8fc00f28 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -1,38 +1,44 @@
<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>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
new file mode 100644
index 00000000000..0189358d82f
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
@@ -0,0 +1,49 @@
+<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>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
new file mode 100644
index 00000000000..fd7ec0366a2
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
@@ -0,0 +1,38 @@
+<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>
diff --git a/app/assets/javascripts/ide/components/ide_context_bar.vue b/app/assets/javascripts/ide/components/ide_context_bar.vue
index 79a83b47994..627fbeb9adf 100644
--- a/app/assets/javascripts/ide/components/ide_context_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_context_bar.vue
@@ -1,5 +1,4 @@
<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"
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index d772cab2d0e..d8d447b80f3 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -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>
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 7e920aa9f30..639195308b2 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -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);
};
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index ddc4b757bf9..61aa0e983fc 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -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);
+};
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index eba325a31df..85f9b75636a 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -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);
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index f536ce6344b..5346bbcdfd9 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -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.');
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index e28f190897c..49eb30302c6 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -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';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index da41fc9285c..5409ec1ec47 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -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,
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index 2500f13db7c..8e739a83270 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -1,4 +1,5 @@
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,
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index 6110f54951c..6c09324e4ba 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -2,6 +2,7 @@ export default () => ({
currentProjectId: '',
currentBranchId: '',
changedFiles: [],
+ stagedFiles: [],
endpoints: {},
lastCommitMsg: '',
lastCommitPath: '',
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 487ea1ead8e..da0069b63a8 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -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);
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 7a8fbfc517d..57393cf65e7 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -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;
diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb
index d96c7e655ba..b242e41df1c 100644
--- a/spec/features/projects/tree/create_directory_spec.rb
+++ b/spec/features/projects/tree/create_directory_spec.rb
@@ -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')
diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb
index a4cbd5cf766..7d65456e049 100644
--- a/spec/features/projects/tree/create_file_spec.rb
+++ b/spec/features/projects/tree/create_file_spec.rb
@@ -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')
diff --git a/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js b/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js
new file mode 100644
index 00000000000..b80d08de7b1
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js
@@ -0,0 +1,95 @@
+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');
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js
index 5b402886b55..65b754b5e5f 100644
--- a/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js
@@ -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');
+ });
+ });
});
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
index 15b66952d99..f1e0bf80819 100644
--- a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
@@ -1,25 +1,33 @@
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', () => {
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_spec.js
index a62c0a28340..189826cec3e 100644
--- a/spec/javascripts/ide/components/commit_sidebar/list_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/list_spec.js
@@ -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();
+ });
+ });
});
diff --git a/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js b/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js
new file mode 100644
index 00000000000..6e34de1d959
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js
@@ -0,0 +1,46 @@
+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);
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js b/spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js
new file mode 100644
index 00000000000..50fa1de2b44
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js
@@ -0,0 +1,39 @@
+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);
+ });
+});
diff --git a/spec/javascripts/ide/components/repo_commit_section_spec.js b/spec/javascripts/ide/components/repo_commit_section_spec.js
index 113ade269e9..c6c565bb0f3 100644
--- a/spec/javascripts/ide/components/repo_commit_section_spec.js
+++ b/spec/javascripts/ide/components/repo_commit_section_spec.js
@@ -28,12 +28,26 @@ 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');
diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js
index cec572f4507..0cbf9f3735f 100644
--- a/spec/javascripts/ide/stores/actions_spec.js
+++ b/spec/javascripts/ide/stores/actions_spec.js
@@ -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
diff --git a/spec/javascripts/ide/stores/getters_spec.js b/spec/javascripts/ide/stores/getters_spec.js
index a613f3a21cc..0c6fca82b1f 100644
--- a/spec/javascripts/ide/stores/getters_spec.js
+++ b/spec/javascripts/ide/stores/getters_spec.js
@@ -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');
});
});
});
diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
index 90ded940227..780e46fe686 100644
--- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js
+++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
@@ -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(() => {
diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js
index 131380248e8..f769bedf50d 100644
--- a/spec/javascripts/ide/stores/mutations/file_spec.js
+++ b/spec/javascripts/ide/stores/mutations/file_spec.js
@@ -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, {
diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js
index 38162a470ad..26e7ed4535e 100644
--- a/spec/javascripts/ide/stores/mutations_spec.js
+++ b/spec/javascripts/ide/stores/mutations_spec.js
@@ -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');