mensa 0.2.4 → 0.2.6

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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/Dockerfile +6 -2
  3. data/.devcontainer/compose.yaml +1 -1
  4. data/.devcontainer/devcontainer.json +31 -29
  5. data/.devcontainer/postCreate.sh +8 -0
  6. data/.devcontainer/postStart.sh +9 -0
  7. data/.gitignore +3 -1
  8. data/.zed/tasks.json +12 -0
  9. data/CHANGELOG.md +6 -0
  10. data/Gemfile.lock +155 -153
  11. data/Procfile +1 -1
  12. data/README.md +95 -60
  13. data/app/assets/stylesheets/mensa/application.css +14 -11
  14. data/app/components/mensa/add_filter/component.css +110 -5
  15. data/app/components/mensa/add_filter/component.html.slim +10 -12
  16. data/app/components/mensa/add_filter/component.rb +8 -2
  17. data/app/components/mensa/add_filter/component_controller.js +697 -85
  18. data/app/components/mensa/cell/component.css +9 -0
  19. data/app/components/mensa/column_customizer/component.css +40 -0
  20. data/app/components/mensa/column_customizer/component.html.slim +14 -0
  21. data/app/components/mensa/column_customizer/component.rb +13 -0
  22. data/app/components/mensa/column_customizer/component_controller.js +383 -0
  23. data/app/components/mensa/control_bar/component.css +127 -4
  24. data/app/components/mensa/control_bar/component.html.slim +41 -14
  25. data/app/components/mensa/control_bar/component.rb +2 -6
  26. data/app/components/mensa/empty_state/component.css +20 -0
  27. data/app/components/mensa/empty_state/component.html.slim +7 -0
  28. data/app/components/mensa/empty_state/component.rb +18 -0
  29. data/app/components/mensa/filter_pill/component.css +23 -0
  30. data/app/components/mensa/filter_pill/component.html.slim +9 -0
  31. data/app/components/mensa/filter_pill/component.rb +24 -0
  32. data/app/components/mensa/filter_pill/component_controller.js +52 -0
  33. data/app/components/mensa/filter_pill_list/component.css +63 -0
  34. data/app/components/mensa/filter_pill_list/component.html.slim +11 -0
  35. data/app/components/mensa/{filter_list → filter_pill_list}/component.rb +1 -1
  36. data/app/components/mensa/filter_pill_list/component_controller.js +749 -0
  37. data/app/components/mensa/header/component.css +41 -43
  38. data/app/components/mensa/header/component.html.slim +7 -7
  39. data/app/components/mensa/header/component.rb +1 -1
  40. data/app/components/mensa/row_action/component.html.slim +2 -2
  41. data/app/components/mensa/row_action/component.rb +1 -1
  42. data/app/components/mensa/search/component.css +68 -9
  43. data/app/components/mensa/search/component.html.slim +19 -15
  44. data/app/components/mensa/search/component.rb +1 -1
  45. data/app/components/mensa/search/component_controller.js +39 -49
  46. data/app/components/mensa/selection/component_controller.js +147 -0
  47. data/app/components/mensa/table/component.css +28 -0
  48. data/app/components/mensa/table/component.html.slim +9 -6
  49. data/app/components/mensa/table/component.rb +1 -0
  50. data/app/components/mensa/table/component_controller.js +524 -76
  51. data/app/components/mensa/table_row/component.css +6 -0
  52. data/app/components/mensa/table_row/component.html.slim +8 -3
  53. data/app/components/mensa/table_row/component.rb +1 -1
  54. data/app/components/mensa/view/component.css +97 -29
  55. data/app/components/mensa/view/component.html.slim +23 -10
  56. data/app/components/mensa/view/component.rb +5 -0
  57. data/app/components/mensa/views/component.css +106 -13
  58. data/app/components/mensa/views/component.html.slim +51 -17
  59. data/app/components/mensa/views/component_controller.js +245 -20
  60. data/app/controllers/mensa/application_controller.rb +1 -1
  61. data/app/controllers/mensa/tables/batch_actions_controller.rb +24 -0
  62. data/app/controllers/mensa/tables/exports_controller.rb +96 -0
  63. data/app/controllers/mensa/tables/filters_controller.rb +6 -2
  64. data/app/controllers/mensa/tables/views_controller.rb +108 -0
  65. data/app/controllers/mensa/tables_controller.rb +5 -14
  66. data/app/helpers/mensa/application_helper.rb +4 -1
  67. data/app/javascript/mensa/application.js +2 -2
  68. data/app/javascript/mensa/controllers/application_controller.js +5 -21
  69. data/app/javascript/mensa/controllers/index.js +16 -7
  70. data/app/jobs/mensa/export_job.rb +77 -85
  71. data/app/models/mensa/export.rb +93 -0
  72. data/app/tables/mensa/action.rb +3 -1
  73. data/app/tables/mensa/base.rb +103 -17
  74. data/app/tables/mensa/batch_action.rb +27 -0
  75. data/app/tables/mensa/cell.rb +21 -6
  76. data/app/tables/mensa/column.rb +30 -25
  77. data/app/tables/mensa/config/action_dsl.rb +1 -1
  78. data/app/tables/mensa/config/batch_dsl.rb +13 -0
  79. data/app/tables/mensa/config/column_dsl.rb +1 -0
  80. data/app/tables/mensa/config/dsl_logic.rb +8 -4
  81. data/app/tables/mensa/config/filter_dsl.rb +4 -1
  82. data/app/tables/mensa/config/render_dsl.rb +1 -1
  83. data/app/tables/mensa/config/table_dsl.rb +14 -4
  84. data/app/tables/mensa/config/view_dsl.rb +2 -0
  85. data/app/tables/mensa/config_readers.rb +34 -3
  86. data/app/tables/mensa/filter.rb +94 -14
  87. data/app/tables/mensa/row.rb +1 -1
  88. data/app/tables/mensa/scope.rb +25 -13
  89. data/app/views/mensa/exports/_badge.html.slim +5 -0
  90. data/app/views/mensa/exports/_dialog.html.slim +42 -0
  91. data/app/views/mensa/exports/_list.html.slim +29 -0
  92. data/app/views/mensa/tables/filters/show.turbo_stream.slim +34 -6
  93. data/app/views/mensa/tables/show.html.slim +2 -0
  94. data/app/views/mensa/tables/show.turbo_stream.slim +1 -1
  95. data/app/views/mensa/tables/views/create.turbo_stream.slim +11 -0
  96. data/app/views/mensa/tables/views/destroy.turbo_stream.slim +11 -0
  97. data/app/views/mensa/tables/views/update.turbo_stream.slim +11 -0
  98. data/bin/setup +1 -1
  99. data/config/locales/en.yml +45 -1
  100. data/config/locales/nl.yml +46 -1
  101. data/config/routes.rb +7 -0
  102. data/db/migrate/20260604120000_create_mensa_exports.rb +25 -0
  103. data/docs/columns.png +0 -0
  104. data/docs/export.png +0 -0
  105. data/docs/filters.png +0 -0
  106. data/docs/table.png +0 -0
  107. data/lib/generators/mensa/tailwind_config_generator.rb +3 -3
  108. data/lib/generators/mensa/templates/config/initializers/mensa.rb +1 -1
  109. data/lib/mensa/configuration.rb +35 -15
  110. data/lib/mensa/engine.rb +15 -10
  111. data/lib/mensa/version.rb +1 -1
  112. data/lib/mensa.rb +2 -2
  113. data/lib/tasks/mensa_tasks.rake +1 -1
  114. data/mensa.gemspec +3 -2
  115. data/mise.toml +8 -0
  116. data/package-lock.json +0 -7
  117. metadata +60 -15
  118. data/app/components/mensa/filter/component_controller.js +0 -12
  119. data/app/components/mensa/filter_list/component.css +0 -14
  120. data/app/components/mensa/filter_list/component.html.slim +0 -14
  121. data/app/components/mensa/filter_list/component_controller.js +0 -14
  122. /data/{rubocop.yml → .rubocop.yml} +0 -0
