Commit b5ab1d91 authored by Rajat Jain's avatar Rajat Jain Committed by Kushal Pandya

Display scoped labels in Issue Boards

This change brings new Scoped labels to Issue board as well.
With the last change, this was missed.
parent d83eb63b
...@@ -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