Commit a5f13e59 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '10921-display-scoped-labels-ce' into 'master'

Display scoped labels in Issue Boards

See merge request gitlab-org/gitlab-ce!27164
parents d83eb63b b5ab1d91
...@@ -16,6 +16,7 @@ import TimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue'; ...@@ -16,6 +16,7 @@ import TimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue';
import MilestoneSelect from '~/milestone_select'; import MilestoneSelect from '~/milestone_select';
import RemoveBtn from './sidebar/remove_issue.vue'; import RemoveBtn from './sidebar/remove_issue.vue';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
import { isScopedLabel } from '~/lib/utils/common_utils';
export default Vue.extend({ export default Vue.extend({
components: { components: {
...@@ -140,5 +141,11 @@ export default Vue.extend({ ...@@ -140,5 +141,11 @@ export default Vue.extend({
Flash(__('An error occurred while saving assignees')); Flash(__('An error occurred while saving assignees'));
}); });
}, },
showScopedLabels(label) {
return boardsStore.scopedLabels.enabled && isScopedLabel(label);
},
helpLink() {
return boardsStore.scopedLabels.helpLink;
},
}, },
}); });
...@@ -9,6 +9,8 @@ import eventHub from '../eventhub'; ...@@ -9,6 +9,8 @@ import eventHub from '../eventhub';
import IssueDueDate from './issue_due_date.vue'; import IssueDueDate from './issue_due_date.vue';
import IssueTimeEstimate from './issue_time_estimate.vue'; import IssueTimeEstimate from './issue_time_estimate.vue';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
import IssueCardInnerScopedLabel from './issue_card_inner_scoped_label.vue';
import { isScopedLabel } from '~/lib/utils/common_utils';
export default { export default {
components: { components: {
...@@ -17,6 +19,7 @@ export default { ...@@ -17,6 +19,7 @@ export default {
TooltipOnTruncate, TooltipOnTruncate,
IssueDueDate, IssueDueDate,
IssueTimeEstimate, IssueTimeEstimate,
IssueCardInnerScopedLabel,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -96,6 +99,9 @@ export default { ...@@ -96,6 +99,9 @@ export default {
orderedLabels() { orderedLabels() {
return _.sortBy(this.issue.labels, 'title'); return _.sortBy(this.issue.labels, 'title');
}, },
helpLink() {
return boardsStore.scopedLabels.helpLink;
},
}, },
methods: { methods: {
isIndexLessThanlimit(index) { isIndexLessThanlimit(index) {
...@@ -159,6 +165,9 @@ export default { ...@@ -159,6 +165,9 @@ export default {
color: label.textColor, color: label.textColor,
}; };
}, },
showScopedLabel(label) {
return boardsStore.scopedLabels.enabled && isScopedLabel(label);
},
}, },
}; };
</script> </script>
...@@ -179,9 +188,18 @@ export default { ...@@ -179,9 +188,18 @@ export default {
</h4> </h4>
</div> </div>
<div v-if="showLabelFooter" class="board-card-labels prepend-top-4 d-flex flex-wrap"> <div v-if="showLabelFooter" class="board-card-labels prepend-top-4 d-flex flex-wrap">
<template v-for="label in orderedLabels" v-if="showLabel(label)">
<issue-card-inner-scoped-label
v-if="showScopedLabel(label)"
:key="label.id"
:label="label"
:label-style="labelStyle(label)"
:scoped-labels-documentation-link="helpLink"
@scoped-label-click="filterByLabel($event)"
/>
<button <button
v-for="label in orderedLabels" v-else
v-if="showLabel(label)"
:key="label.id" :key="label.id"
v-gl-tooltip v-gl-tooltip
:style="labelStyle(label)" :style="labelStyle(label)"
...@@ -192,6 +210,7 @@ export default { ...@@ -192,6 +210,7 @@ export default {
> >
{{ label.title }} {{ label.title }}
</button> </button>
</template>
</div> </div>
<div class="board-card-footer d-flex justify-content-between align-items-end"> <div class="board-card-footer d-flex justify-content-between align-items-end">
<div <div
......
<script>
import { GlLink, GlTooltip } from '@gitlab/ui';
export default {
components: {
GlTooltip,
GlLink,
},
props: {
label: {
type: Object,
required: true,
},
labelStyle: {
type: Object,
required: true,
},
scopedLabelsDocumentationLink: {
type: String,
required: true,
},
},
};
</script>
<template>
<span
class="d-inline-block position-relative scoped-label-wrapper append-right-4 prepend-top-4 board-label"
>
<a @click="$emit('scoped-label-click', label)">
<span :ref="'labelTitleRef'" :style="labelStyle" class="badge label color-label">
{{ label.title }}
</span>
<gl-tooltip :target="() => $refs.labelTitleRef" placement="top" boundary="viewport">
<span class="font-weight-bold scoped-label-tooltip-title">{{ __('Scoped label') }}</span
><br />
{{ label.description }}
</gl-tooltip>
</a>
<gl-link :href="scopedLabelsDocumentationLink" target="_blank" class="label scoped-label"
><i class="fa fa-question-circle" :style="labelStyle"></i
></gl-link>
</span>
</template>
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
import Vue from 'vue'; import Vue from 'vue';
import '~/vue_shared/models/label'; import '~/vue_shared/models/label';
import { isEE } from '~/lib/utils/common_utils'; import { isEE, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import IssueProject from './project'; import IssueProject from './project';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
...@@ -141,7 +141,7 @@ class ListIssue { ...@@ -141,7 +141,7 @@ class ListIssue {
* PATCH the said object. * PATCH the said object.
*/ */
if (body) { if (body) {
this.labels = body.labels; this.labels = convertObjectPropsToCamelCase(body.labels, { deep: true });
} }
}); });
} }
......
...@@ -9,6 +9,10 @@ import { getUrlParamsArray, parseBoolean } from '~/lib/utils/common_utils'; ...@@ -9,6 +9,10 @@ import { getUrlParamsArray, parseBoolean } from '~/lib/utils/common_utils';
const boardsStore = { const boardsStore = {
disabled: false, disabled: false,
scopedLabels: {
helpLink: '',
enabled: false,
},
filter: { filter: {
path: '', path: '',
}, },
......
...@@ -11,7 +11,7 @@ import CreateLabelDropdown from './create_label'; ...@@ -11,7 +11,7 @@ import CreateLabelDropdown from './create_label';
import flash from './flash'; import flash from './flash';
import ModalStore from './boards/stores/modal_store'; import ModalStore from './boards/stores/modal_store';
import boardsStore from './boards/stores/boards_store'; import boardsStore from './boards/stores/boards_store';
import { isEE } from '~/lib/utils/common_utils'; import { isEE, isScopedLabel } from '~/lib/utils/common_utils';
export default class LabelsSelect { export default class LabelsSelect {
constructor(els, options = {}) { constructor(els, options = {}) {
...@@ -546,8 +546,6 @@ export default class LabelsSelect { ...@@ -546,8 +546,6 @@ export default class LabelsSelect {
].join(''), ].join(''),
); );
const isScopedLabel = label => label.title.indexOf('::') !== -1;
const tpl = _.template( const tpl = _.template(
[ [
'<% _.each(labels, function(label){ %>', '<% _.each(labels, function(label){ %>',
......
...@@ -724,6 +724,18 @@ export const NavigationType = { ...@@ -724,6 +724,18 @@ export const NavigationType = {
*/ */
export const isEE = () => window.gon && window.gon.ee; export const isEE = () => window.gon && window.gon.ee;
/**
* Checks if the given Label has a special syntax `::` in
* it's title.
*
* Expected Label to be an Object with `title` as a key:
* { title: 'LabelTitle', ...otherProperties };
*
* @param {Object} label
* @returns Boolean
*/
export const isScopedLabel = ({ title = '' }) => title.indexOf('::') !== -1;
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gl.utils = { window.gl.utils = {
...(window.gl.utils || {}), ...(window.gl.utils || {}),
......
<script> <script>
import DropdownValueScopedLabel from './dropdown_value_scoped_label.vue'; import DropdownValueScopedLabel from './dropdown_value_scoped_label.vue';
import DropdownValueRegularLabel from './dropdown_value_regular_label.vue'; import DropdownValueRegularLabel from './dropdown_value_regular_label.vue';
import { isScopedLabel } from '~/lib/utils/common_utils';
export default { export default {
components: { components: {
...@@ -45,8 +46,8 @@ export default { ...@@ -45,8 +46,8 @@ export default {
scopedLabelsDescription({ description = '' }) { scopedLabelsDescription({ description = '' }) {
return `<span class="font-weight-bold scoped-label-tooltip-title">Scoped label</span><br />${description}`; return `<span class="font-weight-bold scoped-label-tooltip-title">Scoped label</span><br />${description}`;
}, },
showScopedLabels({ title = '' }) { showScopedLabels(label) {
return this.enableScopedLabels && title.indexOf('::') !== -1; return this.enableScopedLabels && isScopedLabel(label);
}, },
}, },
}; };
......
...@@ -424,6 +424,12 @@ ...@@ -424,6 +424,12 @@
margin: 0; margin: 0;
line-height: $gl-line-height; line-height: $gl-line-height;
} }
&.board-label {
.scoped-label {
top: 1px;
}
}
} }
// Label inside title of Delete Label Modal // Label inside title of Delete Label Modal
......
...@@ -7,10 +7,17 @@ ...@@ -7,10 +7,17 @@
.value.issuable-show-labels.dont-hide .value.issuable-show-labels.dont-hide
%span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" } %span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
= _("None") = _("None")
%a{ href: "#", %span{ "v-for" => "label in issue.labels" }
"v-for" => "label in issue.labels" } %span.d-inline-block.position-relative.scoped-label-wrapper{ "v-if" => "showScopedLabels(label)" }
%a{ href: '#' }
%span.badge.color-label.label{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
{{ label.title }}
%a.label.scoped-label{ ":href" => "helpLink()" }
%i.fa.fa-question-circle{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
%a{ href: "#", "v-else" => true }
.badge.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" } .badge.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
{{ label.title }} {{ label.title }}
- if can_admin_issue? - if can_admin_issue?
.selectbox .selectbox
%input{ type: "hidden", %input{ type: "hidden",
......
---
title: Display scoped labels in Issue Boards
merge_request: 27164
author:
type: fixed
import Vue from 'vue';
import IssueCardInnerScopedLabel from '~/boards/components/issue_card_inner_scoped_label.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('IssueCardInnerScopedLabel Component', () => {
let vm;
const Component = Vue.extend(IssueCardInnerScopedLabel);
const props = {
label: { title: 'Foo::Bar', description: 'Some Random Description' },
labelStyle: { background: 'white', color: 'black' },
scopedLabelsDocumentationLink: '/docs-link',
};
const createComponent = () => mountComponent(Component, { ...props });
beforeEach(() => {
vm = createComponent();
});
afterEach(() => {
vm.$destroy();
});
it('should render label title', () => {
expect(vm.$el.querySelector('.color-label').textContent.trim()).toEqual('Foo::Bar');
});
it('should render question mark symbol', () => {
expect(vm.$el.querySelector('.fa-question-circle')).not.toBeNull();
});
it('should render label style provided', () => {
const node = vm.$el.querySelector('.color-label');
expect(node.style.background).toEqual(props.labelStyle.background);
expect(node.style.color).toEqual(props.labelStyle.color);
});
it('should render the docs link', () => {
expect(vm.$el.querySelector('a.scoped-label').href).toContain(
props.scopedLabelsDocumentationLink,
);
});
});
...@@ -894,4 +894,14 @@ describe('common_utils', () => { ...@@ -894,4 +894,14 @@ describe('common_utils', () => {
expect(commonUtils.isInViewport(el)).toBe(false); expect(commonUtils.isInViewport(el)).toBe(false);
}); });
}); });
describe('isScopedLabel', () => {
it('returns true when `::` is present in title', () => {
expect(commonUtils.isScopedLabel({ title: 'foo::bar' })).toBe(true);
});
it('returns false when `::` is not present', () => {
expect(commonUtils.isScopedLabel({ title: 'foobar' })).toBe(false);
});
});
}); });
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