Commit 2e052925 authored by Alexis Reigel's avatar Alexis Reigel

use lazy ajax filter dropdown for runner tags

the potential number of available runner tags is too large to load it
statically to a dropdown. we use the same lazy loaded dropdown as is
used for the users dropdown already.
parent 315361e0
import createFlash from '../flash';
import AjaxFilter from '../droplab/plugins/ajax_filter';
import FilteredSearchDropdown from './filtered_search_dropdown';
import DropdownUtils from './dropdown_utils';
import FilteredSearchTokenizer from './filtered_search_tokenizer';
import { __ } from '~/locale';
export default class DropdownAjaxFilter extends FilteredSearchDropdown {
constructor(options = {}) {
const { tokenKeys, endpoint, symbol } = options;
super(options);
this.tokenKeys = tokenKeys;
this.endpoint = endpoint;
this.symbol = symbol;
this.config = {
AjaxFilter: this.ajaxFilterConfig(),
};
}
ajaxFilterConfig() {
return {
endpoint: `${gon.relative_url_root || ''}${this.endpoint}`,
searchKey: 'search',
searchValueFunction: this.getSearchInput.bind(this),
loadingTemplate: this.loadingTemplate,
onError() {
createFlash(__('An error occurred fetching the dropdown data.'));
},
};
}
itemClicked(e) {
super.itemClicked(e, selected =>
selected.querySelector('.dropdown-light-content').innerText.trim(),
);
}
renderContent(forceShowList = false) {
this.droplab.changeHookList(this.hookId, this.dropdown, [AjaxFilter], this.config);
super.renderContent(forceShowList);
}
getSearchInput() {
const query = DropdownUtils.getSearchInput(this.input);
const { lastToken } = FilteredSearchTokenizer.processTokens(query, this.tokenKeys.get());
let value = lastToken || '';
if (value[0] === this.symbol) {
value = value.slice(1);
}
// Removes the first character if it is a quotation so that we can search
// with multiple words
if (value[0] === '"' || value[0] === "'") {
value = value.slice(1);
}
return value;
}
init() {
this.droplab.addHook(this.input, this.dropdown, [AjaxFilter], this.config).init();
}
}
...@@ -7,6 +7,7 @@ import DropdownHint from './dropdown_hint'; ...@@ -7,6 +7,7 @@ import DropdownHint from './dropdown_hint';
import DropdownEmoji from './dropdown_emoji'; import DropdownEmoji from './dropdown_emoji';
import DropdownNonUser from './dropdown_non_user'; import DropdownNonUser from './dropdown_non_user';
import DropdownUser from './dropdown_user'; import DropdownUser from './dropdown_user';
import DropdownAjaxFilter from './dropdown_ajax_filter';
import NullDropdown from './null_dropdown'; import NullDropdown from './null_dropdown';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens'; import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
...@@ -113,7 +114,7 @@ export default class FilteredSearchDropdownManager { ...@@ -113,7 +114,7 @@ export default class FilteredSearchDropdownManager {
}, },
tag: { tag: {
reference: null, reference: null,
gl: DropdownNonUser, gl: DropdownAjaxFilter,
extraArguments: { extraArguments: {
endpoint: this.getRunnerTagsEndpoint(), endpoint: this.getRunnerTagsEndpoint(),
symbol: '~', symbol: '~',
......
...@@ -49,7 +49,9 @@ class Admin::RunnersController < Admin::ApplicationController ...@@ -49,7 +49,9 @@ class Admin::RunnersController < Admin::ApplicationController
end end
def tag_list def tag_list
render json: AutocompleteTagsService.new(Ci::Runner).run tags = Autocomplete::ActsAsTaggableOn::TagsFinder.new(taggable_type: Ci::Runner, params: params).execute
render json: ActsAsTaggableOn::TagSerializer.new.represent(tags)
end end
private private
......
# frozen_string_literal: true
module Autocomplete
module ActsAsTaggableOn
class TagsFinder
LIMIT = 20
def initialize(taggable_type:, params:)
@taggable_type = taggable_type
@params = params
end
def execute
@tags = @taggable_type.all_tags
search!
limit!
@tags
end
def search!
search = @params[:search]
return unless search
if search.empty?
@tags = @taggable_type.none
return
end
@tags =
if search.length >= Gitlab::SQL::Pattern::MIN_CHARS_FOR_PARTIAL_MATCHING
@tags.named_like(search)
else
@tags.named(search)
end
end
def limit!
@tags = @tags.limit(LIMIT) # rubocop: disable CodeReuse/ActiveRecord
end
end
end
end
# frozen_string_literal: true
class ActsAsTaggableOn::TagEntity < Grape::Entity
expose :id
expose :name
end
# frozen_string_literal: true
class ActsAsTaggableOn::TagSerializer < BaseSerializer
entity ActsAsTaggableOn::TagEntity
end
# frozen_string_literal: true
class AutocompleteTagsService
def initialize(taggable_type)
@taggable_type = taggable_type
end
# rubocop: disable CodeReuse/ActiveRecord
def run
@taggable_type
.all_tags
.pluck(:id, :name).map do |id, name|
{ id: id, title: name }
end
end
# rubocop: enable CodeReuse/ActiveRecord
end
...@@ -108,7 +108,8 @@ ...@@ -108,7 +108,8 @@
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item %li.filter-dropdown-item
%button.btn.btn-link.js-data-value %button.btn.btn-link.js-data-value
{{title}} %span.dropdown-light-content
{{name}}
= button_tag class: %w[clear-search hidden] do = button_tag class: %w[clear-search hidden] do
= icon('times') = icon('times')
......
...@@ -615,6 +615,9 @@ msgstr "" ...@@ -615,6 +615,9 @@ msgstr ""
msgid "An error occurred creating the new branch." msgid "An error occurred creating the new branch."
msgstr "" msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
msgid "An error occurred previewing the blob" msgid "An error occurred previewing the blob"
msgstr "" msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
describe Autocomplete::ActsAsTaggableOn::TagsFinder do
describe '#execute' do
context 'with empty params' do
it 'returns all tags' do
create :ci_runner, tag_list: ['tag1']
create :ci_runner, tag_list: ['tag2']
tags = described_class.new(taggable_type: Ci::Runner, params: {}).execute.map(&:name)
expect(tags).to match_array %w(tag1 tag2)
end
end
context 'filter by search' do
context 'with an empty search term' do
it 'returns an empty collection' do
create :ci_runner, tag_list: ['tag1']
create :ci_runner, tag_list: ['tag2']
tags = described_class.new(taggable_type: Ci::Runner, params: { search: '' }).execute.map(&:name)
expect(tags).to be_empty
end
end
context 'with a search containing 2 characters' do
it 'returns the tag that strictly matches the search term' do
create :ci_runner, tag_list: ['t1']
create :ci_runner, tag_list: ['t11']
tags = described_class.new(taggable_type: Ci::Runner, params: { search: 't1' }).execute.map(&:name)
expect(tags).to match_array ['t1']
end
end
context 'with a search containing 3 characters' do
it 'returns the tag that partially matches the search term' do
create :ci_runner, tag_list: ['tag1']
create :ci_runner, tag_list: ['tag11']
tags = described_class.new(taggable_type: Ci::Runner, params: { search: 'ag1' }).execute.map(&:name)
expect(tags).to match_array %w(tag1 tag11)
end
end
end
context 'limit' do
it 'limits the result set by the limit constant' do
stub_const("#{described_class}::LIMIT", 1)
create :ci_runner, tag_list: ['tag1']
create :ci_runner, tag_list: ['tag2']
tags = described_class.new(taggable_type: Ci::Runner, params: { search: 'tag' }).execute
expect(tags.count).to eq 1
end
end
end
end
require 'rails_helper'
RSpec.describe AutocompleteTagsService do
describe '#run' do
it 'returns a hash of all tags' do
create(:ci_runner, tag_list: %w[tag1 tag2])
create(:ci_runner, tag_list: %w[tag1 tag3])
create(:project, tag_list: %w[tag4])
expect(described_class.new(Ci::Runner).run).to match_array [
{ id: kind_of(Integer), title: 'tag1' },
{ id: kind_of(Integer), title: 'tag2' },
{ id: kind_of(Integer), title: 'tag3' }
]
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