Commit e82626cf authored by Phil Hughes's avatar Phil Hughes

Merge branch '14615-compare-convert-to-vue' into 'master'

Convert Repo Compare to Vue

See merge request gitlab-org/gitlab!52567
parents 49bbf5d0 c61d8d28
import initCompareSelector from '~/projects/compare';
initCompareSelector();
<script>
import { GlButton } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
import RevisionDropdown from './revision_dropdown.vue';
export default {
csrf,
components: {
RevisionDropdown,
GlButton,
},
props: {
projectCompareIndexPath: {
type: String,
required: true,
},
refsProjectPath: {
type: String,
required: true,
},
paramsFrom: {
type: String,
required: false,
default: null,
},
paramsTo: {
type: String,
required: false,
default: null,
},
projectMergeRequestPath: {
type: String,
required: true,
},
createMrPath: {
type: String,
required: true,
},
},
methods: {
onSubmit() {
this.$refs.form.submit();
},
},
};
</script>
<template>
<form
ref="form"
class="form-inline js-requires-input js-signature-container"
method="POST"
:action="projectCompareIndexPath"
>
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
<revision-dropdown
:refs-project-path="refsProjectPath"
revision-text="Source"
params-name="to"
:params-branch="paramsTo"
/>
<div class="compare-ellipsis gl-display-inline" data-testid="ellipsis">...</div>
<revision-dropdown
:refs-project-path="refsProjectPath"
revision-text="Target"
params-name="from"
:params-branch="paramsFrom"
/>
<gl-button category="primary" variant="success" class="gl-ml-3" @click="onSubmit">
{{ s__('CompareRevisions|Compare') }}
</gl-button>
<a
v-if="projectMergeRequestPath"
:href="projectMergeRequestPath"
data-testid="projectMrButton"
class="btn btn-default gl-button gl-ml-3"
>
{{ s__('CompareRevisions|View open merge request') }}
</a>
<a
v-else-if="createMrPath"
:href="createMrPath"
data-testid="createMrButton"
class="btn btn-default gl-button gl-ml-3"
>
{{ s__('CompareRevisions|Create merge request') }}
</a>
</form>
</template>
<script>
import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlDropdownSectionHeader } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import { s__ } from '~/locale';
export default {
components: {
GlDropdown,
GlDropdownItem,
GlDropdownSectionHeader,
GlSearchBoxByType,
},
props: {
refsProjectPath: {
type: String,
required: true,
},
revisionText: {
type: String,
required: true,
},
paramsName: {
type: String,
required: true,
},
paramsBranch: {
type: String,
required: false,
default: null,
},
},
data() {
return {
branches: [],
tags: [],
loading: true,
searchTerm: '',
selectedRevision: this.getDefaultBranch(),
};
},
computed: {
filteredBranches() {
return this.branches.filter((branch) =>
branch.toLowerCase().includes(this.searchTerm.toLowerCase()),
);
},
hasFilteredBranches() {
return this.filteredBranches.length;
},
filteredTags() {
return this.tags.filter((tag) => tag.toLowerCase().includes(this.searchTerm.toLowerCase()));
},
hasFilteredTags() {
return this.filteredTags.length;
},
},
mounted() {
this.fetchBranchesAndTags();
},
methods: {
fetchBranchesAndTags() {
const endpoint = this.refsProjectPath;
return axios
.get(endpoint)
.then(({ data }) => {
this.branches = data.Branches;
this.tags = data.Tags;
})
.catch(() => {
createFlash({
message: `${s__(
'CompareRevisions|There was an error while updating the branch/tag list. Please try again.',
)}`,
});
})
.finally(() => {
this.loading = false;
});
},
getDefaultBranch() {
return this.paramsBranch || s__('CompareRevisions|Select branch/tag');
},
onClick(revision) {
this.selectedRevision = revision;
},
onSearchEnter() {
this.selectedRevision = this.searchTerm;
},
},
};
</script>
<template>
<div class="form-group compare-form-group" :class="`js-compare-${paramsName}-dropdown`">
<div class="input-group inline-input-group">
<span class="input-group-prepend">
<div class="input-group-text">
{{ revisionText }}
</div>
</span>
<input type="hidden" :name="paramsName" :value="selectedRevision" />
<gl-dropdown
class="gl-flex-grow-1 gl-flex-basis-0 gl-min-w-0 gl-font-monospace"
toggle-class="form-control compare-dropdown-toggle js-compare-dropdown gl-min-w-0 gl-rounded-top-left-none! gl-rounded-bottom-left-none!"
:text="selectedRevision"
header-text="Select Git revision"
:loading="loading"
>
<template #header>
<gl-search-box-by-type
v-model.trim="searchTerm"
:placeholder="s__('CompareRevisions|Filter by Git revision')"
@keyup.enter="onSearchEnter"
/>
</template>
<gl-dropdown-section-header v-if="hasFilteredBranches">
{{ s__('CompareRevisions|Branches') }}
</gl-dropdown-section-header>
<gl-dropdown-item
v-for="(branch, index) in filteredBranches"
:key="`branch${index}`"
is-check-item
:is-checked="selectedRevision === branch"
@click="onClick(branch)"
>
{{ branch }}
</gl-dropdown-item>
<gl-dropdown-section-header v-if="hasFilteredTags">
{{ s__('CompareRevisions|Tags') }}
</gl-dropdown-section-header>
<gl-dropdown-item
v-for="(tag, index) in filteredTags"
:key="`tag${index}`"
is-check-item
:is-checked="selectedRevision === tag"
@click="onClick(tag)"
>
{{ tag }}
</gl-dropdown-item>
</gl-dropdown>
</div>
</div>
</template>
import Vue from 'vue';
import CompareApp from './components/app.vue';
export default function init() {
const el = document.getElementById('js-compare-selector');
const {
refsProjectPath,
paramsFrom,
paramsTo,
projectCompareIndexPath,
projectMergeRequestPath,
createMrPath,
} = el.dataset;
return new Vue({
el,
components: {
CompareApp,
},
render(createElement) {
return createElement(CompareApp, {
props: {
refsProjectPath,
paramsFrom,
paramsTo,
projectCompareIndexPath,
projectMergeRequestPath,
createMrPath,
},
});
},
});
}
...@@ -992,6 +992,20 @@ pre.light-well { ...@@ -992,6 +992,20 @@ pre.light-well {
width: auto; width: auto;
} }
} }
// Remove once gitlab/ui solution is implemented:
// https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1157
// https://gitlab.com/gitlab-org/gitlab/-/issues/300405
.gl-search-box-by-type-input {
width: 100%;
}
// Remove once gitlab/ui solution is implemented
// https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1158
// https://gitlab.com/gitlab-org/gitlab/-/issues/300405
.gl-new-dropdown-button-text {
@include str-truncated;
}
} }
.clearable-input { .clearable-input {
......
...@@ -13,4 +13,8 @@ ...@@ -13,4 +13,8 @@
= html_escape(_("Changes are shown as if the %{b_open}source%{b_close} revision was being merged into the %{b_open}target%{b_close} revision.")) % { b_open: '<b>'.html_safe, b_close: '</b>'.html_safe } = html_escape(_("Changes are shown as if the %{b_open}source%{b_close} revision was being merged into the %{b_open}target%{b_close} revision.")) % { b_open: '<b>'.html_safe, b_close: '</b>'.html_safe }
.prepend-top-20 .prepend-top-20
= render "form" #js-compare-selector{ data: { project_compare_index_path: project_compare_index_path(@project),
refs_project_path: refs_project_path(@project),
params_from: params[:from], params_to: params[:to],
project_merge_request_path: @merge_request.present? ? project_merge_request_path(@project, @merge_request) : '',
create_mr_path: create_mr_button? ? create_mr_path : '' } }
...@@ -7343,6 +7343,30 @@ msgstr "" ...@@ -7343,6 +7343,30 @@ msgstr ""
msgid "CompareBranches|There isn't anything to compare." msgid "CompareBranches|There isn't anything to compare."
msgstr "" msgstr ""
msgid "CompareRevisions|Branches"
msgstr ""
msgid "CompareRevisions|Compare"
msgstr ""
msgid "CompareRevisions|Create merge request"
msgstr ""
msgid "CompareRevisions|Filter by Git revision"
msgstr ""
msgid "CompareRevisions|Select branch/tag"
msgstr ""
msgid "CompareRevisions|Tags"
msgstr ""
msgid "CompareRevisions|There was an error while updating the branch/tag list. Please try again."
msgstr ""
msgid "CompareRevisions|View open merge request"
msgstr ""
msgid "Complete" msgid "Complete"
msgstr "" msgstr ""
......
...@@ -203,10 +203,11 @@ RSpec.describe 'User browses commits' do ...@@ -203,10 +203,11 @@ RSpec.describe 'User browses commits' do
context 'when click the compare tab' do context 'when click the compare tab' do
before do before do
wait_for_requests
click_link('Compare') click_link('Compare')
end end
it 'does not render create merge request button' do it 'does not render create merge request button', :js do
expect(page).not_to have_link 'Create merge request' expect(page).not_to have_link 'Create merge request'
end end
end end
...@@ -236,10 +237,11 @@ RSpec.describe 'User browses commits' do ...@@ -236,10 +237,11 @@ RSpec.describe 'User browses commits' do
context 'when click the compare tab' do context 'when click the compare tab' do
before do before do
wait_for_requests
click_link('Compare') click_link('Compare')
end end
it 'renders create merge request button' do it 'renders create merge request button', :js do
expect(page).to have_link 'Create merge request' expect(page).to have_link 'Create merge request'
end end
end end
...@@ -276,10 +278,11 @@ RSpec.describe 'User browses commits' do ...@@ -276,10 +278,11 @@ RSpec.describe 'User browses commits' do
context 'when click the compare tab' do context 'when click the compare tab' do
before do before do
wait_for_requests
click_link('Compare') click_link('Compare')
end end
it 'renders button to the merge request' do it 'renders button to the merge request', :js do
expect(page).not_to have_link 'Create merge request' expect(page).not_to have_link 'Create merge request'
expect(page).to have_link 'View open merge request', href: project_merge_request_path(project, merge_request) expect(page).to have_link 'View open merge request', href: project_merge_request_path(project, merge_request)
end end
......
...@@ -17,10 +17,10 @@ RSpec.describe "Compare", :js do ...@@ -17,10 +17,10 @@ RSpec.describe "Compare", :js do
visit project_compare_index_path(project, from: 'master', to: 'master') visit project_compare_index_path(project, from: 'master', to: 'master')
select_using_dropdown 'from', 'feature' select_using_dropdown 'from', 'feature'
expect(find('.js-compare-from-dropdown .dropdown-toggle-text')).to have_content('feature') expect(find('.js-compare-from-dropdown .gl-new-dropdown-button-text')).to have_content('feature')
select_using_dropdown 'to', 'binary-encoding' select_using_dropdown 'to', 'binary-encoding'
expect(find('.js-compare-to-dropdown .dropdown-toggle-text')).to have_content('binary-encoding') expect(find('.js-compare-to-dropdown .gl-new-dropdown-button-text')).to have_content('binary-encoding')
click_button 'Compare' click_button 'Compare'
...@@ -32,8 +32,8 @@ RSpec.describe "Compare", :js do ...@@ -32,8 +32,8 @@ RSpec.describe "Compare", :js do
it "pre-populates fields" do it "pre-populates fields" do
visit project_compare_index_path(project, from: "master", to: "master") visit project_compare_index_path(project, from: "master", to: "master")
expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("master") expect(find(".js-compare-from-dropdown .gl-new-dropdown-button-text")).to have_content("master")
expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("master") expect(find(".js-compare-to-dropdown .gl-new-dropdown-button-text")).to have_content("master")
end end
it_behaves_like 'compares branches' it_behaves_like 'compares branches'
...@@ -99,7 +99,7 @@ RSpec.describe "Compare", :js do ...@@ -99,7 +99,7 @@ RSpec.describe "Compare", :js do
find(".js-compare-from-dropdown .compare-dropdown-toggle").click find(".js-compare-from-dropdown .compare-dropdown-toggle").click
expect(find(".js-compare-from-dropdown .dropdown-content")).to have_selector("li", count: 3) expect(find(".js-compare-from-dropdown .gl-new-dropdown-contents")).to have_selector('li.gl-new-dropdown-item', count: 1)
end end
context 'when commit has overflow', :js do context 'when commit has overflow', :js do
...@@ -125,10 +125,10 @@ RSpec.describe "Compare", :js do ...@@ -125,10 +125,10 @@ RSpec.describe "Compare", :js do
visit project_compare_index_path(project, from: "master", to: "master") visit project_compare_index_path(project, from: "master", to: "master")
select_using_dropdown "from", "v1.0.0" select_using_dropdown "from", "v1.0.0"
expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("v1.0.0") expect(find(".js-compare-from-dropdown .gl-new-dropdown-button-text")).to have_content("v1.0.0")
select_using_dropdown "to", "v1.1.0" select_using_dropdown "to", "v1.1.0"
expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("v1.1.0") expect(find(".js-compare-to-dropdown .gl-new-dropdown-button-text")).to have_content("v1.1.0")
click_button "Compare" click_button "Compare"
expect(page).to have_content "Commits" expect(page).to have_content "Commits"
...@@ -136,19 +136,22 @@ RSpec.describe "Compare", :js do ...@@ -136,19 +136,22 @@ RSpec.describe "Compare", :js do
end end
def select_using_dropdown(dropdown_type, selection, commit: false) def select_using_dropdown(dropdown_type, selection, commit: false)
wait_for_requests
dropdown = find(".js-compare-#{dropdown_type}-dropdown") dropdown = find(".js-compare-#{dropdown_type}-dropdown")
dropdown.find(".compare-dropdown-toggle").click dropdown.find(".compare-dropdown-toggle").click
# find input before using to wait for the inputs visibility # find input before using to wait for the inputs visibility
dropdown.find('.dropdown-menu') dropdown.find('.dropdown-menu')
dropdown.fill_in("Filter by Git revision", with: selection) dropdown.fill_in("Filter by Git revision", with: selection)
wait_for_requests wait_for_requests
if commit if commit
dropdown.find('input[type="search"]').send_keys(:return) dropdown.find('.gl-search-box-by-type-input').send_keys(:return)
else else
# find before all to wait for the items visibility # find before all to wait for the items visibility
dropdown.find("a[data-ref=\"#{selection}\"]", match: :first) dropdown.find(".js-compare-#{dropdown_type}-dropdown .dropdown-item", text: selection, match: :first)
dropdown.all("a[data-ref=\"#{selection}\"]").last.click dropdown.all(".js-compare-#{dropdown_type}-dropdown .dropdown-item", text: selection).first.click
end end
end end
end end
import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import CompareApp from '~/projects/compare/components/app.vue';
import RevisionDropdown from '~/projects/compare/components/revision_dropdown.vue';
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 createComponent = (props = {}) => {
wrapper = shallowMount(CompareApp, {
propsData: {
projectCompareIndexPath,
refsProjectPath,
paramsFrom,
paramsTo,
projectMergeRequestPath: '',
createMrPath: '',
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
beforeEach(() => {
createComponent();
});
it('renders component with prop', () => {
expect(wrapper.props()).toEqual(
expect.objectContaining({
projectCompareIndexPath,
refsProjectPath,
paramsFrom,
paramsTo,
}),
);
});
it('contains the correct form attributes', () => {
expect(wrapper.attributes('action')).toBe(projectCompareIndexPath);
expect(wrapper.attributes('method')).toBe('POST');
});
it('has input with csrf token', () => {
expect(wrapper.find('input[name="authenticity_token"]').attributes('value')).toBe(
'mock-csrf-token',
);
});
it('has ellipsis', () => {
expect(wrapper.find('[data-testid="ellipsis"]').exists()).toBe(true);
});
it('render Source and Target BranchDropdown components', () => {
const branchDropdowns = wrapper.findAll(RevisionDropdown);
expect(branchDropdowns.length).toBe(2);
expect(branchDropdowns.at(0).props('revisionText')).toBe('Source');
expect(branchDropdowns.at(1).props('revisionText')).toBe('Target');
});
describe('compare button', () => {
const findCompareButton = () => wrapper.find(GlButton);
it('renders button', () => {
expect(findCompareButton().exists()).toBe(true);
});
it('submits form', () => {
findCompareButton().vm.$emit('click');
expect(wrapper.find('form').element.submit).toHaveBeenCalled();
});
it('has compare text', () => {
expect(findCompareButton().text()).toBe('Compare');
});
});
describe('merge request buttons', () => {
const findProjectMrButton = () => wrapper.find('[data-testid="projectMrButton"]');
const findCreateMrButton = () => wrapper.find('[data-testid="createMrButton"]');
it('does not have merge request buttons', () => {
createComponent();
expect(findProjectMrButton().exists()).toBe(false);
expect(findCreateMrButton().exists()).toBe(false);
});
it('has "View open merge request" button', () => {
createComponent({
projectMergeRequestPath: 'some/project/merge/request/path',
});
expect(findProjectMrButton().exists()).toBe(true);
expect(findCreateMrButton().exists()).toBe(false);
});
it('has "Create merge request" button', () => {
createComponent({
createMrPath: 'some/create/create/mr/path',
});
expect(findProjectMrButton().exists()).toBe(false);
expect(findCreateMrButton().exists()).toBe(true);
});
});
});
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import { GlDropdown } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import RevisionDropdown from '~/projects/compare/components/revision_dropdown.vue';
import createFlash from '~/flash';
const defaultProps = {
refsProjectPath: 'some/refs/path',
revisionText: 'Target',
paramsName: 'from',
paramsBranch: 'master',
};
jest.mock('~/flash');
describe('RevisionDropdown component', () => {
let wrapper;
let axiosMock;
const createComponent = (props = {}) => {
wrapper = shallowMount(RevisionDropdown, {
propsData: {
...defaultProps,
...props,
},
});
};
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
});
afterEach(() => {
wrapper.destroy();
axiosMock.restore();
});
const findGlDropdown = () => wrapper.find(GlDropdown);
it('sets hidden input', () => {
createComponent();
expect(wrapper.find('input[type="hidden"]').attributes('value')).toBe(
defaultProps.paramsBranch,
);
});
it('update the branches on success', async () => {
const Branches = ['branch-1', 'branch-2'];
const Tags = ['tag-1', 'tag-2', 'tag-3'];
axiosMock.onGet(defaultProps.refsProjectPath).replyOnce(200, {
Branches,
Tags,
});
createComponent();
await axios.waitForAll();
expect(wrapper.vm.branches).toEqual(Branches);
expect(wrapper.vm.tags).toEqual(Tags);
});
it('shows flash message on error', async () => {
axiosMock.onGet('some/invalid/path').replyOnce(404);
createComponent();
await wrapper.vm.fetchBranchesAndTags();
expect(createFlash).toHaveBeenCalled();
});
describe('GlDropdown component', () => {
it('renders props', () => {
createComponent();
expect(wrapper.props()).toEqual(expect.objectContaining(defaultProps));
});
it('display default text', () => {
createComponent({
paramsBranch: null,
});
expect(findGlDropdown().props('text')).toBe('Select branch/tag');
});
it('display params branch text', () => {
createComponent();
expect(findGlDropdown().props('text')).toBe(defaultProps.paramsBranch);
});
});
});
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