mensa 0.3.4 → 0.5.0

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/.zed/tasks.json +1 -1
  3. data/Gemfile.lock +1 -4
  4. data/README.md +26 -14
  5. data/app/components/mensa/add_filter/component.html.erb +1 -1
  6. data/app/components/mensa/column_customizer/component_controller.js +1 -1
  7. data/app/components/mensa/control_bar/component.css +23 -3
  8. data/app/components/mensa/control_bar/component.html.erb +5 -5
  9. data/app/components/mensa/empty_state/component.rb +1 -1
  10. data/app/components/mensa/filter_pill/component.css +17 -3
  11. data/app/components/mensa/table/component.html.erb +1 -1
  12. data/app/components/mensa/table/component.rb +3 -3
  13. data/app/components/mensa/table/component_controller.js +97 -28
  14. data/app/components/mensa/view/component.html.erb +1 -1
  15. data/app/components/mensa/view/component.rb +1 -1
  16. data/app/controllers/mensa/tables/exports_controller.rb +25 -5
  17. data/app/controllers/mensa/tables_controller.rb +7 -9
  18. data/app/helpers/mensa/application_helper.rb +2 -2
  19. data/app/jobs/mensa/export_job.rb +4 -0
  20. data/app/jobs/mensa/recurring_exports_job.rb +17 -0
  21. data/app/models/mensa/export.rb +54 -2
  22. data/app/tables/mensa/base.rb +52 -12
  23. data/app/tables/mensa/column.rb +16 -6
  24. data/app/tables/mensa/config/dsl_logic.rb +2 -2
  25. data/app/tables/mensa/config/table_dsl.rb +2 -0
  26. data/app/tables/mensa/filter.rb +8 -1
  27. data/app/tables/mensa/scope.rb +14 -13
  28. data/app/tables/mensa/search.rb +70 -0
  29. data/app/views/mensa/exports/_dialog.html.erb +23 -0
  30. data/app/views/mensa/exports/_list.html.erb +13 -0
  31. data/app/views/mensa/tables/show.html.erb +0 -2
  32. data/app/views/mensa/tables/standard_error.html.erb +9 -0
  33. data/config/locales/en.yml +22 -0
  34. data/config/locales/nl.yml +21 -0
  35. data/config/routes.rb +1 -1
  36. data/db/migrate/20260612110000_add_repeat_to_mensa_exports.rb +8 -0
  37. data/lib/mensa/configuration.rb +6 -7
  38. data/lib/mensa/engine.rb +0 -1
  39. data/lib/mensa/version.rb +1 -1
  40. data/lib/tasks/mensa_tasks.rake +7 -0
  41. data/mensa.gemspec +0 -1
  42. metadata +6 -16
@@ -1,7 +1,7 @@
1
1
  module Mensa
2
2
  module ApplicationHelper
3
- def table(name, config = {}, **options)
4
- component = ::Mensa::Table::Component.new(name, config, **options)
3
+ def table(name, params: {}, **options)
4
+ component = ::Mensa::Table::Component.new(name, params: params, **options)
5
5
  component.original_view_context = self
6
6
  render(component)
7
7
  end
@@ -23,6 +23,7 @@ module Mensa
23
23
 
24
24
  data, filename, content_type = generate(table, export)
25
25
 
26
+ export.asset.purge if export.asset.attached?
26
27
  export.asset.attach(io: StringIO.new(data), filename: filename, content_type: content_type)
27
28
  finalize(export, status: "completed", filename: filename)
28
29
 
@@ -94,6 +95,9 @@ module Mensa
94
95
  def finalize(export, status:, filename: nil)
95
96
  attributes = {status: status}
96
97
  attributes[:filename] = filename if filename
98
+
99
+ attributes[:last_repeat_run_at] = Time.current if status == "completed" && export.repeat.present?
100
+
97
101
  export.update(attributes)
98
102
  # Refresh the export button badge (download count) and the downloads list
99
103
  # inside the export dialog for everyone viewing this table.
