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.
- checksums.yaml +4 -4
- data/.devcontainer/Dockerfile +6 -2
- data/.devcontainer/compose.yaml +1 -1
- data/.devcontainer/devcontainer.json +31 -29
- data/.devcontainer/postCreate.sh +8 -0
- data/.devcontainer/postStart.sh +9 -0
- data/.gitignore +3 -1
- data/.zed/tasks.json +12 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile.lock +155 -153
- data/Procfile +1 -1
- data/README.md +95 -60
- data/app/assets/stylesheets/mensa/application.css +14 -11
- data/app/components/mensa/add_filter/component.css +110 -5
- data/app/components/mensa/add_filter/component.html.slim +10 -12
- data/app/components/mensa/add_filter/component.rb +8 -2
- data/app/components/mensa/add_filter/component_controller.js +697 -85
- data/app/components/mensa/cell/component.css +9 -0
- data/app/components/mensa/column_customizer/component.css +40 -0
- data/app/components/mensa/column_customizer/component.html.slim +14 -0
- data/app/components/mensa/column_customizer/component.rb +13 -0
- data/app/components/mensa/column_customizer/component_controller.js +383 -0
- data/app/components/mensa/control_bar/component.css +127 -4
- data/app/components/mensa/control_bar/component.html.slim +41 -14
- data/app/components/mensa/control_bar/component.rb +2 -6
- data/app/components/mensa/empty_state/component.css +20 -0
- data/app/components/mensa/empty_state/component.html.slim +7 -0
- data/app/components/mensa/empty_state/component.rb +18 -0
- data/app/components/mensa/filter_pill/component.css +23 -0
- data/app/components/mensa/filter_pill/component.html.slim +9 -0
- data/app/components/mensa/filter_pill/component.rb +24 -0
- data/app/components/mensa/filter_pill/component_controller.js +52 -0
- data/app/components/mensa/filter_pill_list/component.css +63 -0
- data/app/components/mensa/filter_pill_list/component.html.slim +11 -0
- data/app/components/mensa/{filter_list → filter_pill_list}/component.rb +1 -1
- data/app/components/mensa/filter_pill_list/component_controller.js +749 -0
- data/app/components/mensa/header/component.css +41 -43
- data/app/components/mensa/header/component.html.slim +7 -7
- data/app/components/mensa/header/component.rb +1 -1
- data/app/components/mensa/row_action/component.html.slim +2 -2
- data/app/components/mensa/row_action/component.rb +1 -1
- data/app/components/mensa/search/component.css +68 -9
- data/app/components/mensa/search/component.html.slim +19 -15
- data/app/components/mensa/search/component.rb +1 -1
- data/app/components/mensa/search/component_controller.js +39 -49
- data/app/components/mensa/selection/component_controller.js +147 -0
- data/app/components/mensa/table/component.css +28 -0
- data/app/components/mensa/table/component.html.slim +9 -6
- data/app/components/mensa/table/component.rb +1 -0
- data/app/components/mensa/table/component_controller.js +524 -76
- data/app/components/mensa/table_row/component.css +6 -0
- data/app/components/mensa/table_row/component.html.slim +8 -3
- data/app/components/mensa/table_row/component.rb +1 -1
- data/app/components/mensa/view/component.css +97 -29
- data/app/components/mensa/view/component.html.slim +23 -10
- data/app/components/mensa/view/component.rb +5 -0
- data/app/components/mensa/views/component.css +106 -13
- data/app/components/mensa/views/component.html.slim +51 -17
- data/app/components/mensa/views/component_controller.js +245 -20
- data/app/controllers/mensa/application_controller.rb +1 -1
- data/app/controllers/mensa/tables/batch_actions_controller.rb +24 -0
- data/app/controllers/mensa/tables/exports_controller.rb +96 -0
- data/app/controllers/mensa/tables/filters_controller.rb +6 -2
- data/app/controllers/mensa/tables/views_controller.rb +108 -0
- data/app/controllers/mensa/tables_controller.rb +5 -14
- data/app/helpers/mensa/application_helper.rb +4 -1
- data/app/javascript/mensa/application.js +2 -2
- data/app/javascript/mensa/controllers/application_controller.js +5 -21
- data/app/javascript/mensa/controllers/index.js +16 -7
- data/app/jobs/mensa/export_job.rb +77 -85
- data/app/models/mensa/export.rb +93 -0
- data/app/tables/mensa/action.rb +3 -1
- data/app/tables/mensa/base.rb +103 -17
- data/app/tables/mensa/batch_action.rb +27 -0
- data/app/tables/mensa/cell.rb +21 -6
- data/app/tables/mensa/column.rb +30 -25
- data/app/tables/mensa/config/action_dsl.rb +1 -1
- data/app/tables/mensa/config/batch_dsl.rb +13 -0
- data/app/tables/mensa/config/column_dsl.rb +1 -0
- data/app/tables/mensa/config/dsl_logic.rb +8 -4
- data/app/tables/mensa/config/filter_dsl.rb +4 -1
- data/app/tables/mensa/config/render_dsl.rb +1 -1
- data/app/tables/mensa/config/table_dsl.rb +14 -4
- data/app/tables/mensa/config/view_dsl.rb +2 -0
- data/app/tables/mensa/config_readers.rb +34 -3
- data/app/tables/mensa/filter.rb +94 -14
- data/app/tables/mensa/row.rb +1 -1
- data/app/tables/mensa/scope.rb +25 -13
- data/app/views/mensa/exports/_badge.html.slim +5 -0
- data/app/views/mensa/exports/_dialog.html.slim +42 -0
- data/app/views/mensa/exports/_list.html.slim +29 -0
- data/app/views/mensa/tables/filters/show.turbo_stream.slim +34 -6
- data/app/views/mensa/tables/show.html.slim +2 -0
- data/app/views/mensa/tables/show.turbo_stream.slim +1 -1
- data/app/views/mensa/tables/views/create.turbo_stream.slim +11 -0
- data/app/views/mensa/tables/views/destroy.turbo_stream.slim +11 -0
- data/app/views/mensa/tables/views/update.turbo_stream.slim +11 -0
- data/bin/setup +1 -1
- data/config/locales/en.yml +45 -1
- data/config/locales/nl.yml +46 -1
- data/config/routes.rb +7 -0
- data/db/migrate/20260604120000_create_mensa_exports.rb +25 -0
- data/docs/columns.png +0 -0
- data/docs/export.png +0 -0
- data/docs/filters.png +0 -0
- data/docs/table.png +0 -0
- data/lib/generators/mensa/tailwind_config_generator.rb +3 -3
- data/lib/generators/mensa/templates/config/initializers/mensa.rb +1 -1
- data/lib/mensa/configuration.rb +35 -15
- data/lib/mensa/engine.rb +15 -10
- data/lib/mensa/version.rb +1 -1
- data/lib/mensa.rb +2 -2
- data/lib/tasks/mensa_tasks.rake +1 -1
- data/mensa.gemspec +3 -2
- data/mise.toml +8 -0
- data/package-lock.json +0 -7
- metadata +60 -15
- data/app/components/mensa/filter/component_controller.js +0 -12
- data/app/components/mensa/filter_list/component.css +0 -14
- data/app/components/mensa/filter_list/component.html.slim +0 -14
- data/app/components/mensa/filter_list/component_controller.js +0 -14
- /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
|
data/app/tables/mensa/action.rb
CHANGED
data/app/tables/mensa/base.rb
CHANGED
|
@@ -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 :
|
|
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 :
|
|
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 ||=
|
|
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(:
|
|
59
|
-
|
|
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:
|
|
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(
|
|
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,
|
|
140
|
+
views += TableView.where(table_name: name).where(user: [nil, current_user])
|
|
92
141
|
views
|
|
93
142
|
end
|
|
94
143
|
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
data/app/tables/mensa/cell.rb
CHANGED
|
@@ -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}"
|
|
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,
|
|
38
|
+
content_tag(:i, "", class: "fa-solid fa-check")
|
|
39
39
|
when FalseClass
|
|
40
|
-
content_tag(:i,
|
|
40
|
+
content_tag(:i, "", class: "fa-solid fa-xmark")
|
|
41
41
|
when Date
|
|
42
|
-
respond_to?(:dt) ? dt(value) : value.strftime(
|
|
42
|
+
respond_to?(:dt) ? dt(value) : value.strftime("%d.%m.%Y")
|
|
43
43
|
when Time, DateTime
|
|
44
|
-
respond_to?(:ln) ? ln(value) : value.strftime(
|
|
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
|
data/app/tables/mensa/column.rb
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
module Mensa
|
|
4
4
|
class Column
|
|
5
5
|
include ConfigReaders
|
|
6
|
-
|
|
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)
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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(:
|
|
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:
|
|
90
|
+
Satis::Menus::Builder.build(:filter_menu, event: "click") do |m|
|
|
79
91
|
if sortable?
|
|
80
|
-
m.item :sort_ascending, icon:
|
|
81
|
-
m.item :sort_descending, icon:
|
|
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
|
|
@@ -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
|
|
@@ -65,11 +65,11 @@ module Mensa::Config
|
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
###
|
|
68
|
-
# Define a DSL hash
|
|
69
|
-
# by calling option :action,
|
|
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
|
|
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
|
|
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: :
|
|
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
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# render do # default Standard components
|
|
6
6
|
# html # Mensa::TableComponent::Default
|
|
7
7
|
# json # Mensa::JsonRenderer::Default
|
|
8
|
-
#
|
|
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
|
-
|
|
80
|
-
option :
|
|
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
|
|
@@ -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.
|
|
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
|