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
data/app/tables/mensa/filter.rb
CHANGED
|
@@ -3,11 +3,31 @@
|
|
|
3
3
|
module Mensa
|
|
4
4
|
class Filter
|
|
5
5
|
include ConfigReaders
|
|
6
|
-
attr_reader :column, :config, :table
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
defined_by Mensa::Config::FilterDsl
|
|
8
|
+
|
|
9
|
+
attr_reader :column, :table
|
|
10
|
+
|
|
11
|
+
config_reader :operator, cast: :to_sym
|
|
9
12
|
config_reader :value
|
|
10
13
|
config_reader :scope
|
|
14
|
+
config_reader :multiple
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def OPERATORS
|
|
18
|
+
[
|
|
19
|
+
[:is, I18n.t("mensa.operators.is"), true],
|
|
20
|
+
[:isnt, I18n.t("mensa.operators.isnt"), true],
|
|
21
|
+
[:matches, I18n.t("mensa.operators.matches"), true],
|
|
22
|
+
[:does_not_match, I18n.t("mensa.operators.does_not_match"), true],
|
|
23
|
+
[:gt, I18n.t("mensa.operators.gt"), true],
|
|
24
|
+
[:gteq, I18n.t("mensa.operators.gteq"), true],
|
|
25
|
+
[:lt, I18n.t("mensa.operators.lt"), true],
|
|
26
|
+
[:lteq, I18n.t("mensa.operators.lteq"), true],
|
|
27
|
+
[:is_current, I18n.t("mensa.operators.is_current"), false]
|
|
28
|
+
].freeze
|
|
29
|
+
end
|
|
30
|
+
end
|
|
11
31
|
|
|
12
32
|
def initialize(column:, config:, table:)
|
|
13
33
|
@column = column
|
|
@@ -15,6 +35,10 @@ module Mensa
|
|
|
15
35
|
@table = table
|
|
16
36
|
end
|
|
17
37
|
|
|
38
|
+
def multiple?
|
|
39
|
+
!!multiple
|
|
40
|
+
end
|
|
41
|
+
|
|
18
42
|
def collection
|
|
19
43
|
return unless config&.key?(:collection)
|
|
20
44
|
|
|
@@ -25,36 +49,92 @@ module Mensa
|
|
|
25
49
|
end
|
|
26
50
|
end
|
|
27
51
|
|
|
52
|
+
# This defines how the filter should be displayed in the value popover
|
|
53
|
+
# :select => as a select input
|
|
54
|
+
# :checkbox => as a checkbox input
|
|
55
|
+
# :string => as a text input
|
|
56
|
+
def as
|
|
57
|
+
config[:as]
|
|
58
|
+
end
|
|
59
|
+
|
|
28
60
|
def to_s
|
|
29
|
-
|
|
61
|
+
parts = [column.human_name, operator_label]
|
|
62
|
+
formatted_value = value.is_a?(Array) ? value.join(", ") : value
|
|
63
|
+
parts << formatted_value if formatted_value.present? && operator_with_value?
|
|
64
|
+
parts.join(" ")
|
|
30
65
|
end
|
|
31
66
|
|
|
32
|
-
def filter_scope(
|
|
67
|
+
def filter_scope(record_scope)
|
|
33
68
|
if scope
|
|
34
|
-
|
|
69
|
+
record_scope.instance_exec(normalize(value), &scope)
|
|
35
70
|
else
|
|
36
71
|
case operator
|
|
72
|
+
when :is_current
|
|
73
|
+
record_scope.where("#{column.attribute_for_condition} = ?", Current.send(column.name))
|
|
37
74
|
when :matches
|
|
38
|
-
|
|
39
|
-
when :
|
|
40
|
-
|
|
75
|
+
record_scope.where("#{column.attribute_for_condition} LIKE ?", "%#{normalize(value)}%")
|
|
76
|
+
when :does_not_match
|
|
77
|
+
record_scope.where("#{column.attribute_for_condition} NOT LIKE ?", "%#{normalize(value)}%")
|
|
78
|
+
when :is
|
|
79
|
+
val = value.is_a?(Array) ? value : normalize(value)
|
|
80
|
+
record_scope.where(column.attribute_for_condition => val)
|
|
81
|
+
when :isnt
|
|
82
|
+
val = value.is_a?(Array) ? value : normalize(value)
|
|
83
|
+
record_scope.where.not(column.attribute_for_condition => val)
|
|
84
|
+
when :gt
|
|
85
|
+
record_scope.where(column.table.model.arel_table[column.attribute_for_condition].gt(normalize(value)))
|
|
86
|
+
when :lt
|
|
87
|
+
record_scope.where(column.table.model.arel_table[column.attribute_for_condition].lt(normalize(value)))
|
|
88
|
+
when :gteq
|
|
89
|
+
record_scope.where(column.table.model.arel_table[column.attribute_for_condition].gteq(normalize(value)))
|
|
90
|
+
when :lteq
|
|
91
|
+
record_scope.where(column.table.model.arel_table[column.attribute_for_condition].lteq(normalize(value)))
|
|
41
92
|
else
|
|
42
93
|
# Ignore unknown operators
|
|
43
|
-
|
|
94
|
+
record_scope
|
|
44
95
|
end
|
|
45
96
|
end
|
|
46
97
|
end
|
|
47
98
|
|
|
48
|
-
|
|
99
|
+
def operators
|
|
100
|
+
operators = Mensa::Filter.OPERATORS.dup
|
|
101
|
+
if config[:operators].present?
|
|
102
|
+
operators = operators.select { |op| config[:operators].include?(op[0]) }
|
|
103
|
+
else
|
|
104
|
+
operators.delete_if { |op| op[0] == :is_current } unless Current.method_defined?(column.name, false)
|
|
105
|
+
operators.delete_if { |op| [:matches, :does_not_match].include?(op[0]) } if collection.present?
|
|
106
|
+
operators.delete_if { |op| [:matches, :does_not_match].include?(op[0]) } if column.type == :integer || column.type == :date || column.type == :datetime
|
|
107
|
+
operators.delete_if { |op| [:is, :isnt].include?(op[0]) } if column.type == :date || column.type == :datetime
|
|
108
|
+
operators.delete_if { |op| [:gt, :lt, :gteq, :lteq].include?(op[0]) } if column.type == :string || column.type.blank?
|
|
109
|
+
end
|
|
110
|
+
operators
|
|
111
|
+
end
|
|
49
112
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
113
|
+
def operator_label
|
|
114
|
+
Mensa::Filter.OPERATORS.find { |op| op[0] == operator }[1]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def operator_with_value?
|
|
118
|
+
Mensa::Filter.OPERATORS.find { |op| op[0] == operator }[2]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def input_type
|
|
122
|
+
case column.type
|
|
123
|
+
when :integer
|
|
124
|
+
"number"
|
|
125
|
+
when :date
|
|
126
|
+
"date"
|
|
127
|
+
when :datetime
|
|
128
|
+
"datetime-local"
|
|
129
|
+
else
|
|
130
|
+
"text"
|
|
53
131
|
end
|
|
54
132
|
end
|
|
55
133
|
|
|
134
|
+
private
|
|
135
|
+
|
|
56
136
|
def normalize(query)
|
|
57
|
-
query.to_s.gsub(/\s(?![
|
|
137
|
+
query.to_s.gsub(/\s(?![&!|])/, '\\\\ ')
|
|
58
138
|
end
|
|
59
139
|
end
|
|
60
140
|
end
|
data/app/tables/mensa/row.rb
CHANGED
|
@@ -26,7 +26,7 @@ module Mensa
|
|
|
26
26
|
def link_attributes
|
|
27
27
|
return {} unless link
|
|
28
28
|
|
|
29
|
-
{
|
|
29
|
+
{href: link, data: {controller: "satis-link", action: "click->satis-link#follow tap->satis-link#follow"}}
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def link
|
data/app/tables/mensa/scope.rb
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Mensa
|
|
4
|
+
# scope -> filtered_scope -> ordered_scope -> selected_scope ->
|
|
4
5
|
module Scope
|
|
5
6
|
extend ActiveSupport::Concern
|
|
6
7
|
|
|
7
8
|
included do
|
|
8
9
|
end
|
|
9
10
|
|
|
10
|
-
# Returns the records we want to display, using the Active Record Query Interface
|
|
11
|
-
# By default it returns all records
|
|
12
|
-
def scope
|
|
13
|
-
model.all
|
|
14
|
-
end
|
|
15
|
-
|
|
16
11
|
# Returns the scope, but filtered
|
|
17
12
|
def filtered_scope
|
|
18
13
|
return @filtered_scope if @filtered_scope
|
|
@@ -20,7 +15,13 @@ module Mensa
|
|
|
20
15
|
@filtered_scope = scope
|
|
21
16
|
# See https://github.com/textacular/textacular
|
|
22
17
|
# This has problems - not all table fields are searched
|
|
23
|
-
|
|
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
|
|
24
25
|
|
|
25
26
|
# Use inject
|
|
26
27
|
active_filters.each do |filter|
|
|
@@ -35,7 +36,7 @@ module Mensa
|
|
|
35
36
|
return @ordered_scope if @ordered_scope
|
|
36
37
|
|
|
37
38
|
@ordered_scope = filtered_scope
|
|
38
|
-
@ordered_scope = @ordered_scope.reorder(
|
|
39
|
+
@ordered_scope = @ordered_scope.reorder(effective_order)
|
|
39
40
|
|
|
40
41
|
@ordered_scope
|
|
41
42
|
end
|
|
@@ -45,7 +46,7 @@ module Mensa
|
|
|
45
46
|
return @selected_scope if @selected_scope
|
|
46
47
|
|
|
47
48
|
@selected_scope = ordered_scope
|
|
48
|
-
@selected_scope = @selected_scope.select([:id] + columns.
|
|
49
|
+
@selected_scope = @selected_scope.select([:id] + columns.filter_map(&:attribute))
|
|
49
50
|
|
|
50
51
|
@selected_scope
|
|
51
52
|
end
|
|
@@ -68,11 +69,22 @@ module Mensa
|
|
|
68
69
|
@pagy_details, @records = selected_scope.is_a?(Array) ? pagy(:offset, ordered_scope, anchor_string: 'data-turbo-frame="_self"') : pagy(:offset, selected_scope, anchor_string: 'data-turbo-frame="_self"')
|
|
69
70
|
end
|
|
70
71
|
|
|
71
|
-
#
|
|
72
|
+
# Effective ordering for SQL: when the request includes any order[] params
|
|
73
|
+
# (even with blank values), use only those — blank means "explicitly no sort".
|
|
74
|
+
# Falls back to the view/config default only when no order params were sent.
|
|
75
|
+
def effective_order
|
|
76
|
+
result = params.key?(:order) ? (params[:order] || {}) : (config[:order] || {})
|
|
77
|
+
result = result.symbolize_keys.compact_blank.transform_values(&:to_sym)
|
|
78
|
+
result.transform_keys { column(_1).attribute_for_condition }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Builds an order hash for URL generation. Merges current order with overrides;
|
|
82
|
+
# nil values become "" so they appear in the URL as order[col]= (which tells
|
|
83
|
+
# the server the user explicitly cleared that column's sort direction).
|
|
72
84
|
def order_hash(new_params = {})
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
85
|
+
base = params[:order]&.symbolize_keys || config[:order]&.symbolize_keys || {}
|
|
86
|
+
merged = base.merge(new_params.symbolize_keys)
|
|
87
|
+
merged.transform_values { |v| v.nil? ? "" : v.to_sym }
|
|
76
88
|
end
|
|
77
89
|
end
|
|
78
90
|
end
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/ Download counter shown on the export button. Hidden while there are no
|
|
2
|
+
/ completed downloads. Replaced via Turbo stream when a job finishes.
|
|
3
|
+
- count = Mensa::Export.completed_count(table_name, user)
|
|
4
|
+
span id=Mensa::Export.badge_dom_id(table_name, user) class="mensa-table__control_bar__badge #{'hidden' unless count.positive?}" aria-hidden=(count.positive? ? "false" : "true")
|
|
5
|
+
= count
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/ Export dialog rendered once per table inside the control bar. Opening it
|
|
2
|
+
/ refreshes the downloads list; submitting the form creates a new export.
|
|
3
|
+
- user = table.current_user
|
|
4
|
+
- exports = Mensa::Export.for_table(table.name).for_user(user).recent
|
|
5
|
+
dialog.mensa-table__export-dialog data-mensa-table-target="exportDialog" data-action="click->mensa-table#exportDialogBackdrop"
|
|
6
|
+
.mensa-table__export-dialog__panel
|
|
7
|
+
header.mensa-table__export-dialog__header
|
|
8
|
+
h2.mensa-table__export-dialog__title
|
|
9
|
+
= t("mensa.exports.title", default: "Export %{table}", table: table.name.to_s.humanize)
|
|
10
|
+
button.mensa-table__export-dialog__close type="button" data-action="mensa-table#cancelExport" aria-label=t("mensa.exports.close", default: "Close")
|
|
11
|
+
i.fa-solid.fa-xmark
|
|
12
|
+
|
|
13
|
+
.mensa-table__export-dialog__body
|
|
14
|
+
= render partial: "mensa/exports/list", locals: {table_name: table.name, user: user, exports: exports}
|
|
15
|
+
|
|
16
|
+
form.mensa-table__export-dialog__form data-action="submit->mensa-table#confirmExport"
|
|
17
|
+
fieldset.mensa-table__export-dialog__fieldset
|
|
18
|
+
legend.mensa-table__export-dialog__section-title
|
|
19
|
+
= t("mensa.exports.new_export", default: "New export")
|
|
20
|
+
label.mensa-table__export-dialog__option
|
|
21
|
+
input type="radio" name="scope" value="all" checked=true
|
|
22
|
+
span = t("mensa.exports.scope_all", default: "All records (matching current filters)")
|
|
23
|
+
label.mensa-table__export-dialog__option
|
|
24
|
+
input type="radio" name="scope" value="current_page"
|
|
25
|
+
span = t("mensa.exports.scope_current_page", default: "Current page")
|
|
26
|
+
|
|
27
|
+
fieldset.mensa-table__export-dialog__fieldset
|
|
28
|
+
legend.mensa-table__export-dialog__section-title
|
|
29
|
+
= t("mensa.exports.export_as", default: "Export as")
|
|
30
|
+
label.mensa-table__export-dialog__option
|
|
31
|
+
input type="radio" name="export_format" value="csv_excel" checked=true
|
|
32
|
+
span = t("mensa.exports.format_excel", default: "CSV for Excel, Numbers, or other spreadsheet programs")
|
|
33
|
+
label.mensa-table__export-dialog__option
|
|
34
|
+
input type="radio" name="export_format" value="plain_csv"
|
|
35
|
+
span = t("mensa.exports.format_plain", default: "Plain CSV file")
|
|
36
|
+
|
|
37
|
+
.mensa-table__export-dialog__actions
|
|
38
|
+
button.mensa-table__export-dialog__button.mensa-table__export-dialog__button--secondary type="button" data-action="mensa-table#cancelExport"
|
|
39
|
+
= t("mensa.exports.cancel", default: "Cancel")
|
|
40
|
+
button.mensa-table__export-dialog__button.mensa-table__export-dialog__button--primary type="submit"
|
|
41
|
+
i.fa-solid.fa-file-export
|
|
42
|
+
= t("mensa.exports.submit", default: "Export")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/ The list of a user's downloads for a table. Replaced via Turbo stream when a
|
|
2
|
+
/ new export is created or finishes processing.
|
|
3
|
+
div id=Mensa::Export.list_dom_id(table_name, user) class="mensa-table__export-dialog__downloads"
|
|
4
|
+
h3.mensa-table__export-dialog__section-title
|
|
5
|
+
= t("mensa.exports.available_downloads", default: "Available downloads")
|
|
6
|
+
- if exports.blank?
|
|
7
|
+
p.mensa-table__export-dialog__empty
|
|
8
|
+
= t("mensa.exports.empty", default: "You have no downloads yet. Create one below.")
|
|
9
|
+
- else
|
|
10
|
+
ul.mensa-table__export-dialog__list
|
|
11
|
+
- exports.each do |export|
|
|
12
|
+
li.mensa-table__export-dialog__item
|
|
13
|
+
.mensa-table__export-dialog__item-info
|
|
14
|
+
span.mensa-table__export-dialog__item-name
|
|
15
|
+
= export.filename.presence || t("mensa.exports.item_name", default: "%{table} export", table: export.table_name.to_s.humanize)
|
|
16
|
+
span.mensa-table__export-dialog__item-meta
|
|
17
|
+
= export.created_at.strftime("%Y-%m-%d %H:%M")
|
|
18
|
+
.mensa-table__export-dialog__item-action
|
|
19
|
+
- if export.downloadable?
|
|
20
|
+
= link_to mensa.download_table_export_path(export.table_name, export), class: "mensa-table__export-dialog__download", data: {turbo: false} do
|
|
21
|
+
i.fa-solid.fa-download
|
|
22
|
+
= t("mensa.exports.download", default: "Download")
|
|
23
|
+
- elsif export.failed?
|
|
24
|
+
span.mensa-table__export-dialog__status.mensa-table__export-dialog__status--failed
|
|
25
|
+
= t("mensa.exports.failed", default: "Failed")
|
|
26
|
+
- else
|
|
27
|
+
span.mensa-table__export-dialog__status.mensa-table__export-dialog__status--pending
|
|
28
|
+
i.fa-solid.fa-spinner.fa-spin
|
|
29
|
+
= t("mensa.exports.processing", default: "Preparing…")
|
|
@@ -1,8 +1,36 @@
|
|
|
1
1
|
= turbo_stream.update params[:target] do
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
.mensa-table__add_filter__popover_container__heading
|
|
3
|
+
= @column.human_name
|
|
4
|
+
- if @column.filter.operator_with_value?
|
|
5
|
+
- collection = @column.filter.collection
|
|
6
|
+
- if collection.present?
|
|
7
|
+
- unless @multiple
|
|
8
|
+
input[type="hidden" data-mensa-add-filter-target="value" value=@values.first]
|
|
9
|
+
ul.mensa-table__add_filter__popover_container__values data-multiple=("true" if @multiple)
|
|
10
|
+
- collection.each do |item|
|
|
11
|
+
- opt_label = item.is_a?(Array) ? item.first.to_s : item.to_s
|
|
12
|
+
- opt_value = item.is_a?(Array) ? item.last.to_s : item.to_s
|
|
13
|
+
- is_selected = @values.include?(opt_value)
|
|
14
|
+
li.mensa-table__add_filter__popover_container__value[data-mensa-add-filter-target="valueOption" data-value=opt_value data-label=opt_label data-selected=("true" if is_selected) data-action="click->mensa-add-filter#selectValue mouseenter->mensa-add-filter#highlightItem"]
|
|
15
|
+
- if @multiple
|
|
16
|
+
span.mensa-table__add_filter__checkbox class=("mensa-table__add_filter__checkbox--checked" if is_selected)
|
|
17
|
+
- else
|
|
18
|
+
i.mensa-table__add_filter__popover_container__value__check.fa-solid.fa-check class=("invisible" unless is_selected)
|
|
19
|
+
span.flex-1
|
|
20
|
+
= opt_label
|
|
21
|
+
span.mensa-table__add_filter__enter-hint
|
|
22
|
+
| ↵ Enter
|
|
5
23
|
- else
|
|
6
|
-
=
|
|
7
|
-
|
|
8
|
-
|
|
24
|
+
input.mensa-table__add_filter__popover_container__input[type=@column.filter.input_type value=@values.first autocomplete="off" inputmode=(@column.type == :integer ? "numeric" : nil) step=(@column.type == :integer ? 1 : (@column.type == :datetime ? 1 : nil)) data-mensa-add-filter-target="value" data-action="input->mensa-add-filter#manualValueChanged keydown.enter->mensa-add-filter#applyManualValue"]
|
|
25
|
+
hr.mensa-table__add_filter__popover_container__separator
|
|
26
|
+
ul.mensa-table__add_filter__popover_container__operators
|
|
27
|
+
- @column.filter.operators.each do |operator_name, label, requires_value|
|
|
28
|
+
li.mensa-table__add_filter__popover_container__operator[data-mensa-add-filter-target="operatorOption" data-operator=operator_name data-requires-value=(requires_value ? "true" : "false") data-selected=("true" if @operator == operator_name.to_s) data-action="click->mensa-add-filter#selectOperator mouseenter->mensa-add-filter#highlightItem"]
|
|
29
|
+
i.mensa-table__add_filter__popover_container__operator__check.fa-solid.fa-check class=("invisible" unless @operator == operator_name.to_s)
|
|
30
|
+
span.flex-1
|
|
31
|
+
= label
|
|
32
|
+
span.mensa-table__add_filter__enter-hint
|
|
33
|
+
| ↵ Enter
|
|
34
|
+
hr.mensa-table__add_filter__popover_container__separator
|
|
35
|
+
a.mensa-table__add_filter__popover_container__clear[href="#" data-action="click->mensa-add-filter#reset"]
|
|
36
|
+
| Clear
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/ Replace the views tabs so the newly saved view tab appears, selected.
|
|
2
|
+
= turbo_stream.replace "mensa-views-#{@table.table_id}" do
|
|
3
|
+
= render Mensa::Views::Component.new(table: @table)
|
|
4
|
+
|
|
5
|
+
/ Clear the filter pills — the saved view starts fresh with no extra filters.
|
|
6
|
+
= turbo_stream.update "filters-#{@table.table_id}" do
|
|
7
|
+
= render Mensa::FilterPillList::Component.new(table: @table)
|
|
8
|
+
|
|
9
|
+
/ Re-render the table body with the saved view's data.
|
|
10
|
+
= turbo_stream.update @table.table_id do
|
|
11
|
+
= render Mensa::View::Component.new(@table)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/ Replace the views dropdown without the deleted view; @table is at the default view.
|
|
2
|
+
= turbo_stream.replace "mensa-views-#{@table.table_id}" do
|
|
3
|
+
= render Mensa::Views::Component.new(table: @table)
|
|
4
|
+
|
|
5
|
+
/ Reset the search bar to the default view state.
|
|
6
|
+
= turbo_stream.update "filters-#{@table.table_id}" do
|
|
7
|
+
= render Mensa::FilterPillList::Component.new(table: @table)
|
|
8
|
+
|
|
9
|
+
/ Re-render the table body with the default view data.
|
|
10
|
+
= turbo_stream.update @table.table_id do
|
|
11
|
+
= render Mensa::View::Component.new(@table)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/ Replace the views tabs so the updated view tab stays selected.
|
|
2
|
+
= turbo_stream.replace "mensa-views-#{@table.table_id}" do
|
|
3
|
+
= render Mensa::Views::Component.new(table: @table)
|
|
4
|
+
|
|
5
|
+
/ Clear the filter pills — the view now reflects the saved config.
|
|
6
|
+
= turbo_stream.update "filters-#{@table.table_id}" do
|
|
7
|
+
= render Mensa::FilterPillList::Component.new(table: @table)
|
|
8
|
+
|
|
9
|
+
/ Re-render the table body with the updated view's data.
|
|
10
|
+
= turbo_stream.update @table.table_id do
|
|
11
|
+
= render Mensa::View::Component.new(@table)
|
data/bin/setup
CHANGED
|
@@ -58,7 +58,7 @@ FileUtils.chdir APP_ROOT do
|
|
|
58
58
|
system! "bin/rails log:clear tmp:clear"
|
|
59
59
|
|
|
60
60
|
puts "\n== Configuring tailwindcss =="
|
|
61
|
-
system! "bin/rails
|
|
61
|
+
system! "pushd test/dummy && bin/rails tailwindcss:config && bin/rails tailwindcss:build && popd"
|
|
62
62
|
|
|
63
63
|
puts "\n== Done, welcome to Mensa =="
|
|
64
64
|
puts "To start the application, run 'bin/overmind s'"
|
data/config/locales/en.yml
CHANGED
|
@@ -1,11 +1,55 @@
|
|
|
1
1
|
en:
|
|
2
2
|
mensa:
|
|
3
|
+
operators:
|
|
4
|
+
is: is
|
|
5
|
+
isnt: isn't
|
|
6
|
+
matches: matches
|
|
7
|
+
does_not_match: doesn't match
|
|
8
|
+
is_current: is current
|
|
9
|
+
gt: greater than
|
|
10
|
+
lt: less than
|
|
11
|
+
gteq: greater than or equal to
|
|
12
|
+
lteq: less than or equal to
|
|
3
13
|
add_filter:
|
|
4
14
|
component:
|
|
5
15
|
add_filter: Add filter
|
|
16
|
+
filter_pill_list:
|
|
17
|
+
component:
|
|
18
|
+
search: Search and filter
|
|
19
|
+
search_only: Search
|
|
6
20
|
search:
|
|
7
21
|
component:
|
|
22
|
+
search_in: Search in %{view}
|
|
8
23
|
cancel: Cancel
|
|
9
24
|
save: Save
|
|
25
|
+
save_view_title: Save view
|
|
26
|
+
save_view_subtitle: Save the current filters, ordering and search as a reusable view.
|
|
27
|
+
view_name: Name
|
|
28
|
+
view_name_placeholder: e.g. Active customers
|
|
29
|
+
view_description: Description
|
|
30
|
+
view_description_placeholder: Optional notes about this view
|
|
31
|
+
exports:
|
|
32
|
+
title: "Export %{table}"
|
|
33
|
+
close: Close
|
|
34
|
+
available_downloads: Available downloads
|
|
35
|
+
empty: You have no downloads yet. Create one below.
|
|
36
|
+
item_name: "%{table} export"
|
|
37
|
+
download: Download
|
|
38
|
+
processing: Preparing…
|
|
39
|
+
failed: Failed
|
|
40
|
+
new_export: New export
|
|
41
|
+
scope_all: All records (matching current filters)
|
|
42
|
+
scope_current_page: Current page
|
|
43
|
+
export_as: Export as
|
|
44
|
+
format_excel: CSV for Excel, Numbers, or other spreadsheet programs
|
|
45
|
+
format_plain: Plain CSV file
|
|
46
|
+
cancel: Cancel
|
|
47
|
+
submit: Export
|
|
48
|
+
paging:
|
|
49
|
+
info: Displaying %{model} %{from}-%{to} of %{count}
|
|
50
|
+
empty_state:
|
|
51
|
+
title: No %{model} found
|
|
52
|
+
subtitle: Try changing the filters or search terms for this view
|
|
53
|
+
clear_button: Clear search and filters
|
|
10
54
|
views:
|
|
11
|
-
|
|
55
|
+
default: All
|
data/config/locales/nl.yml
CHANGED
|
@@ -1,11 +1,56 @@
|
|
|
1
1
|
nl:
|
|
2
2
|
mensa:
|
|
3
|
+
operators:
|
|
4
|
+
is: is
|
|
5
|
+
isnt: is niet
|
|
6
|
+
matches: matcht
|
|
7
|
+
does_not_match: matcht niet
|
|
8
|
+
is_current: is huidige
|
|
9
|
+
gt: groter dan
|
|
10
|
+
lt: kleiner dan
|
|
11
|
+
gteq: groter dan of gelijk aan
|
|
12
|
+
lteq: kleiner dan of gelijk aan
|
|
3
13
|
add_filter:
|
|
4
14
|
component:
|
|
5
15
|
add_filter: Filter toevoegen
|
|
16
|
+
filter_pill_list:
|
|
17
|
+
component:
|
|
18
|
+
search: Zoeken en filteren
|
|
19
|
+
search_only: Zoeken
|
|
6
20
|
search:
|
|
7
21
|
component:
|
|
22
|
+
search: Zoek en filter
|
|
23
|
+
search_in: Zoek in %{view}
|
|
8
24
|
cancel: Annuleer
|
|
9
25
|
save: Bewaar
|
|
26
|
+
save_view_title: Weergave bewaren
|
|
27
|
+
save_view_subtitle: Bewaar de huidige filters, sortering en zoekopdracht als een herbruikbare weergave.
|
|
28
|
+
view_name: Naam
|
|
29
|
+
view_name_placeholder: bijv. Actieve klanten
|
|
30
|
+
view_description: Omschrijving
|
|
31
|
+
view_description_placeholder: Optionele notities over deze weergave
|
|
32
|
+
exports:
|
|
33
|
+
title: "%{table} exporteren"
|
|
34
|
+
close: Sluiten
|
|
35
|
+
available_downloads: Beschikbare downloads
|
|
36
|
+
empty: Je hebt nog geen downloads. Maak er hieronder een aan.
|
|
37
|
+
item_name: "%{table} export"
|
|
38
|
+
download: Downloaden
|
|
39
|
+
processing: Bezig…
|
|
40
|
+
failed: Mislukt
|
|
41
|
+
new_export: Nieuwe export
|
|
42
|
+
scope_all: Alle records (volgens huidige filters)
|
|
43
|
+
scope_current_page: Huidige pagina
|
|
44
|
+
export_as: Exporteren als
|
|
45
|
+
format_excel: CSV voor Excel, Numbers of andere spreadsheetprogramma's
|
|
46
|
+
format_plain: Eenvoudig CSV-bestand
|
|
47
|
+
cancel: Annuleer
|
|
48
|
+
submit: Exporteren
|
|
49
|
+
paging:
|
|
50
|
+
info: "%{model} %{from}-%{to} van %{count} weergeven"
|
|
51
|
+
empty_state:
|
|
52
|
+
title: Geen %{model} gevonden
|
|
53
|
+
subtitle: Probeer de filters of zoektermen voor deze weergave te wijzigen
|
|
54
|
+
clear_button: Zoekopdracht en filters wissen
|
|
10
55
|
views:
|
|
11
|
-
|
|
56
|
+
default: Alles
|
data/config/routes.rb
CHANGED
|
@@ -2,6 +2,13 @@ Mensa::Engine.routes.draw do
|
|
|
2
2
|
resources :tables do
|
|
3
3
|
scope module: :tables do
|
|
4
4
|
resources :filters
|
|
5
|
+
resources :views, only: [:create, :update, :destroy]
|
|
6
|
+
resources :batch_actions, only: [:create]
|
|
7
|
+
resources :exports, only: [:index, :create] do
|
|
8
|
+
member do
|
|
9
|
+
get :download
|
|
10
|
+
end
|
|
11
|
+
end
|
|
5
12
|
end
|
|
6
13
|
end
|
|
7
14
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class CreateMensaExports < ActiveRecord::Migration[7.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :mensa_exports, id: :uuid do |t|
|
|
4
|
+
t.string :table_name, null: false
|
|
5
|
+
# The view (system or custom) the export was generated for, if applicable.
|
|
6
|
+
t.references :table_view, null: true, type: :uuid, foreign_key: {to_table: :mensa_table_views}
|
|
7
|
+
|
|
8
|
+
# Scope ("all" / "current_page") and output format ("csv_excel" / "plain_csv").
|
|
9
|
+
t.string :scope
|
|
10
|
+
t.string :format
|
|
11
|
+
# Lifecycle: pending -> processing -> completed / failed.
|
|
12
|
+
t.string :status, null: false, default: "pending"
|
|
13
|
+
# The request configuration (filters, query, order, page) used to build the export.
|
|
14
|
+
t.jsonb :config, null: false, default: {}
|
|
15
|
+
t.string :filename
|
|
16
|
+
|
|
17
|
+
t.references :user, null: true, foreign_key: true, type: :uuid
|
|
18
|
+
|
|
19
|
+
t.timestamps
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
add_index :mensa_exports, :table_name
|
|
23
|
+
add_index :mensa_exports, :status
|
|
24
|
+
end
|
|
25
|
+
end
|
data/docs/columns.png
ADDED
|
Binary file
|
data/docs/export.png
ADDED
|
Binary file
|
data/docs/filters.png
CHANGED
|
Binary file
|
data/docs/table.png
CHANGED
|
Binary file
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
require
|
|
1
|
+
require "rails/generators/base"
|
|
2
2
|
|
|
3
3
|
module Mensa
|
|
4
4
|
module Generators
|
|
5
5
|
class TailwindConfigGenerator < Rails::Generators::Base
|
|
6
|
-
source_root File.expand_path(
|
|
7
|
-
desc
|
|
6
|
+
source_root File.expand_path("../templates", __dir__)
|
|
7
|
+
desc "Configures tailwind.config.js and application.tailwindcss.css"
|
|
8
8
|
|
|
9
9
|
def add_content_to_tailwind_config
|
|
10
10
|
inject_into_file "config/tailwind.config.js", before: "],\n theme: {" do
|