@@ -0,0 +1,93 @@
1
+ module Mensa
2
+ # An export request for a Mensa table. Each export captures the table it was
3
+ # generated for, the view (if any), the requesting user and the request
4
+ # configuration (filters/query/order/page) needed to rebuild the data. Once
5
+ # processed by Mensa::ExportJob the generated CSV is stored in +asset+.
6
+ class Export < ApplicationRecord
7
+ STATUSES = %w[pending processing completed failed].freeze
8
+ FORMATS = %w[csv_excel plain_csv].freeze
9
+ SCOPES = %w[all current_page].freeze
10
+
11
+ belongs_to :user, optional: true
12
+ has_one_attached :asset
13
+
14
+ validates :table_name, presence: true
15
+ validates :status, inclusion: {in: STATUSES}
16
+
17
+ scope :for_table, ->(table_name) { where(table_name: table_name.to_s) }
18
+ scope :for_user, ->(user) { where(user_id: user.respond_to?(:id) ? user&.id : user) }
19
+ scope :completed, -> { where(status: "completed") }
20
+ scope :recent, -> { order(created_at: :desc) }
21
+
22
+ def completed?
23
+ status == "completed"
24
+ end
25
+
26
+ def failed?
27
+ status == "failed"
28
+ end
29
+
30
+ def processing?
31
+ status == "processing"
32
+ end
33
+
34
+ def pending?
35
+ status == "pending"
36
+ end
37
+
38
+ # True once the asset is ready to be downloaded by the user.
39
+ def downloadable?
40
+ completed? && asset.attached?
41
+ end
42
+
43
+ # Number of completed (downloadable) exports for a table/user combination.
44
+ # This is the number rendered in the export button badge.
45
+ def self.completed_count(table_name, user)
46
+ for_table(table_name).for_user(user).completed.count
47
+ end
48
+
49
+ # A stable, page-independent key identifying the exports of a table/user
50
+ # combination, used for Turbo stream names and DOM ids so background jobs
51
+ # can target them after completion.
52
+ def self.token(table_name, user)
53
+ user_key = user.respond_to?(:id) ? user&.id : user
54
+ [table_name.to_s, user_key || "anonymous"].join("-").parameterize
55
+ end
56
+
57
+ def self.stream_name(table_name, user)
58
+ "mensa-exports-#{token(table_name, user)}"
59
+ end
60
+
61
+ def self.badge_dom_id(table_name, user)
62
+ "mensa-export-badge-#{token(table_name, user)}"
63
+ end
64
+
65
+ def self.list_dom_id(table_name, user)
66
+ "mensa-export-list-#{token(table_name, user)}"
67
+ end
68
+
69
+ # Re-renders the export button badge and downloads list for everyone
70
+ # subscribed to this table/user's export stream. Best-effort: a missing
71
+ # Action Cable backend (or other broadcast failure) must never break the
72
+ # caller (job completion, download cleanup, ...).
73
+ def self.broadcast_refresh(table_name, user)
74
+ stream = stream_name(table_name, user)
75
+
76
+ Turbo::StreamsChannel.broadcast_replace_to(
77
+ stream,
78
+ target: badge_dom_id(table_name, user),
79
+ partial: "mensa/exports/badge",
80
+ locals: {table_name: table_name, user: user}
81
+ )
82
+
83
+ Turbo::StreamsChannel.broadcast_replace_to(
84
+ stream,
85
+ target: list_dom_id(table_name, user),
86
+ partial: "mensa/exports/list",
87
+ locals: {table_name: table_name, user: user, exports: for_table(table_name).for_user(user).recent}
88
+ )
89
+ rescue => e
90
+ Mensa.config.logger&.warn("Mensa::Export broadcast failed: #{e.class}: #{e.message}")
91
+ end
92
+ end
93
+ end
@@ -11,7 +11,9 @@ module Mensa
11
11
  class Action
12
12
  include ConfigReaders
13
13
 
14
- attr_reader :name, :table, :config
14
+ defined_by Mensa::Config::ActionDsl
15
+
16
+ attr_reader :name, :table
15
17
 
16
18
  def initialize(name, config:, table:)
17
19
  @name = name
@@ -2,20 +2,21 @@ module Mensa
2
2
  class Base
3
3
  include Pagy::Method
4
4
  include ConfigReaders
5
+
6
+ defined_by Mensa::Config::TableDsl
5
7
  include Scope
6
8
 
7
9
  attr_writer :original_view_context
8
10
  attr_accessor :component, :name, :table_view, :request
9
- attr_reader :config, :params
11
+ attr_reader :params
10
12
 
11
13
  config_reader :model
14
+ config_reader :scope
12
15
  config_reader :link, call: false
13
16
  config_reader :supports_views?
14
17
  config_reader :supports_custom_views?
15
18
  config_reader :supports_filters?
16
- config_reader :view_condensed?
17
- config_reader :view_condensed_toggle?
18
- config_reader :view_columns_sorting?
19
+ config_reader :view_columns_ordering?
19
20
  config_reader :show_header?
20
21
  config_reader :exportable?
21
22
  config_reader :export_with_password?
@@ -23,10 +24,26 @@ module Mensa
23
24
  def initialize(config = {})
24
25
  @params = config.to_h.deep_symbolize_keys
25
26
  @config = self.class.definition.merge(@params || {})
27
+
28
+ ensure_internal_columns_for_joined_associations
29
+
30
+ params[:hidden_columns]&.each do |column_name|
31
+ c = columns.find { |c| c.name == column_name.to_sym }
32
+ c.config[:visible] = false
33
+ end
26
34
  end
27
35
 
28
36
  def column_order
29
- config[:column_order] || config[:columns]&.keys
37
+ order = config[:column_order].presence || config[:columns]&.keys
38
+ order = order&.map(&:to_sym)
39
+ return order if order.nil?
40
+
41
+ # Internal columns are never shown in the column customizer UI, so they
42
+ # are absent from any URL-supplied column_order. Always append them so
43
+ # that columns and selected_scope include their attributes.
44
+ all_keys = (config[:columns]&.keys || []).map(&:to_sym)
45
+ internal_keys = all_keys.select { |key| config.dig(:columns, key, :internal) }
46
+ (order | internal_keys)
30
47
  end
31
48
 
32
49
  # Returns all columns
@@ -40,9 +57,15 @@ module Mensa
40
57
  columns.find { |c| c.name == name.to_sym }
41
58
  end
42
59
 
43
- # Returns the columns to be displayed
60
+ # Returns the columns to be displayed, ordered by column_order.
44
61
  def display_columns
45
- @display_columns ||= columns.select(&:visible?).reject(&:internal?)
62
+ @display_columns ||= begin
63
+ order = column_order || []
64
+ columns
65
+ .select(&:visible?)
66
+ .reject(&:internal?)
67
+ .sort_by { |col| order.index(col.name) || Float::INFINITY }
68
+ end
46
69
  end
47
70
 
48
71
  # Returns the rows to be displayed
@@ -55,8 +78,8 @@ module Mensa
55
78
  end
56
79
 
57
80
  def system_views
58
- [Mensa::SystemView.new(:all, config: {name: I18n.t("mensa.views.all")}, table: self)] +
59
- (config[:views] || {}).keys.map { |view_name| Mensa::SystemView.new(view_name, config: config.dig(:views, view_name), table: self) }
81
+ views = config[:views]&.key?(:default) ? [] : [Mensa::SystemView.new(:default, config: {name: I18n.t("mensa.views.default")}, table: self)]
82
+ views + (config[:views] || {}).keys.map { |view_name| Mensa::SystemView.new(view_name, config: config.dig(:views, view_name), table: self) }
60
83
  end
61
84
 
62
85
  # Returns true if the table has filters
@@ -64,6 +87,15 @@ module Mensa
64
87
  columns.any?(&:filter?)
65
88
  end
66
89
 
90
+ # Returns the active filters, skipping any whose column no longer exists.
91
+ def active_filters
92
+ (config[:filters] || {}).filter_map do |column_name, filter_config|
93
+ col = column(column_name)
94
+ next unless col
95
+ Mensa::Filter.new(column: col, config: filter_config, table: self)
96
+ end
97
+ end
98
+
67
99
  def actions?
68
100
  config[:actions].present?
69
101
  end
@@ -74,10 +106,27 @@ module Mensa
74
106
  @actions ||= config[:actions].keys.map { |action_name| Mensa::Action.new(action_name, config: config.dig(:actions, action_name), table: self) }
75
107
  end
76
108
 
109
+ def batch_actions?
110
+ config[:batches].present?
111
+ end
112
+
113
+ def batch_actions
114
+ return @batch_actions if @batch_actions
115
+
116
+ @batch_actions ||= config[:batches].keys.map { |batch_name| Mensa::BatchAction.new(batch_name, config: config.dig(:batches, batch_name), table: self) }
117
+ end
118
+
77
119
  # Returns the current path with configuration
78
- def path(order: {}, turbo_frame_id: nil, table_view_id: nil)
120
+ def path(order: {}, turbo_frame_id: nil, table_view_id: params[:table_view_id], column_order: params[:column_order], hidden_columns: params[:hidden_columns])
79
121
  # FIXME: if someone doesn't use as: :mensa in the routes, it breaks
80
- original_view_context.mensa.table_path(name, order: order_hash(order), turbo_frame_id: turbo_frame_id, table_view_id: table_view_id)
122
+ original_view_context.mensa.table_path(
123
+ name,
124
+ order: order_hash(order),
125
+ turbo_frame_id: turbo_frame_id,
126
+ table_view_id: table_view_id,
127
+ column_order: column_order,
128
+ hidden_columns: hidden_columns
129
+ )
81
130
  end
82
131
 
83
132
  def menu
@@ -88,12 +137,16 @@ module Mensa
88
137
 
89
138
  def all_views
90
139
  views = system_views
91
- views += TableView.where(table_name: name).where(user: [nil, Current.user])
140
+ views += TableView.where(table_name: name).where(user: [nil, current_user])
92
141
  views
93
142
  end
94
143
 
95
- def active_filters
96
- (config[:filters] || {}).map { |column_name, filter_config| Mensa::Filter.new(column: column(column_name), config: filter_config, table: self) }
144
+ # The user that owns custom views. Returns nil when the host application has
145
+ # no current user, in which case views cannot be saved.
146
+ def current_user
147
+ return Current.user if defined?(Current) && Current.respond_to?(:user)
148
+
149
+ nil
97
150
  end
98
151
 
99
152
  def table_id
@@ -108,9 +161,42 @@ module Mensa
108
161
 
109
162
  private
110
163
 
111
- class << self
112
- def definition(&)
113
- @definition ||= Mensa::Config::TableDsl.new(name, &).config
164
+ def ensure_internal_columns_for_joined_associations
165
+ config[:columns] ||= {}
166
+
167
+ auto_internal_column_names.each do |column_name|
168
+ config[:columns][column_name] ||= {internal: true}
169
+ end
170
+ end
171
+
172
+ def auto_internal_column_names
173
+ joined_association_names.filter_map do |association_name|
174
+ reflection = model.reflect_on_association(association_name)
175
+ reflection&.foreign_key&.to_sym
176
+ end.uniq
177
+ end
178
+
179
+ def joined_association_names
180
+ relation = scope
181
+ return [] unless relation.respond_to?(:joins_values) && relation.respond_to?(:left_outer_joins_values)
182
+
183
+ (relation.joins_values + relation.left_outer_joins_values).flat_map do |join_value|
184
+ association_names_from_join_value(join_value)
185
+ end.compact.uniq
186
+ end
187
+
188
+ def association_names_from_join_value(join_value)
189
+ case join_value
190
+ when Symbol, String
191
+ [join_value.to_sym]
192
+ when Array
193
+ join_value.flat_map { |value| association_names_from_join_value(value) }
194
+ when Hash
195
+ join_value.flat_map do |association_name, nested_join_values|
196
+ [association_name.to_sym, *association_names_from_join_value(nested_join_values)]
197
+ end
198
+ else
199
+ []
114
200
  end
115
201
  end
116
202
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mensa
4
+ # Represents a batch action that can be applied to multiple selected records.
5
+ #
6
+ # batch :archive do
7
+ # title "Archive"
8
+ # process { |records| records.update_all(archived: true) }
9
+ # end
10
+ class BatchAction
11
+ include ConfigReaders
12
+
13
+ defined_by Mensa::Config::BatchDsl
14
+
15
+ attr_reader :name, :table
16
+
17
+ def initialize(name, config:, table:)
18
+ @name = name
19
+ @table = table
20
+ @config = config
21
+ end
22
+
23
+ config_reader :title
24
+ config_reader :description
25
+ config_reader :process, call: false
26
+ end
27
+ end
@@ -24,7 +24,7 @@ module Mensa
24
24
  if proc
25
25
  row.table.original_view_context.instance_exec(row.record, &proc)
26
26
  else
27
- send("to_#{format}".to_sym)
27
+ send(:"to_#{format}")
28
28
  end
29
29
  end
30
30
 
@@ -33,18 +33,33 @@ module Mensa
33
33
  def to_html
34
34
  case value
35
35
  when NilClass
36
- ''
36
+ ""
37
37
  when TrueClass
38
- content_tag(:i, '', class: 'fa-solid fa-check')
38
+ content_tag(:i, "", class: "fa-solid fa-check")
39
39
  when FalseClass
40
- content_tag(:i, '', class: 'fa-solid fa-xmark')
40
+ content_tag(:i, "", class: "fa-solid fa-xmark")
41
41
  when Date
42
- respond_to?(:dt) ? dt(value) : value.strftime('%d.%m.%Y')
42
+ respond_to?(:dt) ? dt(value) : value.strftime("%d.%m.%Y")
43
43
  when Time, DateTime
44
- respond_to?(:ln) ? ln(value) : value.strftime('%d-%m-%Y %H:%M:%S')
44
+ respond_to?(:ln) ? ln(value) : value.strftime("%d-%m-%Y %H:%M:%S")
45
45
  else
46
46
  column.sanitize? ? sanitize(value.to_s) : value.to_s.html_safe
47
47
  end
48
48
  end
49
+
50
+ def to_csv
51
+ case value
52
+ when NilClass
53
+ ""
54
+ when TrueClass, FalseClass
55
+ value.to_s
56
+ when Date
57
+ respond_to?(:dt) ? dt(value) : value.strftime("%d.%m.%Y")
58
+ when Time, DateTime
59
+ respond_to?(:ln) ? ln(value) : value.strftime("%d-%m-%Y %H:%M:%S")
60
+ else
61
+ strip_tags(value.to_s)
62
+ end
63
+ end
49
64
  end
50
65
  end
@@ -3,7 +3,10 @@
3
3
  module Mensa
4
4
  class Column
5
5
  include ConfigReaders
6
- attr_reader :name, :table, :config
6
+
7
+ defined_by Mensa::Config::ColumnDsl
8
+
9
+ attr_reader :name, :table
7
10
 
8
11
  def initialize(name, config:, table:)
9
12
  @name = name
@@ -18,7 +21,8 @@ module Mensa
18
21
  config_reader :method # When a method needs to be called on the model, slow!
19
22
 
20
23
  def sort_direction
21
- table.config.dig(:order, name)&.to_sym
24
+ value = table.config.dig(:order, name)
25
+ value.present? ? value.to_sym : nil
22
26
  end
23
27
 
24
28
  def next_sort_direction
@@ -35,24 +39,32 @@ module Mensa
35
39
  return @attribute if @attribute
36
40
 
37
41
  @attribute = if config[:attribute].present?
38
- "#{config[:attribute]} AS #{name}"
39
- elsif table.model.column_names.include? name.to_s
40
- name.to_s
41
- else
42
- nil
43
- end
42
+ "#{config[:attribute]} AS #{name}"
43
+ elsif table.model.column_names.include? name.to_s
44
+ name.to_s
45
+ end
46
+ end
47
+
48
+ def active_record_column
49
+ @active_record_column ||= table.model&.columns&.find { _1.name == name.to_s }
50
+ end
51
+
52
+ def active_record_column_type
53
+ active_record_column&.type
54
+ end
55
+
56
+ def type
57
+ config[:type] || active_record_column_type
44
58
  end
45
59
 
46
60
  def attribute_for_condition
47
61
  return @attribute_for_condition if @attribute_for_condition
48
62
 
49
63
  @attribute_for_condition = if config[:attribute].present?
50
- config[:attribute]
51
- elsif table.model.column_names.include? name.to_s
52
- name.to_s
53
- else
54
- nil
55
- end
64
+ config[:attribute]
65
+ elsif table.model.column_names.include? name.to_s
66
+ name.to_s
67
+ end
56
68
  end
57
69
 
58
70
  # Returns true if the column supports filtering
@@ -63,7 +75,7 @@ module Mensa
63
75
  def filter
64
76
  return unless filter?
65
77
 
66
- @filter ||= Mensa::Filter.new(column: self, config: table.config.dig(:filters, name) || {}, table: table)
78
+ @filter ||= Mensa::Filter.new(column: self, config: table.config.dig(:columns, name, :filter) || {}, table: table)
67
79
  end
68
80
 
69
81
  def human_name
@@ -75,19 +87,12 @@ module Mensa
75
87
  end
76
88
 
77
89
  def menu
78
- Satis::Menus::Builder.build(:filter_menu, event: 'click') do |m|
90
+ Satis::Menus::Builder.build(:filter_menu, event: "click") do |m|
79
91
  if sortable?
80
- m.item :sort_ascending, icon: 'fa-solid fa-arrow-up-short-wide'.freeze, link: table.path(order: { name => :asc }), link_attributes: { "data-turbo-frame": "_self" }
81
- m.item :sort_descending, icon: 'fa-solid fa-arrow-down-wide-short'.freeze, link: table.path(order: { name => :asc }), link_attributes: { "data-turbo-frame": "_self" }
92
+ m.item :sort_ascending, icon: "fa-solid fa-arrow-up-short-wide", link: table.path(order: {name => :asc}), link_attributes: {"data-turbo-frame": "_self"}
93
+ m.item :sort_descending, icon: "fa-solid fa-arrow-down-wide-short", link: table.path(order: {name => :desc}), link_attributes: {"data-turbo-frame": "_self"}
82
94
  end
83
95
  end
84
96
  end
85
-
86
- private
87
- class << self
88
- def definition(&)
89
- @definition ||= Mensa::Config::ColumnDsl.new(self.name, &).config
90
- end
91
- end
92
97
  end
93
98
  end
@@ -4,7 +4,7 @@ module Mensa::Config
4
4
  class ActionDsl
5
5
  include DslLogic
6
6
 
7
- option :title, default: ->() { name }
7
+ option :title, default: -> { name }
8
8
  option :icon
9
9
  option :show, default: ->(record) { true }
10
10
  option :link
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mensa::Config
4
+ class BatchDsl
5
+ include DslLogic
6
+
7
+ option :title, default: -> { name.to_s.humanize }
8
+ option :description, default: ""
9
+ option :process, default: ->(records) {}
10
+
11
+ delegate :t, to: :I18n
12
+ end
13
+ end
@@ -15,6 +15,7 @@ module Mensa::Config
15
15
  # Mensa doesn't select the whole records, to only select what we need
16
16
  option :internal, default: false
17
17
  option :method
18
+ option :type
18
19
 
19
20
  option :visible, default: true
20
21
  option :render, dsl: Mensa::Config::RenderDsl
@@ -65,11 +65,11 @@ module Mensa::Config
65
65
  end
66
66
 
67
67
  ###
68
- # Define a DSL hash
69
- # by calling option :action, dsl: Mensa::ActionDsl
68
+ # Define a DSL that builds a hash
69
+ # by calling option :action, dsl_hash: Mensa::ActionDsl
70
70
  #
71
71
  def dsl_hash(option_name, klass, config_name:)
72
- config_name = config_name || option_name.to_s.pluralize.to_sym
72
+ config_name ||= option_name.to_s.pluralize.to_sym
73
73
 
74
74
  @default_config ||= {}
75
75
  @default_config[option_name.to_sym] = {}
@@ -80,8 +80,12 @@ module Mensa::Config
80
80
  end
81
81
  end
82
82
 
83
+ ###
84
+ # Define a DSL that builds an array
85
+ # by calling option :action, dsl_array: Mensa::ActionDsl
86
+ #
83
87
  def dsl_array(option_name, klass, config_name: option_name.to_s.pluralize.to_sym)
84
- config_name = config_name || option_name.to_s.pluralize.to_sym
88
+ config_name ||= option_name.to_s.pluralize.to_sym
85
89
 
86
90
  @default_config ||= {}
87
91
  @default_config[name.to_sym] = []
@@ -2,9 +2,12 @@ module Mensa::Config
2
2
  class FilterDsl
3
3
  include DslLogic
4
4
 
5
- option :operator, default: :equals
5
+ option :operator, default: :is
6
6
  option :value
7
7
  option :collection
8
8
  option :scope
9
+ option :multiple, default: false
10
+ option :as
11
+ option :operators
9
12
  end