@@ -0,0 +1,17 @@
1
+ module Mensa
2
+ # Sweeps recurring exports and re-enqueues any whose next scheduled run is due.
3
+ # This job is intended to be invoked by a daily cron entry or scheduler.
4
+ class RecurringExportsJob < ApplicationJob
5
+ queue_as :default
6
+
7
+ def perform(reference_time = Time.current)
8
+ reference_time = reference_time.in_time_zone if reference_time.respond_to?(:in_time_zone)
9
+
10
+ Mensa::Export.repeating.find_each do |export|
11
+ next unless export.repeat_due?(reference_time)
12
+
13
+ Mensa::ExportJob.perform_later(export)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -7,17 +7,21 @@ module Mensa
7
7
  STATUSES = %w[pending processing completed failed].freeze
8
8
  FORMATS = %w[csv_excel plain_csv].freeze
9
9
  SCOPES = %w[all current_page].freeze
10
+ REPEATS = ["", "daily", "weekly", "monthly", "quarterly", "bi-yearly", "yearly"].freeze
10
11
 
11
12
  belongs_to :user, optional: true
12
13
  has_one_attached :asset
13
14
 
14
15
  validates :table_name, presence: true
15
16
  validates :status, inclusion: {in: STATUSES}
17
+ validates :repeat, inclusion: {in: REPEATS}
16
18
 
17
19
  scope :for_table, ->(table_name) { where(table_name: table_name.to_s) }
18
20
  scope :for_user, ->(user) { where(user_id: user.respond_to?(:id) ? user&.id : user) }
19
21
  scope :completed, -> { where(status: "completed") }
20
22
  scope :recent, -> { order(created_at: :desc) }
23
+ scope :repeating, -> { where.not(repeat: [nil, ""]) }
24
+ scope :with_downloadable_asset, -> { completed.joins(:asset_attachment).distinct }
21
25
 
22
26
  def completed?
23
27
  status == "completed"
@@ -35,15 +39,63 @@ module Mensa
35
39
  status == "pending"
36
40
  end
37
41
 
42
+ def next_repeat_run_at(from: nil)
43
+ return if repeat.blank?
44
+
45
+ anchor = from || last_repeat_run_at || created_at
46
+
47
+ case repeat
48
+ when "daily"
49
+ anchor + 1.day
50
+ when "weekly"
51
+ anchor + 1.week
52
+ when "monthly"
53
+ anchor.advance(months: 1)
54
+ when "quarterly"
55
+ anchor.advance(months: 3)
56
+ when "bi-yearly"
57
+ anchor.advance(months: 6)
58
+ when "yearly"
59
+ anchor.advance(years: 1)
60
+ end
61
+ end
62
+
63
+ def repeat_due?(reference_time = Time.current)
64
+ return false if repeat.blank? || pending? || processing?
65
+
66
+ next_run_at = next_repeat_run_at
67
+ next_run_at.present? && next_run_at <= reference_time
68
+ end
69
+
70
+ def repeating?
71
+ repeat.present?
72
+ end
73
+
74
+ def repeat_label
75
+ return if repeat.blank?
76
+
77
+ I18n.t(
78
+ "mensa.exports.repeats_with_interval",
79
+ default: "Repeats %{interval}",
80
+ interval: repeat_interval_label
81
+ )
82
+ end
83
+
84
+ def repeat_interval_label
85
+ return if repeat.blank?
86
+
87
+ I18n.t("mensa.exports.repeat_intervals.#{repeat}", default: repeat)
88
+ end
89
+
38
90
  # True once the asset is ready to be downloaded by the user.
39
91
  def downloadable?
40
92
  completed? && asset.attached?
41
93
  end
42
94
 
43
- # Number of completed (downloadable) exports for a table/user combination.
95
+ # Number of currently-downloadable exports for a table/user combination.
44
96
  # This is the number rendered in the export button badge.
45
97
  def self.completed_count(table_name, user)
