Commit 8a07ce2c authored by Phil Hughes's avatar Phil Hughes

Merge branch '225345-re-add-swap-revisions' into 'master'

Add ability to swap revisions

See merge request gitlab-org/gitlab!60491
parents d232ebae 75c036f4
<script>
import { GlButton } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
import { joinPaths } from '~/lib/utils/url_utility';
import RevisionCard from './revision_card.vue';
export default {
......@@ -36,11 +37,46 @@ export default {
type: String,
required: true,
},
defaultProject: {
type: Object,
required: true,
},
projects: {
type: Array,
required: true,
},
},
data() {
return {
from: {
projects: this.projects,
selectedProject: this.defaultProject,
revision: this.paramsFrom,
refsProjectPath: this.refsProjectPath,
},
to: {
selectedProject: this.defaultProject,
revision: this.paramsTo,
refsProjectPath: this.refsProjectPath,
},
};
},
methods: {
onSubmit() {
this.$refs.form.submit();
},
onSelectProject({ direction, project }) {
const refsPath = joinPaths(gon.relative_url_root || '', `/${project.name}`, '/refs');
// direction is either 'from' or 'to'
this[direction].refsProjectPath = refsPath;
this[direction].selectedProject = project;
},
onSelectRevision({ direction, revision }) {
this[direction].revision = revision; // direction is either 'from' or 'to'
},
onSwapRevision() {
[this.from, this.to] = [this.to, this.from]; // swaps 'from' and 'to'
},
},
};
</script>
......@@ -57,10 +93,15 @@ export default {
class="gl-lg-flex-direction-row gl-lg-display-flex gl-align-items-center compare-revision-cards"
>
<revision-card
:refs-project-path="refsProjectPath"
data-testid="sourceRevisionCard"
:refs-project-path="to.refsProjectPath"
revision-text="Source"
params-name="to"
:params-branch="paramsTo"
:params-branch="to.revision"
:projects="to.projects"
:selected-project="to.selectedProject"
@selectProject="onSelectProject"
@selectRevision="onSelectRevision"
/>
<div
class="compare-ellipsis gl-display-flex gl-justify-content-center gl-align-items-center gl-my-4 gl-md-my-0"
......@@ -69,16 +110,24 @@ export default {
...
</div>
<revision-card
:refs-project-path="refsProjectPath"
data-testid="targetRevisionCard"
:refs-project-path="from.refsProjectPath"
revision-text="Target"
params-name="from"
:params-branch="paramsFrom"
:params-branch="from.revision"
:projects="from.projects"
:selected-project="from.selectedProject"
@selectProject="onSelectProject"
@selectRevision="onSelectRevision"
/>
</div>
<div class="gl-mt-4">
<gl-button category="primary" variant="success" @click="onSubmit">
{{ s__('CompareRevisions|Compare') }}
</gl-button>
<gl-button data-testid="swapRevisionsButton" class="btn btn-default" @click="onSwapRevision">
{{ s__('CompareRevisions|Swap revisions') }}
</gl-button>
<gl-button
v-if="projectMergeRequestPath"
:href="projectMergeRequestPath"
......
<script>
import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
const SOURCE_PARAM_NAME = 'to';
export default {
components: {
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
},
inject: ['projectTo', 'projectsFrom'],
props: {
paramsName: {
type: String,
required: true,
},
projects: {
type: Array,
required: false,
default: null,
},
selectedProject: {
type: Object,
required: true,
},
},
data() {
return {
searchTerm: '',
selectedRepo: {},
};
},
computed: {
disableRepoDropdown() {
return this.projects === null;
},
filteredRepos() {
const lowerCaseSearchTerm = this.searchTerm.toLowerCase();
return this?.projectsFrom.filter(({ name }) =>
name.toLowerCase().includes(lowerCaseSearchTerm),
);
},
isSourceRevision() {
return this.paramsName === SOURCE_PARAM_NAME;
return this?.projects.filter(({ name }) => name.toLowerCase().includes(lowerCaseSearchTerm));
},
inputName() {
return `${this.paramsName}_project_id`;
},
},
mounted() {
this.setDefaultRepo();
},
methods: {
onClick(repo) {
this.selectedRepo = repo;
this.emitTargetProject(repo.name);
},
setDefaultRepo() {
this.selectedRepo = this.projectTo;
onClick(project) {
this.emitTargetProject(project);
},
emitTargetProject(name) {
if (!this.isSourceRevision) {
this.$emit('changeTargetProject', name);
}
emitTargetProject(project) {
this.$emit('selectProject', { direction: this.paramsName, project });
},
},
};
......@@ -59,23 +53,23 @@ export default {
<template>
<div>
<input type="hidden" :name="inputName" :value="selectedRepo.id" />
<input type="hidden" :name="inputName" :value="selectedProject.id" />
<gl-dropdown
:text="selectedRepo.name"
:text="selectedProject.name"
:header-text="s__(`CompareRevisions|Select target project`)"
class="gl-w-full gl-font-monospace gl-sm-pr-3"
toggle-class="gl-min-w-0"
:disabled="isSourceRevision"
:disabled="disableRepoDropdown"
>
<template #header>
<gl-search-box-by-type v-if="!isSourceRevision" v-model.trim="searchTerm" />
<gl-search-box-by-type v-if="!disableRepoDropdown" v-model.trim="searchTerm" />
</template>
<template v-if="!isSourceRevision">
<template v-if="!disableRepoDropdown">
<gl-dropdown-item
v-for="repo in filteredRepos"
:key="repo.id"
is-check-item
:is-checked="selectedRepo.id === repo.id"
:is-checked="selectedProject.id === repo.id"
@click="onClick(repo)"
>
{{ repo.name }}
......
......@@ -27,17 +27,14 @@ export default {
required: false,
default: null,
},
},
data() {
return {
selectedRefsProjectPath: this.refsProjectPath,
};
},
methods: {
onChangeTargetProject(targetProjectName) {
if (this.paramsName === 'from') {
this.selectedRefsProjectPath = `/${targetProjectName}/refs`;
}
projects: {
type: Array,
required: false,
default: null,
},
selectedProject: {
type: Object,
required: true,
},
},
};
......@@ -52,13 +49,16 @@ export default {
<repo-dropdown
class="gl-sm-w-half"
:params-name="paramsName"
@changeTargetProject="onChangeTargetProject"
:projects="projects"
:selected-project="selectedProject"
v-on="$listeners"
/>
<revision-dropdown
class="gl-sm-w-half gl-mt-3 gl-sm-mt-0"
:refs-project-path="selectedRefsProjectPath"
:refs-project-path="refsProjectPath"
:params-name="paramsName"
:params-branch="paramsBranch"
v-on="$listeners"
/>
</div>
</gl-card>
......
......@@ -56,6 +56,9 @@ export default {
searchTerm: debounce(function debounceSearch() {
this.searchBranchesAndTags();
}, SEARCH_DEBOUNCE_MS),
paramsBranch(newBranch) {
this.setSelectedRevision(newBranch);
},
},
mounted() {
this.fetchBranchesAndTags();
......@@ -84,7 +87,7 @@ export default {
this.loading = true;
if (reset) {
this.selectedRevision = this.getDefaultBranch();
this.setSelectedRevision(this.paramsBranch);
}
return axios
......@@ -108,10 +111,14 @@ export default {
return this.paramsBranch || EMPTY_DROPDOWN_TEXT;
},
onClick(revision) {
this.selectedRevision = revision;
this.setSelectedRevision(revision);
this.$emit('selectRevision', { direction: this.paramsName, revision });
},
onSearchEnter() {
this.selectedRevision = this.searchTerm;
this.setSelectedRevision(this.searchTerm);
},
setSelectedRevision(revision) {
this.selectedRevision = revision || EMPTY_DROPDOWN_TEXT;
},
},
};
......
......@@ -22,10 +22,6 @@ export default function init() {
components: {
CompareApp,
},
provide: {
projectTo: JSON.parse(projectTo),
projectsFrom: JSON.parse(projectsFrom),
},
render(createElement) {
return createElement(CompareApp, {
props: {
......@@ -35,6 +31,8 @@ export default function init() {
projectCompareIndexPath,
projectMergeRequestPath,
createMrPath,
defaultProject: JSON.parse(projectTo),
projects: JSON.parse(projectsFrom),
},
});
},
......
---
title: Add ability to swap revisions when comparing
merge_request: 60491
author:
type: added
......@@ -2,26 +2,19 @@ import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import CompareApp from '~/projects/compare/components/app.vue';
import RevisionCard from '~/projects/compare/components/revision_card.vue';
import { appDefaultProps as defaultProps } from './mock_data';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
const projectCompareIndexPath = 'some/path';
const refsProjectPath = 'some/refs/path';
const paramsFrom = 'master';
const paramsTo = 'master';
describe('CompareApp component', () => {
let wrapper;
const findSourceRevisionCard = () => wrapper.find('[data-testid="sourceRevisionCard"]');
const findTargetRevisionCard = () => wrapper.find('[data-testid="targetRevisionCard"]');
const createComponent = (props = {}) => {
wrapper = shallowMount(CompareApp, {
propsData: {
projectCompareIndexPath,
refsProjectPath,
paramsFrom,
paramsTo,
projectMergeRequestPath: '',
createMrPath: '',
...defaultProps,
...props,
},
});
......@@ -39,16 +32,16 @@ describe('CompareApp component', () => {
it('renders component with prop', () => {
expect(wrapper.props()).toEqual(
expect.objectContaining({
projectCompareIndexPath,
refsProjectPath,
paramsFrom,
paramsTo,
projectCompareIndexPath: defaultProps.projectCompareIndexPath,
refsProjectPath: defaultProps.refsProjectPath,
paramsFrom: defaultProps.paramsFrom,
paramsTo: defaultProps.paramsTo,
}),
);
});
it('contains the correct form attributes', () => {
expect(wrapper.attributes('action')).toBe(projectCompareIndexPath);
expect(wrapper.attributes('action')).toBe(defaultProps.projectCompareIndexPath);
expect(wrapper.attributes('method')).toBe('POST');
});
......@@ -87,6 +80,58 @@ describe('CompareApp component', () => {
});
});
it('sets the selected project when the "selectProject" event is emitted', async () => {
const project = {
name: 'some-to-name',
id: '1',
};
findTargetRevisionCard().vm.$emit('selectProject', {
direction: 'to',
project,
});
await wrapper.vm.$nextTick();
expect(findTargetRevisionCard().props('selectedProject')).toEqual(
expect.objectContaining(project),
);
});
it('sets the selected revision when the "selectRevision" event is emitted', async () => {
const revision = 'some-revision';
findTargetRevisionCard().vm.$emit('selectRevision', {
direction: 'to',
revision,
});
await wrapper.vm.$nextTick();
expect(findSourceRevisionCard().props('paramsBranch')).toBe(revision);
});
describe('swap revisions button', () => {
const findSwapRevisionsButton = () => wrapper.find('[data-testid="swapRevisionsButton"]');
it('renders the swap revisions button', () => {
expect(findSwapRevisionsButton().exists()).toBe(true);
});
it('has the correct text', () => {
expect(findSwapRevisionsButton().text()).toBe('Swap revisions');
});
it('swaps revisions when clicked', async () => {
findSwapRevisionsButton().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(findTargetRevisionCard().props('paramsBranch')).toBe(defaultProps.paramsTo);
expect(findSourceRevisionCard().props('paramsBranch')).toBe(defaultProps.paramsFrom);
});
});
describe('merge request buttons', () => {
const findProjectMrButton = () => wrapper.find('[data-testid="projectMrButton"]');
const findCreateMrButton = () => wrapper.find('[data-testid="createMrButton"]');
......
const refsProjectPath = 'some/refs/path';
const paramsName = 'to';
const paramsBranch = 'main';
const defaultProject = {
name: 'some-to-name',
id: '1',
};
export const appDefaultProps = {
projectCompareIndexPath: 'some/path',
projectMergeRequestPath: '',
projects: [defaultProject],
paramsFrom: 'main',
paramsTo: 'target/branch',
createMrPath: '',
refsProjectPath,
defaultProject,
};
export const revisionCardDefaultProps = {
selectedProject: defaultProject,
paramsBranch,
revisionText: 'Source',
refsProjectPath,
paramsName,
};
export const repoDropdownDefaultProps = {
selectedProject: defaultProject,
paramsName,
};
export const revisionDropdownDefaultProps = {
refsProjectPath,
paramsBranch,
paramsName,
};
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import RepoDropdown from '~/projects/compare/components/repo_dropdown.vue';
const defaultProps = {
paramsName: 'to',
};
const projectToId = '1';
const projectToName = 'some-to-name';
const projectFromId = '2';
const projectFromName = 'some-from-name';
const defaultProvide = {
projectTo: { id: projectToId, name: projectToName },
projectsFrom: [
{ id: projectFromId, name: projectFromName },
{ id: 3, name: 'some-from-another-name' },
],
};
import { revisionCardDefaultProps as defaultProps } from './mock_data';
describe('RepoDropdown component', () => {
let wrapper;
const createComponent = (props = {}, provide = {}) => {
const createComponent = (props = {}) => {
wrapper = shallowMount(RepoDropdown, {
propsData: {
...defaultProps,
...props,
},
provide: {
...defaultProvide,
...provide,
},
});
};
......@@ -49,11 +29,11 @@ describe('RepoDropdown component', () => {
});
it('set hidden input', () => {
expect(findHiddenInput().attributes('value')).toBe(projectToId);
expect(findHiddenInput().attributes('value')).toBe(defaultProps.selectedProject.id);
});
it('displays the project name in the disabled dropdown', () => {
expect(findGlDropdown().props('text')).toBe(projectToName);
expect(findGlDropdown().props('text')).toBe(defaultProps.selectedProject.name);
expect(findGlDropdown().props('disabled')).toBe(true);
});
......@@ -66,31 +46,39 @@ describe('RepoDropdown component', () => {
describe('Target Revision', () => {
beforeEach(() => {
createComponent({ paramsName: 'from' });
const projects = [
{
name: 'some-to-name',
id: '1',
},
];
createComponent({ paramsName: 'from', projects });
});
it('set hidden input of the selected project', () => {
expect(findHiddenInput().attributes('value')).toBe(projectToId);
expect(findHiddenInput().attributes('value')).toBe(defaultProps.selectedProject.id);
});
it('displays matching project name of the source revision initially in the dropdown', () => {
expect(findGlDropdown().props('text')).toBe(projectToName);
expect(findGlDropdown().props('text')).toBe(defaultProps.selectedProject.name);
});
it('updates the hiddin input value when onClick method is triggered', async () => {
const repoId = '100';
it('updates the hidden input value when onClick method is triggered', async () => {
const repoId = '1';
wrapper.vm.onClick({ id: repoId });
await wrapper.vm.$nextTick();
expect(findHiddenInput().attributes('value')).toBe(repoId);
});
it('emits `changeTargetProject` event when another target project is selected', async () => {
const index = 1;
const { projectsFrom } = defaultProvide;
findGlDropdown().findAll(GlDropdownItem).at(index).vm.$emit('click');
it('emits `selectProject` event when another target project is selected', async () => {
findGlDropdown().findAll(GlDropdownItem).at(0).vm.$emit('click');
await wrapper.vm.$nextTick();
expect(wrapper.emitted('changeTargetProject')[0][0]).toEqual(projectsFrom[index].name);
expect(wrapper.emitted('selectProject')[0][0]).toEqual({
direction: 'from',
project: { id: '1', name: 'some-to-name' },
});
});
});
});
......@@ -3,13 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import RepoDropdown from '~/projects/compare/components/repo_dropdown.vue';
import RevisionCard from '~/projects/compare/components/revision_card.vue';
import RevisionDropdown from '~/projects/compare/components/revision_dropdown.vue';
const defaultProps = {
refsProjectPath: 'some/refs/path',
revisionText: 'Source',
paramsName: 'to',
paramsBranch: 'master',
};
import { revisionCardDefaultProps as defaultProps } from './mock_data';
describe('RepoDropdown component', () => {
let wrapper;
......
import { GlDropdown, GlSearchBoxByType } from '@gitlab/ui';
import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import RevisionDropdown from '~/projects/compare/components/revision_dropdown.vue';
const defaultProps = {
refsProjectPath: 'some/refs/path',
paramsName: 'from',
paramsBranch: 'master',
};
import { revisionDropdownDefaultProps as defaultProps } from './mock_data';
jest.mock('~/flash');
......@@ -142,4 +137,17 @@ describe('RevisionDropdown component', () => {
expect(findGlDropdown().props('text')).toBe(defaultProps.paramsBranch);
});
});
it('emits `selectRevision` event when another revision is selected', async () => {
createComponent();
wrapper.vm.branches = ['some-branch'];
await wrapper.vm.$nextTick();
findGlDropdown().findAll(GlDropdownItem).at(0).vm.$emit('click');
expect(wrapper.emitted('selectRevision')[0][0]).toEqual({
direction: 'to',
revision: 'some-branch',
});
});
});
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