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 +4 -4
- data/Gemfile.lock +1 -4
- data/app/components/mensa/filter_pill/component.css +17 -3
- data/app/controllers/mensa/tables_controller.rb +2 -2
- data/app/tables/mensa/base.rb +1 -1
- data/app/tables/mensa/column.rb +16 -6
- data/app/tables/mensa/config/dsl_logic.rb +2 -2
- data/app/tables/mensa/config/table_dsl.rb +1 -0
- data/app/tables/mensa/filter.rb +8 -1
- data/app/tables/mensa/scope.rb +10 -11
- data/app/tables/mensa/search.rb +70 -0
- data/app/views/mensa/tables/show.html.erb +0 -2
- data/config/locales/en.yml +1 -0
- data/config/locales/nl.yml +1 -0
- data/lib/mensa/configuration.rb +1 -7
- data/lib/mensa/engine.rb +0 -1
- data/lib/mensa/version.rb +1 -1
- data/mensa.gemspec +0 -1
- metadata +3 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 10a862acae838d21598f9ce5d3e07b553a7a5e002b66c4993e32e0cb665d5a5d
|
|
4
|
+
data.tar.gz: 36115f04215a21212ad1f5f81ecd108f75e68199d4ae9a5db1c11cf74e136350
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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-
|
|
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-
|
|
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
|
data/app/tables/mensa/base.rb
CHANGED
data/app/tables/mensa/column.rb
CHANGED
|
@@ -22,7 +22,7 @@ module Mensa
|
|
|
22
22
|
|
|
23
23
|
def sort_direction
|
|
24
24
|
value = table.config.dig(:order, name)
|
|
25
|
-
value.
|
|
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
|
|
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 {
|
|
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
|
-
|
|
74
|
+
raw_attribute
|
|
65
75
|
elsif table.model.column_names.include? name.to_s
|
|
66
|
-
name
|
|
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
|
|
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 |
|
|
63
|
-
config[option_name.to_sym] = block
|
|
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
|
|
data/app/tables/mensa/filter.rb
CHANGED
|
@@ -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
|
data/app/tables/mensa/scope.rb
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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(
|
|
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
|
data/config/locales/en.yml
CHANGED
data/config/locales/nl.yml
CHANGED
data/lib/mensa/configuration.rb
CHANGED
|
@@ -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
data/lib/mensa/version.rb
CHANGED
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.
|
|
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
|
+
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
|