46
- for_table(table_name).for_user(user).completed.count
98
+ for_table(table_name).for_user(user).with_downloadable_asset.count
47
99
  end
48
100
 
49
101
  # A stable, page-independent key identifying the exports of a table/user
@@ -8,8 +8,8 @@ module Mensa
8
8
 
9
9
  attr_writer :original_view_context
10
10
  attr_accessor :component, :name, :table_view, :request
11
- attr_reader :params
12
11
 
12
+ config_reader :params
13
13
  config_reader :model
14
14
  config_reader :scope
15
15
  config_reader :link, call: false
@@ -22,12 +22,12 @@ module Mensa
22
22
  config_reader :export_with_password?
23
23
 
24
24
  def initialize(config = {})
25
- @params = config.to_h.deep_symbolize_keys
26
- @config = self.class.definition.merge(@params || {})
25
+ normalized_config = config.to_h.deep_symbolize_keys
26
+ @config = self.class.definition.merge(normalized_config)
27
+ @params = (@config[:params].presence || {}).deep_symbolize_keys
28
+ @config[:params] = @params
27
29
 
28
- ensure_internal_columns_for_joined_associations
29
-
30
- params[:hidden_columns]&.each do |column_name|
30
+ current_hidden_columns&.each do |column_name|
31
31
  c = columns.find { |c| c.name == column_name.to_sym }
32
32
  c.config[:visible] = false
33
33
  end
@@ -48,12 +48,17 @@ module Mensa
48
48
 
49
49
  # Returns all columns
50
50
  def columns
51
+ ensure_internal_columns_for_joined_associations
51
52
  @columns ||= column_order.map { |column_name| Mensa::Column.new(column_name, config: config.dig(:columns, column_name), table: self) }
52
53
  end
53
54
 
54
55
  # Returns a column by name
55
56
  # @param [String] name
56
57
  def column(name)
58
+ found_column = columns.find { |c| c.name == name.to_sym }
59
+ return found_column if found_column || @internal_columns_ensured
60
+
61
+ @columns = nil
57
62
  columns.find { |c| c.name == name.to_sym }
58
63
  end
59
64
 
@@ -117,16 +122,19 @@ module Mensa
117
122
  end
118
123
 
119
124
  # Returns the current path with configuration
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])
125
+ def path(order: {}, turbo_frame_id: current_turbo_frame_id, table_view_id: current_table_view_id, column_order: current_column_order, hidden_columns: current_hidden_columns, user_params: nil)
121
126
  # FIXME: if someone doesn't use as: :mensa in the routes, it breaks
122
- original_view_context.mensa.table_path(
123
- name,
127
+ path = original_view_context.mensa.table_path(name)
128
+ query = {
129
+ params: user_params || params,
124
130
  order: order_hash(order),
125
131
  turbo_frame_id: turbo_frame_id,
126
132
  table_view_id: table_view_id,
127
133
  column_order: column_order,
128
134
  hidden_columns: hidden_columns
129
- )
135
+ }.compact.to_query
136
+
137
+ query.present? ? "#{path}?#{query}" : path
130
138
  end
131
139
 
132
140
  def all_views
@@ -146,21 +154,53 @@ module Mensa
146
154
  def table_id
147
155
  return @table_id if @table_id
148
156
 
149
- @table_id = params[:turbo_frame_id] || "#{name.to_s.gsub("/", "__")}-#{SecureRandom.base36}"
157
+ @table_id = current_turbo_frame_id || "#{name.to_s.gsub("/", "__")}-#{SecureRandom.base36}"
150
158
  end
151
159
 
152
160
  def original_view_context
153
161
  @original_view_context || component.original_view_context
154
162
  end
155
163
 
