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
@@ -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
- config_reader :operator
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
- "#{column.human_name}: #{value}"
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(to_be_filtered_scope)
67
+ def filter_scope(record_scope)
33
68
  if scope
34
- to_be_filtered_scope.instance_exec(normalize(value), &scope)
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
- to_be_filtered_scope.where("#{column.attribute_for_condition} LIKE ?", "%#{normalize(value)}%")
39
- when :equals
40
- to_be_filtered_scope.where(column.attribute_for_condition => normalize(value))
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
- to_be_filtered_scope
94
+ record_scope
44
95
  end
45
96
  end
46
97
  end
47
98
 
48
- private
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
- class << self
51
- def definition(&)
52
- @definition ||= Mensa::Config::FilterDsl.new(self.name, &).config
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
@@ -26,7 +26,7 @@ module Mensa
26
26
  def link_attributes
27
27
  return {} unless link
28
28
 
29
- { href: link, data: { controller: "satis-link", action: "click->satis-link#follow tap->satis-link#follow" } }
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
@@ -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
- @filtered_scope = @filtered_scope.basic_search(params[:query]) if params[:query]
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(order_hash)
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.map(&:attribute).compact)
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
- # Though this works, perhaps moving this in column(s) is nicer
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
- (params[:order] || config[:order]).merge(new_params.symbolize_keys)
74
- .reject { |name, direction| direction.blank? }
75
- .transform_values { |value| value.to_sym }
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
- = sts.form_for(Mensa::Tables::FiltersController::Filter.new, url: '#') do |f|
3
- - if @column.filter.collection
4
- = f.input :value, collection: @column.filter.collection, input_html: { "data-action"=> "keydown.esc->mensa-add-filter#reset keydown.enter->mensa-add-filter#filterValueEntered", autofocus: "autofocus" }, label: @column.human_name
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
- = f.input :value, input_html: { "data-action"=> "keydown.esc->mensa-add-filter#reset keydown.enter->mensa-add-filter#filterValueEntered", autofocus: "autofocus" }, label: @column.human_name
7
- a.sts-table__add_filter__popover_container__clear
8
- | Clear
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
@@ -1,2 +1,4 @@
1
+ | Test
2
+ button.text-xs Esc
1
3
  turbo-frame id=@table.table_id target="_top"
2
4
  = render Mensa::View::Component.new(@table)
@@ -1,5 +1,5 @@
1
1
  = turbo_stream.update "filters-#{@table.table_id}" do
2
- = render Mensa::FilterList::Component.new(table: @table)
2
+ = render Mensa::FilterPillList::Component.new(table: @table)
3
3
 
4
4
  = turbo_stream.update @table.table_id do
5
5
  = render Mensa::View::Component.new(@table)
@@ -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 app:tailwindcss:config && pushd test/dummy && bin/rails tailwindcss:build && popd"
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'"
@@ -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
- all: All
55
+ default: All
@@ -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
- all: Alles
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 'rails/generators/base'
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('../templates', __dir__)
7
- desc 'Configures tailwind.config.js and application.tailwindcss.css'
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
@@ -18,6 +18,6 @@ Mensa.setup do |config|
18
18
  control_bar_compress: "fa-solid fa-compress",
19
19
  control_bar_export: "fa-solid fa-file-export",
20
20
  search: "fa-solid fa-magnifying-glass",
21
- filters_add_filter: "fa-solid fa-plus",
21
+ filters_add_filter: "fa-solid fa-plus"
22
22
  }
23
23
  end