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 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({
|
||||||
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';
|
||||||
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 {
|
||||||
TooltipOnTruncate,
|
TooltipOnTruncate,
|
||||||
IssueDueDate,
|
IssueDueDate,
|
||||||
IssueTimeEstimate,
|
IssueTimeEstimate,
|
||||||
|
IssueCardInnerScopedLabel,
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
GlTooltip: GlTooltipDirective,
|
GlTooltip: GlTooltipDirective,
|
||||||
|
@ -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 {
|
||||||
color: label.textColor,
|
color: label.textColor,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
showScopedLabel(label) {
|
||||||
|
return boardsStore.scopedLabels.enabled && isScopedLabel(label);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -179,19 +188,29 @@ 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">
|
||||||
<button
|
<template v-for="label in orderedLabels" v-if="showLabel(label)">
|
||||||
v-for="label in orderedLabels"
|
<issue-card-inner-scoped-label
|
||||||
v-if="showLabel(label)"
|
v-if="showScopedLabel(label)"
|
||||||
:key="label.id"
|
:key="label.id"
|
||||||
v-gl-tooltip
|
:label="label"
|
||||||
:style="labelStyle(label)"
|
:label-style="labelStyle(label)"
|
||||||
:title="label.description"
|
:scoped-labels-documentation-link="helpLink"
|
||||||
class="badge color-label append-right-4 prepend-top-4"
|
@scoped-label-click="filterByLabel($event)"
|
||||||
type="button"
|
/>
|
||||||
@click="filterByLabel(label)"
|
|
||||||
>
|
<button
|
||||||
{{ label.title }}
|
v-else
|
||||||
</button>
|
: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>
|
||||||
<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
|
||||||
|
|
|
@ -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 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 {
|
||||||
* 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';
|
||||||
|
|
||||||
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';
|
||||||
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 {
|
||||||
].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 = {
|
||||||
*/
|
*/
|
||||||
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 || {}),
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<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 {
|
||||||
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 @@
|
||||||
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 @@
|
||||||
.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)" }
|
||||||
.badge.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
|
%a{ href: '#' }
|
||||||
{{ label.title }}
|
%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?
|
- if can_admin_issue?
|
||||||
.selectbox
|
.selectbox
|
||||||
%input{ type: "hidden",
|
%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);
|
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