164
+ def current_query
165
+ config[:query]
166
+ end
167
+
168
+ def current_order
169
+ config[:order]
170
+ end
171
+
172
+ def current_order_provided?
173
+ config.key?(:order)
174
+ end
175
+
176
+ def current_table_view_id
177
+ config[:table_view_id]
178
+ end
179
+
180
+ def current_column_order
181
+ config[:column_order]
182
+ end
183
+
184
+ def current_hidden_columns
185
+ config[:hidden_columns]
186
+ end
187
+
188
+ def current_turbo_frame_id
189
+ config[:turbo_frame_id]
190
+ end
191
+
156
192
  private
157
193
 
158
194
  def ensure_internal_columns_for_joined_associations
195
+ return if @internal_columns_ensured
196
+
159
197
  config[:columns] ||= {}
160
198
 
161
199
  auto_internal_column_names.each do |column_name|
162
- config[:columns][column_name] ||= {internal: true}
200
+ config[:columns][column_name] ||= {internal: true, filter: false}
163
201
  end
202
+
203
+ @internal_columns_ensured = true
164
204
  end
165
205
 
166
206
  def auto_internal_column_names
@@ -22,7 +22,7 @@ module Mensa
22
22
 
23
23
  def sort_direction
24
24
  value = table.config.dig(:order, name)
25
- value.present? ? value.to_sym : nil
25
+ value.presence&.to_sym
26
26
  end
27
27
 
28
28
  def next_sort_direction
@@ -41,12 +41,22 @@ module Mensa
41
41
  @attribute = if config[:attribute].present?
42
42
  "#{config[:attribute]} AS #{name}"
43
43
  elsif table.model.column_names.include? name.to_s
44
- name.to_s
44
+ "#{table.model.table_name}.#{name}"
45
+ end
46
+ end
47
+
48
+ def raw_attribute
49
+ return @raw_attribute if @raw_attribute
50
+
51
+ @raw_attribute = if config[:attribute].present?
52
+ config[:attribute]
53
+ elsif table.model.column_names.include? name.to_s
54
+ "#{table.model.table_name}.#{name}"
45
55
  end
46
56
  end
47
57
 
48
58
  def active_record_column
49
- @active_record_column ||= table.model&.columns&.find { it.name == name.to_s }
59
+ @active_record_column ||= table.model&.columns&.find { |column| column.name == name.to_s }
50
60
  end
51
61
 
52
62
  def active_record_column_type
@@ -61,15 +71,15 @@ module Mensa
61
71
  return @attribute_for_condition if @attribute_for_condition
62
72
 
63
73
  @attribute_for_condition = if config[:attribute].present?
64
- config[:attribute]
74
+ raw_attribute
65
75
  elsif table.model.column_names.include? name.to_s
66
- name.to_s
76
+ "#{table.model.table_name}.#{name}"
67
77
  end
68
78
  end
69
79
 
70
80
  # Returns true if the column supports filtering
71
81
  def filter?
72
- config.key?(:filter)
82
+ config[:filter] != false
73
83
  end
74
84
 
75
85
  def filter
@@ -59,8 +59,8 @@ module Mensa::Config
59
59
  # by calling option :render, dsl: Mensa::RenderDsl
60
60
  #
61
61
  def dsl_option(option_name, klass)
62
- define_method(option_name) do |name = nil, &block|
63
- config[option_name.to_sym] = block && klass.new(name, &block).config
62
+ define_method(option_name) do |value = nil, &block|
63
+ config[option_name.to_sym] = block ? klass.new(value, &block).config : value
64
64
  end
65
65
  end
66
66
 
@@ -48,6 +48,7 @@ module Mensa::Config
48
48
  class TableDsl
49
49
  include DslLogic
50
50
 
