Commit 36b74174 authored by Frédéric Caplette's avatar Frédéric Caplette Committed by Phil Hughes

Handle non-committed changes when switching branches in PA editor

parent 4bab02a1
......@@ -43,14 +43,25 @@ export default {
},
inject: ['projectFullPath', 'totalBranches'],
props: {
hasUnsavedChanges: {
type: Boolean,
required: false,
default: false,
},
paginationLimit: {
type: Number,
required: false,
default: BRANCH_PAGINATION_LIMIT,
},
shouldLoadNewBranch: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
branchSelected: null,
availableBranches: [],
filteredBranches: [],
isSearchingBranches: false,
......@@ -105,6 +116,13 @@ export default {
return this.branches.length > 0 || this.searchTerm.length > 0;
},
},
watch: {
shouldLoadNewBranch(flag) {
if (flag) {
this.changeBranch(this.branchSelected);
}
},
},
methods: {
availableBranchesQueryVars(varsOverride = {}) {
if (this.searchTerm.length > 0) {
......@@ -149,11 +167,7 @@ export default {
})
.catch(this.showFetchError);
},
async selectBranch(newBranch) {
if (newBranch === this.currentBranch) {
return;
}
async changeBranch(newBranch) {
this.updateCurrentBranch(newBranch);
const updatedPath = setUrlParams({ branch_name: newBranch });
historyPushState(updatedPath);
......@@ -164,6 +178,19 @@ export default {
await this.$nextTick();
this.$emit('refetchContent');
},
selectBranch(newBranch) {
if (newBranch !== this.currentBranch) {
// If there are unsaved changes, we want to show the user
// a modal to confirm what to do with these before changing
// branches.
if (this.hasUnsavedChanges) {
this.branchSelected = newBranch;
this.$emit('select-branch', newBranch);
} else {
this.changeBranch(newBranch);
}
}
},
async setSearchTerm(newSearchTerm) {
this.pageCounter = 0;
this.searchTerm = newSearchTerm.trim();
......
......@@ -5,10 +5,26 @@ export default {
components: {
BranchSwitcher,
},
props: {
hasUnsavedChanges: {
type: Boolean,
required: false,
default: false,
},
shouldLoadNewBranch: {
type: Boolean,
required: false,
default: false,
},
},
};
</script>
<template>
<div class="gl-mb-4">
<branch-switcher v-on="$listeners" />
<branch-switcher
:has-unsaved-changes="hasUnsavedChanges"
:should-load-new-branch="shouldLoadNewBranch"
v-on="$listeners"
/>
</div>
</template>
......@@ -325,8 +325,9 @@ export default {
<pipeline-editor-home
:ci-config-data="ciConfigData"
:ci-file-content="currentCiFileContent"
:is-new-ci-config-file="isNewCiConfigFile"
:commit-sha="commitSha"
:has-unsaved-changes="hasUnsavedChanges"
:is-new-ci-config-file="isNewCiConfigFile"
@commit="updateOnCommit"
@resetContent="resetContent"
@showError="showErrorAlert"
......
<script>
import { GlModal } from '@gitlab/ui';
import { __ } from '~/locale';
import CommitSection from './components/commit/commit_section.vue';
import PipelineEditorDrawer from './components/drawer/pipeline_editor_drawer.vue';
import PipelineEditorFileNav from './components/file_nav/pipeline_editor_file_nav.vue';
......@@ -7,8 +9,23 @@ import PipelineEditorTabs from './components/pipeline_editor_tabs.vue';
import { CREATE_TAB } from './constants';
export default {
commitSectionRef: 'commitSectionRef',
modal: {
switchBranch: {
title: __('You have unsaved changes'),
body: __('Uncommitted changes will be lost if you change branches. Do you want to continue?'),
actionPrimary: {
text: __('Switch Branches'),
},
actionSecondary: {
text: __('Cancel'),
attributes: { variant: 'default' },
},
},
},
components: {
CommitSection,
GlModal,
PipelineEditorDrawer,
PipelineEditorFileNav,
PipelineEditorHeader,
......@@ -28,6 +45,11 @@ export default {
required: false,
default: '',
},
hasUnsavedChanges: {
type: Boolean,
required: false,
default: false,
},
isNewCiConfigFile: {
type: Boolean,
required: true,
......@@ -36,6 +58,8 @@ export default {
data() {
return {
currentTab: CREATE_TAB,
shouldLoadNewBranch: false,
showSwitchBranchModal: false,
};
},
computed: {
......@@ -44,6 +68,16 @@ export default {
},
},
methods: {
closeBranchModal() {
this.showSwitchBranchModal = false;
},
handleConfirmSwitchBranch() {
this.showSwitchBranchModal = true;
},
switchBranch() {
this.showSwitchBranchModal = false;
this.shouldLoadNewBranch = true;
},
setCurrentTab(tabName) {
this.currentTab = tabName;
},
......@@ -53,7 +87,26 @@ export default {
<template>
<div class="gl-pr-9 gl-transition-medium gl-w-full">
<pipeline-editor-file-nav v-on="$listeners" />
<gl-modal
v-if="showSwitchBranchModal"
visible
modal-id="switchBranchModal"
:title="$options.modal.switchBranch.title"
:action-primary="$options.modal.switchBranch.actionPrimary"
:action-secondary="$options.modal.switchBranch.actionSecondary"
@primary="switchBranch"
@secondary="closeBranchModal"
@cancel="closeBranchModal"
@hide="closeBranchModal"
>
{{ $options.modal.switchBranch.body }}
</gl-modal>
<pipeline-editor-file-nav
:has-unsaved-changes="hasUnsavedChanges"
:should-load-new-branch="shouldLoadNewBranch"
@select-branch="handleConfirmSwitchBranch"
v-on="$listeners"
/>
<pipeline-editor-header
:ci-config-data="ciConfigData"
:commit-sha="commitSha"
......@@ -68,6 +121,7 @@ export default {
/>
<commit-section
v-if="showCommitForm"
:ref="$options.commitSectionRef"
:ci-file-content="ciFileContent"
:commit-sha="commitSha"
v-on="$listeners"
......
......@@ -33154,6 +33154,9 @@ msgstr ""
msgid "Survey Response"
msgstr ""
msgid "Switch Branches"
msgstr ""
msgid "Switch branch"
msgstr ""
......@@ -36376,6 +36379,9 @@ msgstr ""
msgid "Unauthenticated web rate limit period in seconds"
msgstr ""
msgid "Uncommitted changes will be lost if you change branches. Do you want to continue?"
msgstr ""
msgid "Undo"
msgstr ""
......@@ -39165,6 +39171,9 @@ msgstr ""
msgid "You have successfully purchased a %{plan} plan subscription for %{seats}. You’ll receive a receipt via email."
msgstr ""
msgid "You have unsaved changes"
msgstr ""
msgid "You left the \"%{membershipable_human_name}\" %{source_type}."
msgstr ""
......
......@@ -36,8 +36,9 @@ describe('Pipeline editor branch switcher', () => {
let mockLastCommitBranchQuery;
const createComponent = (
{ currentBranch, isQueryLoading, mountFn, options } = {
{ currentBranch, isQueryLoading, mountFn, options, props } = {
currentBranch: mockDefaultBranch,
hasUnsavedChanges: false,
isQueryLoading: false,
mountFn: shallowMount,
options: {},
......@@ -45,6 +46,7 @@ describe('Pipeline editor branch switcher', () => {
) => {
wrapper = mountFn(BranchSwitcher, {
propsData: {
...props,
paginationLimit: mockBranchPaginationLimit,
},
provide: {
......@@ -70,7 +72,7 @@ describe('Pipeline editor branch switcher', () => {
});
};
const createComponentWithApollo = (mountFn = shallowMount) => {
const createComponentWithApollo = ({ mountFn = shallowMount, props = {} } = {}) => {
const handlers = [[getAvailableBranchesQuery, mockAvailableBranchQuery]];
const resolvers = {
Query: {
......@@ -86,6 +88,7 @@ describe('Pipeline editor branch switcher', () => {
createComponent({
mountFn,
props,
options: {
localVue,
apolloProvider: mockApollo,
......@@ -149,7 +152,7 @@ describe('Pipeline editor branch switcher', () => {
availableBranches: mockProjectBranches,
currentBranch: mockDefaultBranch,
});
createComponentWithApollo(mount);
createComponentWithApollo({ mountFn: mount });
await waitForPromises();
});
......@@ -201,7 +204,7 @@ describe('Pipeline editor branch switcher', () => {
availableBranches: mockProjectBranches,
currentBranch: mockDefaultBranch,
});
createComponentWithApollo(mount);
createComponentWithApollo({ mountFn: mount });
await waitForPromises();
});
......@@ -247,6 +250,23 @@ describe('Pipeline editor branch switcher', () => {
expect(wrapper.emitted('refetchContent')).toBeUndefined();
});
describe('with unsaved changes', () => {
beforeEach(async () => {
createComponentWithApollo({ mountFn: mount, props: { hasUnsavedChanges: true } });
await waitForPromises();
});
it('emits `select-branch` event and does not switch branch', async () => {
expect(wrapper.emitted('select-branch')).toBeUndefined();
const branch = findDropdownItems().at(1);
await branch.vm.$emit('click');
expect(wrapper.emitted('select-branch')).toEqual([[branch.text()]]);
expect(wrapper.emitted('refetchContent')).toBeUndefined();
});
});
});
describe('when searching', () => {
......@@ -255,7 +275,7 @@ describe('Pipeline editor branch switcher', () => {
availableBranches: mockProjectBranches,
currentBranch: mockDefaultBranch,
});
createComponentWithApollo(mount);
createComponentWithApollo({ mountFn: mount });
await waitForPromises();
});
......@@ -429,7 +449,7 @@ describe('Pipeline editor branch switcher', () => {
availableBranches: mockProjectBranches,
currentBranch: mockDefaultBranch,
});
createComponentWithApollo(mount);
createComponentWithApollo({ mountFn: mount });
await waitForPromises();
await createNewBranch();
});
......
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { GlModal } from '@gitlab/ui';
import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue';
import PipelineEditorDrawer from '~/pipeline_editor/components/drawer/pipeline_editor_drawer.vue';
import PipelineEditorFileNav from '~/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
import BranchSwitcher from '~/pipeline_editor/components/file_nav/branch_switcher.vue';
import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_editor_header.vue';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
import { MERGED_TAB, VISUALIZE_TAB, CREATE_TAB, LINT_TAB } from '~/pipeline_editor/constants';
......@@ -11,11 +12,14 @@ import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue';
import { mockLintResponse, mockCiYml } from './mock_data';
jest.mock('~/lib/utils/common_utils');
describe('Pipeline editor home wrapper', () => {
let wrapper;
const createComponent = ({ props = {}, glFeatures = {} } = {}) => {
const createComponent = ({ props = {}, glFeatures = {}, data = {}, stubs = {} } = {}) => {
wrapper = shallowMount(PipelineEditorHome, {
data: () => data,
propsData: {
ciConfigData: mockLintResponse,
ciFileContent: mockCiYml,
......@@ -24,15 +28,20 @@ describe('Pipeline editor home wrapper', () => {
...props,
},
provide: {
projectFullPath: '',
totalBranches: 19,
glFeatures: {
...glFeatures,
},
},
stubs,
});
};
const findBranchSwitcher = () => wrapper.findComponent(BranchSwitcher);
const findCommitSection = () => wrapper.findComponent(CommitSection);
const findFileNav = () => wrapper.findComponent(PipelineEditorFileNav);
const findModal = () => wrapper.findComponent(GlModal);
const findPipelineEditorDrawer = () => wrapper.findComponent(PipelineEditorDrawer);
const findPipelineEditorHeader = () => wrapper.findComponent(PipelineEditorHeader);
const findPipelineEditorTabs = () => wrapper.findComponent(PipelineEditorTabs);
......@@ -67,6 +76,46 @@ describe('Pipeline editor home wrapper', () => {
});
});
describe('modal when switching branch', () => {
describe('when `showSwitchBranchModal` value is false', () => {
beforeEach(() => {
createComponent();
});
it('is not visible', () => {
expect(findModal().exists()).toBe(false);
});
});
describe('when `showSwitchBranchModal` value is true', () => {
beforeEach(() => {
createComponent({
data: { showSwitchBranchModal: true },
stubs: { PipelineEditorFileNav },
});
});
it('is visible', () => {
expect(findModal().exists()).toBe(true);
});
it('pass down `shouldLoadNewBranch` to the branch switcher when primary is selected', async () => {
expect(findBranchSwitcher().props('shouldLoadNewBranch')).toBe(false);
await findModal().vm.$emit('primary');
expect(findBranchSwitcher().props('shouldLoadNewBranch')).toBe(true);
});
it('closes the modal when secondary action is selected', async () => {
expect(findModal().exists()).toBe(true);
await findModal().vm.$emit('secondary');
expect(findModal().exists()).toBe(false);
});
});
});
describe('commit form toggle', () => {
beforeEach(() => {
createComponent();
......
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