summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSean McGivern <sean@gitlab.com>2019-04-05 10:28:19 +0000
committerSean McGivern <sean@gitlab.com>2019-04-05 10:28:19 +0000
commit934dabaf6da42db7197e07dc95cf88d34e847306 (patch)
tree537ca94aeaf22d8ddb411582b3f66c0cd9c6fad2
parent39eb16aab2dbac3347f61f83fb60f5448d44e965 (diff)
parentea3831986b63b2e070d9b61c8e307488822acf28 (diff)
downloadgitlab-ce-934dabaf6da42db7197e07dc95cf88d34e847306.tar.gz
Merge branch 'keyval-labels' into 'master'
[CE] Add mutually exclusive labels See merge request gitlab-org/gitlab-ce!26804
-rw-r--r--app/assets/javascripts/boards/models/issue.js12
-rw-r--r--app/assets/javascripts/labels_select.js97
-rw-r--r--app/assets/javascripts/pages/groups/labels/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/labels/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/labels/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/labels/new/index.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue19
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue55
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_regular_label.vue35
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue47
-rw-r--r--app/assets/stylesheets/pages/issuable.scss10
-rw-r--r--app/assets/stylesheets/pages/labels.scss36
-rw-r--r--app/controllers/concerns/issuable_actions.rb3
-rw-r--r--app/helpers/labels_helper.rb55
-rw-r--r--app/models/global_label.rb2
-rw-r--r--app/services/issuable_base_service.rb5
-rw-r--r--app/views/shared/_delete_label_modal.html.haml2
-rw-r--r--app/views/shared/_label_row.html.haml2
-rw-r--r--app/views/shared/boards/components/sidebar/_labels.html.haml8
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml8
-rw-r--r--app/views/shared/labels/_form.html.haml4
-rw-r--r--app/views/shared/milestones/_issuable.html.haml3
-rw-r--r--app/views/shared/milestones/_labels_tab.html.haml3
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb12
-rw-r--r--lib/banzai/filter/label_reference_filter.rb6
-rw-r--r--locale/gitlab.pot3
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_sidebar.json3
-rw-r--r--spec/frontend/labels_select_spec.js116
-rw-r--r--spec/helpers/labels_helper_spec.rb20
-rw-r--r--spec/javascripts/boards/issue_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js15
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js20
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js40
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js7
36 files changed, 573 insertions, 99 deletions
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index dd92d3c8552..2edb6723ada 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -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;
+ }
+ });
}
}
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index cca4927c115..7d21a216443 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -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,49 @@ 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}"]`)
+ .last()
+ .remove();
+ });
+ }
} else {
template = '<span class="no-value">None</span>';
}
@@ -358,6 +401,7 @@ export default class LabelsSelect {
} else {
if (!$dropdown.hasClass('js-filter-bulk-update')) {
saveLabelData();
+ $dropdown.data('glDropdown').clearMenu();
}
}
}
@@ -471,19 +515,62 @@ 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, escapeStr }) %>" style="background-color: <%= escapeStr(label.color) %>; color: <%= escapeStr(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: <%= escapeStr(label.color) %>; color: <%= escapeStr(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 />',
+ '<%= escapeStr(label.description) %>',
+ '<% } else { %>',
+ '<%= escapeStr(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, escapeStr, linkAttrs: \'data-html="true"\' }) %>',
+ '<%= infoIconTemplate({ label, scopedLabelsDocumentationLink, escapeStr }) %>',
+ '</span>',
+ '<% } else { %>',
+ '<%= labelTemplate({ label, issueUpdateURL, isScopedLabel, enableScopedLabels, tooltipTitleTemplate, escapeStr, linkAttrs: "" }) %>',
+ '<% } %>',
'<% }); %>',
].join(''),
);
- return tpl(tplData);
+ return tpl({
+ ...tplData,
+ labelTemplate,
+ infoIconTemplate,
+ tooltipTitleTemplate,
+ isScopedLabel,
+ escapeStr: _.escape,
+ });
}
bindEvents() {
diff --git a/app/assets/javascripts/pages/groups/labels/edit/index.js b/app/assets/javascripts/pages/groups/labels/edit/index.js
index fa81ad914ba..83d6ac9fd14 100644
--- a/app/assets/javascripts/pages/groups/labels/edit/index.js
+++ b/app/assets/javascripts/pages/groups/labels/edit/index.js
@@ -1,3 +1,3 @@
-import Labels from '~/labels';
+import Labels from 'ee_else_ce/labels';
document.addEventListener('DOMContentLoaded', () => new Labels());
diff --git a/app/assets/javascripts/pages/groups/labels/new/index.js b/app/assets/javascripts/pages/groups/labels/new/index.js
index fa81ad914ba..83d6ac9fd14 100644
--- a/app/assets/javascripts/pages/groups/labels/new/index.js
+++ b/app/assets/javascripts/pages/groups/labels/new/index.js
@@ -1,3 +1,3 @@
-import Labels from '~/labels';
+import Labels from 'ee_else_ce/labels';
document.addEventListener('DOMContentLoaded', () => new Labels());
diff --git a/app/assets/javascripts/pages/projects/labels/edit/index.js b/app/assets/javascripts/pages/projects/labels/edit/index.js
index fa81ad914ba..83d6ac9fd14 100644
--- a/app/assets/javascripts/pages/projects/labels/edit/index.js
+++ b/app/assets/javascripts/pages/projects/labels/edit/index.js
@@ -1,3 +1,3 @@
-import Labels from '~/labels';
+import Labels from 'ee_else_ce/labels';
document.addEventListener('DOMContentLoaded', () => new Labels());
diff --git a/app/assets/javascripts/pages/projects/labels/new/index.js b/app/assets/javascripts/pages/projects/labels/new/index.js
index fa81ad914ba..83d6ac9fd14 100644
--- a/app/assets/javascripts/pages/projects/labels/new/index.js
+++ b/app/assets/javascripts/pages/projects/labels/new/index.js
@@ -1,3 +1,3 @@
-import Labels from '~/labels';
+import Labels from 'ee_else_ce/labels';
document.addEventListener('DOMContentLoaded', () => new Labels());
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
index f66e81b1e08..9c258c4651f 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
@@ -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
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
index 498b507d11d..1eed8907bb7 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
@@ -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"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue
index 6faf3fafad1..ddc488adbcb 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue
@@ -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>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_regular_label.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_regular_label.vue
new file mode 100644
index 00000000000..282b181f11e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_regular_label.vue
@@ -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>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue
new file mode 100644
index 00000000000..ad5a86de166
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue
@@ -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>
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 6415d902ca6..9be3f8138a0 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -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 {
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 6f98b4f7f13..e7fd7fab32b 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -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;
+ }
+ }
+}
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 8ef3b6502df..85aeecbf90b 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -7,6 +7,9 @@ module IssuableActions
included do
before_action :authorize_destroy_issuable!, only: :destroy
before_action :authorize_admin_issuable!, only: :bulk_update
+ before_action only: :show do
+ push_frontend_feature_flag(:scoped_labels, default_enabled: true)
+ end
end
def permitted_keys
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 8e10fd15f6a..e91e8f85515 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -46,7 +46,7 @@ module LabelsHelper
if block_given?
link_to link, class: css_class, &block
else
- link_to render_colored_label(label, tooltip: tooltip), link, class: css_class
+ render_label(label, tooltip: tooltip, link: link, css: css_class)
end
end
@@ -78,19 +78,33 @@ module LabelsHelper
end
end
- def render_colored_label(label, label_suffix = '', tooltip: true)
+ def render_label(label, tooltip: true, link: nil, css: nil)
+ # if scoped label is used then EE wraps label tag with scoped label
+ # doc link
+ html = render_colored_label(label, tooltip: tooltip)
+ html = link_to(html, link, class: css) if link
+
+ html
+ end
+
+ def render_colored_label(label, label_suffix: '', tooltip: true, title: nil)
text_color = text_color_for_bg(label.color)
+ title ||= tooltip ? label_tooltip_title(label) : ''
# Intentionally not using content_tag here so that this method can be called
# by LabelReferenceFilter
span = %(<span class="badge color-label #{"has-tooltip" if tooltip}" ) +
- %(style="background-color: #{label.color}; color: #{text_color}" ) +
- %(title="#{escape_once(label.description)}" data-container="body">) +
+ %(data-html="true" style="background-color: #{label.color}; color: #{text_color}" ) +
+ %(title="#{escape_once(title)}" data-container="body">) +
%(#{escape_once(label.name)}#{label_suffix}</span>)
span.html_safe
end
+ def label_tooltip_title(label)
+ label.description
+ end
+
def suggested_colors
[
'#0033CC',
@@ -231,6 +245,37 @@ module LabelsHelper
labels.sort_by(&:title)
end
+ def label_dropdown_data(project, opts = {})
+ {
+ toggle: "dropdown",
+ field_name: opts[:field_name] || "label_name[]",
+ show_no: "true",
+ show_any: "true",
+ project_id: project&.try(:id),
+ namespace_path: project&.try(:namespace)&.try(:full_path),
+ project_path: project&.try(:path)
+ }.merge(opts)
+ end
+
+ def sidebar_label_dropdown_data(issuable_type, issuable_sidebar)
+ label_dropdown_data(nil, {
+ default_label: "Labels",
+ field_name: "#{issuable_type}[label_names][]",
+ ability_name: issuable_type,
+ namespace_path: issuable_sidebar[:namespace_path],
+ project_path: issuable_sidebar[:project_path],
+ issue_update: issuable_sidebar[:issuable_json_path],
+ labels: issuable_sidebar[:project_labels_path],
+ display: 'static'
+ })
+ end
+
+ def label_from_hash(hash)
+ klass = hash[:group_id] ? GroupLabel : ProjectLabel
+
+ klass.new(hash.slice(:color, :description, :title, :group_id, :project_id))
+ end
+
# Required for Banzai::Filter::LabelReferenceFilter
- module_function :render_colored_label, :text_color_for_bg, :escape_once
+ module_function :render_colored_label, :text_color_for_bg, :escape_once, :label_tooltip_title
end
diff --git a/app/models/global_label.rb b/app/models/global_label.rb
index c5b2492bbf6..572cb12b26a 100644
--- a/app/models/global_label.rb
+++ b/app/models/global_label.rb
@@ -4,7 +4,7 @@ class GlobalLabel
attr_accessor :title, :labels
alias_attribute :name, :title
- delegate :color, :text_color, :description, to: :@first_label
+ delegate :color, :text_color, :description, :scoped_label?, to: :@first_label
def for_display
@first_label
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 04dfcfbc22d..7a4ccf0d178 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -107,12 +107,13 @@ class IssuableBaseService < BaseService
@labels_service ||= ::Labels::AvailableLabelsService.new(current_user, parent, params)
end
- def process_label_ids(attributes, existing_label_ids: nil)
+ def process_label_ids(attributes, existing_label_ids: nil, extra_label_ids: [])
label_ids = attributes.delete(:label_ids)
add_label_ids = attributes.delete(:add_label_ids)
remove_label_ids = attributes.delete(:remove_label_ids)
new_label_ids = existing_label_ids || label_ids || []
+ new_label_ids |= extra_label_ids
if add_label_ids.blank? && remove_label_ids.blank?
new_label_ids = label_ids if label_ids
@@ -147,7 +148,7 @@ class IssuableBaseService < BaseService
params.delete(:state_event)
params[:author] ||= current_user
- params[:label_ids] = issuable.label_ids.to_a + process_label_ids(params)
+ params[:label_ids] = process_label_ids(params, extra_label_ids: issuable.label_ids.to_a)
issuable.assign_attributes(params)
diff --git a/app/views/shared/_delete_label_modal.html.haml b/app/views/shared/_delete_label_modal.html.haml
index b96380923ac..dbd3bbb43af 100644
--- a/app/views/shared/_delete_label_modal.html.haml
+++ b/app/views/shared/_delete_label_modal.html.haml
@@ -2,7 +2,7 @@
.modal-dialog
.modal-content
.modal-header
- %h3.page-title Delete #{render_colored_label(label, tooltip: false)} ?
+ %h3.page-title Delete #{render_label(label, tooltip: false)} ?
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml
index c5ea15a7f63..6651f12f6de 100644
--- a/app/views/shared/_label_row.html.haml
+++ b/app/views/shared/_label_row.html.haml
@@ -7,7 +7,7 @@
- if defined?(@project)
= link_to_label(label, subject: @project, tooltip: false)
- else
- = render_colored_label(label, tooltip: false)
+ = render_label(label, tooltip: false)
.label-description
.append-right-default.prepend-left-default
- if label.description.present?
diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml
index 19159684420..c6eade3bbbc 100644
--- a/app/views/shared/boards/components/sidebar/_labels.html.haml
+++ b/app/views/shared/boards/components/sidebar/_labels.html.haml
@@ -21,13 +21,7 @@
%button.dropdown-menu-toggle.js-label-select.js-multiselect.js-issue-board-sidebar{ type: "button",
":data-selected" => "selectedLabels",
":data-labels" => "issue.assignableLabelsEndpoint",
- data: { toggle: "dropdown",
- field_name: "issue[label_names][]",
- show_no: "true",
- show_any: "true",
- project_id: @project&.try(:id),
- namespace_path: @namespace_path,
- project_path: @project.try(:path) } }
+ data: label_dropdown_data(@project, namespace_path: @namespace_path, field_name: "issue[label_names][]") }
%span.dropdown-toggle-text
{{ labelDropdownTitle }}
= icon('chevron-down')
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index d5fb85ba0f3..f2c0c77a583 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -8,7 +8,7 @@
- classes = local_assigns.fetch(:classes, [])
- selected = local_assigns.fetch(:selected, nil)
- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by label")
-- dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), labels: labels_filter_path_with_defaults, default_label: "Labels"}
+- dropdown_data = label_dropdown_data(@project, labels: labels_filter_path_with_defaults, default_label: "Labels")
- dropdown_data.merge!(data_options)
- label_name = local_assigns.fetch(:label_name, "Labels")
- no_default_styles = local_assigns.fetch(:no_default_styles, false)
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 9596c1df20e..0798b1da4b7 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -105,10 +105,8 @@
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
.value.issuable-show-labels.dont-hide.hide-collapsed.qa-labels-block{ class: ("has-labels" if selected_labels.any?) }
- if selected_labels.any?
- - selected_labels.each do |label|
- = link_to sidebar_label_filter_path(issuable_sidebar[:project_issuables_path], label[:title]) do
- %span.badge.color-label.has-tooltip{ style: "background-color: #{label[:color]}; color: #{label[:text_color]}", title: label[:description], data: { container: "body" } }
- = label[:title]
+ - selected_labels.each do |label_hash|
+ = render_label(label_from_hash(label_hash), link: sidebar_label_filter_path(issuable_sidebar[:project_issuables_path], label_hash[:title]))
- else
%span.no-value
= _('None')
@@ -116,7 +114,7 @@
- selected_labels.each do |label|
= hidden_field_tag "#{issuable_type}[label_names][]", label[:id], id: nil
.dropdown
- %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable_type}[label_names][]", ability_name: issuable_type, show_no: "true", show_any: "true", namespace_path: issuable_sidebar[:namespace_path], project_path: issuable_sidebar[:project_path], issue_update: issuable_sidebar[:issuable_json_path], labels: issuable_sidebar[:project_labels_path], display: 'static' } }
+ %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: sidebar_label_dropdown_data(issuable_type, issuable_sidebar) }
%span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
= multi_label_name(selected_labels, "Labels")
= icon('chevron-down', 'aria-hidden': 'true')
diff --git a/app/views/shared/labels/_form.html.haml b/app/views/shared/labels/_form.html.haml
index 7619d0a2e9c..743ee1435e8 100644
--- a/app/views/shared/labels/_form.html.haml
+++ b/app/views/shared/labels/_form.html.haml
@@ -4,7 +4,9 @@
.form-group.row
= f.label :title, class: 'col-form-label col-sm-2'
.col-sm-10
- = f.text_field :title, class: "form-control qa-label-title", required: true, autofocus: true
+ = f.text_field :title, class: "form-control js-label-title qa-label-title", required: true, autofocus: true
+ = render_if_exists 'shared/labels/create_label_help_text'
+
.form-group.row
= f.label :description, class: 'col-form-label col-sm-2'
.col-sm-10
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index eba64daaadc..5863f52aa78 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -21,8 +21,7 @@
%span.issuable-number= issuable.to_reference
- labels.each do |label|
- = link_to polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }) do
- - render_colored_label(label)
+ = render_label(label, link: polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }))
%span.assignee-icon
- assignees.each do |assignee|
diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml
index 6797520650d..6b0640bd8cb 100644
--- a/app/views/shared/milestones/_labels_tab.html.haml
+++ b/app/views/shared/milestones/_labels_tab.html.haml
@@ -5,8 +5,7 @@
%li.is-not-draggable
%span.label-row
%span.label-name
- = link_to milestones_label_path(options) do
- - render_colored_label(label, tooltip: false)
+ = render_label(label, tooltip: false, link: milestones_label_path(options))
%span.prepend-description-left
= markdown_field(label, :description)
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index 5f8aca104aa..44b151d01e7 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -195,15 +195,21 @@ module Banzai
content = link_content || object_link_text(object, matches)
- %(<a href="#{url}" #{data}
- title="#{escape_once(title)}"
- class="#{klass}">#{content}</a>)
+ link = %(<a href="#{url}" #{data}
+ title="#{escape_once(title)}"
+ class="#{klass}">#{content}</a>)
+
+ wrap_link(link, object)
else
match
end
end
end
+ def wrap_link(link, object)
+ link
+ end
+
def data_attributes_for(text, parent, object, link_content: false, link_reference: false)
object_parent_type = parent.is_a?(Group) ? :group : :project
diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb
index f90a35952e5..77e4c438bd0 100644
--- a/lib/banzai/filter/label_reference_filter.rb
+++ b/lib/banzai/filter/label_reference_filter.rb
@@ -91,7 +91,11 @@ module Banzai
label_suffix = " <i>in #{reference}</i>" if reference.present?
end
- LabelsHelper.render_colored_label(object, label_suffix)
+ LabelsHelper.render_colored_label(object, label_suffix: label_suffix, title: tooltip_title(object))
+ end
+
+ def tooltip_title(label)
+ nil
end
def full_path_ref?(matches)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index ea08f468616..a857612a59a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -7031,6 +7031,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 ""
diff --git a/spec/fixtures/api/schemas/entities/merge_request_sidebar.json b/spec/fixtures/api/schemas/entities/merge_request_sidebar.json
index 7e9e048a9fd..214b67a9a0f 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_sidebar.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_sidebar.json
@@ -51,6 +51,5 @@
"toggle_subscription_path": { "type": "string" },
"move_issue_path": { "type": "string" },
"projects_autocomplete_path": { "type": "string" }
- },
- "additionalProperties": false
+ }
}
diff --git a/spec/frontend/labels_select_spec.js b/spec/frontend/labels_select_spec.js
index acfdc885032..d54e0eab845 100644
--- a/spec/frontend/labels_select_spec.js
+++ b/spec/frontend/labels_select_spec.js
@@ -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;
-
- beforeEach(() => {
- $labelEl = $(
- LabelsSelect.getLabelTemplate({
- labels: mockLabels,
- issueUpdateURL: mockUrl,
- }),
- );
- });
+ describe('when normal label is present', () => {
+ const label = mockLabels[0];
+ let $labelEl;
- it('generated label item template has correct label URL', () => {
- expect($labelEl.attr('href')).toBe('/foo/bar?label_name[]=Foo%20Label');
- });
+ beforeEach(() => {
+ $labelEl = $(
+ LabelsSelect.getLabelTemplate({
+ labels: mockLabels,
+ issueUpdateURL: mockUrl,
+ enableScopedLabels: true,
+ scopedLabelsDocumentationLink: 'docs-link',
+ }),
+ );
+ });
- it('generated label item template has correct label title', () => {
- expect($labelEl.find('span.label').text()).toBe(label.title);
- });
+ 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 label description as title attribute', () => {
- expect($labelEl.find('span.label').attr('title')).toBe(label.description);
- });
+ 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 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 has a badge class', () => {
- expect($labelEl.find('span').hasClass('badge')).toEqual(true);
+ describe('when scoped label is present', () => {
+ const label = mockScopedLabels[0];
+ let $labelEl;
+
+ beforeEach(() => {
+ $labelEl = $(
+ LabelsSelect.getLabelTemplate({
+ labels: mockScopedLabels,
+ issueUpdateURL: mockUrl,
+ enableScopedLabels: true,
+ scopedLabelsDocumentationLink: 'docs-link',
+ }),
+ );
+ });
+
+ 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 title', () => {
+ expect($labelEl.find('span.label').text()).toBe(label.title);
+ });
+
+ 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);
+ });
});
});
});
diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb
index 012678db9c2..a049b5a6133 100644
--- a/spec/helpers/labels_helper_spec.rb
+++ b/spec/helpers/labels_helper_spec.rb
@@ -249,4 +249,24 @@ describe LabelsHelper do
.to match_array([label2, label4, label1, label3])
end
end
+
+ describe 'label_from_hash' do
+ it 'builds a group label with whitelisted attributes' do
+ label = label_from_hash({ title: 'foo', color: 'bar', id: 1, group_id: 1 })
+
+ expect(label).to be_a(GroupLabel)
+ expect(label.id).to be_nil
+ expect(label.title).to eq('foo')
+ expect(label.color).to eq('bar')
+ end
+
+ it 'builds a project label with whitelisted attributes' do
+ label = label_from_hash({ title: 'foo', color: 'bar', id: 1, project_id: 1 })
+
+ expect(label).to be_a(ProjectLabel)
+ expect(label.id).to be_nil
+ expect(label.title).to eq('foo')
+ expect(label.color).to eq('bar')
+ end
+ end
end
diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js
index 54fb0e8228b..e4ff3eb381f 100644
--- a/spec/javascripts/boards/issue_spec.js
+++ b/spec/javascripts/boards/issue_spec.js
@@ -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();
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
index 5cf6afebd7e..0689fc1cf1f 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
@@ -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', () => {
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
index 804b33422bd..cb49fa31d20 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
@@ -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);
});
});
});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
index 3fff781594f..35a9c300953 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
@@ -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();
+ });
+ });
});
});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js
index 3fcb91b6f5e..70025f041a7 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js
@@ -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 = [