Display scoped labels in Issue Boards
This change brings new Scoped labels to Issue board as well. With the last change, this was missed.
This commit is contained in:
parent
d83eb63bee
commit
b5ab1d91e3
|
@ -16,6 +16,7 @@ import TimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue';
|
|||
import MilestoneSelect from '~/milestone_select';
|
||||
import RemoveBtn from './sidebar/remove_issue.vue';
|
||||
import boardsStore from '../stores/boards_store';
|
||||
import { isScopedLabel } from '~/lib/utils/common_utils';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
|
@ -140,5 +141,11 @@ export default Vue.extend({
|
|||
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';
|
|||
import IssueDueDate from './issue_due_date.vue';
|
||||
import IssueTimeEstimate from './issue_time_estimate.vue';
|
||||
import boardsStore from '../stores/boards_store';
|
||||
import IssueCardInnerScopedLabel from './issue_card_inner_scoped_label.vue';
|
||||
import { isScopedLabel } from '~/lib/utils/common_utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -17,6 +19,7 @@ export default {
|
|||
TooltipOnTruncate,
|
||||
IssueDueDate,
|
||||
IssueTimeEstimate,
|
||||
IssueCardInnerScopedLabel,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
@ -96,6 +99,9 @@ export default {
|
|||
orderedLabels() {
|
||||
return _.sortBy(this.issue.labels, 'title');
|
||||
},
|
||||
helpLink() {
|
||||
return boardsStore.scopedLabels.helpLink;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isIndexLessThanlimit(index) {
|
||||
|
@ -159,6 +165,9 @@ export default {
|
|||
color: label.textColor,
|
||||
};
|
||||
},
|
||||
showScopedLabel(label) {
|
||||
return boardsStore.scopedLabels.enabled && isScopedLabel(label);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -179,19 +188,29 @@ export default {
|
|||
</h4>
|
||||
</div>
|
||||
<div v-if="showLabelFooter" class="board-card-labels prepend-top-4 d-flex flex-wrap">
|
||||
<button
|
||||
v-for="label in orderedLabels"
|
||||
v-if="showLabel(label)"
|
||||
:key="label.id"
|
||||
v-gl-tooltip
|
||||
:style="labelStyle(label)"
|
||||
:title="label.description"
|
||||
class="badge color-label append-right-4 prepend-top-4"
|
||||
type="button"
|
||||
@click="filterByLabel(label)"
|
||||
>
|
||||
{{ label.title }}
|
||||
</button>
|
||||
<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
|
||||
v-else
|
||||
:key="label.id"
|
||||
v-gl-tooltip
|
||||
:style="labelStyle(label)"
|
||||
:title="label.description"
|
||||
class="badge color-label append-right-4 prepend-top-4"
|
||||
type="button"
|
||||
@click="filterByLabel(label)"
|
||||
>
|
||||
{{ label.title }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
<div class="board-card-footer d-flex justify-content-between align-items-end">
|
||||
<div
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
<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 @@
|
|||
|
||||
import Vue from 'vue';
|
||||
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 boardsStore from '../stores/boards_store';
|
||||
|
||||
|
@ -141,7 +141,7 @@ class ListIssue {
|
|||
* PATCH the said object.
|
||||
*/
|
||||
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';
|
|||
|
||||
const boardsStore = {
|
||||
disabled: false,
|
||||
scopedLabels: {
|
||||
helpLink: '',
|
||||
enabled: false,
|
||||
},
|
||||
filter: {
|
||||
path: '',
|
||||
},
|
||||
|
|
|
@ -11,7 +11,7 @@ import CreateLabelDropdown from './create_label';
|
|||
import flash from './flash';
|
||||
import ModalStore from './boards/stores/modal_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 {
|
||||
constructor(els, options = {}) {
|
||||
|
@ -546,8 +546,6 @@ export default class LabelsSelect {
|
|||
].join(''),
|
||||
);
|
||||
|
||||
const isScopedLabel = label => label.title.indexOf('::') !== -1;
|
||||
|
||||
const tpl = _.template(
|
||||
[
|
||||
'<% _.each(labels, function(label){ %>',
|
||||
|
|
|
@ -724,6 +724,18 @@ export const NavigationType = {
|
|||
*/
|
||||
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.utils = {
|
||||
...(window.gl.utils || {}),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import DropdownValueScopedLabel from './dropdown_value_scoped_label.vue';
|
||||
import DropdownValueRegularLabel from './dropdown_value_regular_label.vue';
|
||||
import { isScopedLabel } from '~/lib/utils/common_utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -45,8 +46,8 @@ export default {
|
|||
scopedLabelsDescription({ description = '' }) {
|
||||
return `<span class="font-weight-bold scoped-label-tooltip-title">Scoped label</span><br />${description}`;
|
||||
},
|
||||
showScopedLabels({ title = '' }) {
|
||||
return this.enableScopedLabels && title.indexOf('::') !== -1;
|
||||
showScopedLabels(label) {
|
||||
return this.enableScopedLabels && isScopedLabel(label);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -424,6 +424,12 @@
|
|||
margin: 0;
|
||||
line-height: $gl-line-height;
|
||||
}
|
||||
|
||||
&.board-label {
|
||||
.scoped-label {
|
||||
top: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Label inside title of Delete Label Modal
|
||||
|
|
|
@ -7,10 +7,17 @@
|
|||
.value.issuable-show-labels.dont-hide
|
||||
%span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
|
||||
= _("None")
|
||||
%a{ href: "#",
|
||||
"v-for" => "label in issue.labels" }
|
||||
.badge.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
|
||||
{{ label.title }}
|
||||
%span{ "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 }" }
|
||||
{{ label.title }}
|
||||
|
||||
- if can_admin_issue?
|
||||
.selectbox
|
||||
%input{ type: "hidden",
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Display scoped labels in Issue Boards
|
||||
merge_request: 27164
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,43 @@
|
|||
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', () => {
|
|||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue