mensa 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 194b34ee31bebddc3962cc66e5c789e5369f828eb6087f67f72b54aba97f65d8
4
- data.tar.gz: 1ead92c4d986f5e69c3d5745fcee27541f8205bf0106bd25f757b7603cdc117f
3
+ metadata.gz: 10a862acae838d21598f9ce5d3e07b553a7a5e002b66c4993e32e0cb665d5a5d
4
+ data.tar.gz: 36115f04215a21212ad1f5f81ecd108f75e68199d4ae9a5db1c11cf74e136350
5
5
  SHA512:
6
- metadata.gz: f5c078859d1c6214b8b65ab36fd077a2c36e2f5b50a69efd0dc20621d6373977be10ed543bc68add8b81338b4322903c3c305a15a0bd47c8ea906d77953c2a9d
7
- data.tar.gz: 6e68ccdd3ffea1ef460876f61a9d624a8d7de198addad7c712bfac67eac1cc08fbe0874221ba76f241ce5d47c8dec0b09c7950f058865ee232ca540868bea95d
6
+ metadata.gz: cfa2040a481694e363da898e51ab521ec7b8b4defc6775d46b919f8775296d57839e34afd690da5b7bfb478ab1ef9e727536ff6892db660fb4439026e5c7d2e0
7
+ data.tar.gz: 3080bef3dfe7f0ff02c64e43e7296865c1e835caa86f47bfeaa74d3e0c6232c2c886bd6f2ba3dafb7ffa0c7bc2790cce77a0fe5052e45c72a273874a7b63e226
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mensa (0.3.3)
4
+ mensa (0.3.4)
5
5
  csv
6
6
  importmap-rails
7
7
  pagy (>= 43)
@@ -10,7 +10,6 @@ PATH
10
10
  rubyzip (>= 1.2.2)
11
11
  stimulus-rails
12
12
  tailwindcss-rails (~> 3.3)
13
- textacular (>= 5)
14
13
  turbo-rails
15
14
  view_component (~> 3.11)
16
15
 
@@ -337,8 +336,6 @@ GEM
337
336
  tailwindcss-ruby (3.4.19-arm64-darwin)
338
337
  tailwindcss-ruby (3.4.19-x86_64-darwin)
339
338
  tailwindcss-ruby (3.4.19-x86_64-linux)
340
- textacular (5.7.0)
341
- activerecord (>= 5.0)
342
339
  thor (1.5.0)
343
340
  timeout (0.6.1)
344
341
  tsort (0.2.0)
@@ -1,8 +1,13 @@
1
1
  .mensa-filter-pill {
2
- @apply inline-flex items-stretch text-xs;
2
+ @apply inline-flex items-stretch text-xs rounded-md overflow-hidden;
3
3
 
4
4
  &__chip {
5
- @apply flex items-center gap-1 pl-2 pr-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 rounded-l-md cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors;
5
+ @apply flex items-center gap-1 pl-2 pr-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 rounded-md cursor-pointer transition-all;
6
+ }
7
+
8
+ &:hover &__chip,
9
+ &:focus-within &__chip {
10
+ @apply bg-gray-200 dark:bg-gray-600 rounded-r-none;
6
11
  }
7
12
 
8
13
  &__column {
@@ -18,6 +23,15 @@
18
23
  }
19
24
 
20
25
  &__remove {
21
- @apply inline-flex items-center justify-center px-1.5 text-gray-400 bg-gray-100 dark:bg-gray-700 rounded-r-md hover:text-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 dark:hover:text-gray-200 cursor-pointer transition-colors;
26
+ @apply inline-flex items-center justify-center w-0 px-0 text-gray-400 bg-gray-100 dark:bg-gray-700 cursor-pointer overflow-hidden opacity-0 -translate-x-1 pointer-events-none transition-all duration-200;
27
+ }
28
+
29
+ &:hover &__remove,
30
+ &:focus-within &__remove {
31
+ @apply w-7 px-1.5 opacity-100 translate-x-0 pointer-events-auto;
32
+ }
33
+
34
+ &__remove:hover {
35
+ @apply text-gray-700 bg-gray-200 dark:bg-gray-600 dark:text-gray-200;
22
36
  }
23
37
  }
@@ -7,7 +7,7 @@ module Mensa
7
7
  if params[:table_view_id]
8
8
  @view = Mensa::TableView.find_by(table_name: params[:id], id: params[:table_view_id])
9
9
  @view ||= @table.system_views.find { |v| v.id == params[:table_view_id].to_sym }
10
- config = @view&.config&.deep_transform_keys(&:to_sym)
10
+ config = @view&.config&.deep_transform_keys(&:to_sym) if @view
11
11
  end
12
12
 
13
13
  config = config.merge(params.permit!.to_h)
@@ -21,7 +21,7 @@ module Mensa
21
21
 
22
22
  respond_to do |format|
23
23
  format.turbo_stream # Used for filterering
24
- format.html
24
+ format.html # You shouldn't get here
25
25
  end
26
26
  end
27
27
  end
@@ -159,7 +159,7 @@ module Mensa
159
159
  config[:columns] ||= {}
160
160
 
161
161
  auto_internal_column_names.each do |column_name|
162
- config[:columns][column_name] ||= {internal: true}
162
+ config[:columns][column_name] ||= {internal: true, filter: false}
163
163
  end
164
164
  end
165
165
 
@@ -22,7 +22,7 @@ module Mensa
22
22
 
23
23
  def sort_direction
24
24
  value = table.config.dig(:order, name)
25
- value.present? ? value.to_sym : nil
25
+ value.presence&.to_sym
26
26
  end
27
27
 
28
28
  def next_sort_direction
@@ -41,12 +41,22 @@ module Mensa
41
41
  @attribute = if config[:attribute].present?
42
42
  "#{config[:attribute]} AS #{name}"
43
43
  elsif table.model.column_names.include? name.to_s
44
- name.to_s
44
+ "#{table.model.table_name}.#{name}"
45
+ end
46
+ end
47
+
48
+ def raw_attribute
49
+ return @raw_attribute if @raw_attribute
50
+
51
+ @raw_attribute = if config[:attribute].present?
52
+ config[:attribute]
53
+ elsif table.model.column_names.include? name.to_s
54
+ "#{table.model.table_name}.#{name}"
45
55
  end
46
56
  end
47
57
 
48
58
  def active_record_column
49
- @active_record_column ||= table.model&.columns&.find { it.name == name.to_s }
59
+ @active_record_column ||= table.model&.columns&.find { |column| column.name == name.to_s }
50
60
  end
51
61
 
52
62
  def active_record_column_type
@@ -61,15 +71,15 @@ module Mensa
61
71
  return @attribute_for_condition if @attribute_for_condition
62
72
 
63
73
  @attribute_for_condition = if config[:attribute].present?
64
- config[:attribute]
74
+ raw_attribute
65
75
  elsif table.model.column_names.include? name.to_s
66
- name.to_s
76
+ "#{table.model.table_name}.#{name}"
67
77
  end
68
78
  end
69
79
 
70
80
  # Returns true if the column supports filtering
71
81
  def filter?
72
- config.key?(:filter)
82
+ config[:filter] != false
73
83
  end
74
84
 
75
85
  def filter
@@ -59,8 +59,8 @@ module Mensa::Config
59
59
  # by calling option :render, dsl: Mensa::RenderDsl
60
60
  #
61
61
  def dsl_option(option_name, klass)
62
- define_method(option_name) do |name = nil, &block|
63
- config[option_name.to_sym] = block && klass.new(name, &block).config
62
+ define_method(option_name) do |value = nil, &block|
63
+ config[option_name.to_sym] = block ? klass.new(value, &block).config : value
64
64
  end
65
65
  end
66
66
 
@@ -88,6 +88,7 @@ module Mensa::Config
88
88
  def internal(name, &block)
89
89
  column(name) do
90
90
  internal true
91
+ filter false
91
92
  instance_exec(&block) if block
92
93
  end
93
94
  end
@@ -24,7 +24,8 @@ module Mensa
24
24
  [:gteq, I18n.t("mensa.operators.gteq"), true],
25
25
  [:lt, I18n.t("mensa.operators.lt"), true],
26
26
  [:lteq, I18n.t("mensa.operators.lteq"), true],
27
- [:is_current, I18n.t("mensa.operators.is_current"), false]
27
+ [:is_current, I18n.t("mensa.operators.is_current"), false],
28
+ [:is_empty, I18n.t("mensa.operators.is_empty"), false]
28
29
  ].freeze
29
30
  end
30
31
  end
@@ -69,6 +70,12 @@ module Mensa
69
70
  record_scope.instance_exec(normalize(value), &scope)
70
71
  else
71
72
  case operator
73
+ when :is_empty
74
+ if column.type == :string
75
+ record_scope.where("#{column.attribute_for_condition} IS NULL OR #{column.attribute_for_condition} = ''")
76
+ else
77
+ record_scope.where("#{column.attribute_for_condition} IS NULL")
78
+ end
72
79
  when :is_current
73
80
  record_scope.where("#{column.attribute_for_condition} = ?", Current.send(column.name))
74
81
  when :matches
@@ -4,6 +4,7 @@ module Mensa
4
4
  # scope -> filtered_scope -> ordered_scope -> selected_scope ->
5
5
  module Scope
6
6
  extend ActiveSupport::Concern
7
+ include Search
7
8
 
8
9
  included do
9
10
  end
@@ -13,15 +14,7 @@ module Mensa
13
14
  return @filtered_scope if @filtered_scope
14
15
 
15
16
  @filtered_scope = scope
16
- # See https://github.com/textacular/textacular
17
- # This has problems - not all table fields are searched
18
- if params[:query].present?
19
- @filtered_scope = if Mensa.config.search == :fuzzy
20
- @filtered_scope.fuzzy_search(params[:query])
21
- else
22
- @filtered_scope.basic_search(params[:query])
23
- end
24
- end
17
+ @filtered_scope = search(@filtered_scope, params[:query]) if params[:query].present?
25
18
 
26
19
  # Use inject
27
20
  active_filters.each do |filter|
@@ -36,7 +29,13 @@ module Mensa
36
29
  return @ordered_scope if @ordered_scope
37
30
 
38
31
  @ordered_scope = filtered_scope
39
- @ordered_scope = @ordered_scope.reorder(effective_order)
32
+ @ordered_scope = if effective_order.present?
33
+ @ordered_scope.reorder(effective_order)
34
+ elsif search_order_clause.present?
35
+ @ordered_scope.reorder(Arel.sql(search_order_clause))
36
+ else
37
+ @ordered_scope.reorder(nil)
38
+ end
40
39
 
41
40
  @ordered_scope
42
41
  end
@@ -75,7 +74,7 @@ module Mensa
75
74
  def effective_order
76
75
  result = params.key?(:order) ? (params[:order] || {}) : (config[:order] || {})
77
76
  result = result.symbolize_keys.compact_blank.transform_values(&:to_sym)
78
- result.transform_keys { column(_1).attribute_for_condition }
77
+ result.transform_keys { |column_name| column(column_name).attribute_for_condition }
79
78
  end
80
79
 
81
80
  # Builds an order hash for URL generation. Merges current order with overrides;
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mensa
4
+ module Search
5
+ extend ActiveSupport::Concern
6
+
7
+ private
8
+
9
+ def search(record_scope, query)
10
+ return record_scope if query.blank?
11
+ return record_scope unless record_scope.is_a?(ActiveRecord::Relation)
12
+
13
+ searchable_attributes = columns.filter_map(&:attribute_for_condition).uniq
14
+ return record_scope if searchable_attributes.empty?
15
+
16
+ @search_order_clause = nil
17
+
18
+ fuzzy_search?(record_scope) ? fuzzy_search(record_scope, searchable_attributes, query.to_s) : basic_search(record_scope, searchable_attributes, query.to_s)
19
+ end
20
+
21
+ def basic_search(record_scope, searchable_attributes, query)
22
+ sanitized_query = ActiveRecord::Base.sanitize_sql_like(query)
23
+ conditions = searchable_attributes.map do |attribute|
24
+ "CAST((#{attribute}) AS text) ILIKE :term"
25
+ end.join(" OR ")
26
+
27
+ record_scope.where(conditions, term: "%#{sanitized_query}%")
28
+ end
29
+
30
+ def fuzzy_search(record_scope, searchable_attributes, query)
31
+ sanitized_query = ActiveRecord::Base.sanitize_sql_like(query)
32
+ compare_quoted = record_scope.model.connection.quote(query)
33
+ text_expression = searchable_text_expression(searchable_attributes)
34
+ score_sql = "similarity(#{text_expression}, #{compare_quoted})"
35
+
36
+ @search_order_clause = "#{score_sql} DESC"
37
+
38
+ record_scope
39
+ .select(Arel.sql("#{score_sql} AS mensa_search_score"))
40
+ .where("#{text_expression} % :query OR #{text_expression} ILIKE :term", query: query, term: "%#{sanitized_query}%")
41
+ .order(Arel.sql(@search_order_clause))
42
+ end
43
+
44
+ def searchable_text_expression(searchable_attributes)
45
+ fields = searchable_attributes.map do |attribute|
46
+ "COALESCE(CAST((#{attribute}) AS text), '')"
47
+ end.join(", ")
48
+
49
+ "CONCAT_WS(' ', #{fields})"
50
+ end
51
+
52
+ def search_order_clause
53
+ @search_order_clause
54
+ end
55
+
56
+ def fuzzy_search?(record_scope)
57
+ Mensa.config.search == :fuzzy && pg_trgm_enabled?(record_scope)
58
+ end
59
+
60
+ def pg_trgm_enabled?(record_scope)
61
+ connection = record_scope.model.connection
62
+ return false unless connection.adapter_name.to_s.downcase.include?("postgres")
63
+ return connection.extension_enabled?("pg_trgm") if connection.respond_to?(:extension_enabled?)
64
+
65
+ connection.select_value("SELECT 1 FROM pg_extension WHERE extname = 'pg_trgm' LIMIT 1").present?
66
+ rescue
67
+ false
68
+ end
69
+ end
70
+ end
@@ -1,5 +1,3 @@
1
- Test
2
- <button class="text-xs">Esc</button>
3
1
  <turbo-frame id="<%= @table.table_id %>" target="_top">
