Commit a4c58e6b authored by Phil Hughes's avatar Phil Hughes

Merge branch...

Merge branch '331107-handle-non-committed-changes-when-switching-branches-in-the-pipeline-editor' into 'master'

Handle non-committed changes when switching branches in PA editor

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