mensa 0.7.4 → 0.8.1

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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.shipkit.yml +1 -1
  3. data/CHANGELOG.md +14 -0
  4. data/Gemfile.lock +2 -2
  5. data/README.md +22 -0
  6. data/app/assets/tailwind/mensa/engine.css +6 -0
  7. data/app/components/mensa/add_filter/component.css +68 -70
  8. data/app/components/mensa/add_filter/component.html.erb +1 -1
  9. data/app/components/mensa/column_customizer/component.css +33 -32
  10. data/app/components/mensa/column_customizer/component_controller.js +1 -1
  11. data/app/components/mensa/control_bar/component.css +121 -125
  12. data/app/components/mensa/empty_state/component.css +13 -13
  13. data/app/components/mensa/filter_pill/component.css +26 -26
  14. data/app/components/mensa/filter_pill_list/component.css +35 -37
  15. data/app/components/mensa/filter_pill_list/component_controller.js +5 -5
  16. data/app/components/mensa/group_by/component.css +28 -32
  17. data/app/components/mensa/group_by/component_controller.js +3 -3
  18. data/app/components/mensa/group_header/component.css +21 -23
  19. data/app/components/mensa/group_header/component_controller.js +7 -2
  20. data/app/components/mensa/search/component.css +53 -55
  21. data/app/components/mensa/table/component.css +7 -7
  22. data/app/components/mensa/table/component.html.erb +1 -0
  23. data/app/components/mensa/table/component_controller.js +3 -2
  24. data/app/components/mensa/view/component.css +10 -10
  25. data/app/components/mensa/view/component.html.erb +1 -1
  26. data/app/components/mensa/view/paging.css +2 -2
  27. data/app/components/mensa/views/component.css +103 -105
  28. data/app/javascript/mensa/controllers/application_controller.js +6 -0
  29. data/app/tables/mensa/base.rb +16 -1
  30. data/app/tables/mensa/column.rb +6 -0
  31. data/app/tables/mensa/row.rb +6 -9
  32. data/lib/generators/mensa/tailwind_config_generator.rb +4 -0
  33. data/lib/mensa/verifier.rb +21 -1
  34. data/lib/mensa/version.rb +1 -1
  35. data/lib/tasks/mensa_tasks.rake +3 -1
  36. data/mensa.gemspec +1 -1
  37. metadata +11 -4