51
+ option :params, default: {}
51
52
  option :model, default: -> {
52
53
  begin
53
54
  self.class.name.demodulize.to_s.classify.gsub("Table", "").singularize.constantize
@@ -88,6 +89,7 @@ module Mensa::Config
88
89
  def internal(name, &block)
89
90
  column(name) do
90
91
  internal true
92
+ filter false
91
93
  instance_exec(&block) if block
92
94
  end
93
95
  end
@@ -24,7 +24,8 @@ module Mensa
24
24
  [:gteq, I18n.t("mensa.operators.gteq"), true],
25
25
  [:lt, I18n.t("mensa.operators.lt"), true],
26
26
  [:lteq, I18n.t("mensa.operators.lteq"), true],
27
- [:is_current, I18n.t("mensa.operators.is_current"), false]
27
+ [:is_current, I18n.t("mensa.operators.is_current"), false],
28
+ [:is_empty, I18n.t("mensa.operators.is_empty"), false]
28
29
  ].freeze
29
30
  end
30
31
  end
@@ -69,6 +70,12 @@ module Mensa
69
70
  record_scope.instance_exec(normalize(value), &scope)
70
71
  else
71
72
  case operator
73
+ when :is_empty
74
+ if column.type == :string
75
+ record_scope.where("#{column.attribute_for_condition} IS NULL OR #{column.attribute_for_condition} = ''")
76
+ else
77
+ record_scope.where("#{column.attribute_for_condition} IS NULL")
78
+ end
72
79
  when :is_current
73
80
  record_scope.where("#{column.attribute_for_condition} = ?", Current.send(column.name))
74
81
  when :matches
@@ -4,6 +4,7 @@ module Mensa
4
4
  # scope -> filtered_scope -> ordered_scope -> selected_scope ->
5
5
  module Scope
6
6
  extend ActiveSupport::Concern
7
+ include Search
7
8
 
8
9
  included do
9
10
  end
@@ -13,15 +14,7 @@ module Mensa
13
14
  return @filtered_scope if @filtered_scope
14
15
 
15
16
  @filtered_scope = scope
16
- # See https://github.com/textacular/textacular
17
- # This has problems - not all table fields are searched
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
17
+ @filtered_scope = search(@filtered_scope, current_query) if current_query.present?
25
18
 
26
19
  # Use inject
27
20
  active_filters.each do |filter|
@@ -36,7 +29,13 @@ module Mensa
36
29
  return @ordered_scope if @ordered_scope
37
30
 
38
31
  @ordered_scope = filtered_scope
39
- @ordered_scope = @ordered_scope.reorder(effective_order)
32
+ @ordered_scope = if effective_order.present?
33
+ @ordered_scope.reorder(effective_order)
34
+ elsif search_order_clause.present?
35
+ @ordered_scope.reorder(Arel.sql(search_order_clause))
36
+ else
37
+ @ordered_scope.reorder(nil)
38
+ end
40
39
 
41
40
  @ordered_scope
42
41
  end
@@ -45,6 +44,8 @@ module Mensa
45
44
  def selected_scope
46
45
  return @selected_scope if @selected_scope
47
46
 
47
+ ensure_internal_columns_for_joined_associations
48
+
48
49
  @selected_scope = ordered_scope
49
50
  @selected_scope = @selected_scope.select([:id] + columns.filter_map(&:attribute))
50
51
 
@@ -73,16 +74,16 @@ module Mensa
73
74
  # (even with blank values), use only those — blank means "explicitly no sort".
74
75
  # Falls back to the view/config default only when no order params were sent.
75
76
  def effective_order
76
- result = params.key?(:order) ? (params[:order] || {}) : (config[:order] || {})
77
+ result = current_order_provided? ? (current_order || {}) : (config[:order] || {})
77
78
  result = result.symbolize_keys.compact_blank.transform_values(&:to_sym)
78
- result.transform_keys { column(_1).attribute_for_condition }
79
+ result.transform_keys { |column_name| column(column_name).attribute_for_condition }
79
80
  end
80
81
 
81
82
  # Builds an order hash for URL generation. Merges current order with overrides;
82
83
  # nil values become "" so they appear in the URL as order[col]= (which tells
83
84
  # the server the user explicitly cleared that column's sort direction).
84
85
  def order_hash(new_params = {})
85
- base = params[:order]&.symbolize_keys || config[:order]&.symbolize_keys || {}
86
+ base = current_order&.symbolize_keys || config[:order]&.symbolize_keys || {}
86
87
  merged = base.merge(new_params.symbolize_keys)