10
13
  end
@@ -3,6 +3,6 @@ module Mensa::Config
3
3
  include DslLogic
4
4
 
5
5
  option :html
6
- option :xlsx
6
+ option :csv
7
7
  end
8
8
  end
@@ -5,7 +5,7 @@
5
5
  # render do # default Standard components
6
6
  # html # Mensa::TableComponent::Default
7
7
  # json # Mensa::JsonRenderer::Default
8
- # xlsx # Mensa::XlsxRenderer::Default
8
+ # csv # Mensa::CsvRenderer::Default
9
9
  # end
10
10
  #
11
11
  # column(:first_name) do
@@ -55,6 +55,7 @@ module Mensa::Config
55
55
  raise "No model found for #{self.class.name}"
56
56
  end
57
57
  }
58
+ option :scope, default: -> { model.all }
58
59
  option :column, dsl_hash: Mensa::Config::ColumnDsl
59
60
  option :link
60
61
 
@@ -70,16 +71,25 @@ module Mensa::Config
70
71
  # Actions
71
72
  option :action, dsl_hash: Mensa::Config::ActionDsl
72
73
 
74
+ option :batch, dsl_hash: Mensa::Config::BatchDsl
75
+
73
76
  option :render, dsl: Mensa::Config::RenderDsl
74
77
 
75
78
  option :supports_views, default: false
76
79
  option :supports_custom_views, default: false
77
80
  option :supports_filters, default: true
78
81
  option :show_header, default: true
79
- option :view_columns_sorting, default: true
80
- option :view_condensed, default: false
81
- option :view_condensed_toggle, default: true
82
+ # Whether the table allows to change column ordering
83
+ option :view_columns_ordering, default: true
82
84
 
83
85
  option :view, dsl_hash: Mensa::Config::ViewDsl
86
+
87
+ # Syntactic sugar for `column :carrier_id do internal true end`.
88
+ def internal(name, &block)
89
+ column(name) do
90
+ internal true
91
+ instance_exec(&block) if block
92
+ end
93
+ end
84
94
  end
85
95
  end
@@ -5,5 +5,7 @@ module Mensa::Config
5
5
  option :name
6
6
  option :description
7
7
  option :filter, dsl_hash: Mensa::Config::FilterDsl
8
+ option :column_order
9
+ option :hidden_columns
8
10
  end
9
11
  end
@@ -2,12 +2,43 @@ module Mensa
2
2
  module ConfigReaders
3
3
  extend ActiveSupport::Concern
4
4
 
5
+ included do
6
+ attr_reader :config
7
+ end
8
+
5
9
  class_methods do
10
+ def defined_by(dsl_class)
11
+ # Lazily-built DSL instance that accumulates configuration.
12
+ define_singleton_method(:dsl_definition) do
13
+ @dsl_definition ||= dsl_class.new(name)
14
+ end
15
+
16
+ define_singleton_method(:definition) do |&block|
17
+ dsl_definition.instance_eval(&block) if block
18
+ dsl_definition.config
19
+ end
20
+
21
+ # Forward unknown class-level calls to the DSL
22
+ define_singleton_method(:method_missing) do |method_name, *args, &block|
23
+ if dsl_definition.respond_to?(method_name)
24
+ dsl_definition.public_send(method_name, *args, &block)
25
+ else
26
+ super(method_name, *args, &block)
27
+ end
28
+ end
29
+
30
+ define_singleton_method(:respond_to_missing?) do |method_name, include_private = false|
31
+ dsl_definition.respond_to?(method_name, include_private) || super(method_name, include_private)
32
+ end
33
+ end
34
+
6
35
  # Set call to false to avoid immediate execution of Procs
7
- def config_reader(name, call: true)
36
+ def config_reader(name, call: true, cast: nil)
8
37
  define_method name do
9
- value = config[name.to_s.gsub("?", "").to_sym]
10
- call == true && value.is_a?(Proc) ? instance_exec(&value) : value
38
+ value = config[name.to_s.delete("?").to_sym]
39
+ value = (call == true && value.is_a?(Proc)) ? instance_exec(&value) : value
40
+ value = value.send(cast) if cast
41
+ value
11
42
  end
12
43
  end
13
44
  end