4
2
  <%= render Mensa::View::Component.new(@table) %>
5
3
  </turbo-frame>
@@ -10,6 +10,7 @@ en:
10
10
  lt: less than
11
11
  gteq: greater than or equal to
12
12
  lteq: less than or equal to
13
+ is_empty: is empty
13
14
  add_filter:
14
15
  component:
15
16
  add_filter: Add filter
@@ -10,6 +10,7 @@ nl:
10
10
  lt: kleiner dan
11
11
  gteq: groter dan of gelijk aan
12
12
  lteq: kleiner dan of gelijk aan
13
+ is_empty: is leeg
13
14
  add_filter:
14
15
  component:
15
16
  add_filter: Filter toevoegen
@@ -78,13 +78,7 @@ module Mensa
78
78
 
79
79
  option :row_actions_position, default: :back
80
80
  # It's either :basic or :fuzzy, for fuzzy search you need to have `pg_trgm` extension installed
81
- option :search, default: -> {
82
- @_search_cache ||= begin
83
- (ActiveRecord::Base.connection.execute("SELECT extname FROM pg_extension where extname='pg_trgm';")&.first&.[]("extname") == "pg_trgm") ? :fuzzy : :basic
84
- rescue
85
- :basic
86
- end
87
- }, proc: true
81
+ option :search, default: :basic
88
82
 
89
83
  def initialize
90
84
  set_defaults!
data/lib/mensa/engine.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  require "pagy"
2
- require "textacular"
3
2
  require "tailwindcss-rails"
4
3
  require "importmap-rails"
5
4
  require "turbo-rails"
data/lib/mensa/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Mensa
2
- VERSION = "0.3.4"
2
+ VERSION = "0.4.0"
3
3
  end
data/mensa.gemspec CHANGED
@@ -35,7 +35,6 @@ Gem::Specification.new do |spec|
35
35
  spec.add_dependency "rubyzip", ">= 1.2.2"
36
36
  spec.add_dependency "pagy", ">=43"
37
37
  spec.add_dependency "pg", ">= 1.6"
38
- spec.add_dependency "textacular", ">=5"
39
38
  spec.add_dependency "view_component", "~> 3.11"
40
39
 
41
40
  spec.add_dependency "tailwindcss-rails", "~> 3.3"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mensa
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.4
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tom de Grunt
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-11 00:00:00.000000000 Z
11
+ date: 2026-06-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -80,20 +80,6 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '1.6'
83
- - !ruby/object:Gem::Dependency
84
- name: textacular
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '5'
90
- type: :runtime
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '5'
97
83
  - !ruby/object:Gem::Dependency
98
84
  name: view_component
99
85
  requirement: !ruby/object:Gem::Requirement
@@ -282,6 +268,7 @@ files:
282
268
  - app/tables/mensa/filter.rb
283
269
  - app/tables/mensa/row.rb
284
270
  - app/tables/mensa/scope.rb
271
+ - app/tables/mensa/search.rb
285
272
  - app/tables/mensa/system_view.rb
286
273
  - app/views/mensa/exports/_badge.html.erb
287
274
  - app/views/mensa/exports/_dialog.html.erb