Commit 9b433c37 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'dz-labels-search' into 'master'

Search for label on project labels page by title or description

See merge request gitlab-org/gitlab-ce!20749
parents b690c268 ac05ebc3
...@@ -160,7 +160,10 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -160,7 +160,10 @@ class Projects::LabelsController < Projects::ApplicationController
def find_labels def find_labels
@available_labels ||= @available_labels ||=
LabelsFinder.new(current_user, project_id: @project.id, include_ancestor_groups: params[:include_ancestor_groups]).execute LabelsFinder.new(current_user,
project_id: @project.id,
include_ancestor_groups: params[:include_ancestor_groups],
search: params[:search]).execute
end end
def authorize_admin_labels! def authorize_admin_labels!
......
...@@ -14,6 +14,7 @@ class LabelsFinder < UnionFinder ...@@ -14,6 +14,7 @@ class LabelsFinder < UnionFinder
@skip_authorization = skip_authorization @skip_authorization = skip_authorization
items = find_union(label_ids, Label) || Label.none items = find_union(label_ids, Label) || Label.none
items = with_title(items) items = with_title(items)
items = by_search(items)
sort(items) sort(items)
end end
...@@ -63,6 +64,12 @@ class LabelsFinder < UnionFinder ...@@ -63,6 +64,12 @@ class LabelsFinder < UnionFinder
items.where(title: title) items.where(title: title)
end end
def by_search(labels)
return labels unless search?
labels.search(params[:search])
end
# Gets redacted array of group ids # Gets redacted array of group ids
# which can include the ancestors and descendants of the requested group. # which can include the ancestors and descendants of the requested group.
def group_ids_for(group) def group_ids_for(group)
...@@ -106,6 +113,10 @@ class LabelsFinder < UnionFinder ...@@ -106,6 +113,10 @@ class LabelsFinder < UnionFinder
params[:only_group_labels] params[:only_group_labels]
end end
def search?
params[:search].present?
end
def title def title
params[:title] || params[:name] params[:title] || params[:name]
end end
......
...@@ -2,6 +2,7 @@ class Label < ActiveRecord::Base ...@@ -2,6 +2,7 @@ class Label < ActiveRecord::Base
include CacheMarkdownField include CacheMarkdownField
include Referable include Referable
include Subscribable include Subscribable
include Gitlab::SQL::Pattern
# Represents a "No Label" state used for filtering Issues and Merge # Represents a "No Label" state used for filtering Issues and Merge
# Requests that have no label assigned. # Requests that have no label assigned.
...@@ -103,6 +104,17 @@ class Label < ActiveRecord::Base ...@@ -103,6 +104,17 @@ class Label < ActiveRecord::Base
nil nil
end end
# Searches for labels with a matching title or description.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String.
#
# Returns an ActiveRecord::Relation.
def self.search(query)
fuzzy_search(query, [:title, :description])
end
def open_issues_count(user = nil) def open_issues_count(user = nil)
issues_count(user, state: 'opened') issues_count(user, state: 'opened')
end end
......
...@@ -2,32 +2,45 @@ ...@@ -2,32 +2,45 @@
- page_title "Labels" - page_title "Labels"
- can_admin_label = can?(current_user, :admin_label, @project) - can_admin_label = can?(current_user, :admin_label, @project)
- hide_class = '' - hide_class = ''
- search = params[:search]
- if can_admin_label - if can_admin_label
- content_for(:header_content) do - content_for(:header_content) do
.nav-controls .nav-controls
= link_to _('New label'), new_project_label_path(@project), class: "btn btn-new" = link_to _('New label'), new_project_label_path(@project), class: "btn btn-new"
- if @labels.exists? || @prioritized_labels.exists? - if @labels.exists? || @prioritized_labels.exists? || search.present?
#promote-label-modal #promote-label-modal
%div{ class: container_class } %div{ class: container_class }
.top-area.adjust .top-area.adjust
.nav-text .nav-text
= _('Labels can be applied to issues and merge requests.') = _('Labels can be applied to issues and merge requests.')
- if can_admin_label
= _('Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.')
.labels-container.prepend-top-5 .nav-controls
= form_tag project_labels_path(@project), method: :get do
.input-group
= search_field_tag :search, params[:search], { placeholder: _('Filter'), id: 'label-search', class: 'form-control search-text-input input-short', spellcheck: false }
%span.input-group-append
%button.btn.btn-default{ type: "submit", "aria-label" => _('Submit search') }
= icon("search")
.labels-container.prepend-top-10
- if can_admin_label - if can_admin_label
- if search.blank?
%p.text-muted
= _('Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.')
-# Only show it in the first page -# Only show it in the first page
- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1') - hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
.prioritized-labels{ class: ('hide' if hide) } .prioritized-labels{ class: ('hide' if hide) }
%h5.prepend-top-10= _('Prioritized Labels') %h5.prepend-top-10= _('Prioritized Labels')
.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_project_labels_path(@project) } .content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_project_labels_path(@project) }
#js-priority-labels-empty-state.priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty?}" } #js-priority-labels-empty-state.priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty? && search.blank?}" }
= render 'shared/empty_states/priority_labels' = render 'shared/empty_states/priority_labels'
- if @prioritized_labels.present? - if @prioritized_labels.present?
= render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label, locals: { force_priority: true } = render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label, locals: { force_priority: true }
- elsif search.present?
.nothing-here-block
= _('No prioritised labels with such name or description')
- if @labels.present? - if @labels.present?
.other-labels .other-labels
...@@ -36,6 +49,18 @@ ...@@ -36,6 +49,18 @@
.content-list.manage-labels-list.js-other-labels .content-list.manage-labels-list.js-other-labels
= render partial: 'shared/label', subject: @project, collection: @labels, as: :label = render partial: 'shared/label', subject: @project, collection: @labels, as: :label
= paginate @labels, theme: 'gitlab' = paginate @labels, theme: 'gitlab'
- elsif search.present?
.other-labels
- if @available_labels.any?
%h5
= _('Other Labels')
.nothing-here-block
= _('No other labels with such name or description')
- else
.nothing-here-block
= _('No labels with such name or description')
- else - else
= render 'shared/empty_states/labels' = render 'shared/empty_states/labels'
......
---
title: Search for labels by title or description on project labels page
merge_request: 20749
author:
type: added
...@@ -2522,6 +2522,9 @@ msgstr "" ...@@ -2522,6 +2522,9 @@ msgstr ""
msgid "Files (%{human_size})" msgid "Files (%{human_size})"
msgstr "" msgstr ""
msgid "Filter"
msgstr ""
msgid "Filter by commit message" msgid "Filter by commit message"
msgstr "" msgstr ""
...@@ -3562,12 +3565,21 @@ msgstr "" ...@@ -3562,12 +3565,21 @@ msgstr ""
msgid "No files found." msgid "No files found."
msgstr "" msgstr ""
msgid "No labels with such name or description"
msgstr ""
msgid "No merge requests found" msgid "No merge requests found"
msgstr "" msgstr ""
msgid "No messages were logged" msgid "No messages were logged"
msgstr "" msgstr ""
msgid "No other labels with such name or description"
msgstr ""
msgid "No prioritised labels with such name or description"
msgstr ""
msgid "No public groups" msgid "No public groups"
msgstr "" msgstr ""
...@@ -4901,6 +4913,9 @@ msgstr "" ...@@ -4901,6 +4913,9 @@ msgstr ""
msgid "Submit as spam" msgid "Submit as spam"
msgstr "" msgstr ""
msgid "Submit search"
msgstr ""
msgid "Subscribe" msgid "Subscribe"
msgstr "" msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
describe 'Search for labels', :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
let!(:label1) { create(:label, title: 'Foo', description: 'Lorem ipsum', project: project) }
let!(:label2) { create(:label, title: 'Bar', description: 'Fusce consequat', project: project) }
before do
project.add_maintainer(user)
sign_in(user)
visit project_labels_path(project)
end
it 'searches for label by title' do
fill_in 'label-search', with: 'Bar'
find('#label-search').native.send_keys(:enter)
expect(page).to have_content(label2.title)
expect(page).to have_content(label2.description)
expect(page).not_to have_content(label1.title)
expect(page).not_to have_content(label1.description)
end
it 'searches for label by title' do
fill_in 'label-search', with: 'Lorem'
find('#label-search').native.send_keys(:enter)
expect(page).to have_content(label1.title)
expect(page).to have_content(label1.description)
expect(page).not_to have_content(label2.title)
expect(page).not_to have_content(label2.description)
end
it 'shows nothing found message' do
fill_in 'label-search', with: 'nonexistent'
find('#label-search').native.send_keys(:enter)
expect(page).to have_content('No labels with such name or description')
expect(page).not_to have_content(label1.title)
expect(page).not_to have_content(label1.description)
expect(page).not_to have_content(label2.title)
expect(page).not_to have_content(label2.description)
end
context 'priority labels' do
let!(:label_priority) { create(:label_priority, label: label1, project: project) }
it 'searches for priority label' do
fill_in 'label-search', with: 'Foo'
find('#label-search').native.send_keys(:enter)
page.within('.prioritized-labels') do
expect(page).to have_content(label1.title)
expect(page).to have_content(label1.description)
end
page.within('.other-labels') do
expect(page).to have_content('No other labels with such name or description')
end
end
it 'searches for other label' do
fill_in 'label-search', with: 'Bar'
find('#label-search').native.send_keys(:enter)
page.within('.prioritized-labels') do
expect(page).to have_content('No prioritised labels with such name or description')
end
page.within('.other-labels') do
expect(page).to have_content(label2.title)
expect(page).to have_content(label2.description)
end
end
end
end
...@@ -14,7 +14,7 @@ describe LabelsFinder do ...@@ -14,7 +14,7 @@ describe LabelsFinder do
let(:project_4) { create(:project, :public) } let(:project_4) { create(:project, :public) }
let(:project_5) { create(:project, namespace: group_1) } let(:project_5) { create(:project, namespace: group_1) }
let!(:project_label_1) { create(:label, project: project_1, title: 'Label 1') } let!(:project_label_1) { create(:label, project: project_1, title: 'Label 1', description: 'awesome label') }
let!(:project_label_2) { create(:label, project: project_2, title: 'Label 2') } let!(:project_label_2) { create(:label, project: project_2, title: 'Label 2') }
let!(:project_label_4) { create(:label, project: project_4, title: 'Label 4') } let!(:project_label_4) { create(:label, project: project_4, title: 'Label 4') }
let!(:project_label_5) { create(:label, project: project_5, title: 'Label 5') } let!(:project_label_5) { create(:label, project: project_5, title: 'Label 5') }
...@@ -196,5 +196,19 @@ describe LabelsFinder do ...@@ -196,5 +196,19 @@ describe LabelsFinder do
expect(finder.execute).to be_empty expect(finder.execute).to be_empty
end end
end end
context 'search by title and description' do
it 'returns labels with a partially matching title' do
finder = described_class.new(user, search: '(group)')
expect(finder.execute).to eq [group_label_1]
end
it 'returns labels with a partially matching description' do
finder = described_class.new(user, search: 'awesome')
expect(finder.execute).to eq [project_label_1]
end
end
end end
end end
...@@ -139,4 +139,20 @@ describe Label do ...@@ -139,4 +139,20 @@ describe Label do
end end
end end
end end
describe '.search' do
let(:label) { create(:label, title: 'bug', description: 'incorrect behavior') }
it 'returns labels with a partially matching title' do
expect(described_class.search(label.title[0..2])).to eq([label])
end
it 'returns labels with a partially matching description' do
expect(described_class.search(label.description[0..5])).to eq([label])
end
it 'returns nothing' do
expect(described_class.search('feature')).to be_empty
end
end
end end
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