@@ -1,111 +1,109 @@
1
- .mensa-table {
2
- &__views {
3
- @apply relative flex-none;
4
-
5
- &__trigger {
6
- @apply h-full flex items-center gap-1.5 px-3 text-sm font-semibold text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 whitespace-nowrap cursor-pointer transition-colors;
7
- }
8
-
9
- &__trigger-icon {
10
- @apply text-gray-500 dark:text-gray-400 text-xs;
11
- }
12
-
13
- &__dropdown {
14
- @apply absolute left-0 top-full z-20 mt-1 min-w-[12rem] rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black/5 dark:ring-white/10 py-1;
15
- }
16
-
17
- &__option {
18
- @apply flex items-center justify-between px-1 hover:bg-gray-50 dark:hover:bg-gray-700;
19
-
20
- &-btn {
21
- @apply flex items-center gap-2 flex-1 px-2 py-1.5 text-sm text-left text-gray-700 dark:text-gray-200 cursor-pointer;
22
- }
23
-
24
- &-check {
25
- @apply text-primary-600 text-xs w-4;
26
- }
27
-
28
- &-menu {
29
- @apply p-1.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 rounded cursor-pointer opacity-0 transition-opacity;
30
- }
31
-
32
- &:hover &-menu {
33
- @apply opacity-100;
34
- }
35
-
36
- &-system {
37
- @apply p-1.5 text-gray-400 opacity-0;
38
- }
39
-
40
- &:hover &-system {
41
- @apply p-1.5 text-gray-300 opacity-100;
42
- }
43
- }
44
-
45
- &__submenu {
46
- @apply absolute z-30 min-w-[11rem] rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black/5 dark:ring-white/10 py-1;
47
-
48
- &-item {
49
- @apply flex items-center gap-2.5 w-full px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer;
50
-
51
- i {
52
- @apply w-4 text-gray-400;
53
- }
54
-
55
- &--danger {
56
- @apply text-red-600 dark:text-red-400;
57
-
58
- i {
59
- @apply text-red-500;
60
- }
61
- }
62
- }
63
- }
64
-
65
- &__rename-dialog {
66
- @apply w-80 max-w-[90vw] rounded-xl border-0 p-5 bg-white dark:bg-gray-800 shadow-2xl ring-1 ring-black/5 dark:ring-white/10;
67
-
68
- &::backdrop {
69
- @apply bg-gray-900/40;
70
- backdrop-filter: blur(2px);
71
- }
72
- }
73
-
74
- &__rename-form {
75
- @apply flex flex-col gap-4;
76
- }
77
-
78
- &__rename-title {
79
- @apply text-base font-semibold text-gray-900 dark:text-gray-100;
80
- }
81
-
82
- &__rename-label {
83
- @apply flex flex-col gap-1.5 text-sm font-medium text-gray-700 dark:text-gray-300;
84
- }
85
-
86
- input&__rename-input {
87
- @apply w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm text-gray-900 dark:text-gray-100 shadow-sm placeholder:text-gray-400 focus:border-primary-500 focus:ring-2 focus:ring-primary-500 focus:outline-none;
88
- border-radius: 0.5rem;
89
- }
90
-
91
- &__rename-actions {
92
- @apply flex justify-end gap-2;
93
- }
94
-
95
- &__rename-btn {
96
- @apply rounded-lg px-3 py-1.5 text-sm font-semibold shadow-sm focus:outline-none transition-colors;
97
-
98
- &--secondary {
99
- @apply bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600;
100
- }
101
-
102
- &--primary {
103
- @apply bg-primary-600 text-white hover:bg-primary-500;
104
- }
105
- }
1
+ .mensa-table__views {
2
+ @apply relative flex-none;
3
+ }
4
+
5
+ .mensa-table__views__trigger {
6
+ @apply h-full flex items-center gap-1.5 px-3 text-sm font-semibold text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 whitespace-nowrap cursor-pointer transition-colors;
7
+ }
8
+
9
+ .mensa-table__views__trigger-icon {
10
+ @apply text-gray-500 dark:text-gray-400 text-xs;
11
+ }
12
+
13
+ .mensa-table__views__dropdown {
14
+ @apply absolute left-0 top-full z-20 mt-1 min-w-[12rem] rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black/5 dark:ring-white/10 py-1;
15
+ }
16
+
17
+ .mensa-table__views__option {
18
+ @apply flex items-center justify-between px-1 hover:bg-gray-50 dark:hover:bg-gray-700;
19
+ }
20
+
21
+ .mensa-table__views__option-btn {
22
+ @apply flex items-center gap-2 flex-1 px-2 py-1.5 text-sm text-left text-gray-700 dark:text-gray-200 cursor-pointer;
23
+ }
24
+
25
+ .mensa-table__views__option-check {
26
+ @apply text-primary-600 text-xs w-4;
27
+ }
28
+
29
+ .mensa-table__views__option-menu {
30
+ @apply p-1.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 rounded cursor-pointer opacity-0 transition-opacity;
31
+ }
32
+
33
+ .mensa-table__views__option:hover .mensa-table__views__option-menu {
34
+ @apply opacity-100;
35
+ }
36
+
37
+ .mensa-table__views__option-system {
38
+ @apply p-1.5 text-gray-400 opacity-0;
39
+ }
40
+
41
+ .mensa-table__views__option:hover .mensa-table__views__option-system {
42
+ @apply p-1.5 text-gray-300 opacity-100;
43
+ }
44
+
45
+ .mensa-table__views__submenu {
46
+ @apply absolute z-30 min-w-[11rem] rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black/5 dark:ring-white/10 py-1;
47
+ }
48
+
49
+ .mensa-table__views__submenu-item {
50
+ @apply flex items-center gap-2.5 w-full px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer;
51
+
52
+ i {
53
+ @apply w-4 text-gray-400;
106
54
  }
107
55
  }
108
56
 
57
+ .mensa-table__views__submenu-item--danger {
58
+ @apply text-red-600 dark:text-red-400;
59
+
60
+ i {
61
+ @apply text-red-500;
62
+ }
63
+ }
64
+
65
+ .mensa-table__views__rename-dialog {
66
+ @apply w-80 max-w-[90vw] rounded-xl border-0 p-5 bg-white dark:bg-gray-800 shadow-2xl ring-1 ring-black/5 dark:ring-white/10;
67
+
68
+ &::backdrop {
69
+ @apply bg-gray-900/40;
70
+ backdrop-filter: blur(2px);
71
+ }
72
+ }
73
+
74
+ .mensa-table__views__rename-form {
75
+ @apply flex flex-col gap-4;
76
+ }
77
+
78
+ .mensa-table__views__rename-title {
79
+ @apply text-base font-semibold text-gray-900 dark:text-gray-100;
80
+ }
81
+
82
+ .mensa-table__views__rename-label {
83
+ @apply flex flex-col gap-1.5 text-sm font-medium text-gray-700 dark:text-gray-300;
84
+ }
85
+
86
+ input.mensa-table__views__rename-input {
87
+ @apply w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm text-gray-900 dark:text-gray-100 shadow-sm placeholder:text-gray-400 focus:border-primary-500 focus:ring-2 focus:ring-primary-500 focus:outline-none;
88
+ border-radius: 0.5rem;
89
+ }
90
+
91
+ .mensa-table__views__rename-actions {
92
+ @apply flex justify-end gap-2;
93
+ }
94
+
95
+ .mensa-table__views__rename-btn {
96
+ @apply rounded-lg px-3 py-1.5 text-sm font-semibold shadow-sm focus:outline-none transition-colors;
97
+ }
98
+
99
+ .mensa-table__views__rename-btn--secondary {
100
+ @apply bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600;
101
+ }
102
+
103
+ .mensa-table__views__rename-btn--primary {
104
+ @apply bg-primary-600 text-white hover:bg-primary-500;
105
+ }
106
+
109
107
  .tippy-box[data-theme~="mensa"] {
110
108
  @apply bg-gray-200 text-gray-800 dark:text-gray-400;
111
109
  }
@@ -6,6 +6,12 @@ export default class ApplicationController extends Controller {
6
6
  this.element[this.identifier] = this
7
7
  }
8
8
 
9
+ // Turbo streams re-render parts of the table via Mensa's own controllers, so the key
10
+ // rendered on the page's table element is the only stable one.
11
+ get tableStorageKey() {
12
+ return this.element.closest('.mensa-table')?.dataset.mensaTableStorageKeyValue
13
+ }
14
+
9
15
  // Appends a member id to a collection URL while keeping its query string
10
16
  // (e.g. the table's params[...]) intact.
11
17
  memberUrl(collectionUrl, id) {
@@ -145,6 +145,8 @@ module Mensa
145
145
  path = original_view_context.mensa.table_path(name)
146
146
  query = {
147
147
  params: user_params || params,
148
+ filters: filters_param,
149
+ query: current_query,
148
150
  order: order_hash(order),
149
151
  turbo_frame_id: turbo_frame_id,
150
152
  table_view_id: table_view_id,
@@ -167,7 +169,13 @@ module Mensa
167
169
  # Key under which the browser persists this table's state (filters, order,
168
170
  # column order, ...). Tables built with different params get separate state.
169
171
  def storage_key
170
- [name, Mensa::TableParams.digest(params)].compact.join(":")
172
+ context = original_view_context if @original_view_context || component
173
+ scope = if context.respond_to?(:controller_name) && context.respond_to?(:action_name)
174
+ "#{context.controller_name}_#{context.action_name}_#{name}"
175
+ else
176
+ name
177
+ end
178
+ [scope, Mensa::TableParams.digest(params)].compact.join(":")
171
179
  end
172
180
 
173
181
  def all_views
@@ -257,6 +265,13 @@ module Mensa
257
265
  current_aggregates.presence || ("" unless config[:aggregates].nil?)
258
266
  end
259
267
 
268
+ # Blank means "explicitly no filters", which overrides the view's default
269
+ def filters_param
270
+ return unless config.key?(:filters)
271
+
272
+ config[:filters].presence || ""
273
+ end
274
+
260
275
  private
261
276
 
262
277
  def ensure_internal_columns_for_joined_associations
@@ -22,6 +22,12 @@ module Mensa
22
22
 
23
23
  AGGREGATE_FUNCTIONS = %i[count sum min max].freeze
24
24
 
25
+ # The record method that gives the value: `method :to_s` names one,
26
+ # `method true` uses the column's name
27
+ def value_method
28
+ config[:method] == true ? name : config[:method].presence
29
+ end
30
+
25
31
  def sort_direction
26
32
  value = table.config.dig(:order, name)
27
33
  value.presence&.to_sym
@@ -9,17 +9,14 @@ module Mensa
9
9
  @record = record
10
10
  end
11
11
 
12
+ # A model method is only called when the column asks for one with
13
+ # `method`, otherwise the value is what was selected under the column's
14
+ # name, so a model method with the same name doesn't shadow it
12
15
  def value(column)
13
- if column.attribute && record.respond_to?(column.attribute)
14
- record.public_send(column.attribute)
15
- elsif column.method && record.respond_to?(column.method)
16
- record.public_send(column.method)
17
- elsif record.respond_to?(column.name)
18
- record.public_send(column.name)
19
- elsif record.respond_to?(:[])
16
+ if column.value_method
17
+ record.public_send(column.value_method)
18
+ elsif record.has_attribute?(column.name)
20
19
  record[column.name]
21
- else
22
- "-"
23
20
  end
24
21
  end
25
22
 
@@ -7,6 +7,8 @@ module Mensa
7
7
  desc "Configures tailwind.config.js and application.tailwindcss.css"
8
8
 
9
9
  def add_content_to_tailwind_config
10
+ return unless File.exist?(File.join(destination_root, "config/tailwind.config.js"))
11
+
10
12
  inject_into_file "config/tailwind.config.js", before: "],\n theme: {" do
11
13
  " // Mensa content\n" +
12
14
  %w[/app/views/**/* /app/helpers/**/* /app/controllers/**/* /app/components/**/* /app/javascript/**/*.js /app/assets/**/*.css].map { |path| " \"#{Mensa::Engine.root}#{path}\"" }.join(",\n") +
@@ -15,6 +17,8 @@ module Mensa
15
17
  end
16
18
 
17
19
  def add_content_application_tailwind_css
20
+ return unless File.exist?(File.join(destination_root, "app/assets/stylesheets/application.tailwind.css"))
21
+
18
22
  inject_into_file "app/assets/stylesheets/application.tailwind.css", before: "@tailwind base;" do
19
23
  "@import '#{Mensa::Engine.root}/app/assets/stylesheets/mensa/application.css';\n"
20
24
  end
@@ -453,6 +453,7 @@ module Mensa
453
453
  def verify_columns
454
454
  @table.columns.each do |column|
455
455
  verify_column_select(column)
456
+ warn_empty_column(column) unless column.internal?
456
457
  verify_column_sort(column) if column.sortable? && !column.internal?
457
458
  verify_column_render(column) unless column.internal?
458
459
  end
@@ -460,7 +461,7 @@ module Mensa
460
461
 
461
462
  def verify_column_select(column)
462
463
  check(:columns, column.name, "select") do |c|
463
- skip!("no SQL attribute, the value comes from #{column.method ? "method :#{column.method}" : "the record"}") if column.attribute.blank?
464
+ skip!("no SQL attribute, the value comes from #{column.value_method ? "method :#{column.value_method}" : "the scope"}") if column.attribute.blank?
464
465
 
465
466
  # What Mensa selects besides the column: the id and internal columns,
466
467
  # which preloads in the scope need
@@ -473,6 +474,25 @@ module Mensa
473
474
  end
474
475
  end
475
476
 
477
+ # A column only calls a model method when it asks for one with `method`,
478
+ # without one its value is what the scope selects under its name. When
479
+ # nothing is selected under it, and no render block shows something else,
480
+ # the column is always empty.
481
+ def warn_empty_column(column)
482
+ return if column.value_method || column.config[:render].present?
483
+
484
+ record = @rows&.first&.record
485
+ return if record.nil? || record.has_attribute?(column.name)
486
+
487
+ fix = if record.respond_to?(column.name)
488
+ "Fix: add `method true` to the column to show the model's ##{column.name}, or give it an `attribute`."
489
+ else
490
+ "Fix: give the column an `attribute`, select #{column.name} in the table's scope, or add a `method`."
491
+ end
492
+ warn!(:columns, column.name, "empty column",
493
+ "nothing is selected as #{column.name} and it has no `method` or `render`, so it is always empty.\n#{fix}")
494
+ end
495
+
476
496
  # The SQL type of an `attribute` decides which filter operators and values
477
497
  # fit it. Without `type`, Mensa treats it as text: it offers LIKE and
478
498
  # compares with text, which fails for e.g. a boolean expression.
data/lib/mensa/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Mensa
2
- VERSION = "0.7.4"
2
+ VERSION = "0.8.1"
3
3
  end
@@ -14,7 +14,9 @@ namespace :mensa do
14
14
  end
15
15
  end
16
16
 
17
- if Rake::Task.task_defined?("tailwindcss:build")
17
+ # tailwindcss-rails v4 picks up app/assets/tailwind/mensa/engine.css itself (via tailwindcss:engines),
18
+ # so only hook in the v3 config generator when that task is absent.
19
+ if Rake::Task.task_defined?("tailwindcss:build") && !Rake::Task.task_defined?("tailwindcss:engines")
18
20
  Rake::Task["tailwindcss:build"].enhance(["mensa:tailwindcss:config"])
19
21
  Rake::Task["tailwindcss:watch"].enhance(["mensa:tailwindcss:config"])
20
22
  end
data/mensa.gemspec CHANGED
@@ -37,7 +37,7 @@ Gem::Specification.new do |spec|
37
37
  spec.add_dependency "pg", ">= 1.6"
38
38
  spec.add_dependency "view_component", ">=3"
39
39
 
40
- spec.add_dependency "tailwindcss-rails", "~> 3.3"
40
+ spec.add_dependency "tailwindcss-rails", ">= 3.3", "< 5"
41
41
  spec.add_dependency "importmap-rails"
42
42
  spec.add_dependency "turbo-rails"
43
43
  spec.add_dependency "stimulus-rails"
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.7.4
4
+ version: 0.8.1
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-10-14 00:00:00.000000000 Z
11
+ date: 2026-10-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -98,16 +98,22 @@ dependencies:
98
98
  name: tailwindcss-rails
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - "~>"
101
+ - - ">="
102
102
  - !ruby/object:Gem::Version
103
103
  version: '3.3'
104
+ - - "<"
105
+ - !ruby/object:Gem::Version
106
+ version: '5'
104
107
  type: :runtime
105
108
  prerelease: false
106
109
  version_requirements: !ruby/object:Gem::Requirement
107
110
  requirements:
108
- - - "~>"
111
+ - - ">="
109
112
  - !ruby/object:Gem::Version
110
113
  version: '3.3'
114
+ - - "<"
115
+ - !ruby/object:Gem::Version
116
+ version: '5'
111
117
  - !ruby/object:Gem::Dependency
112
118
  name: importmap-rails
113
119
  requirement: !ruby/object:Gem::Requirement
@@ -182,6 +188,7 @@ files:
182
188
  - app/assets/config/mensa_manifest.js
183
189
  - app/assets/images/mensa/.keep
184
190
  - app/assets/stylesheets/mensa/application.css
191
+ - app/assets/tailwind/mensa/engine.css
185
192
  - app/components/mensa/add_filter/component.css
186
193
  - app/components/mensa/add_filter/component.html.erb
187
194
  - app/components/mensa/add_filter/component.rb