Commit 6dbb970f authored by Jan Provaznik's avatar Jan Provaznik

Add mutually exclusive labels

If a label title contains `::` then everything until last `::`
(greedy matching) is considered as a label 'key' and labels with
the same key are mutually exclusive:
* if multiple labels with the same key are added, only last selected
is added, also any existing labels with the same key are removed
* when removing an exclusive key, and multiple exclusive keys are
assigned (from before we introduced exclusive keys), all other
exclusive keys are kept
parent b0e4d7e7
......@@ -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
......
......@@ -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 ||= 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,33 @@ 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
# 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
LabelsHelper.prepend(EE::LabelsHelper)
......@@ -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
......
......@@ -258,3 +258,5 @@ class Label < ApplicationRecord
%w(color title).each { |attr| self[attr] = self[attr]&.strip }
end
end
Label.prepend(EE::Label)
......@@ -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)
......
......@@ -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;
......
......@@ -8,7 +8,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?
......
......@@ -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')
......
......@@ -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)
......
......@@ -105,10 +105,9 @@
= 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|
- label = Label.new(label_hash.slice(:color, :description, :title))
= render_label(label, link: sidebar_label_filter_path(issuable_sidebar[:project_issuables_path], label[:title]))
- else
%span.no-value
= _('None')
......@@ -116,7 +115,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')
......
......@@ -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
......
......@@ -22,7 +22,7 @@
- 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)
%span.assignee-icon
- assignees.each do |assignee|
......
......@@ -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)
......
......@@ -31,7 +31,9 @@ module EE
board_weight: board.weight,
focus_mode_available: parent.feature_available?(:issue_board_focus_mode),
weight_feature_available: parent.feature_available?(:issue_weights).to_s,
show_promotion: show_feature_promotion
show_promotion: show_feature_promotion,
scoped_labels: License.feature_available?(:scoped_labels),
scoped_labels_documentation_link: help_page_path('user/project/labels.md', anchor: 'scoped-labels')
}
super.merge(data)
......
# frozen_string_literal: true
module EE
module LabelsHelper
def render_label(label, tooltip: true, link: nil, css: nil)
content = super
content = scoped_label_wrapper(content, label) if label.scoped_label?
content
end
def scoped_label_wrapper(link, label)
%(<span class="d-inline-block position-relative scoped-label-wrapper">#{link}#{scoped_labels_doc_link(label)}</span>).html_safe
end
def scoped_labels_doc_link(label)
text_color = ::LabelsHelper.text_color_for_bg(label.color)
content = %(<i class="fa fa-question-circle" style="background-color: #{label.color}; color: #{text_color}"></i>)
help_url = ::Gitlab::Routing.url_helpers.help_page_url('user/project/labels.md', anchor: 'scoped-labels')
%(<a href="#{help_url}" class="label scoped-label" target="_blank" rel="noopener">#{content}</a>)
end
def label_tooltip_title(label)
# can't use `super` because this is called also as a module method from
# banzai
tooltip = ::LabelsHelper.label_tooltip_title(label)
tooltip = %(<span class='font-weight-bold scoped-label-tooltip-title'>Scoped label</span><br />#{tooltip}) if label.scoped_label?
tooltip
end
def label_dropdown_data(project, opts = {})
super.merge({
scoped_labels: License.feature_available?(:scoped_labels)&.to_s,
scoped_labels_documentation_link: help_page_path('user/project/labels.md', anchor: 'scoped-labels')
})
end
module_function :scoped_label_wrapper, :scoped_labels_doc_link, :label_tooltip_title
end
end
......@@ -40,7 +40,9 @@ module EpicsHelper
labels_path: group_labels_path(group, format: :json, only_group_labels: true, include_ancestor_groups: true),
toggle_subscription_path: toggle_subscription_group_epic_path(group, epic),
labels_web_url: group_labels_path(group),
epics_web_url: group_epics_path(group)
epics_web_url: group_epics_path(group),
scoped_labels: License.feature_available?(:scoped_labels),
scoped_labels_documentation_link: help_page_path('user/project/labels.md', anchor: 'scoped-labels')
}
epic_meta[:todo_delete_path] = dashboard_todo_path(todo) if todo.present?
......
# frozen_string_literal: true
module EE
module Label
extend ActiveSupport::Concern
SCOPED_LABEL_PATTERN = /^.*::/.freeze
def scoped_label?
SCOPED_LABEL_PATTERN.match?(name) && ::License.feature_available?(:scoped_labels)
end
def scoped_label_key
title[Label::SCOPED_LABEL_PATTERN]
end
end
end
......@@ -52,6 +52,7 @@ class License < ApplicationRecord
geo
github_project_service_integration
jira_dev_panel_integration
scoped_labels
ldap_group_sync_filter
multiple_clusters
multiple_group_issue_boards
......
# frozen_string_literal: true
class ScopedLabelSet
attr_reader :labels, :key
def self.from_label_ids(ids)
by_key = Hash.new { |hash, key| hash[key] = new(key) }
labels = Label.select(:id, :title).where(id: ids)
labels.each do |label|
key = label.scoped_label_key
by_key[key].add(label)
end
by_key.values
end
def initialize(key, labels = [])
@key = key
@labels = labels
end
def add(label)
labels << label
end
def last_id_by_order(label_ids_order)
by_index = label_ids_order.map.with_index { |id, idx| [id.to_i, idx] }.to_h
label_ids.max do |id1, id2|
by_index[id1].to_i <=> by_index[id2].to_i
end
end
def valid?
key.nil? || labels.count < 2
end
def contains_any?(ids)
(label_ids & ids).present?
end
def label_ids
labels.map(&:id)
end
end
......@@ -5,6 +5,8 @@ module EE
extend ::Gitlab::Utils::Override
include ::Gitlab::Utils::StrongMemoize
attr_reader :label_ids_ordered_by_selection
private
override :filter_params
......@@ -20,10 +22,39 @@ module EE
super
end
override :filter_labels
def filter_labels
@label_ids_ordered_by_selection = params[:add_label_ids].to_a + params[:label_ids].to_a # rubocop:disable Gitlab/ModuleWithInstanceVariables
super
end
def update_task_event?
strong_memoize(:update_task_event) do
params.key?(:update_task)
end
end
override :process_label_ids
def process_label_ids(attributes, existing_label_ids: nil, extra_label_ids: [])
ids = super
added_label_ids = ids - existing_label_ids.to_a
filter_mutually_exclusive_labels(ids, added_label_ids)
end
def filter_mutually_exclusive_labels(ids, added_label_ids)
return ids if added_label_ids.empty? || !::License.feature_available?(:scoped_labels)
label_sets = ScopedLabelSet.from_label_ids(ids)
label_sets.map do |set|
if set.valid? || !set.contains_any?(added_label_ids)
set.label_ids
else
set.last_id_by_order(label_ids_ordered_by_selection)
end
end.flatten
end
end
end
......@@ -15,4 +15,4 @@
- if epic.labels.any?
&nbsp;
- epic.labels.each do |label|
= link_to render_colored_label(label, tooltip: true), group_epics_path(@group, label_name:[label.name]), class: 'label-link'
= render_label(label, tooltip: true, link: group_epics_path(@group, label_name:[label.name]), css: 'label-link')
- docs_link = help_page_path('user/project/labels.md', anchor: 'scoped-labels')
.col-sm-10.offset-sm-2.form-text.text-muted.js-has-scoped-labels.hidden
Using
%code ::
denotes a
%a{ href: docs_link, target: '_blank', rel: 'noopener noreferrer' } scoped label set
.col-sm-10.offset-sm-2.form-text.text-muted.js-use-scoped-labels
Use
%code ::
to create a
%a{ href: docs_link, target: '_blank', rel: 'noopener noreferrer' } scoped label set
(eg.
%code priority::1
)
---
title: Added mutually exclusive key value labels
merge_request:
author:
type: added
# frozen_string_literal: true
module EE
module Banzai
module Filter
module LabelReferenceFilter
extend ::Gitlab::Utils::Override
override :wrap_link
def wrap_link(link, label)
content = super
content = ::EE::LabelsHelper.scoped_label_wrapper(content, label) if label.scoped_label?
content
end
def tooltip_title(label)
::EE::LabelsHelper.label_tooltip_title(label)
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe LabelsHelper do
set(:project) { create(:project) }
set(:label) { create(:label, project: project) }
set(:scoped_label) { create(:label, name: 'key::value', project: project) }
describe '#render_label' do
context 'with scoped labels enabled' do
before do
stub_licensed_features(scoped_labels: true)
end
it 'includes link to scoped labels documentation' do
expect(render_label(scoped_label)).to match(%r(<span.+>#{scoped_label.name}</span><a.+>.*question-circle.*</a>))
end
it 'does not include link to scoped label documentation for common labels' do
expect(render_label(label)).to match(%r(<span.+>#{label.name}</span>$))
end
end
context 'with scoped labels disabled' do
before do
stub_licensed_features(scoped_labels: false)
end
it 'does not include link to scoped documentation' do
expect(render_label(scoped_label)).to match(%r(<span.+>#{scoped_label.name}</span>$))
end
end
end
end
......@@ -39,7 +39,7 @@ describe EpicsHelper do
start_date_sourcing_milestone_title start_date_sourcing_milestone_dates
due_date due_date_is_fixed due_date_fixed due_date_from_milestones due_date_sourcing_milestone_title
due_date_sourcing_milestone_dates end_date state namespace labels_path toggle_subscription_path
labels_web_url epics_web_url lock_version
labels_web_url epics_web_url lock_version scoped_labels scoped_labels_documentation_link
])
expect(meta_data['author']).to eq({
'name' => user.name,
......@@ -111,7 +111,7 @@ describe EpicsHelper do
start_date_sourcing_milestone_title start_date_sourcing_milestone_dates
due_date due_date_is_fixed due_date_fixed due_date_from_milestones due_date_sourcing_milestone_title
due_date_sourcing_milestone_dates end_date state namespace labels_path toggle_subscription_path
labels_web_url epics_web_url lock_version
labels_web_url epics_web_url lock_version scoped_labels scoped_labels_documentation_link
])
expect(meta_data['start_date']).to eq('2000-01-01')
expect(meta_data['start_date_sourcing_milestone_title']).to eq(milestone1.title)
......
# frozen_string_literal: true
require 'spec_helper'
describe Banzai::Filter::LabelReferenceFilter do
include FilterSpecHelper
let(:project) { create(:project, :public, name: 'sample-project') }
let(:label) { create(:label, name: 'label', project: project) }
let(:scoped_label) { create(:label, name: 'key::value', project: project) }
context 'with scoped labels enabled' do
before do
stub_licensed_features(scoped_labels: true)
end
it 'includes link to scoped documentation' do
doc = reference_filter("See #{scoped_label.to_reference}")
expect(doc.to_html).to match(%r(<a.+><span.+>#{scoped_label.name}</span></a><a.+>.*question-circle.*</a>))
end
it 'does not include link to scoped documentation for common labels' do
doc = reference_filter("See #{label.to_reference}")
expect(doc.to_html).to match(%r(<a.+><span.+>#{label.name}</span></a>$))
end
end
context 'with scoped labels disabled' do
before do
stub_licensed_features(scoped_labels: false)
end
it 'does not include link to scoped labels documentation' do
doc = reference_filter("See #{scoped_label.to_reference}")
expect(doc.to_html).to match(%r(<a.+><span.+>#{scoped_label.name}</span></a>$))
end
end
end
# frozen_string_literal: true
require 'rails_helper'
describe ScopedLabelSet do
set(:kv_label1) { create(:label, title: 'key::label1') }
set(:kv_label2) { create(:label, title: 'key::label2') }
set(:kv_label3) { create(:label, title: 'key::label3') }
describe '.from_label_ids' do
def get_labels(sets, key)
sets.find { |set| set.key == key }.label_ids
end
it 'groups labels by their key' do
labels = [
create(:label, title: 'label1'),
create(:label, title: 'label2'),
create(:label, title: 'key::label1'),
create(:label, title: 'key::label2'),
create(:label, title: 'key::another key::label1'),
create(:label, title: 'key::another key::label2')
]
sets = described_class.from_label_ids(labels)
expect(sets.size).to eq 3
expect(get_labels(sets, nil)).to match_array([labels[0].id, labels[1].id])
expect(get_labels(sets, 'key::')).to match_array([labels[2].id, labels[3].id])
expect(get_labels(sets, 'key::another key::')).to match_array([labels[4].id, labels[5].id])
end
end
describe '#valid?' do
it 'returns true for not scoped labels' do
label1 = build(:label, title: 'label1')
label2 = build(:label, title: 'label2')
set = described_class.new(nil, [label1, label2])
expect(set.valid?).to eq(true)
end
it 'returns true for scoped labels with single label' do
set = described_class.new(nil, [kv_label1])
expect(set.valid?).to eq(true)
end
it 'returns false for scoped labels with multiple labels' do
set = described_class.new('key', [kv_label1, kv_label2])
expect(set.valid?).to eq(false)
end
end
describe '#add' do
it 'adds a label to the set' do
set = described_class.new('key')
set.add(kv_label1)
expect(set.labels).to eq([kv_label1])
end
end
describe '#contains_any?' do
it 'returns true if any of label ids is in set' do
set = described_class.new('key', [kv_label1, kv_label2])
expect(set.contains_any?([kv_label2.id])).to eq(true)
end
it 'returns true if any of label ids is in set' do
set = described_class.new('key', [kv_label1])
expect(set.contains_any?([kv_label2.id])).to eq(false)
end
end
describe '#last_id_by_order' do
it 'returns last label present in the set ordered by custom order of superset of label ids' do
set = described_class.new('key', [kv_label1, kv_label3])
expect(set.last_id_by_order([kv_label1.id, kv_label3.id, kv_label2.id])).to eq(kv_label3.id)
end
end
end
......@@ -31,4 +31,10 @@ describe Issues::CreateService do
end
end
end
describe '#execute' do
it_behaves_like 'new issuable with scoped labels' do
let(:parent) { project }
end
end
end
......@@ -118,5 +118,10 @@ describe Issues::UpdateService do
end
end
end
it_behaves_like 'existing issuable with scoped labels' do
let(:issuable) { issue }
let(:parent) { project }
end
end
end
......@@ -52,5 +52,9 @@ describe MergeRequests::CreateService do
end
end
end
it_behaves_like 'new issuable with scoped labels' do
let(:parent) { project }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe MergeRequests::UpdateService do
include ProjectForksHelper
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:service) { described_class.new(project, user, {}) }
let(:merge_request) do
create(:merge_request, :simple, title: 'Old title',
source_project: project,
author: user)
end
before do
allow(service).to receive(:execute_hooks)
end
describe '#execute' do
it_behaves_like 'existing issuable with scoped labels' do
let(:issuable) { merge_request }
let(:parent) { project }
end
end
end
......@@ -19,5 +19,9 @@ describe Epics::CreateService do
expect(epic.description).to eq('epic description')
expect(NewEpicWorker).to have_received(:perform_async).with(epic.id, user.id)
end
it_behaves_like 'new issuable with scoped labels' do
let(:parent) { group }
end
end
end
......@@ -211,5 +211,10 @@ describe Epics::UpdateService do
end
end
end
it_behaves_like 'existing issuable with scoped labels' do
let(:issuable) { epic }
let(:parent) { group }
end
end
end
# frozen_string_literal: true
shared_context 'exclusive labels creation' do
def create_label(title)
if parent.is_a?(Group)
create(:group_label, group: parent, title: title)
else
create(:label, project: parent, title: title)
end
end
before do
parent.add_developer(user)
end
end
shared_examples_for 'new issuable with scoped labels' do
include_context 'exclusive labels creation' do
context 'scoped labels are avaialble' do
before do
stub_licensed_features(scoped_labels: true)
end
it 'adds only last selected exclusive scoped label' do
label1 = create_label('label1')
label2 = create_label('key::label1')
label3 = create_label('key::label2')
label4 = create_label('key::label3')
issuable = described_class.new(
parent, user, title: 'test', label_ids: [label1.id, label3.id, label4.id, label2.id]
).execute
expect(issuable.labels).to match_array([label1, label2])
end
end
context 'scoped labels are not available' do
before do
stub_licensed_features(scoped_labels: false)
end
it 'adds only last selected exclusive scoped label' do
label1 = create_label('label1')
label2 = create_label('key::label1')
label3 = create_label('key::label2')
label4 = create_label('key::label3')
issuable = described_class.new(
parent, user, title: 'test', label_ids: [label1.id, label3.id, label4.id, label2.id]
).execute
expect(issuable.labels).to match_array([label1, label2, label3, label4])
end
end
end
end
shared_examples_for 'existing issuable with scoped labels' do
include_context 'exclusive labels creation' do
let(:label1) { create_label('key::label1') }
let(:label2) { create_label('key::label2') }
let(:label3) { create_label('key::label3') }
context 'scoped labels are avaialble' do
before do
stub_licensed_features(scoped_labels: true, epics: true)
end
it 'adds only last selected exclusive scoped label' do
create(:label_link, label: label1, target: issuable)
create(:label_link, label: label2, target: issuable)
issuable.reload
described_class.new(
parent, user, label_ids: [label1.id, label3.id]
).execute(issuable)
expect(issuable.reload.labels).to match_array([label3])
end
it 'it preserves multiple exclusive scoped labels when only removing labels' do
create(:label_link, label: label1, target: issuable)
create(:label_link, label: label2, target: issuable)
create(:label_link, label: label3, target: issuable)
issuable.reload
described_class.new(
parent, user, label_ids: [label2.id, label3.id]
).execute(issuable)
expect(issuable.reload.labels).to match_array([label2, label3])
end
end
context 'scoped labels are not available' do
before do
stub_licensed_features(scoped_labels: false, epics: true)
end
it 'adds all scoped labels' do
create(:label_link, label: label1, target: issuable)
create(:label_link, label: label2, target: issuable)
issuable.reload
described_class.new(
parent, user, label_ids: [label1.id, label2.id, label3.id]
).execute(issuable)
expect(issuable.reload.labels).to match_array([label1, label2, label3])
end
end
end
end
......@@ -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
......
......@@ -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)
......@@ -113,3 +117,5 @@ module Banzai
end
end
end
Banzai::Filter::LabelReferenceFilter.prepend(EE::Banzai::Filter::LabelReferenceFilter)
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment