Commit b621b721 authored by Kushal Pandya's avatar Kushal Pandya

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

Display Scoped Labels on Issue Board

Closes #10921

See merge request gitlab-org/gitlab-ee!10669
parents f33ea0ea f7907057
......@@ -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,9 +190,18 @@ export default {
</h4>
</div>
<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
v-for="label in orderedLabels"
v-if="showLabel(label)"
v-else
:key="label.id"
v-gl-tooltip
:style="labelStyle(label)"
......@@ -194,6 +212,7 @@ export default {
>
{{ 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" }
%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