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

Display Scoped Labels on Issue Board

This change brings Scoped Labels to the Issue Board as well.
With the last change where we introduced these special labels, we
sort of missed Issue Boards.
parent f33ea0ea
......@@ -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;
},
},
});
......@@ -10,6 +10,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: {
......@@ -19,6 +21,7 @@ export default {
IssueDueDate,
IssueCardWeight,
IssueTimeEstimate,
IssueCardInnerScopedLabel,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -98,6 +101,9 @@ export default {
orderedLabels() {
return _.sortBy(this.issue.labels, 'title');
},
helpLink() {
return boardsStore.scopedLabels.helpLink;
},
},
methods: {
isIndexLessThanlimit(index) {
......@@ -161,6 +167,9 @@ export default {
color: label.textColor,
};
},
showScopedLabel(label) {
return boardsStore.scopedLabels.enabled && isScopedLabel(label);
},
},
};
</script>
......@@ -181,19 +190,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
......
<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>
......@@ -396,8 +396,6 @@ export default () => {
groupId: Number(dataset.groupId),
scopedIssueBoardFeatureEnabled: parseBoolean(dataset.scopedIssueBoardFeatureEnabled),
weights: JSON.parse(dataset.weights),
enableScopedLabels: $boardApp.dataset.scopedLabels,
keyValueDocumentationLink: $boardApp.dataset.keyValueDocumentationLink,
};
return { boardsSelectorProps };
......
......@@ -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 });
}
});
}
......
......@@ -10,6 +10,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 || {}),
......
<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",
......
......@@ -22,17 +22,31 @@ class BoardsStoreEE {
this.store.create = () => {
baseCreate();
if (this.$boardApp) {
const {
dataset: {
boardMilestoneId,
boardMilestoneTitle,
boardAssigneeUsername,
labels,
boardWeight,
weightFeatureAvailable,
scopedLabels,
scopedLabelsDocumentationLink,
},
} = this.$boardApp;
this.store.boardConfig = {
milestoneId: parseInt(this.$boardApp.dataset.boardMilestoneId, 10),
milestoneTitle: this.$boardApp.dataset.boardMilestoneTitle || '',
assigneeUsername: this.$boardApp.dataset.boardAssigneeUsername,
labels: JSON.parse(this.$boardApp.dataset.labels || []),
weight: parseInt(this.$boardApp.dataset.boardWeight, 10),
milestoneId: parseInt(boardMilestoneId, 10),
milestoneTitle: boardMilestoneTitle || '',
assigneeUsername: boardAssigneeUsername,
labels: JSON.parse(labels || []),
weight: parseInt(boardWeight, 10),
};
this.store.cantEdit = [];
this.store.weightFeatureAvailable = parseBoolean(
this.$boardApp.dataset.weightFeatureAvailable,
);
this.store.weightFeatureAvailable = parseBoolean(weightFeatureAvailable);
this.store.scopedLabels = {
enabled: parseBoolean(scopedLabels),
helpLink: scopedLabelsDocumentationLink,
};
this.initBoardFilters();
}
};
......
import Labels from '~/labels';
import $ from 'jquery';
import { isScopedLabel } from '~/lib/utils/common_utils';
class LabelsEE extends Labels {
addBinding() {
......@@ -11,12 +12,12 @@ class LabelsEE extends Labels {
const title = $(this).val();
const $parentEl = $('.label-form');
const hasKeyValue = $parentEl.find('.js-has-scoped-labels');
const useKeyValue = $parentEl.find('.js-use-scoped-labels');
const hasScoped = $parentEl.find('.js-has-scoped-labels');
const useScoped = $parentEl.find('.js-use-scoped-labels');
const isKeyVal = title.indexOf('::') === -1;
hasKeyValue.toggleClass('hidden', isKeyVal);
useKeyValue.toggleClass('hidden', !isKeyVal);
const isScoped = isScopedLabel({ title });
hasScoped.toggleClass('hidden', isScoped);
useScoped.toggleClass('hidden', !isScoped);
}
}
......
......@@ -32,7 +32,7 @@ module EE
focus_mode_available: parent.feature_available?(:issue_board_focus_mode),
weight_feature_available: parent.feature_available?(:issue_weights).to_s,
show_promotion: show_feature_promotion,
scoped_labels: parent.feature_available?(:scoped_labels),
scoped_labels: parent.feature_available?(:scoped_labels)&.to_s,
scoped_labels_documentation_link: help_page_path('user/project/labels.md', anchor: 'scoped-labels')
}
......
---
title: Display Scoped Labels on Issue Board
merge_request: 10669
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', () => {
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