[frontend] backport of scoped labels
Scoped labels in EE require additional changes in CE code.
This commit is contained in:
parent
d0a0d3d3d5
commit
97ab853996
20 changed files with 466 additions and 64 deletions
|
@ -119,7 +119,17 @@ class ListIssue {
|
|||
}
|
||||
|
||||
const projectPath = this.project ? this.project.path : '';
|
||||
return Vue.http.patch(`${this.path}.json`, data);
|
||||
return Vue.http.patch(`${this.path}.json`, data).then(({ body = {} } = {}) => {
|
||||
/**
|
||||
* Since post implementation of Scoped labels, server can reject
|
||||
* same key-ed labels. To keep the UI and server Model consistent,
|
||||
* we're just assigning labels that server echo's back to us when we
|
||||
* PATCH the said object.
|
||||
*/
|
||||
if (body) {
|
||||
this.labels = body.labels;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +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';
|
||||
|
||||
export default class LabelsSelect {
|
||||
constructor(els, options = {}) {
|
||||
|
@ -86,8 +87,9 @@ export default class LabelsSelect {
|
|||
return this.value;
|
||||
})
|
||||
.get();
|
||||
const scopedLabels = $dropdown.data('scopedLabels');
|
||||
const scopedLabelsDocumentationLink = $dropdown.data('scopedLabelsDocumentationLink');
|
||||
const { handleClick } = options;
|
||||
|
||||
$sidebarLabelTooltip.tooltip();
|
||||
|
||||
if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
|
||||
|
@ -132,8 +134,48 @@ export default class LabelsSelect {
|
|||
template = LabelsSelect.getLabelTemplate({
|
||||
labels: data.labels,
|
||||
issueUpdateURL,
|
||||
enableScopedLabels: scopedLabels,
|
||||
scopedLabelsDocumentationLink,
|
||||
});
|
||||
labelCount = data.labels.length;
|
||||
|
||||
// EE Specific
|
||||
if (isEE) {
|
||||
/**
|
||||
* For Scoped labels, the last label selected with the
|
||||
* same key will be applied to the current issueable.
|
||||
*
|
||||
* If these are the labels - priority::1, priority::2; and if
|
||||
* we apply them in the same order, only priority::2 will stick
|
||||
* with the issuable.
|
||||
*
|
||||
* In the current dropdown implementation, we keep track of all
|
||||
* the labels selected via a hidden DOM element. Since a User
|
||||
* can select priority::1 and priority::2 at the same time, the
|
||||
* DOM will have 2 hidden input and the dropdown will show both
|
||||
* the items selected but in reality server only applied
|
||||
* priority::2.
|
||||
*
|
||||
* We find all the labels then find all the labels server accepted
|
||||
* and then remove the excess ones.
|
||||
*/
|
||||
const toRemoveIds = Array.from(
|
||||
$form.find("input[type='hidden'][name='" + fieldName + "']"),
|
||||
)
|
||||
.map(el => el.value)
|
||||
.map(Number);
|
||||
|
||||
data.labels.forEach(label => {
|
||||
const index = toRemoveIds.indexOf(label.id);
|
||||
toRemoveIds.splice(index, 1);
|
||||
});
|
||||
|
||||
toRemoveIds.forEach(id => {
|
||||
$form
|
||||
.find("input[type='hidden'][name='" + fieldName + "'][value='" + id + "']")
|
||||
.remove();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
template = '<span class="no-value">None</span>';
|
||||
}
|
||||
|
@ -358,6 +400,7 @@ export default class LabelsSelect {
|
|||
} else {
|
||||
if (!$dropdown.hasClass('js-filter-bulk-update')) {
|
||||
saveLabelData();
|
||||
$dropdown.data('glDropdown').clearMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -471,19 +514,61 @@ export default class LabelsSelect {
|
|||
// so best approach is to use traditional way of
|
||||
// concatenation
|
||||
// see: http://2ality.com/2016/05/template-literal-whitespace.html#joining-arrays
|
||||
const tpl = _.template(
|
||||
|
||||
const labelTemplate = _.template(
|
||||
[
|
||||
'<% _.each(labels, function(label){ %>',
|
||||
'<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">',
|
||||
'<span class="badge label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">',
|
||||
'<span class="badge label has-tooltip color-label" <%= linkAttrs %> title="<%= tooltipTitleTemplate({ label, isScopedLabel, enableScopedLabels }) %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">',
|
||||
'<%- label.title %>',
|
||||
'</span>',
|
||||
'</a>',
|
||||
].join(''),
|
||||
);
|
||||
|
||||
const infoIconTemplate = _.template(
|
||||
[
|
||||
'<a href="<%= scopedLabelsDocumentationLink %>" class="label scoped-label" target="_blank" rel="noopener">',
|
||||
'<i class="fa fa-question-circle" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"></i>',
|
||||
'</a>',
|
||||
].join(''),
|
||||
);
|
||||
|
||||
const tooltipTitleTemplate = _.template(
|
||||
[
|
||||
'<% if (isScopedLabel(label) && enableScopedLabels) { %>',
|
||||
"<span class='font-weight-bold scoped-label-tooltip-title'>Scoped label</span>",
|
||||
'<br />',
|
||||
'<%- label.description %>',
|
||||
'<% } else { %>',
|
||||
'<%- label.description %>',
|
||||
'<% } %>',
|
||||
].join(''),
|
||||
);
|
||||
|
||||
const isScopedLabel = label => label.title.indexOf('::') !== -1;
|
||||
|
||||
const tpl = _.template(
|
||||
[
|
||||
'<% _.each(labels, function(label){ %>',
|
||||
'<% if (isScopedLabel(label) && enableScopedLabels) { %>',
|
||||
'<span class="d-inline-block position-relative scoped-label-wrapper">',
|
||||
'<%= labelTemplate({ label, issueUpdateURL, isScopedLabel, enableScopedLabels, tooltipTitleTemplate, linkAttrs: \'data-html="true"\' }) %>',
|
||||
'<%= infoIconTemplate({ label,scopedLabelsDocumentationLink }) %>',
|
||||
'</span>',
|
||||
'<% } else { %>',
|
||||
'<%= labelTemplate({ label, issueUpdateURL, isScopedLabel, enableScopedLabels, tooltipTitleTemplate, linkAttrs: "" }) %>',
|
||||
'<% } %>',
|
||||
'<% }); %>',
|
||||
].join(''),
|
||||
);
|
||||
|
||||
return tpl(tplData);
|
||||
return tpl({
|
||||
...tplData,
|
||||
labelTemplate,
|
||||
infoIconTemplate,
|
||||
tooltipTitleTemplate,
|
||||
isScopedLabel,
|
||||
});
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import Labels from '~/labels';
|
||||
import Labels from 'ee_else_ce/labels';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => new Labels());
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import Labels from '~/labels';
|
||||
import Labels from 'ee_else_ce/labels';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => new Labels());
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import Labels from '~/labels';
|
||||
import Labels from 'ee_else_ce/labels';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => new Labels());
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import Labels from '~/labels';
|
||||
import Labels from 'ee_else_ce/labels';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => new Labels());
|
||||
|
|
|
@ -75,6 +75,16 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
enableScopedLabels: {
|
||||
type: Boolean,
|
||||
require: false,
|
||||
default: false,
|
||||
},
|
||||
scopedLabelsDocumentationLink: {
|
||||
type: String,
|
||||
require: false,
|
||||
default: '#',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
hiddenInputName() {
|
||||
|
@ -123,7 +133,12 @@ export default {
|
|||
@onValueClick="handleCollapsedValueClick"
|
||||
/>
|
||||
<dropdown-title :can-edit="canEdit" />
|
||||
<dropdown-value :labels="context.labels" :label-filter-base-path="labelFilterBasePath">
|
||||
<dropdown-value
|
||||
:labels="context.labels"
|
||||
:label-filter-base-path="labelFilterBasePath"
|
||||
:scoped-labels-documentation-link="scopedLabelsDocumentationLink"
|
||||
:enable-scoped-labels="enableScopedLabels"
|
||||
>
|
||||
<slot></slot>
|
||||
</dropdown-value>
|
||||
<div v-if="canEdit" class="selectbox js-selectbox" style="display: none;">
|
||||
|
@ -142,6 +157,8 @@ export default {
|
|||
:namespace="namespace"
|
||||
:labels="context.labels"
|
||||
:show-extra-options="!showCreate"
|
||||
:scoped-labels-documentation-link="scopedLabelsDocumentationLink"
|
||||
:enable-scoped-labels="enableScopedLabels"
|
||||
/>
|
||||
<div
|
||||
class="dropdown-menu dropdown-select dropdown-menu-paging
|
||||
|
|
|
@ -31,6 +31,16 @@ export default {
|
|||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
enableScopedLabels: {
|
||||
type: Boolean,
|
||||
require: false,
|
||||
default: false,
|
||||
},
|
||||
scopedLabelsDocumentationLink: {
|
||||
type: String,
|
||||
require: false,
|
||||
default: '#',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
dropdownToggleText() {
|
||||
|
@ -61,6 +71,8 @@ export default {
|
|||
:data-labels="labelsPath"
|
||||
:data-namespace-path="namespace"
|
||||
:data-show-any="showExtraOptions"
|
||||
:data-scoped-labels="enableScopedLabels"
|
||||
:data-scoped-labels-documentation-link="scopedLabelsDocumentationLink"
|
||||
type="button"
|
||||
class="dropdown-menu-toggle wide js-label-select js-multiselect js-context-config-modal"
|
||||
data-toggle="dropdown"
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
<script>
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import DropdownValueScopedLabel from './dropdown_value_scoped_label.vue';
|
||||
import DropdownValueRegularLabel from './dropdown_value_regular_label.vue';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
tooltip,
|
||||
components: {
|
||||
DropdownValueScopedLabel,
|
||||
DropdownValueRegularLabel,
|
||||
},
|
||||
props: {
|
||||
labels: {
|
||||
|
@ -14,6 +16,16 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
enableScopedLabels: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
scopedLabelsDocumentationLink: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '#',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isEmpty() {
|
||||
|
@ -30,6 +42,12 @@ export default {
|
|||
backgroundColor: label.color,
|
||||
};
|
||||
},
|
||||
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;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -44,17 +62,24 @@ export default {
|
|||
<span v-if="isEmpty" class="text-secondary">
|
||||
<slot>{{ __('None') }}</slot>
|
||||
</span>
|
||||
<a v-for="label in labels" v-else :key="label.id" :href="labelFilterUrl(label)">
|
||||
<span
|
||||
v-tooltip
|
||||
:style="labelStyle(label)"
|
||||
:title="label.description"
|
||||
class="badge color-label"
|
||||
data-placement="bottom"
|
||||
data-container="body"
|
||||
>
|
||||
{{ label.title }}
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<template v-for="label in labels" v-else>
|
||||
<dropdown-value-scoped-label
|
||||
v-if="showScopedLabels(label)"
|
||||
:key="label.id"
|
||||
:label="label"
|
||||
:label-filter-url="labelFilterUrl(label)"
|
||||
:label-style="labelStyle(label)"
|
||||
:scoped-labels-documentation-link="scopedLabelsDocumentationLink"
|
||||
/>
|
||||
|
||||
<dropdown-value-regular-label
|
||||
v-else
|
||||
:key="label.id"
|
||||
:label="label"
|
||||
:label-filter-url="labelFilterUrl(label)"
|
||||
:label-style="labelStyle(label)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
<script>
|
||||
import { GlLink, GlTooltip } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlTooltip,
|
||||
GlLink,
|
||||
},
|
||||
props: {
|
||||
label: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
labelStyle: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
labelFilterUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a ref="regularLabelRef" :href="labelFilterUrl">
|
||||
<span :style="labelStyle" class="badge color-label">
|
||||
{{ label.title }}
|
||||
</span>
|
||||
<gl-tooltip :target="() => $refs.regularLabelRef" placement="top" boundary="viewport">
|
||||
{{ label.description }}
|
||||
</gl-tooltip>
|
||||
</a>
|
||||
</template>
|
|
@ -0,0 +1,47 @@
|
|||
<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,
|
||||
},
|
||||
labelFilterUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="d-inline-block position-relative scoped-label-wrapper">
|
||||
<a :href="labelFilterUrl">
|
||||
<span :ref="`labelTitleRef`" :style="labelStyle" class="badge color-label 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>
|
|
@ -110,6 +110,16 @@
|
|||
font-size: 0;
|
||||
margin-bottom: -5px;
|
||||
}
|
||||
|
||||
.scoped-label-wrapper {
|
||||
.color-label {
|
||||
padding-right: $gl-padding-24;
|
||||
}
|
||||
|
||||
.scoped-label {
|
||||
right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-sidebar {
|
||||
|
|
|
@ -402,3 +402,39 @@
|
|||
.priority-labels-empty-state .svg-content img {
|
||||
max-width: $priority-label-empty-state-width;
|
||||
}
|
||||
|
||||
.scoped-label-tooltip-title {
|
||||
color: $indigo-300;
|
||||
}
|
||||
|
||||
.scoped-label-wrapper {
|
||||
&.label-link .color-label a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.color-label {
|
||||
padding-right: $gl-padding-24;
|
||||
}
|
||||
|
||||
.scoped-label {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 8px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
line-height: $gl-line-height;
|
||||
}
|
||||
}
|
||||
|
||||
// Label inside title of Delete Label Modal
|
||||
.modal-header .page-title {
|
||||
.scoped-label-wrapper {
|
||||
.scoped-label {
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
span.color-label {
|
||||
padding-right: $gl-padding-24;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6974,6 +6974,9 @@ msgstr ""
|
|||
msgid "Scope not supported with disabled 'users_search' feature!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Scoped label"
|
||||
msgstr ""
|
||||
|
||||
msgid "Scroll down to <strong>Google Code Project Hosting</strong> and enable the switch on the right."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -13,40 +13,104 @@ const mockLabels = [
|
|||
},
|
||||
];
|
||||
|
||||
const mockScopedLabels = [
|
||||
{
|
||||
id: 27,
|
||||
title: 'Foo::Bar',
|
||||
description: 'Foobar',
|
||||
color: '#333ABC',
|
||||
text_color: '#FFFFFF',
|
||||
},
|
||||
];
|
||||
|
||||
describe('LabelsSelect', () => {
|
||||
describe('getLabelTemplate', () => {
|
||||
const label = mockLabels[0];
|
||||
let $labelEl;
|
||||
describe('when normal label is present', () => {
|
||||
const label = mockLabels[0];
|
||||
let $labelEl;
|
||||
|
||||
beforeEach(() => {
|
||||
$labelEl = $(
|
||||
LabelsSelect.getLabelTemplate({
|
||||
labels: mockLabels,
|
||||
issueUpdateURL: mockUrl,
|
||||
}),
|
||||
);
|
||||
beforeEach(() => {
|
||||
$labelEl = $(
|
||||
LabelsSelect.getLabelTemplate({
|
||||
labels: mockLabels,
|
||||
issueUpdateURL: mockUrl,
|
||||
enableScopedLabels: true,
|
||||
scopedLabelsDocumentationLink: 'docs-link',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('generated label item template has correct label URL', () => {
|
||||
expect($labelEl.attr('href')).toBe('/foo/bar?label_name[]=Foo%20Label');
|
||||
});
|
||||
|
||||
it('generated label item template has correct label title', () => {
|
||||
expect($labelEl.find('span.label').text()).toBe(label.title);
|
||||
});
|
||||
|
||||
it('generated label item template has label description as title attribute', () => {
|
||||
expect($labelEl.find('span.label').attr('title')).toBe(label.description);
|
||||
});
|
||||
|
||||
it('generated label item template has correct label styles', () => {
|
||||
expect($labelEl.find('span.label').attr('style')).toBe(
|
||||
`background-color: ${label.color}; color: ${label.text_color};`,
|
||||
);
|
||||
});
|
||||
|
||||
it('generated label item has a badge class', () => {
|
||||
expect($labelEl.find('span').hasClass('badge')).toEqual(true);
|
||||
});
|
||||
|
||||
it('generated label item template does not have scoped-label class', () => {
|
||||
expect($labelEl.find('.scoped-label')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('generated label item template has correct label URL', () => {
|
||||
expect($labelEl.attr('href')).toBe('/foo/bar?label_name[]=Foo%20Label');
|
||||
});
|
||||
describe('when scoped label is present', () => {
|
||||
const label = mockScopedLabels[0];
|
||||
let $labelEl;
|
||||
|
||||
it('generated label item template has correct label title', () => {
|
||||
expect($labelEl.find('span.label').text()).toBe(label.title);
|
||||
});
|
||||
beforeEach(() => {
|
||||
$labelEl = $(
|
||||
LabelsSelect.getLabelTemplate({
|
||||
labels: mockScopedLabels,
|
||||
issueUpdateURL: mockUrl,
|
||||
enableScopedLabels: true,
|
||||
scopedLabelsDocumentationLink: 'docs-link',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('generated label item template has label description as title attribute', () => {
|
||||
expect($labelEl.find('span.label').attr('title')).toBe(label.description);
|
||||
});
|
||||
it('generated label item template has correct label URL', () => {
|
||||
expect($labelEl.find('a').attr('href')).toBe('/foo/bar?label_name[]=Foo%3A%3ABar');
|
||||
});
|
||||
|
||||
it('generated label item template has correct label styles', () => {
|
||||
expect($labelEl.find('span.label').attr('style')).toBe(
|
||||
`background-color: ${label.color}; color: ${label.text_color};`,
|
||||
);
|
||||
});
|
||||
it('generated label item template has correct label title', () => {
|
||||
expect($labelEl.find('span.label').text()).toBe(label.title);
|
||||
});
|
||||
|
||||
it('generated label item has a badge class', () => {
|
||||
expect($labelEl.find('span').hasClass('badge')).toEqual(true);
|
||||
it('generated label item template has html flag as true', () => {
|
||||
expect($labelEl.find('span.label').attr('data-html')).toBe('true');
|
||||
});
|
||||
|
||||
it('generated label item template has question icon', () => {
|
||||
expect($labelEl.find('i.fa-question-circle')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('generated label item template has scoped-label class', () => {
|
||||
expect($labelEl.find('.scoped-label')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('generated label item template has correct label styles', () => {
|
||||
expect($labelEl.find('span.label').attr('style')).toBe(
|
||||
`background-color: ${label.color}; color: ${label.text_color};`,
|
||||
);
|
||||
});
|
||||
|
||||
it('generated label item has a badge class', () => {
|
||||
expect($labelEl.find('span').hasClass('badge')).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -178,6 +178,7 @@ describe('Issue model', () => {
|
|||
spyOn(Vue.http, 'patch').and.callFake((url, data) => {
|
||||
expect(data.issue.assignee_ids).toEqual([1]);
|
||||
done();
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
issue.update('url');
|
||||
|
@ -187,6 +188,7 @@ describe('Issue model', () => {
|
|||
spyOn(Vue.http, 'patch').and.callFake((url, data) => {
|
||||
expect(data.issue.assignee_ids).toEqual([0]);
|
||||
done();
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
issue.removeAllAssignees();
|
||||
|
|
|
@ -45,12 +45,21 @@ describe('DropdownButtonComponent', () => {
|
|||
});
|
||||
const vmMoreLabels = createComponent(mockMoreLabels);
|
||||
|
||||
expect(vmMoreLabels.dropdownToggleText).toBe('Foo Label +1 more');
|
||||
expect(vmMoreLabels.dropdownToggleText).toBe(
|
||||
`Foo Label +${mockMoreLabels.labels.length - 1} more`,
|
||||
);
|
||||
vmMoreLabels.$destroy();
|
||||
});
|
||||
|
||||
it('returns first label name when `labels` prop has only one item present', () => {
|
||||
expect(vm.dropdownToggleText).toBe('Foo Label');
|
||||
const singleLabel = Object.assign({}, componentConfig, {
|
||||
labels: [mockLabels[0]],
|
||||
});
|
||||
const vmSingleLabel = createComponent(singleLabel);
|
||||
|
||||
expect(vmSingleLabel.dropdownToggleText).toBe(mockLabels[0].title);
|
||||
|
||||
vmSingleLabel.$destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -73,7 +82,7 @@ describe('DropdownButtonComponent', () => {
|
|||
const dropdownToggleTextEl = vm.$el.querySelector('.dropdown-toggle-text');
|
||||
|
||||
expect(dropdownToggleTextEl).not.toBeNull();
|
||||
expect(dropdownToggleTextEl.innerText.trim()).toBe('Foo Label');
|
||||
expect(dropdownToggleTextEl.innerText.trim()).toBe('Foo Label +1 more');
|
||||
});
|
||||
|
||||
it('renders dropdown button icon', () => {
|
||||
|
|
|
@ -35,9 +35,12 @@ describe('DropdownValueCollapsedComponent', () => {
|
|||
});
|
||||
|
||||
it('returns labels names separated by coma when `labels` prop has more than one item', () => {
|
||||
const vmMoreLabels = createComponent(mockLabels.concat(mockLabels));
|
||||
const labels = mockLabels.concat(mockLabels);
|
||||
const vmMoreLabels = createComponent(labels);
|
||||
|
||||
expect(vmMoreLabels.labelsList).toBe('Foo Label, Foo Label');
|
||||
const expectedText = labels.map(label => label.title).join(', ');
|
||||
|
||||
expect(vmMoreLabels.labelsList).toBe(expectedText);
|
||||
vmMoreLabels.$destroy();
|
||||
});
|
||||
|
||||
|
@ -49,14 +52,19 @@ describe('DropdownValueCollapsedComponent', () => {
|
|||
|
||||
const vmMoreLabels = createComponent(mockMoreLabels);
|
||||
|
||||
expect(vmMoreLabels.labelsList).toBe(
|
||||
'Foo Label, Foo Label, Foo Label, Foo Label, Foo Label, and 2 more',
|
||||
);
|
||||
const expectedText = `${mockMoreLabels
|
||||
.slice(0, 5)
|
||||
.map(label => label.title)
|
||||
.join(', ')}, and ${mockMoreLabels.length - 5} more`;
|
||||
|
||||
expect(vmMoreLabels.labelsList).toBe(expectedText);
|
||||
vmMoreLabels.$destroy();
|
||||
});
|
||||
|
||||
it('returns first label name when `labels` prop has only one item present', () => {
|
||||
expect(vm.labelsList).toBe('Foo Label');
|
||||
const text = mockLabels.map(label => label.title).join(', ');
|
||||
|
||||
expect(vm.labelsList).toBe(text);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import Vue from 'vue';
|
||||
import $ from 'jquery';
|
||||
|
||||
import dropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue';
|
||||
|
||||
|
@ -15,6 +16,7 @@ const createComponent = (
|
|||
return mountComponent(Component, {
|
||||
labels,
|
||||
labelFilterBasePath,
|
||||
enableScopedLabels: true,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -67,6 +69,26 @@ describe('DropdownValueComponent', () => {
|
|||
expect(styleObj.backgroundColor).toBe(label.color);
|
||||
});
|
||||
});
|
||||
|
||||
describe('scopedLabelsDescription', () => {
|
||||
it('returns html for tooltip', () => {
|
||||
const html = vm.scopedLabelsDescription(mockLabels[1]);
|
||||
const $el = $.parseHTML(html);
|
||||
|
||||
expect($el[0]).toHaveClass('scoped-label-tooltip-title');
|
||||
expect($el[2].textContent).toEqual(mockLabels[1].description);
|
||||
});
|
||||
});
|
||||
|
||||
describe('showScopedLabels', () => {
|
||||
it('returns true if the label is scoped label', () => {
|
||||
expect(vm.showScopedLabels(mockLabels[1])).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when label is a regular label', () => {
|
||||
expect(vm.showScopedLabels(mockLabels[0])).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
|
@ -91,15 +113,25 @@ describe('DropdownValueComponent', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('renders label element with tooltip and styles based on label details', () => {
|
||||
it('renders label element and styles based on label details', () => {
|
||||
const labelEl = vm.$el.querySelector('a span.badge.color-label');
|
||||
|
||||
expect(labelEl).not.toBeNull();
|
||||
expect(labelEl.dataset.placement).toBe('bottom');
|
||||
expect(labelEl.dataset.container).toBe('body');
|
||||
expect(labelEl.dataset.originalTitle).toBe(mockLabels[0].description);
|
||||
expect(labelEl.getAttribute('style')).toBe('background-color: rgb(186, 218, 85);');
|
||||
expect(labelEl.innerText.trim()).toBe(mockLabels[0].title);
|
||||
});
|
||||
|
||||
describe('label is of scoped-label type', () => {
|
||||
it('renders a scoped-label-wrapper span to incorporate 2 anchors', () => {
|
||||
expect(vm.$el.querySelector('span.scoped-label-wrapper')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('renders anchor tag containing question icon', () => {
|
||||
const anchor = vm.$el.querySelector('.scoped-label-wrapper a.scoped-label');
|
||||
|
||||
expect(anchor).not.toBeNull();
|
||||
expect(anchor.querySelector('i.fa-question-circle')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,6 +6,13 @@ export const mockLabels = [
|
|||
color: '#BADA55',
|
||||
text_color: '#FFFFFF',
|
||||
},
|
||||
{
|
||||
id: 27,
|
||||
title: 'Foo::Bar',
|
||||
description: 'Foobar',
|
||||
color: '#0033CC',
|
||||
text_color: '#FFFFFF',
|
||||
},
|
||||
];
|
||||
|
||||
export const mockSuggestedColors = [
|
||||
|
|
Loading…
Reference in a new issue