87
88
  merged.transform_values { |v| v.nil? ? "" : v.to_sym }
88
89
  end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mensa
4
+ module Search
5
+ extend ActiveSupport::Concern
6
+
7
+ private
8
+
9
+ def search(record_scope, query)
10
+ return record_scope if query.blank?
11
+ return record_scope unless record_scope.is_a?(ActiveRecord::Relation)
12
+
13
+ searchable_attributes = columns.filter_map(&:attribute_for_condition).uniq
14
+ return record_scope if searchable_attributes.empty?
15
+
16
+ @search_order_clause = nil
17
+
18
+ fuzzy_search?(record_scope) ? fuzzy_search(record_scope, searchable_attributes, query.to_s) : basic_search(record_scope, searchable_attributes, query.to_s)
19
+ end
20
+
21
+ def basic_search(record_scope, searchable_attributes, query)
22
+ sanitized_query = ActiveRecord::Base.sanitize_sql_like(query)
23
+ conditions = searchable_attributes.map do |attribute|
24
+ "CAST((#{attribute}) AS text) ILIKE :term"
25
+ end.join(" OR ")
26
+
27
+ record_scope.where(conditions, term: "%#{sanitized_query}%")
28
+ end
29
+
30
+ def fuzzy_search(record_scope, searchable_attributes, query)
31
+ sanitized_query = ActiveRecord::Base.sanitize_sql_like(query)
32
+ compare_quoted = record_scope.model.connection.quote(query)
33
+ text_expression = searchable_text_expression(searchable_attributes)
34
+ score_sql = "similarity(#{text_expression}, #{compare_quoted})"
35
+
36
+ @search_order_clause = "#{score_sql} DESC"
37
+
38
+ record_scope
39
+ .select(Arel.sql("#{score_sql} AS mensa_search_score"))
40
+ .where("#{text_expression} % :query OR #{text_expression} ILIKE :term", query: query, term: "%#{sanitized_query}%")
41
+ .order(Arel.sql(@search_order_clause))
42
+ end
43
+
44
+ def searchable_text_expression(searchable_attributes)
45
+ fields = searchable_attributes.map do |attribute|
46
+ "COALESCE(CAST((#{attribute}) AS text), '')"
47
+ end.join(", ")
48
+
49
+ "CONCAT_WS(' ', #{fields})"
50
+ end
51
+
52
+ def search_order_clause
53
+ @search_order_clause
54
+ end
55
+
56
+ def fuzzy_search?(record_scope)
57
+ Mensa.config.search == :fuzzy && pg_trgm_enabled?(record_scope)
58
+ end
59
+
60
+ def pg_trgm_enabled?(record_scope)
61
+ connection = record_scope.model.connection
62
+ return false unless connection.adapter_name.to_s.downcase.include?("postgres")
63
+ return connection.extension_enabled?("pg_trgm") if connection.respond_to?(:extension_enabled?)
64
+
65
+ connection.select_value("SELECT 1 FROM pg_extension WHERE extname = 'pg_trgm' LIMIT 1").present?
66
+ rescue
67
+ false
68
+ end
69
+ end
70
+ end
@@ -37,6 +37,29 @@
37
37
  </label>
38
38
  </fieldset>
39
39
 
