Commit 8891fbd0 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'fl-mr-widget-1' into 'master'

Moves more mr widget components into vue files

See merge request gitlab-org/gitlab-ce!16857
parents 756e1969 fb1c6650
import tooltip from '../../vue_shared/directives/tooltip';
import { pluralize } from '../../lib/utils/text_utility';
import icon from '../../vue_shared/components/icon.vue';
export default {
name: 'MRWidgetHeader',
props: {
mr: { type: Object, required: true },
},
directives: {
tooltip,
},
components: {
icon,
},
computed: {
shouldShowCommitsBehindText() {
return this.mr.divergedCommitsCount > 0;
},
commitsText() {
return pluralize('commit', this.mr.divergedCommitsCount);
},
branchNameClipboardData() {
// This supports code in app/assets/javascripts/copy_to_clipboard.js that
// works around ClipboardJS limitations to allow the context-specific
// copy/pasting of plain text or GFM.
return JSON.stringify({
text: this.mr.sourceBranch,
gfm: `\`${this.mr.sourceBranch}\``,
});
},
},
methods: {
isBranchTitleLong(branchTitle) {
return branchTitle.length > 32;
},
},
template: `
<div class="mr-source-target">
<div class="normal">
<strong>
Request to merge
<span
class="label-branch"
:class="{'label-truncated': isBranchTitleLong(mr.sourceBranch)}"
:title="isBranchTitleLong(mr.sourceBranch) ? mr.sourceBranch : ''"
data-placement="bottom"
:v-tooltip="isBranchTitleLong(mr.sourceBranch)"
v-html="mr.sourceBranchLink"></span>
<button
v-tooltip
class="btn btn-transparent btn-clipboard"
data-title="Copy branch name to clipboard"
:data-clipboard-text="branchNameClipboardData">
<i
aria-hidden="true"
class="fa fa-clipboard"></i>
</button>
into
<span
class="label-branch"
:v-tooltip="isBranchTitleLong(mr.sourceBranch)"
:class="{'label-truncatedtooltip': isBranchTitleLong(mr.targetBranch)}"
:title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''"
data-placement="bottom">
<a :href="mr.targetBranchTreePath">{{mr.targetBranch}}</a>
</span>
</strong>
<span
v-if="shouldShowCommitsBehindText"
class="diverged-commits-count">
(<a :href="mr.targetBranchPath">{{mr.divergedCommitsCount}} {{commitsText}} behind</a>)
</span>
</div>
<div v-if="mr.isOpen">
<a
href="#modal_merge_info"
data-toggle="modal"
:disabled="mr.sourceBranchRemoved"
class="btn btn-sm inline">
Check out branch
</a>
<span class="dropdown prepend-left-10">
<a
class="btn btn-sm inline dropdown-toggle"
data-toggle="dropdown"
aria-label="Download as"
role="button">
<icon
name="download">
</icon>
<i
class="fa fa-caret-down"
aria-hidden="true">
</i>
</a>
<ul class="dropdown-menu dropdown-menu-align-right">
<li>
<a
:href="mr.emailPatchesPath"
download>
Email patches
</a>
</li>
<li>
<a
:href="mr.plainDiffPath"
download>
Plain diff
</a>
</li>
</ul>
</span>
</div>
</div>
`,
};
<script>
import tooltip from '~/vue_shared/directives/tooltip';
import { n__ } from '~/locale';
import icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default {
name: 'MRWidgetHeader',
directives: {
tooltip,
},
components: {
icon,
clipboardButton,
},
props: {
mr: {
type: Object,
required: true,
},
},
computed: {
shouldShowCommitsBehindText() {
return this.mr.divergedCommitsCount > 0;
},
commitsText() {
return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount);
},
branchNameClipboardData() {
// This supports code in app/assets/javascripts/copy_to_clipboard.js that
// works around ClipboardJS limitations to allow the context-specific
// copy/pasting of plain text or GFM.
return JSON.stringify({
text: this.mr.sourceBranch,
gfm: `\`${this.mr.sourceBranch}\``,
});
},
isSourceBranchLong() {
return this.isBranchTitleLong(this.mr.sourceBranch);
},
isTargetBranchLong() {
return this.isBranchTitleLong(this.mr.targetBranch);
},
},
methods: {
isBranchTitleLong(branchTitle) {
return branchTitle.length > 32;
},
},
};
</script>
<template>
<div class="mr-source-target">
<div class="normal">
<strong>
{{ s__("mrWidget|Request to merge") }}
<span
class="label-branch js-source-branch"
:class="{ 'label-truncated': isSourceBranchLong }"
:title="isSourceBranchLong ? mr.sourceBranch : ''"
data-placement="bottom"
:v-tooltip="isSourceBranchLong"
v-html="mr.sourceBranchLink"
>
</span>
<clipboard-button
:text="branchNameClipboardData"
:title="__('Copy branch name to clipboard')"
/>
{{ s__("mrWidget|into") }}
<span
class="label-branch"
:v-tooltip="isTargetBranchLong"
:class="{ 'label-truncatedtooltip': isTargetBranchLong }"
:title="isTargetBranchLong ? mr.targetBranch : ''"
data-placement="bottom"
>
<a
:href="mr.targetBranchTreePath"
class="js-target-branch"
>
{{ mr.targetBranch }}
</a>
</span>
</strong>
<span
v-if="shouldShowCommitsBehindText"
class="diverged-commits-count"
>
(<a :href="mr.targetBranchPath">{{ commitsText }}</a>)
</span>
</div>
<div v-if="mr.isOpen">
<button
data-target="#modal_merge_info"
data-toggle="modal"
:disabled="mr.sourceBranchRemoved"
class="btn btn-sm btn-default inline js-check-out-branch"
type="button"
>
{{ s__("mrWidget|Check out branch") }}
</button>
<span class="dropdown prepend-left-10">
<button
type="button"
class="btn btn-sm inline dropdown-toggle"
data-toggle="dropdown"
aria-label="Download as"
aria-haspopup="true"
aria-expanded="false"
>
<icon name="download" />
<i
class="fa fa-caret-down"
aria-hidden="true">
</i>
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li>
<a
class="js-download-email-patches"
:href="mr.emailPatchesPath"
download
>
{{ s__("mrWidget|Email patches") }}
</a>
</li>
<li>
<a
class="js-download-plain-diff"
:href="mr.plainDiffPath"
download
>
{{ s__("mrWidget|Plain diff") }}
</a>
</li>
</ul>
</span>
</div>
</div>
</template>
export default {
name: 'MRWidgetMergeHelp',
props: {
missingBranch: { type: String, required: false, default: '' },
},
template: `
<section class="mr-widget-help">
<template
v-if="missingBranch">
If the {{missingBranch}} branch exists in your local repository, you
</template>
<template v-else>
You
</template>
can merge this merge request manually using the
<a
data-toggle="modal"
href="#modal_merge_info">
command line
</a>
</section>
`,
};
<script>
import { sprintf, s__ } from '~/locale';
export default {
name: 'MRWidgetMergeHelp',
props: {
missingBranch: {
type: String,
required: false,
default: '',
},
},
computed: {
missingBranchInfo() {
return sprintf(
s__('mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the'),
{ branch: this.missingBranch },
);
},
},
};
</script>
<template>
<section class="mr-widget-help">
<template v-if="missingBranch">
{{ missingBranchInfo }}
</template>
<template v-else>
{{ s__("mrWidget|You can merge this merge request manually using the") }}
</template>
<button
type="button"
class="btn-link btn-blank js-open-modal-help"
data-toggle="modal"
data-target="#modal_merge_info"
>
{{ s__("mrWidget|command line") }}
</button>
</section>
</template>
import statusIcon from '../mr_widget_status_icon.vue'; import statusIcon from '../mr_widget_status_icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
import mrWidgetMergeHelp from '../../components/mr_widget_merge_help'; import mrWidgetMergeHelp from '../../components/mr_widget_merge_help.vue';
export default { export default {
name: 'MRWidgetMissingBranch', name: 'MRWidgetMissingBranch',
......
...@@ -11,8 +11,8 @@ ...@@ -11,8 +11,8 @@
export { default as Vue } from 'vue'; export { default as Vue } from 'vue';
export { default as SmartInterval } from '~/smart_interval'; export { default as SmartInterval } from '~/smart_interval';
export { default as WidgetHeader } from './components/mr_widget_header'; export { default as WidgetHeader } from './components/mr_widget_header.vue';
export { default as WidgetMergeHelp } from './components/mr_widget_merge_help'; export { default as WidgetMergeHelp } from './components/mr_widget_merge_help.vue';
export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue'; export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue';
export { default as WidgetDeployment } from './components/mr_widget_deployment'; export { default as WidgetDeployment } from './components/mr_widget_deployment';
export { default as WidgetRelatedLinks } from './components/mr_widget_related_links.vue'; export { default as WidgetRelatedLinks } from './components/mr_widget_related_links.vue';
......
...@@ -61,7 +61,7 @@ describe 'Merge request > User selects branches for new MR', :js do ...@@ -61,7 +61,7 @@ describe 'Merge request > User selects branches for new MR', :js do
fill_in "merge_request_title", with: "Orphaned MR test" fill_in "merge_request_title", with: "Orphaned MR test"
click_button "Submit merge request" click_button "Submit merge request"
click_link "Check out branch" click_button "Check out branch"
expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch' expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch'
end end
......
import Vue from 'vue'; import Vue from 'vue';
import headerComponent from '~/vue_merge_request_widget/components/mr_widget_header'; import headerComponent from '~/vue_merge_request_widget/components/mr_widget_header.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
const createComponent = (mr) => {
const Component = Vue.extend(headerComponent);
return new Component({
el: document.createElement('div'),
propsData: { mr },
});
};
describe('MRWidgetHeader', () => { describe('MRWidgetHeader', () => {
describe('props', () => { let vm;
it('should have props', () => { let Component;
const { mr } = headerComponent.props;
expect(mr.type instanceof Object).toBeTruthy(); beforeEach(() => {
expect(mr.required).toBeTruthy(); Component = Vue.extend(headerComponent);
}); });
afterEach(() => {
vm.$destroy();
}); });
describe('computed', () => { describe('computed', () => {
let vm; describe('shouldShowCommitsBehindText', () => {
beforeEach(() => { it('return true when there are divergedCommitsCount', () => {
vm = createComponent({ vm = mountComponent(Component, { mr: {
divergedCommitsCount: 12, divergedCommitsCount: 12,
sourceBranch: 'mr-widget-refactor', sourceBranch: 'mr-widget-refactor',
sourceBranchLink: '/foo/bar/mr-widget-refactor', sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
targetBranch: 'master', targetBranch: 'master',
} });
expect(vm.shouldShowCommitsBehindText).toEqual(true);
});
it('returns false where there are no divergedComits count', () => {
vm = mountComponent(Component, { mr: {
divergedCommitsCount: 0,
sourceBranch: 'mr-widget-refactor',
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
targetBranch: 'master',
} });
expect(vm.shouldShowCommitsBehindText).toEqual(false);
}); });
}); });
it('shouldShowCommitsBehindText', () => { describe('commitsText', () => {
expect(vm.shouldShowCommitsBehindText).toBeTruthy(); it('returns singular when there is one commit', () => {
vm = mountComponent(Component, { mr: {
divergedCommitsCount: 1,
sourceBranch: 'mr-widget-refactor',
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
targetBranch: 'master',
} });
vm.mr.divergedCommitsCount = 0; expect(vm.commitsText).toEqual('1 commit behind');
expect(vm.shouldShowCommitsBehindText).toBeFalsy(); });
});
it('commitsText', () => { it('returns plural when there is more than one commit', () => {
expect(vm.commitsText).toEqual('commits'); vm = mountComponent(Component, { mr: {
divergedCommitsCount: 2,
sourceBranch: 'mr-widget-refactor',
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
targetBranch: 'master',
} });
vm.mr.divergedCommitsCount = 1; expect(vm.commitsText).toEqual('2 commits behind');
expect(vm.commitsText).toEqual('commit'); });
}); });
}); });
describe('template', () => { describe('template', () => {
let vm; describe('common elements', () => {
let el; beforeEach(() => {
let mr; vm = mountComponent(Component, { mr: {
const sourceBranchPath = '/foo/bar/mr-widget-refactor'; divergedCommitsCount: 12,
sourceBranch: 'mr-widget-refactor',
beforeEach(() => { sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
mr = { sourceBranchRemoved: false,
divergedCommitsCount: 12, targetBranchPath: 'foo/bar/commits-path',
sourceBranch: 'mr-widget-refactor', targetBranchTreePath: 'foo/bar/tree/path',
sourceBranchLink: `<a href="${sourceBranchPath}">mr-widget-refactor</a>`, targetBranch: 'master',
sourceBranchRemoved: false, isOpen: true,
targetBranchPath: 'foo/bar/commits-path', emailPatchesPath: '/mr/email-patches',
targetBranchTreePath: 'foo/bar/tree/path', plainDiffPath: '/mr/plainDiffPath',
targetBranch: 'master', } });
isOpen: true, });
emailPatchesPath: '/mr/email-patches',
plainDiffPath: '/mr/plainDiffPath', it('renders source branch link', () => {
}; expect(
vm.$el.querySelector('.js-source-branch').innerHTML,
vm = createComponent(mr); ).toEqual('<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>');
el = vm.$el; });
it('renders clipboard button', () => {
expect(vm.$el.querySelector('.btn-clipboard')).not.toEqual(null);
});
it('renders target branch', () => {
expect(vm.$el.querySelector('.js-target-branch').textContent.trim()).toEqual('master');
});
}); });
it('should render template elements correctly', () => { describe('with an open merge request', () => {
expect(el.classList.contains('mr-source-target')).toBeTruthy(); afterEach(() => {
const sourceBranchLink = el.querySelectorAll('.label-branch')[0]; vm.$destroy();
const targetBranchLink = el.querySelectorAll('.label-branch')[1]; });
const commitsCount = el.querySelector('.diverged-commits-count');
expect(sourceBranchLink.textContent).toContain(mr.sourceBranch); beforeEach(() => {
expect(targetBranchLink.textContent).toContain(mr.targetBranch); vm = mountComponent(Component, { mr: {
expect(sourceBranchLink.querySelector('a').getAttribute('href')).toEqual(sourceBranchPath); divergedCommitsCount: 12,
expect(targetBranchLink.querySelector('a').getAttribute('href')).toEqual(mr.targetBranchTreePath); sourceBranch: 'mr-widget-refactor',
expect(commitsCount.textContent).toContain('12 commits behind'); sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
expect(commitsCount.querySelector('a').getAttribute('href')).toEqual(mr.targetBranchPath); sourceBranchRemoved: false,
targetBranchPath: 'foo/bar/commits-path',
targetBranchTreePath: 'foo/bar/tree/path',
targetBranch: 'master',
isOpen: true,
emailPatchesPath: '/mr/email-patches',
plainDiffPath: '/mr/plainDiffPath',
} });
});
it('renders checkout branch button with modal trigger', () => {
const button = vm.$el.querySelector('.js-check-out-branch');
expect(button.textContent.trim()).toEqual('Check out branch');
expect(button.getAttribute('data-target')).toEqual('#modal_merge_info');
expect(button.getAttribute('data-toggle')).toEqual('modal');
});
it('renders download dropdown with links', () => {
expect(
vm.$el.querySelector('.js-download-email-patches').textContent.trim(),
).toEqual('Email patches');
expect(el.textContent).toContain('Check out branch'); expect(
expect(el.querySelectorAll('.dropdown li a')[0].getAttribute('href')).toEqual(mr.emailPatchesPath); vm.$el.querySelector('.js-download-email-patches').getAttribute('href'),
expect(el.querySelectorAll('.dropdown li a')[1].getAttribute('href')).toEqual(mr.plainDiffPath); ).toEqual('/mr/email-patches');
expect(el.querySelector('a[href="#modal_merge_info"]').getAttribute('disabled')).toBeNull(); expect(
vm.$el.querySelector('.js-download-plain-diff').textContent.trim(),
).toEqual('Plain diff');
expect(
vm.$el.querySelector('.js-download-plain-diff').getAttribute('href'),
).toEqual('/mr/plainDiffPath');
});
}); });
it('should not have right action links if the MR state is not open', (done) => { describe('with a closed merge request', () => {
vm.mr.isOpen = false; beforeEach(() => {
Vue.nextTick(() => { vm = mountComponent(Component, { mr: {
expect(el.textContent).not.toContain('Check out branch'); divergedCommitsCount: 12,
expect(el.querySelectorAll('.dropdown li a').length).toEqual(0); sourceBranch: 'mr-widget-refactor',
done(); sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
sourceBranchRemoved: false,
targetBranchPath: 'foo/bar/commits-path',
targetBranchTreePath: 'foo/bar/tree/path',
targetBranch: 'master',
isOpen: false,
emailPatchesPath: '/mr/email-patches',
plainDiffPath: '/mr/plainDiffPath',
} });
});
it('does not render checkout branch button with modal trigger', () => {
const button = vm.$el.querySelector('.js-check-out-branch');
expect(button).toEqual(null);
});
it('does not render download dropdown with links', () => {
expect(
vm.$el.querySelector('.js-download-email-patches'),
).toEqual(null);
expect(
vm.$el.querySelector('.js-download-plain-diff'),
).toEqual(null);
}); });
}); });
it('should not render diverged commits count if the MR has no diverged commits', (done) => { describe('without diverged commits', () => {
vm.mr.divergedCommitsCount = null; beforeEach(() => {
Vue.nextTick(() => { vm = mountComponent(Component, { mr: {
expect(el.textContent).not.toContain('commits behind'); divergedCommitsCount: 0,
expect(el.querySelectorAll('.diverged-commits-count').length).toEqual(0); sourceBranch: 'mr-widget-refactor',
done(); sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
sourceBranchRemoved: false,
targetBranchPath: 'foo/bar/commits-path',
targetBranchTreePath: 'foo/bar/tree/path',
targetBranch: 'master',
isOpen: true,
emailPatchesPath: '/mr/email-patches',
plainDiffPath: '/mr/plainDiffPath',
} });
});
it('does not render diverged commits info', () => {
expect(vm.$el.querySelector('.diverged-commits-count')).toEqual(null);
}); });
}); });
it('should disable check out branch button if source branch has been removed', (done) => { describe('with diverged commits', () => {
vm.mr.sourceBranchRemoved = true; beforeEach(() => {
vm = mountComponent(Component, { mr: {
divergedCommitsCount: 12,
sourceBranch: 'mr-widget-refactor',
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
sourceBranchRemoved: false,
targetBranchPath: 'foo/bar/commits-path',
targetBranchTreePath: 'foo/bar/tree/path',
targetBranch: 'master',
isOpen: true,
emailPatchesPath: '/mr/email-patches',
plainDiffPath: '/mr/plainDiffPath',
} });
});
Vue.nextTick() it('renders diverged commits info', () => {
.then(() => { expect(vm.$el.querySelector('.diverged-commits-count').textContent.trim()).toEqual('(12 commits behind)');
expect(el.querySelector('a[href="#modal_merge_info"]').getAttribute('disabled')).toBe('disabled'); });
done();
})
.catch(done.fail);
}); });
}); });
}); });
import Vue from 'vue'; import Vue from 'vue';
import mergeHelpComponent from '~/vue_merge_request_widget/components/mr_widget_merge_help'; import mergeHelpComponent from '~/vue_merge_request_widget/components/mr_widget_merge_help.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
const props = {
missingBranch: 'this-is-not-the-branch-you-are-looking-for',
};
const text = `If the ${props.missingBranch} branch exists in your local repository`;
const createComponent = () => {
const Component = Vue.extend(mergeHelpComponent);
return new Component({
el: document.createElement('div'),
propsData: props,
});
};
describe('MRWidgetMergeHelp', () => { describe('MRWidgetMergeHelp', () => {
describe('props', () => { let vm;
it('should have props', () => { let Component;
const { missingBranch } = mergeHelpComponent.props;
const MissingBranchTypeClass = missingBranch.type; beforeEach(() => {
Component = Vue.extend(mergeHelpComponent);
expect(new MissingBranchTypeClass() instanceof String).toBeTruthy();
expect(missingBranch.required).toBeFalsy();
expect(missingBranch.default).toEqual('');
});
}); });
describe('template', () => { afterEach(() => {
let vm; vm.$destroy();
let el; });
describe('with missing branch', () => {
beforeEach(() => { beforeEach(() => {
vm = createComponent(); vm = mountComponent(Component, {
el = vm.$el; missingBranch: 'this-is-not-the-branch-you-are-looking-for',
});
}); });
it('should have the correct elements', () => { it('renders missing branch information', () => {
expect(el.classList.contains('mr-widget-help')).toBeTruthy(); expect(
expect(el.textContent).toContain(text); vm.$el.textContent.trim().replace(/[\r\n]+/g, ' ').replace(/\s\s+/g, ' '),
).toEqual(
'If the this-is-not-the-branch-you-are-looking-for branch exists in your local repository, you can merge this merge request manually using the command line',
);
}); });
it('should not show missing branch name if missingBranch props is not provided', (done) => { it('renders button to open help modal', () => {
vm.missingBranch = null; expect(vm.$el.querySelector('.js-open-modal-help').getAttribute('data-target')).toEqual('#modal_merge_info');
Vue.nextTick(() => { expect(vm.$el.querySelector('.js-open-modal-help').getAttribute('data-toggle')).toEqual('modal');
expect(el.textContent).not.toContain(text); });
done(); });
});
describe('without missing branch', () => {
beforeEach(() => {
vm = mountComponent(Component);
});
it('renders information about how to merge manually', () => {
expect(
vm.$el.textContent.trim().replace(/[\r\n]+/g, ' ').replace(/\s\s+/g, ' '),
).toEqual(
'You can merge this merge request manually using the command line',
);
});
it('renders element to open a modal', () => {
expect(vm.$el.querySelector('.js-open-modal-help').getAttribute('data-target')).toEqual('#modal_merge_info');
expect(vm.$el.querySelector('.js-open-modal-help').getAttribute('data-toggle')).toEqual('modal');
}); });
}); });
}); });
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