diff --git a/app/assets/javascripts/rails_admin/ra.filter-box.js b/app/assets/javascripts/rails_admin/ra.filter-box.js index ba2f41e8cf..522b9249f7 100644 --- a/app/assets/javascripts/rails_admin/ra.filter-box.js +++ b/app/assets/javascripts/rails_admin/ra.filter-box.js @@ -164,6 +164,7 @@ ) ); break; + case "citext": case "string": case "text": case "belongs_to_association": @@ -180,6 +181,13 @@ .prop("selected", field_operator == "like") .text(RailsAdmin.I18n.t("contains")) ) + .append( + $( + '' + ) + .prop("selected", field_operator == "not_like") + .text(RailsAdmin.I18n.t("does_not_contain")) + ) .append( $( '' diff --git a/app/assets/javascripts/rails_admin/ra.nested-form-hooks.js b/app/assets/javascripts/rails_admin/ra.nested-form-hooks.js index cee6ef82ad..4b87880b14 100644 --- a/app/assets/javascripts/rails_admin/ra.nested-form-hooks.js +++ b/app/assets/javascripts/rails_admin/ra.nested-form-hooks.js @@ -30,7 +30,10 @@ content = parent_group.children(".tab-content"); toggler = controls.find(".toggler"); nav.append(new_tab); - $(window.document).trigger("rails_admin.dom_ready", [field, parent_group]); + + const event = new CustomEvent("rails_admin.dom_ready", { detail: field }); + document.dispatchEvent(event); + new_tab.children("a").tab("show"); if (!one_to_one) { nav.filter(":hidden").show("slow"); diff --git a/app/assets/javascripts/rails_admin/ra.remote-form.js b/app/assets/javascripts/rails_admin/ra.remote-form.js index f416c812e5..df262132bf 100644 --- a/app/assets/javascripts/rails_admin/ra.remote-form.js +++ b/app/assets/javascripts/rails_admin/ra.remote-form.js @@ -105,7 +105,8 @@ }) .html(saveButtonText); - $(document).trigger("rails_admin.dom_ready", [form]); + const event = new CustomEvent("rails_admin.dom_ready", { detail: form }); + document.dispatchEvent(event); form.bind("ajax:complete", function (event) { var data = event.detail[0], diff --git a/app/assets/javascripts/rails_admin/ra.sidescroll.js b/app/assets/javascripts/rails_admin/ra.sidescroll.js index 4987845eb6..389608eb1e 100644 --- a/app/assets/javascripts/rails_admin/ra.sidescroll.js +++ b/app/assets/javascripts/rails_admin/ra.sidescroll.js @@ -1,7 +1,7 @@ (function ($) { "use strict"; - $(document).on("rails_admin.dom_ready", () => { + document.addEventListener("rails_admin.dom_ready", () => { const listForm = document.getElementById("bulk_form"); if (!listForm || !listForm.classList.contains("ra-sidescroll")) { return; diff --git a/app/assets/javascripts/rails_admin/ra.widgets.js b/app/assets/javascripts/rails_admin/ra.widgets.js index dc8d571cc2..ca159be345 100644 --- a/app/assets/javascripts/rails_admin/ra.widgets.js +++ b/app/assets/javascripts/rails_admin/ra.widgets.js @@ -1,5 +1,5 @@ (function ($) { - $(document).on("rails_admin.dom_ready", function (e, content) { + document.addEventListener("rails_admin.dom_ready", function (event) { var $editors, array, config_options, @@ -9,7 +9,7 @@ goFroalaWysiwygs, goSimpleMDEs, options; - content = content ? content : $("form"); + var content = event.detail || $("form"); if (content.length) { $.fn.datetimepicker.defaults.icons = { time: "fa fa-clock-o", diff --git a/app/assets/javascripts/rails_admin/ui.js b/app/assets/javascripts/rails_admin/ui.js index 729c7a9b6f..b39d25ef53 100644 --- a/app/assets/javascripts/rails_admin/ui.js +++ b/app/assets/javascripts/rails_admin/ui.js @@ -94,14 +94,17 @@ $("html").attr("lang"), $("#admin-js").data("i18nOptions") ); - $(document).trigger("rails_admin.dom_ready"); + + const event = new CustomEvent("rails_admin.dom_ready"); + document.dispatchEvent(event); }); $(document).on("pjax:end", function () { - $(document).trigger("rails_admin.dom_ready"); + const event = new CustomEvent("rails_admin.dom_ready"); + document.dispatchEvent(event); }); - $(document).on("rails_admin.dom_ready", function () { + document.addEventListener("rails_admin.dom_ready", function () { $(".nav.nav-pills li.active").removeClass("active"); $( '.nav.nav-pills li[data-model="' + $(".page-header").data("model") + '"]' diff --git a/config/locales/rails_admin.en.yml b/config/locales/rails_admin.en.yml index fb395e5b10..0e34dfb9d2 100644 --- a/config/locales/rails_admin.en.yml +++ b/config/locales/rails_admin.en.yml @@ -14,6 +14,7 @@ en: time: Time ... number: Number ... contains: Contains + does_not_contain: Does not contain is_exactly: Is exactly starts_with: Starts with ends_with: Ends with diff --git a/lib/rails_admin/adapters/active_record.rb b/lib/rails_admin/adapters/active_record.rb index 244c762c10..5f6b4b4271 100644 --- a/lib/rails_admin/adapters/active_record.rb +++ b/lib/rails_admin/adapters/active_record.rb @@ -220,7 +220,7 @@ def build_statement_for_type case @type when :boolean then build_statement_for_boolean when :integer, :decimal, :float then build_statement_for_integer_decimal_or_float - when :string, :text then build_statement_for_string_or_text + when :string, :text, :citext then build_statement_for_string_or_text when :enum then build_statement_for_enum when :belongs_to_association then build_statement_for_belongs_to_association when :uuid then build_statement_for_uuid @@ -252,7 +252,7 @@ def build_statement_for_string_or_text @value = begin case @operator - when 'default', 'like' + when 'default', 'like', 'not_like' "%#{@value}%" when 'starts_with' "#{@value}%" @@ -264,7 +264,13 @@ def build_statement_for_string_or_text end if ['postgresql', 'postgis'].include? ar_adapter - ["(#{@column} ILIKE ?)", @value] + if @operator == 'not_like' + ["(#{@column} NOT ILIKE ?)", @value] + else + ["(#{@column} ILIKE ?)", @value] + end + elsif @operator == 'not_like' + ["(LOWER(#{@column}) NOT LIKE ?)", @value] else ["(LOWER(#{@column}) LIKE ?)", @value] end diff --git a/lib/rails_admin/adapters/mongoid.rb b/lib/rails_admin/adapters/mongoid.rb index d9f1123362..310f90a7b2 100644 --- a/lib/rails_admin/adapters/mongoid.rb +++ b/lib/rails_admin/adapters/mongoid.rb @@ -260,6 +260,8 @@ def build_statement_for_string_or_text return if @value.blank? @value = begin case @operator + when 'not_like' + Regexp.compile("^((?!#{Regexp.escape(@value)}).)*$", Regexp::IGNORECASE) when 'default', 'like' Regexp.compile(Regexp.escape(@value), Regexp::IGNORECASE) when 'starts_with' diff --git a/lib/rails_admin/config.rb b/lib/rails_admin/config.rb index 3bbf399665..8d47d814b1 100644 --- a/lib/rails_admin/config.rb +++ b/lib/rails_admin/config.rb @@ -218,7 +218,7 @@ def current_user_method(&block) end def default_search_operator=(operator) - if %w(default like starts_with ends_with is =).include? operator + if %w(default like not_like starts_with ends_with is =).include? operator @default_search_operator = operator else raise(ArgumentError.new("Search operator '#{operator}' not supported")) diff --git a/lib/rails_admin/config/fields/types/all.rb b/lib/rails_admin/config/fields/types/all.rb index 9ba58c66e8..f431256689 100644 --- a/lib/rails_admin/config/fields/types/all.rb +++ b/lib/rails_admin/config/fields/types/all.rb @@ -38,3 +38,4 @@ require 'rails_admin/config/fields/types/json' require 'rails_admin/config/fields/types/inet' require 'rails_admin/config/fields/types/uuid' +require 'rails_admin/config/fields/types/citext' diff --git a/lib/rails_admin/config/fields/types/citext.rb b/lib/rails_admin/config/fields/types/citext.rb new file mode 100644 index 0000000000..c37de29c3e --- /dev/null +++ b/lib/rails_admin/config/fields/types/citext.rb @@ -0,0 +1,13 @@ +require 'rails_admin/config/fields/types/text' + +module RailsAdmin + module Config + module Fields + module Types + class Citext < Text + RailsAdmin::Config::Fields::Types.register(:citext, self) + end + end + end + end +end diff --git a/spec/rails_admin/adapters/active_record_spec.rb b/spec/rails_admin/adapters/active_record_spec.rb index 715deba334..45fef2376b 100644 --- a/spec/rails_admin/adapters/active_record_spec.rb +++ b/spec/rails_admin/adapters/active_record_spec.rb @@ -16,6 +16,13 @@ '(LOWER(field) LIKE ?)' end end + let(:not_like) do + if ['postgresql', 'postgis'].include? activerecord_config[:adapter] + '(field NOT ILIKE ?)' + else + '(LOWER(field) NOT LIKE ?)' + end + end def predicates_for(scope) scope.where_clause.instance_variable_get(:@predicates) @@ -274,6 +281,7 @@ def build_statement(type, value, operator) expect(build_statement(:string, 'foo', 'was')).to be_nil expect(build_statement(:string, 'foo', 'default')).to eq([like, '%foo%']) expect(build_statement(:string, 'foo', 'like')).to eq([like, '%foo%']) + expect(build_statement(:string, 'foo', 'not_like')).to eq([not_like, '%foo%']) expect(build_statement(:string, 'foo', 'starts_with')).to eq([like, 'foo%']) expect(build_statement(:string, 'foo', 'ends_with')).to eq([like, '%foo']) expect(build_statement(:string, 'foo', 'is')).to eq(['(field = ?)', 'foo']) diff --git a/spec/rails_admin/adapters/mongoid_spec.rb b/spec/rails_admin/adapters/mongoid_spec.rb index a3e09f794b..0cce88a049 100644 --- a/spec/rails_admin/adapters/mongoid_spec.rb +++ b/spec/rails_admin/adapters/mongoid_spec.rb @@ -385,6 +385,7 @@ def parse_value(value) expect(@abstract_model.send(:build_statement, :field, :string, 'foo', 'was')).to be_nil expect(@abstract_model.send(:build_statement, :field, :string, 'foo', 'default')).to eq(field: /foo/i) expect(@abstract_model.send(:build_statement, :field, :string, 'foo', 'like')).to eq(field: /foo/i) + expect(@abstract_model.send(:build_statement, :field, :string, 'foo', 'not_like')).to eq(field: /^((?!foo).)*$/i) expect(@abstract_model.send(:build_statement, :field, :string, 'foo', 'starts_with')).to eq(field: /^foo/i) expect(@abstract_model.send(:build_statement, :field, :string, 'foo', 'ends_with')).to eq(field: /foo$/i) expect(@abstract_model.send(:build_statement, :field, :string, 'foo', 'is')).to eq(field: 'foo') diff --git a/spec/rails_admin/config/fields/types/citext_spec.rb b/spec/rails_admin/config/fields/types/citext_spec.rb new file mode 100644 index 0000000000..d9d05270a9 --- /dev/null +++ b/spec/rails_admin/config/fields/types/citext_spec.rb @@ -0,0 +1,7 @@ +require 'spec_helper' + +RSpec.describe RailsAdmin::Config::Fields::Types::Citext do + it_behaves_like 'a generic field type', :string_field + + it_behaves_like 'a string-like field type', :string_field +end