40
+ <fieldset class="mensa-table__export-dialog__fieldset">
41
+ <legend class="mensa-table__export-dialog__section-title"><%= t("mensa.exports.repeat_section", default: "Repeat") %></legend>
42
+ <label class="mensa-table__export-dialog__option">
43
+ <input type="radio" name="repeat_mode" value="once" checked data-action="change->mensa-table#toggleExportRepeat">
44
+ <span><%= t("mensa.exports.repeat_once", default: "Only once") %></span>
45
+ </label>
46
+ <label class="mensa-table__export-dialog__option">
47
+ <input type="radio" name="repeat_mode" value="repeating" data-action="change->mensa-table#toggleExportRepeat">
48
+ <span><%= t("mensa.exports.repeat_repeating", default: "Repeating") %></span>
49
+ </label>
50
+
51
+ <div class="mensa-table__export-dialog__repeat-options" hidden data-mensa-table-repeat-options>
52
+ <select name="repeat" class="mensa-table__export-dialog__select">
53
+ <option value="daily"><%= t("mensa.exports.repeat_intervals.daily", default: "daily") %></option>
54
+ <option value="weekly"><%= t("mensa.exports.repeat_intervals.weekly", default: "weekly") %></option>
55
+ <option value="monthly"><%= t("mensa.exports.repeat_intervals.monthly", default: "monthly") %></option>
56
+ <option value="quarterly"><%= t("mensa.exports.repeat_intervals.quarterly", default: "quarterly") %></option>
57
+ <option value="bi-yearly"><%= t("mensa.exports.repeat_intervals.bi-yearly", default: "bi-yearly") %></option>
58
+ <option value="yearly"><%= t("mensa.exports.repeat_intervals.yearly", default: "yearly") %></option>
59
+ </select>
60
+ </div>
61
+ </fieldset>
62
+
40
63
  <div class="mensa-table__export-dialog__actions">
41
64
  <button class="mensa-table__export-dialog__button mensa-table__export-dialog__button--secondary" type="button" data-action="mensa-table#cancelExport">
42
65
  <%= t("mensa.exports.cancel", default: "Cancel") %>
@@ -6,8 +6,19 @@
6
6
  <ul class="mensa-table__export-dialog__list">
7
7
  <% exports.each do |export| %>
8
8
  <li class="mensa-table__export-dialog__item">
9
+ <%= button_to mensa.table_export_path(export.table_name, export),
10
+ method: :delete,
11
+ class: "mensa-table__export-dialog__delete",
12
+ form_class: "mensa-table__export-dialog__delete-form",
13
+ aria: {label: t("mensa.exports.delete", default: "Delete export") } do %>
14
+ <i class="fa-solid fa-trash"></i>
15
+ <% end %>
16
+
9
17
  <div class="mensa-table__export-dialog__item-info">
10
18
  <span class="mensa-table__export-dialog__item-name"><%= export.filename.presence || t("mensa.exports.item_name", default: "%{table} export", table: export.table_name.to_s.humanize) %></span>
19
+ <% if export.repeating? %>
20
+ <span class="mensa-table__export-dialog__item-repeat"><%= export.repeat_label %></span>
21
+ <% end %>
11
22
  <span class="mensa-table__export-dialog__item-meta"><%= export.created_at.strftime("%Y-%m-%d %H:%M") %></span>
12
23
  </div>
13
24
  <div class="mensa-table__export-dialog__item-action">
@@ -18,6 +29,8 @@
18
29
  <% end %>
19
30
  <% elsif export.failed? %>
20
31
  <span class="mensa-table__export-dialog__status mensa-table__export-dialog__status--failed"><%= t("mensa.exports.failed", default: "Failed") %></span>
32
+ <% elsif export.completed? %>
33
+ <span class="mensa-table__export-dialog__status mensa-table__export-dialog__status--pending"><%= t("mensa.exports.waiting", default: "Waiting") %></span>
21
34
  <% else %>
22
35
  <span class="mensa-table__export-dialog__status mensa-table__export-dialog__status--pending">
23
36
  <i class="fa-solid fa-spinner fa-spin"></i>
@@ -1,5 +1,3 @@
1
- Test
2
- <button class="text-xs">Esc</button>
3
1
  <turbo-frame id="<%= @table.table_id %>" target="_top">
4
2
  <%= render Mensa::View::Component.new(@table) %>
5
3
  </turbo-frame>
