[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 : '';
|
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 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';
|
||||||
|
|
||||||
export default class LabelsSelect {
|
export default class LabelsSelect {
|
||||||
constructor(els, options = {}) {
|
constructor(els, options = {}) {
|
||||||
|
@ -86,8 +87,9 @@ export default class LabelsSelect {
|
||||||
return this.value;
|
return this.value;
|
||||||
})
|
})
|
||||||
.get();
|
.get();
|
||||||
|
const scopedLabels = $dropdown.data('scopedLabels');
|
||||||
|
const scopedLabelsDocumentationLink = $dropdown.data('scopedLabelsDocumentationLink');
|
||||||
const { handleClick } = options;
|
const { handleClick } = options;
|
||||||
|
|
||||||
$sidebarLabelTooltip.tooltip();
|
$sidebarLabelTooltip.tooltip();
|
||||||
|
|
||||||
if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
|
if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
|
||||||
|
@ -132,8 +134,48 @@ export default class LabelsSelect {
|
||||||
template = LabelsSelect.getLabelTemplate({
|
template = LabelsSelect.getLabelTemplate({
|
||||||
labels: data.labels,
|
labels: data.labels,
|
||||||
issueUpdateURL,
|
issueUpdateURL,
|
||||||
|
enableScopedLabels: scopedLabels,
|
||||||
|
scopedLabelsDocumentationLink,
|
||||||
});
|
});
|
||||||
labelCount = data.labels.length;
|
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 {
|
} else {
|
||||||
template = '<span class="no-value">None</span>';
|
template = '<span class="no-value">None</span>';
|
||||||
}
|
}
|
||||||
|
@ -358,6 +400,7 @@ export default class LabelsSelect {
|
||||||
} else {
|
} else {
|
||||||
if (!$dropdown.hasClass('js-filter-bulk-update')) {
|
if (!$dropdown.hasClass('js-filter-bulk-update')) {
|
||||||
saveLabelData();
|
saveLabelData();
|
||||||
|
$dropdown.data('glDropdown').clearMenu();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -471,19 +514,61 @@ export default class LabelsSelect {
|
||||||
// so best approach is to use traditional way of
|
// so best approach is to use traditional way of
|
||||||
// concatenation
|
// concatenation
|
||||||
// see: http://2ality.com/2016/05/template-literal-whitespace.html#joining-arrays
|
// 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) %>">',
|
'<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 %>',
|
'<%- label.title %>',
|
||||||
'</span>',
|
'</span>',
|
||||||
'</a>',
|
'</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(''),
|
].join(''),
|
||||||
);
|
);
|
||||||
|
|
||||||
return tpl(tplData);
|
return tpl({
|
||||||
|
...tplData,
|
||||||
|
labelTemplate,
|
||||||
|
infoIconTemplate,
|
||||||
|
tooltipTitleTemplate,
|
||||||
|
isScopedLabel,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bindEvents() {
|
bindEvents() {
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
import Labels from '~/labels';
|
import Labels from 'ee_else_ce/labels';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => new 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());
|
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());
|
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());
|
document.addEventListener('DOMContentLoaded', () => new Labels());
|
||||||
|
|
|
@ -75,6 +75,16 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
enableScopedLabels: {
|
||||||
|
type: Boolean,
|
||||||
|
require: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
scopedLabelsDocumentationLink: {
|
||||||
|
type: String,
|
||||||
|
require: false,
|
||||||
|
default: '#',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hiddenInputName() {
|
hiddenInputName() {
|
||||||
|
@ -123,7 +133,12 @@ export default {
|
||||||
@onValueClick="handleCollapsedValueClick"
|
@onValueClick="handleCollapsedValueClick"
|
||||||
/>
|
/>
|
||||||
<dropdown-title :can-edit="canEdit" />
|
<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>
|
<slot></slot>
|
||||||
</dropdown-value>
|
</dropdown-value>
|
||||||
<div v-if="canEdit" class="selectbox js-selectbox" style="display: none;">
|
<div v-if="canEdit" class="selectbox js-selectbox" style="display: none;">
|
||||||
|
@ -142,6 +157,8 @@ export default {
|
||||||
:namespace="namespace"
|
:namespace="namespace"
|
||||||
:labels="context.labels"
|
:labels="context.labels"
|
||||||
:show-extra-options="!showCreate"
|
:show-extra-options="!showCreate"
|
||||||
|
:scoped-labels-documentation-link="scopedLabelsDocumentationLink"
|
||||||
|
:enable-scoped-labels="enableScopedLabels"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="dropdown-menu dropdown-select dropdown-menu-paging
|
class="dropdown-menu dropdown-select dropdown-menu-paging
|
||||||
|
|
|
@ -31,6 +31,16 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
enableScopedLabels: {
|
||||||
|
type: Boolean,
|
||||||
|
require: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
scopedLabelsDocumentationLink: {
|
||||||
|
type: String,
|
||||||
|
require: false,
|
||||||
|
default: '#',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
dropdownToggleText() {
|
dropdownToggleText() {
|
||||||
|
@ -61,6 +71,8 @@ export default {
|
||||||
:data-labels="labelsPath"
|
:data-labels="labelsPath"
|
||||||
:data-namespace-path="namespace"
|
:data-namespace-path="namespace"
|
||||||
:data-show-any="showExtraOptions"
|
:data-show-any="showExtraOptions"
|
||||||
|
:data-scoped-labels="enableScopedLabels"
|
||||||
|
:data-scoped-labels-documentation-link="scopedLabelsDocumentationLink"
|
||||||
type="button"
|
type="button"
|
||||||
class="dropdown-menu-toggle wide js-label-select js-multiselect js-context-config-modal"
|
class="dropdown-menu-toggle wide js-label-select js-multiselect js-context-config-modal"
|
||||||
data-toggle="dropdown"
|
data-toggle="dropdown"
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
<script>
|
<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 {
|
export default {
|
||||||
directives: {
|
components: {
|
||||||
tooltip,
|
DropdownValueScopedLabel,
|
||||||
|
DropdownValueRegularLabel,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
labels: {
|
labels: {
|
||||||
|
@ -14,6 +16,16 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
enableScopedLabels: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
scopedLabelsDocumentationLink: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '#',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isEmpty() {
|
isEmpty() {
|
||||||
|
@ -30,6 +42,12 @@ export default {
|
||||||
backgroundColor: label.color,
|
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>
|
</script>
|
||||||
|
@ -44,17 +62,24 @@ export default {
|
||||||
<span v-if="isEmpty" class="text-secondary">
|
<span v-if="isEmpty" class="text-secondary">
|
||||||
<slot>{{ __('None') }}</slot>
|
<slot>{{ __('None') }}</slot>
|
||||||
</span>
|
</span>
|
||||||
<a v-for="label in labels" v-else :key="label.id" :href="labelFilterUrl(label)">
|
|
||||||
<span
|
<template v-for="label in labels" v-else>
|
||||||
v-tooltip
|
<dropdown-value-scoped-label
|
||||||
:style="labelStyle(label)"
|
v-if="showScopedLabels(label)"
|
||||||
:title="label.description"
|
:key="label.id"
|
||||||
class="badge color-label"
|
:label="label"
|
||||||
data-placement="bottom"
|
:label-filter-url="labelFilterUrl(label)"
|
||||||
data-container="body"
|
:label-style="labelStyle(label)"
|
||||||
>
|
:scoped-labels-documentation-link="scopedLabelsDocumentationLink"
|
||||||
{{ label.title }}
|
/>
|
||||||
</span>
|
|
||||||
</a>
|
<dropdown-value-regular-label
|
||||||
|
v-else
|
||||||
|
:key="label.id"
|
||||||
|
:label="label"
|
||||||
|
:label-filter-url="labelFilterUrl(label)"
|
||||||
|
:label-style="labelStyle(label)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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;
|
font-size: 0;
|
||||||
margin-bottom: -5px;
|
margin-bottom: -5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scoped-label-wrapper {
|
||||||
|
.color-label {
|
||||||
|
padding-right: $gl-padding-24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoped-label {
|
||||||
|
right: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-sidebar {
|
.right-sidebar {
|
||||||
|
|
|
@ -402,3 +402,39 @@
|
||||||
.priority-labels-empty-state .svg-content img {
|
.priority-labels-empty-state .svg-content img {
|
||||||
max-width: $priority-label-empty-state-width;
|
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!"
|
msgid "Scope not supported with disabled 'users_search' feature!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Scoped label"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Scroll down to <strong>Google Code Project Hosting</strong> and enable the switch on the right."
|
msgid "Scroll down to <strong>Google Code Project Hosting</strong> and enable the switch on the right."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -13,40 +13,104 @@ const mockLabels = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const mockScopedLabels = [
|
||||||
|
{
|
||||||
|
id: 27,
|
||||||
|
title: 'Foo::Bar',
|
||||||
|
description: 'Foobar',
|
||||||
|
color: '#333ABC',
|
||||||
|
text_color: '#FFFFFF',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
describe('LabelsSelect', () => {
|
describe('LabelsSelect', () => {
|
||||||
describe('getLabelTemplate', () => {
|
describe('getLabelTemplate', () => {
|
||||||
const label = mockLabels[0];
|
describe('when normal label is present', () => {
|
||||||
let $labelEl;
|
const label = mockLabels[0];
|
||||||
|
let $labelEl;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
$labelEl = $(
|
$labelEl = $(
|
||||||
LabelsSelect.getLabelTemplate({
|
LabelsSelect.getLabelTemplate({
|
||||||
labels: mockLabels,
|
labels: mockLabels,
|
||||||
issueUpdateURL: mockUrl,
|
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', () => {
|
describe('when scoped label is present', () => {
|
||||||
expect($labelEl.attr('href')).toBe('/foo/bar?label_name[]=Foo%20Label');
|
const label = mockScopedLabels[0];
|
||||||
});
|
let $labelEl;
|
||||||
|
|
||||||
it('generated label item template has correct label title', () => {
|
beforeEach(() => {
|
||||||
expect($labelEl.find('span.label').text()).toBe(label.title);
|
$labelEl = $(
|
||||||
});
|
LabelsSelect.getLabelTemplate({
|
||||||
|
labels: mockScopedLabels,
|
||||||
|
issueUpdateURL: mockUrl,
|
||||||
|
enableScopedLabels: true,
|
||||||
|
scopedLabelsDocumentationLink: 'docs-link',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('generated label item template has label description as title attribute', () => {
|
it('generated label item template has correct label URL', () => {
|
||||||
expect($labelEl.find('span.label').attr('title')).toBe(label.description);
|
expect($labelEl.find('a').attr('href')).toBe('/foo/bar?label_name[]=Foo%3A%3ABar');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('generated label item template has correct label styles', () => {
|
it('generated label item template has correct label title', () => {
|
||||||
expect($labelEl.find('span.label').attr('style')).toBe(
|
expect($labelEl.find('span.label').text()).toBe(label.title);
|
||||||
`background-color: ${label.color}; color: ${label.text_color};`,
|
});
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('generated label item has a badge class', () => {
|
it('generated label item template has html flag as true', () => {
|
||||||
expect($labelEl.find('span').hasClass('badge')).toEqual(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) => {
|
spyOn(Vue.http, 'patch').and.callFake((url, data) => {
|
||||||
expect(data.issue.assignee_ids).toEqual([1]);
|
expect(data.issue.assignee_ids).toEqual([1]);
|
||||||
done();
|
done();
|
||||||
|
return Promise.resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
issue.update('url');
|
issue.update('url');
|
||||||
|
@ -187,6 +188,7 @@ describe('Issue model', () => {
|
||||||
spyOn(Vue.http, 'patch').and.callFake((url, data) => {
|
spyOn(Vue.http, 'patch').and.callFake((url, data) => {
|
||||||
expect(data.issue.assignee_ids).toEqual([0]);
|
expect(data.issue.assignee_ids).toEqual([0]);
|
||||||
done();
|
done();
|
||||||
|
return Promise.resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
issue.removeAllAssignees();
|
issue.removeAllAssignees();
|
||||||
|
|
|
@ -45,12 +45,21 @@ describe('DropdownButtonComponent', () => {
|
||||||
});
|
});
|
||||||
const vmMoreLabels = createComponent(mockMoreLabels);
|
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();
|
vmMoreLabels.$destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns first label name when `labels` prop has only one item present', () => {
|
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');
|
const dropdownToggleTextEl = vm.$el.querySelector('.dropdown-toggle-text');
|
||||||
|
|
||||||
expect(dropdownToggleTextEl).not.toBeNull();
|
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', () => {
|
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', () => {
|
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();
|
vmMoreLabels.$destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -49,14 +52,19 @@ describe('DropdownValueCollapsedComponent', () => {
|
||||||
|
|
||||||
const vmMoreLabels = createComponent(mockMoreLabels);
|
const vmMoreLabels = createComponent(mockMoreLabels);
|
||||||
|
|
||||||
expect(vmMoreLabels.labelsList).toBe(
|
const expectedText = `${mockMoreLabels
|
||||||
'Foo Label, Foo Label, Foo Label, Foo Label, Foo Label, and 2 more',
|
.slice(0, 5)
|
||||||
);
|
.map(label => label.title)
|
||||||
|
.join(', ')}, and ${mockMoreLabels.length - 5} more`;
|
||||||
|
|
||||||
|
expect(vmMoreLabels.labelsList).toBe(expectedText);
|
||||||
vmMoreLabels.$destroy();
|
vmMoreLabels.$destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns first label name when `labels` prop has only one item present', () => {
|
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 Vue from 'vue';
|
||||||
|
import $ from 'jquery';
|
||||||
|
|
||||||
import dropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue';
|
import dropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue';
|
||||||
|
|
||||||
|
@ -15,6 +16,7 @@ const createComponent = (
|
||||||
return mountComponent(Component, {
|
return mountComponent(Component, {
|
||||||
labels,
|
labels,
|
||||||
labelFilterBasePath,
|
labelFilterBasePath,
|
||||||
|
enableScopedLabels: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -67,6 +69,26 @@ describe('DropdownValueComponent', () => {
|
||||||
expect(styleObj.backgroundColor).toBe(label.color);
|
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', () => {
|
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');
|
const labelEl = vm.$el.querySelector('a span.badge.color-label');
|
||||||
|
|
||||||
expect(labelEl).not.toBeNull();
|
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.getAttribute('style')).toBe('background-color: rgb(186, 218, 85);');
|
||||||
expect(labelEl.innerText.trim()).toBe(mockLabels[0].title);
|
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',
|
color: '#BADA55',
|
||||||
text_color: '#FFFFFF',
|
text_color: '#FFFFFF',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 27,
|
||||||
|
title: 'Foo::Bar',
|
||||||
|
description: 'Foobar',
|
||||||
|
color: '#0033CC',
|
||||||
|
text_color: '#FFFFFF',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const mockSuggestedColors = [
|
export const mockSuggestedColors = [
|
||||||
|
|
Loading…
Reference in a new issue