Commit 0973a860 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch '49110-update-mr-widget-styles' into 'master'

Resolve "Follow up to "Styling of the MR widget's info and pipeline sections""

Closes #49110

See merge request gitlab-org/gitlab-ce!21206
parents 0fc5bc31 7b179610
<script> <script>
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import timeagoMixin from '../../vue_shared/mixins/timeago'; import timeagoMixin from '../../vue_shared/mixins/timeago';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import LoadingButton from '../../vue_shared/components/loading_button.vue'; import LoadingButton from '../../vue_shared/components/loading_button.vue';
...@@ -16,6 +17,7 @@ export default { ...@@ -16,6 +17,7 @@ export default {
MemoryUsage, MemoryUsage,
StatusIcon, StatusIcon,
Icon, Icon,
TooltipOnTruncate,
}, },
directives: { directives: {
tooltip, tooltip,
...@@ -88,14 +90,20 @@ export default { ...@@ -88,14 +90,20 @@ export default {
<span> <span>
Deployed to Deployed to
</span> </span>
<tooltip-on-truncate
:title="deployment.name"
truncate-target="child"
class="deploy-link label-truncate"
>
<a <a
:href="deployment.url" :href="deployment.url"
target="_blank" target="_blank"
rel="noopener noreferrer nofollow" rel="noopener noreferrer nofollow"
class="deploy-link js-deploy-meta" class="js-deploy-meta"
> >
{{ deployment.name }} {{ deployment.name }}
</a> </a>
</tooltip-on-truncate>
</template> </template>
<span <span
v-tooltip v-tooltip
......
<script> <script>
import tooltip from '~/vue_shared/directives/tooltip'; import _ from 'underscore';
import { n__ } from '~/locale'; import { n__, s__, sprintf } from '~/locale';
import { mergeUrlParams, webIDEUrl } from '~/lib/utils/url_utility'; import { mergeUrlParams, webIDEUrl } from '~/lib/utils/url_utility';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue'; import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
export default { export default {
name: 'MRWidgetHeader', name: 'MRWidgetHeader',
directives: {
tooltip,
},
components: { components: {
Icon, Icon,
clipboardButton, clipboardButton,
TooltipOnTruncate,
}, },
props: { props: {
mr: { mr: {
...@@ -24,8 +23,12 @@ export default { ...@@ -24,8 +23,12 @@ export default {
shouldShowCommitsBehindText() { shouldShowCommitsBehindText() {
return this.mr.divergedCommitsCount > 0; return this.mr.divergedCommitsCount > 0;
}, },
commitsText() { commitsBehindText() {
return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount); return sprintf(s__('mrWidget|The source branch is %{commitsBehindLinkStart}%{commitsBehind}%{commitsBehindLinkEnd} the target branch'), {
commitsBehindLinkStart: `<a href="${_.escape(this.mr.targetBranchPath)}">`,
commitsBehind: n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount),
commitsBehindLinkEnd: '</a>',
}, false);
}, },
branchNameClipboardData() { branchNameClipboardData() {
// This supports code in app/assets/javascripts/copy_to_clipboard.js that // This supports code in app/assets/javascripts/copy_to_clipboard.js that
...@@ -36,12 +39,6 @@ export default { ...@@ -36,12 +39,6 @@ export default {
gfm: `\`${this.mr.sourceBranch}\``, gfm: `\`${this.mr.sourceBranch}\``,
}); });
}, },
isSourceBranchLong() {
return this.isBranchTitleLong(this.mr.sourceBranch);
},
isTargetBranchLong() {
return this.isBranchTitleLong(this.mr.targetBranch);
},
webIdePath() { webIdePath() {
return mergeUrlParams({ return mergeUrlParams({
target_project: this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath ? target_project: this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath ?
...@@ -49,11 +46,6 @@ export default { ...@@ -49,11 +46,6 @@ export default {
}, webIDEUrl(`/${this.mr.sourceProjectFullPath}/merge_requests/${this.mr.iid}`)); }, webIDEUrl(`/${this.mr.sourceProjectFullPath}/merge_requests/${this.mr.iid}`));
}, },
}, },
methods: {
isBranchTitleLong(branchTitle) {
return branchTitle.length > 32;
},
},
}; };
</script> </script>
<template> <template>
...@@ -65,30 +57,21 @@ export default { ...@@ -65,30 +57,21 @@ export default {
<div class="normal"> <div class="normal">
<strong> <strong>
{{ s__("mrWidget|Request to merge") }} {{ s__("mrWidget|Request to merge") }}
<span <tooltip-on-truncate
:class="{ 'label-truncated': isSourceBranchLong }" :title="mr.sourceBranch"
:title="isSourceBranchLong ? mr.sourceBranch : ''" truncate-target="child"
:v-tooltip="isSourceBranchLong" class="label-branch label-truncate js-source-branch"
class="label-branch js-source-branch"
data-placement="bottom"
v-html="mr.sourceBranchLink" v-html="mr.sourceBranchLink"
> /><clipboard-button
</span>
<clipboard-button
:text="branchNameClipboardData" :text="branchNameClipboardData"
:title="__('Copy branch name to clipboard')" :title="__('Copy branch name to clipboard')"
css-class="btn-default btn-transparent btn-clipboard" css-class="btn-default btn-transparent btn-clipboard"
/> />
{{ s__("mrWidget|into") }} {{ s__("mrWidget|into") }}
<tooltip-on-truncate
<span :title="mr.targetBranch"
:v-tooltip="isTargetBranchLong" truncate-target="child"
:class="{ 'label-truncatedtooltip': isTargetBranchLong }" class="label-branch label-truncate"
:title="isTargetBranchLong ? mr.targetBranch : ''"
class="label-branch"
data-placement="bottom"
> >
<a <a
:href="mr.targetBranchTreePath" :href="mr.targetBranchTreePath"
...@@ -96,15 +79,13 @@ export default { ...@@ -96,15 +79,13 @@ export default {
> >
{{ mr.targetBranch }} {{ mr.targetBranch }}
</a> </a>
</span> </tooltip-on-truncate>
</strong> </strong>
<div <div
v-if="shouldShowCommitsBehindText" v-if="shouldShowCommitsBehindText"
class="diverged-commits-count" class="diverged-commits-count"
v-html="commitsBehindText"
> >
<span class="monospace">{{ mr.sourceBranch }}</span>
is {{ commitsText }}
<span class="monospace">{{ mr.targetBranch }}</span>
</div> </div>
</div> </div>
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import PipelineStage from '~/pipelines/components/stage.vue'; import PipelineStage from '~/pipelines/components/stage.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
export default { export default {
name: 'MRWidgetPipeline', name: 'MRWidgetPipeline',
...@@ -10,6 +11,7 @@ export default { ...@@ -10,6 +11,7 @@ export default {
PipelineStage, PipelineStage,
CiIcon, CiIcon,
Icon, Icon,
TooltipOnTruncate,
}, },
props: { props: {
pipeline: { pipeline: {
...@@ -30,6 +32,10 @@ export default { ...@@ -30,6 +32,10 @@ export default {
type: String, type: String,
required: false, required: false,
}, },
sourceBranch: {
type: String,
required: false,
},
}, },
computed: { computed: {
hasPipeline() { hasPipeline() {
...@@ -107,11 +113,12 @@ export default { ...@@ -107,11 +113,12 @@ export default {
> >
{{ pipeline.commit.short_id }}</a> {{ pipeline.commit.short_id }}</a>
on on
<span <tooltip-on-truncate
class="label-branch" :title="sourceBranch"
truncate-target="child"
class="label-branch label-truncate"
v-html="sourceBranchLink" v-html="sourceBranchLink"
> />
</span>
</template> </template>
</div> </div>
<div <div
......
...@@ -254,6 +254,7 @@ export default { ...@@ -254,6 +254,7 @@ export default {
:pipeline="mr.pipeline" :pipeline="mr.pipeline"
:ci-status="mr.ciStatus" :ci-status="mr.ciStatus"
:has-ci="mr.hasCI" :has-ci="mr.hasCI"
:source-branch="mr.sourceBranch"
:source-branch-link="mr.sourceBranchLink" :source-branch-link="mr.sourceBranchLink"
/> />
<deployment <deployment
......
<script>
import _ from 'underscore';
import tooltip from '../directives/tooltip';
export default {
directives: {
tooltip,
},
props: {
title: {
type: String,
required: false,
default: '',
},
placement: {
type: String,
required: false,
default: 'top',
},
truncateTarget: {
type: [String, Function],
required: false,
default: '',
},
},
data() {
return {
showTooltip: false,
};
},
mounted() {
const target = this.selectTarget();
if (target && target.scrollWidth > target.offsetWidth) {
this.showTooltip = true;
}
},
methods: {
selectTarget() {
if (_.isFunction(this.truncateTarget)) {
return this.truncateTarget(this.$el);
} else if (this.truncateTarget === 'child') {
return this.$el.childNodes[0];
}
return this.$el;
},
},
};
</script>
<template>
<span
v-tooltip
v-if="showTooltip"
:title="title"
:data-placement="placement"
class="js-show-tooltip"
>
<slot></slot>
</span>
<span
v-else
>
<slot></slot>
</span>
</template>
...@@ -195,6 +195,7 @@ ...@@ -195,6 +195,7 @@
.ci-widget-content { .ci-widget-content {
display: flex; display: flex;
align-items: center; align-items: center;
flex: 1;
} }
} }
...@@ -222,6 +223,7 @@ ...@@ -222,6 +223,7 @@
.normal { .normal {
flex: 1; flex: 1;
flex-basis: auto;
} }
.capitalize { .capitalize {
...@@ -235,22 +237,23 @@ ...@@ -235,22 +237,23 @@
font-weight: normal; font-weight: normal;
overflow: hidden; overflow: hidden;
word-break: break-all; word-break: break-all;
}
&.label-truncated { .deploy-link,
position: relative; .label-branch {
&.label-truncate {
// NOTE: This selector targets its children because some of the HTML comes from
// 'source_branch_link'. Once this external HTML is no longer used, we could
// simplify this.
> a,
> span {
display: inline-block; display: inline-block;
width: 250px; max-width: 12.5em;
margin-bottom: -3px; margin-bottom: -3px;
white-space: nowrap; white-space: nowrap;
text-overflow: clip; text-overflow: ellipsis;
line-height: 14px; line-height: 14px;
overflow: hidden;
&::after {
position: absolute;
content: '...';
right: 0;
font-family: $regular-font;
background-color: $gray-light;
} }
} }
} }
...@@ -582,7 +585,7 @@ ...@@ -582,7 +585,7 @@
@include media-breakpoint-down(md) { @include media-breakpoint-down(md) {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: stretch;
.branch-actions { .branch-actions {
margin-top: 16px; margin-top: 16px;
...@@ -593,13 +596,13 @@ ...@@ -593,13 +596,13 @@
.branch-actions { .branch-actions {
align-self: center; align-self: center;
margin-left: $gl-padding; margin-left: $gl-padding;
white-space: nowrap;
} }
} }
} }
.diverged-commits-count { .diverged-commits-count {
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
font-size: 12px;
} }
} }
...@@ -918,7 +921,7 @@ ...@@ -918,7 +921,7 @@
flex: 1; flex: 1;
flex-direction: row; flex-direction: row;
@include media-breakpoint-down(md) { @include media-breakpoint-down(sm) {
flex-direction: column; flex-direction: column;
.stage-cell .stage-container { .stage-cell .stage-container {
......
---
title: Truncate branch names and update "commits behind" text in MR page
merge_request: 21206
author:
type: changed
...@@ -6706,6 +6706,9 @@ msgstr "" ...@@ -6706,6 +6706,9 @@ msgstr ""
msgid "mrWidget|The source branch has been removed" msgid "mrWidget|The source branch has been removed"
msgstr "" msgstr ""
msgid "mrWidget|The source branch is %{commitsBehindLinkStart}%{commitsBehind}%{commitsBehindLinkEnd} the target branch"
msgstr ""
msgid "mrWidget|The source branch is being removed" msgid "mrWidget|The source branch is being removed"
msgstr "" msgstr ""
......
import Vue from 'vue';
const mountComponent = (Component, props = {}, el = null) => const mountComponent = (Component, props = {}, el = null) =>
new Component({ new Component({
propsData: props, propsData: props,
...@@ -25,4 +27,12 @@ export const mountComponentWithSlots = (Component, { props, slots }) => { ...@@ -25,4 +27,12 @@ export const mountComponentWithSlots = (Component, { props, slots }) => {
return component.$mount(); return component.$mount();
}; };
/**
* Mount a component with the given render method.
*
* This helps with inserting slots that need to be compiled.
*/
export const mountComponentWithRender = (render, el = null) =>
mountComponent(Vue.extend({ render }), {}, el);
export default mountComponent; export default mountComponent;
...@@ -45,7 +45,7 @@ describe('MRWidgetHeader', () => { ...@@ -45,7 +45,7 @@ describe('MRWidgetHeader', () => {
}); });
}); });
describe('commitsText', () => { describe('commitsBehindText', () => {
it('returns singular when there is one commit', () => { it('returns singular when there is one commit', () => {
vm = mountComponent(Component, { vm = mountComponent(Component, {
mr: { mr: {
...@@ -53,11 +53,12 @@ describe('MRWidgetHeader', () => { ...@@ -53,11 +53,12 @@ describe('MRWidgetHeader', () => {
sourceBranch: 'mr-widget-refactor', sourceBranch: 'mr-widget-refactor',
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>', sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
targetBranch: 'master', targetBranch: 'master',
targetBranchPath: '/foo/bar/master',
statusPath: 'abc', statusPath: 'abc',
}, },
}); });
expect(vm.commitsText).toEqual('1 commit behind'); expect(vm.commitsBehindText).toEqual('The source branch is <a href="/foo/bar/master">1 commit behind</a> the target branch');
}); });
it('returns plural when there is more than one commit', () => { it('returns plural when there is more than one commit', () => {
...@@ -67,11 +68,12 @@ describe('MRWidgetHeader', () => { ...@@ -67,11 +68,12 @@ describe('MRWidgetHeader', () => {
sourceBranch: 'mr-widget-refactor', sourceBranch: 'mr-widget-refactor',
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>', sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
targetBranch: 'master', targetBranch: 'master',
targetBranchPath: '/foo/bar/master',
statusPath: 'abc', statusPath: 'abc',
}, },
}); });
expect(vm.commitsText).toEqual('2 commits behind'); expect(vm.commitsBehindText).toEqual('The source branch is <a href="/foo/bar/master">2 commits behind</a> the target branch');
}); });
}); });
}); });
...@@ -280,9 +282,9 @@ describe('MRWidgetHeader', () => { ...@@ -280,9 +282,9 @@ describe('MRWidgetHeader', () => {
}); });
it('renders diverged commits info', () => { it('renders diverged commits info', () => {
expect(vm.$el.querySelector('.diverged-commits-count').textContent).toMatch( expect(vm.$el.querySelector('.diverged-commits-count').textContent).toEqual('The source branch is 12 commits behind the target branch');
/(mr-widget-refactor[\s\S]+?is 12 commits behind[\s\S]+?master)/, expect(vm.$el.querySelector('.diverged-commits-count a').textContent).toEqual('12 commits behind');
); expect(vm.$el.querySelector('.diverged-commits-count a')).toHaveAttr('href', vm.mr.targetBranchPath);
}); });
}); });
}); });
......
import { mountComponentWithRender } from 'spec/helpers/vue_mount_component_helper';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
const TEST_TITLE = 'lorem-ipsum-dolar-sit-amit-consectur-adipiscing-elit-sed-do';
const CLASS_SHOW_TOOLTIP = 'js-show-tooltip';
const STYLE_TRUNCATED = {
display: 'inline-block',
'max-width': '20px',
};
const STYLE_NORMAL = {
display: 'inline-block',
'max-width': '1000px',
};
function mountTooltipOnTruncate(options, createChildren) {
return mountComponentWithRender(h => h(TooltipOnTruncate, options, createChildren(h)), '#app');
}
describe('TooltipOnTruncate component', () => {
let vm;
beforeEach(() => {
const el = document.createElement('div');
el.id = 'app';
document.body.appendChild(el);
});
afterEach(() => {
vm.$destroy();
});
describe('with default target', () => {
it('renders tooltip if truncated', done => {
const options = {
style: STYLE_TRUNCATED,
props: {
title: TEST_TITLE,
},
};
vm = mountTooltipOnTruncate(options, () => [TEST_TITLE]);
vm.$nextTick()
.then(() => {
expect(vm.$el).toHaveClass(CLASS_SHOW_TOOLTIP);
expect(vm.$el).toHaveData('original-title', TEST_TITLE);
expect(vm.$el).toHaveData('placement', 'top');
})
.then(done)
.catch(done.fail);
});
it('does not render tooltip if normal', done => {
const options = {
style: STYLE_NORMAL,
props: {
title: TEST_TITLE,
},
};
vm = mountTooltipOnTruncate(options, () => [TEST_TITLE]);
vm.$nextTick()
.then(() => {
expect(vm.$el).not.toHaveClass(CLASS_SHOW_TOOLTIP);
})
.then(done)
.catch(done.fail);
});
});
describe('with child target', () => {
it('renders tooltip if truncated', done => {
const options = {
style: STYLE_NORMAL,
props: {
title: TEST_TITLE,
truncateTarget: 'child',
},
};
vm = mountTooltipOnTruncate(options, (h) => [
h('a', { style: STYLE_TRUNCATED }, TEST_TITLE),
]);
vm.$nextTick()
.then(() => {
expect(vm.$el).toHaveClass(CLASS_SHOW_TOOLTIP);
})
.then(done)
.catch(done.fail);
});
it('does not render tooltip if normal', done => {
const options = {
props: {
title: TEST_TITLE,
truncateTarget: 'child',
},
};
vm = mountTooltipOnTruncate(options, (h) => [
h('a', { style: STYLE_NORMAL }, TEST_TITLE),
]);
vm.$nextTick()
.then(() => {
expect(vm.$el).not.toHaveClass(CLASS_SHOW_TOOLTIP);
})
.then(done)
.catch(done.fail);
});
});
describe('with fn target', () => {
it('renders tooltip if truncated', done => {
const options = {
style: STYLE_NORMAL,
props: {
title: TEST_TITLE,
truncateTarget: (el) => el.childNodes[1],
},
};
vm = mountTooltipOnTruncate(options, (h) => [
h('a', { style: STYLE_NORMAL }, TEST_TITLE),
h('span', { style: STYLE_TRUNCATED }, TEST_TITLE),
]);
vm.$nextTick()
.then(() => {
expect(vm.$el).toHaveClass(CLASS_SHOW_TOOLTIP);
})
.then(done)
.catch(done.fail);
});
});
describe('placement', () => {
it('sets data-placement when tooltip is rendered', done => {
const options = {
props: {
title: TEST_TITLE,
truncateTarget: 'child',
placement: 'bottom',
},
};
vm = mountTooltipOnTruncate(options, (h) => [
h('a', { style: STYLE_TRUNCATED }, TEST_TITLE),
]);
vm.$nextTick()
.then(() => {
expect(vm.$el).toHaveClass(CLASS_SHOW_TOOLTIP);
expect(vm.$el).toHaveData('placement', options.props.placement);
})
.then(done)
.catch(done.fail);
});
});
});
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