@@ -0,0 +1,9 @@
1
+ <turbo-frame id="<%= @frame_id %>">
2
+ <div class="mensa-empty-state">
3
+ <div class="mensa-empty-state__icon">
4
+ <i class="fa-solid fa-circle-exclamation"></i>
5
+ </div>
6
+ <h3 class="mensa-empty-state__title">Could not load table</h3>
7
+ <p class="mensa-empty-state__subtitle"><%= @error_message %></p>
8
+ </div>
9
+ </turbo-frame>
@@ -1,5 +1,10 @@
1
1
  en:
2
2
  mensa:
3
+ actions: Actions
4
+ save: Save
5
+ update_view: Update view
6
+ save_view: Save as new view
7
+ save_as_new_view: Save as new view
3
8
  operators:
4
9
  is: is
5
10
  isnt: isn't
@@ -10,7 +15,9 @@ en:
10
15
  lt: less than
11
16
  gteq: greater than or equal to
12
17
  lteq: less than or equal to
18
+ is_empty: is empty
13
19
  add_filter:
20
+ add: Add filter
14
21
  component:
15
22
  add_filter: Add filter
16
23
  filter_pill_list:
@@ -19,6 +26,7 @@ en:
19
26
  search_only: Search
20
27
  search:
21
28
  component:
29
+ search: Search and filter
22
30
  search_in: Search in %{view}
23
31
  cancel: Cancel
24
32
  save: Save
@@ -34,8 +42,22 @@ en:
34
42
  available_downloads: Available downloads
35
43
  empty: You have no downloads yet. Create one below.
36
44
  item_name: "%{table} export"
45
+ repeats_with_interval: "Repeats %{interval}"
46
+ repeat_section: Repeat
47
+ repeat: Repeat
48
+ repeat_once: "Only once"
49
+ repeat_repeating: Repeating
50
+ repeat_intervals:
51
+ daily: daily
52
+ weekly: weekly
53
+ monthly: monthly
54
+ quarterly: quarterly
55
+ bi-yearly: bi-yearly
56
+ yearly: yearly
37
57
  download: Download
58
+ delete: Delete export
38
59
  processing: Preparing…
60
+ waiting: Waiting
39
61
  failed: Failed
40
62
  new_export: New export
41
63
  scope_all: All records (matching current filters)
@@ -1,5 +1,10 @@
1
1
  nl:
2
2
  mensa:
3
+ actions: Acties
4
+ save: Bewaar
5
+ update_view: Bewaar weergave
6
+ save_view: Bewaar als nieuwe weergave
7
+ save_as_new_view: Bewaar als nieuwe weergave
3
8
  operators:
4
9
  is: is
5
10
  isnt: is niet
@@ -10,7 +15,9 @@ nl:
10
15
  lt: kleiner dan
11
16
  gteq: groter dan of gelijk aan
12
17
  lteq: kleiner dan of gelijk aan
18
+ is_empty: is leeg
13
19
  add_filter:
20
+ add: Filter toevoegen
14
21
  component:
15
22
  add_filter: Filter toevoegen
16
23
  filter_pill_list:
@@ -35,8 +42,22 @@ nl:
35
42
  available_downloads: Beschikbare downloads
36
43
  empty: Je hebt nog geen downloads. Maak er hieronder een aan.
37
44
  item_name: "%{table} export"
45
+ repeats_with_interval: "Herhaalt %{interval}"
46
+ repeat_section: Herhalen
47
+ repeat: Herhalen
48
+ repeat_once: "Eenmalig"
49
+ repeat_repeating: Herhalend
50
+ repeat_intervals:
51
+ daily: dagelijks
52
+ weekly: wekelijks
53
+ monthly: maandelijks
54
+ quarterly: per kwartaal
55
+ bi-yearly: halfjaarlijks
56
+ yearly: jaarlijks
38
57
  download: Downloaden
58
+ delete: Export verwijderen
39
59
  processing: Bezig…
60
+ waiting: Wachtend
40
61
  failed: Mislukt
41
62
  new_export: Nieuwe export
42
63
  scope_all: Alle records (volgens huidige filters)