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.
This commit is contained in:
parent
315361e025
commit
2e05292562
11 changed files with 200 additions and 37 deletions
|
@ -0,0 +1,68 @@
|
|||
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';
|
|||
import DropdownEmoji from './dropdown_emoji';
|
||||
import DropdownNonUser from './dropdown_non_user';
|
||||
import DropdownUser from './dropdown_user';
|
||||
import DropdownAjaxFilter from './dropdown_ajax_filter';
|
||||
import NullDropdown from './null_dropdown';
|
||||
import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
|
||||
|
||||
|
@ -113,7 +114,7 @@ export default class FilteredSearchDropdownManager {
|
|||
},
|
||||
tag: {
|
||||
reference: null,
|
||||
gl: DropdownNonUser,
|
||||
gl: DropdownAjaxFilter,
|
||||
extraArguments: {
|
||||
endpoint: this.getRunnerTagsEndpoint(),
|
||||
symbol: '~',
|
||||
|
|
|
@ -49,7 +49,9 @@ class Admin::RunnersController < Admin::ApplicationController
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
private
|
||||
|
|
45
app/finders/autocomplete/acts_as_taggable_on/tags_finder.rb
Normal file
45
app/finders/autocomplete/acts_as_taggable_on/tags_finder.rb
Normal file
|
@ -0,0 +1,45 @@
|
|||
# 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
|
6
app/serializers/acts_as_taggable_on/tag_entity.rb
Normal file
6
app/serializers/acts_as_taggable_on/tag_entity.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActsAsTaggableOn::TagEntity < Grape::Entity
|
||||
expose :id
|
||||
expose :name
|
||||
end
|
5
app/serializers/acts_as_taggable_on/tag_serializer.rb
Normal file
5
app/serializers/acts_as_taggable_on/tag_serializer.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActsAsTaggableOn::TagSerializer < BaseSerializer
|
||||
entity ActsAsTaggableOn::TagEntity
|
||||
end
|
|
@ -1,17 +0,0 @@
|
|||
# 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 @@
|
|||
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
|
||||
%li.filter-dropdown-item
|
||||
%button.btn.btn-link.js-data-value
|
||||
{{title}}
|
||||
%span.dropdown-light-content
|
||||
{{name}}
|
||||
|
||||
= button_tag class: %w[clear-search hidden] do
|
||||
= icon('times')
|
||||
|
|
|
@ -615,6 +615,9 @@ msgstr ""
|
|||
msgid "An error occurred creating the new branch."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred fetching the dropdown data."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred previewing the blob"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
# 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
|
|
@ -1,17 +0,0 @@
|
|||
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
|
Loading…
Reference in a new issue