ruby_cms 0.2.0.5 → 0.2.0.8

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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -1
  3. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table.rb +9 -1
  4. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_actions.rb +3 -3
  5. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_delete_modal.rb +8 -8
  6. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header_bar.rb +15 -5
  7. data/app/components/ruby_cms/admin/bulk_action_table/bulk_actions.rb +57 -12
  8. data/app/controllers/concerns/ruby_cms/page_tracking.rb +15 -0
  9. data/app/controllers/ruby_cms/admin/analytics_controller.rb +12 -0
  10. data/app/controllers/ruby_cms/admin/commands_controller.rb +6 -6
  11. data/app/javascript/controllers/ruby_cms/bulk_action_table_controller.js +65 -13
  12. data/app/services/ruby_cms/analytics/report.rb +119 -7
  13. data/app/services/ruby_cms/command_runner.rb +3 -3
  14. data/app/views/ruby_cms/admin/analytics/index.html.erb +160 -67
  15. data/app/views/ruby_cms/admin/analytics/partials/_browser_device.html.erb +21 -15
  16. data/app/views/ruby_cms/admin/analytics/partials/_daily_activity_chart.html.erb +36 -16
  17. data/app/views/ruby_cms/admin/analytics/partials/_hourly_activity_chart.html.erb +29 -13
  18. data/app/views/ruby_cms/admin/analytics/partials/_landing_pages.html.erb +27 -15
  19. data/app/views/ruby_cms/admin/analytics/partials/_os_stats.html.erb +29 -18
  20. data/app/views/ruby_cms/admin/analytics/partials/_popular_pages.html.erb +17 -12
  21. data/app/views/ruby_cms/admin/analytics/partials/_recent_activity.html.erb +25 -14
  22. data/app/views/ruby_cms/admin/analytics/partials/_top_referrers.html.erb +27 -15
  23. data/app/views/ruby_cms/admin/analytics/partials/_top_visitors.html.erb +20 -10
  24. data/app/views/ruby_cms/admin/analytics/partials/_utm_sources.html.erb +27 -15
  25. data/app/views/ruby_cms/admin/content_blocks/index.html.erb +8 -1
  26. data/config/locales/en.yml +69 -5
  27. data/config/locales/nl.yml +98 -0
  28. data/lib/generators/ruby_cms/install_generator.rb +21 -6
  29. data/lib/generators/ruby_cms/templates/ruby_cms.rb +40 -3
  30. data/lib/ruby_cms/settings_registry.rb +35 -0
  31. data/lib/ruby_cms/version.rb +1 -1
  32. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 747105700c925ba3d9140a015fa477116795644d8a99c52b3aee53c4e786678d
4
- data.tar.gz: 8a7bf4cea26c24f7f73f5c6800c6a8dcfb784031a8f313bf2fb7e421fb8bbf31
3
+ metadata.gz: b2871a67fff389ef55e7c894100dcfbe9b7eeb4ccbec079986d64f6c768c9f87
4
+ data.tar.gz: 7658b768eed4dbc05b88312e3b25d85ccee84180441d55f91023af3c797be32c
5
5
  SHA512:
6
- metadata.gz: 78ebfe96b5853ad7ab9698bdc0fca3eaa568f6a02ae691d7ba007c804e4076afe5bde33380d966875f2e24eb72f554d6918408f8fbe04ab80b3644808c11f9ae
7
- data.tar.gz: e410f195decbe969b4a873624e440b9331a47081a305bd504eea5a44c0ae90b63e6fe4d0b734fd72a01ea09c471cff7ee7c733eca971525fa9ff0c094b5e0e2c
6
+ metadata.gz: 25b55ee8d4aa7f9a9ef0b3d861cc1238c1c97b1550703e06336c11df8ac7aa0552bcddf93c90af3dca456ee9149647501c9a37ea71c858181d70f992dd19b33d
7
+ data.tar.gz: 2d9aa367ab09809bde3781a654b88fa3d7b9c7eed9bf37899977c18ab57d69e7e5695f60db91c0358365a978c228129525055a1653828007178051ab22404760
data/CHANGELOG.md CHANGED
@@ -1,6 +1,27 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [0.2.0.3] - 2026-04-02
3
+ ## [0.2.0.8] - 2026-04-09
4
+
5
+ - Analytics performance: migration adds `ahoy_events (name, time)`, `ahoy_events (visit_id, time)`, `ahoy_visits (started_at)`, `ahoy_visits (visitor_token)` indexes
6
+ - Analytics performance: `compute_new_visitor_percentage` uses subquery instead of plucking all historical visitor tokens into Ruby memory
7
+ - Analytics performance: `exit_pages_data` uses a single DB join-subquery instead of loading all page view rows into Ruby
8
+
9
+ ## [0.2.0.7] - 2026-04-09
10
+
11
+ - Analytics: add `EVENT_PAGE_VIEW` / `EVENT_CONVERSION` constants for consistent ahoy.track usage
12
+ - Analytics: conversion tracking — `Report` queries `conversion` events and surfaces totals + goal breakdown in dashboard
13
+ - Analytics: exit pages — last `page_view` per visit in selected range, displayed as new dashboard section
14
+ - Analytics: period-over-period comparison — KPI deltas (↑/↓ %) shown on page views, unique visitors, and sessions stat cards
15
+ - Analytics: expanded bot-filtering documentation in install template (`analytics_visit_scope` examples)
16
+ - Analytics: register `analytics_max_exit_pages`, `analytics_max_conversions`, `analytics_max_referrers`, `analytics_max_landing_pages`, `analytics_max_utm_sources` in SettingsRegistry
17
+ - PageTracking: document conversion tracking convention in concern comments
18
+ - Locales: add analytics i18n keys for exit pages, conversions, and period comparison (en + nl)
19
+
20
+ ## [0.2.0.6] - 2026-04-09
21
+
22
+ - Analytics improvements
23
+
24
+ ## [0.2.0.5] - 2026-04-08
4
25
 
5
26
  - The host app no longer needs to scan de gem for tailwind
6
27
 
@@ -128,7 +128,15 @@ module RubyCms
128
128
  data: {
129
129
  controller: @controller_name,
130
130
  "#{@controller_name}-csrf-token-value": csrf_token,
131
- "#{@controller_name}-item-name-value": @item_name
131
+ "#{@controller_name}-item-name-value": @item_name,
132
+ "#{@controller_name}-processing-label-value": t("ruby_cms.admin.bulk_action_table.processing", default: "Processing..."),
133
+ "#{@controller_name}-confirm-label-value": t("ruby_cms.admin.bulk_action_table.confirm", default: "Confirm"),
134
+ "#{@controller_name}-select-at-least-one-message-value": t("ruby_cms.admin.bulk_action_table.select_at_least_one", default: "Please select at least one item."),
135
+ "#{@controller_name}-item-id-not-found-message-value": t("ruby_cms.admin.bulk_action_table.item_id_not_found", default: "Item ID not found for deletion."),
136
+ "#{@controller_name}-delete-path-not-found-message-value": t("ruby_cms.admin.bulk_action_table.delete_path_not_found", default: "Delete path not found."),
137
+ "#{@controller_name}-action-url-not-configured-message-value": t("ruby_cms.admin.bulk_action_table.action_url_not_configured", default: "Action URL not configured. Please configure an action URL for this page."),
138
+ "#{@controller_name}-default-confirm-message-value": t("ruby_cms.admin.bulk_action_table.default_confirm", default: "Are you sure you want to proceed?"),
139
+ "#{@controller_name}-generic-action-error-message-value": t("ruby_cms.admin.bulk_action_table.generic_action_error", default: "An error occurred while performing %{action}.")
132
140
  }
133
141
  }
134
142
 
@@ -45,8 +45,8 @@ module RubyCms
45
45
  def render_edit_button
46
46
  link_options = {
47
47
  href: @edit_path,
48
- class: "inline-flex size-8 items-center justify-center rounded-md text-muted-foreground " \
49
- "hover:bg-muted hover:text-foreground transition-colors"
48
+ class: "inline-flex size-8 items-center justify-center rounded-md text-blue-600 " \
49
+ "hover:bg-blue-50 hover:text-blue-700 transition-colors"
50
50
  }
51
51
  link_options[:data] = { turbo_frame: @turbo_frame } if @turbo_frame
52
52
 
@@ -71,7 +71,7 @@ module RubyCms
71
71
  item_id = @item_id || extract_item_id_from_path
72
72
  button(
73
73
  type: "button",
74
- class: "inline-flex size-8 items-center justify-center rounded-md text-muted-foreground " \
74
+ class: "inline-flex size-8 items-center justify-center rounded-md text-destructive " \
75
75
  "hover:bg-destructive/10 hover:text-destructive transition-colors",
76
76
  data: {
77
77
  action: "click->#{@controller_name}#showIndividualDeleteDialog",
@@ -79,10 +79,10 @@ module RubyCms
79
79
  data: {
80
80
  action: "click->#{@controller_name}#closeDialog"
81
81
  },
82
- aria_label: "Close"
82
+ aria_label: t("ruby_cms.admin.bulk_action_table.close", default: "Close")
83
83
  ) do
84
84
  render_close_icon
85
- span(class: "sr-only") { "Close" }
85
+ span(class: "sr-only") { t("ruby_cms.admin.bulk_action_table.close", default: "Close") }
86
86
  end
87
87
  end
88
88
 
@@ -123,7 +123,7 @@ module RubyCms
123
123
  data: {
124
124
  "#{@controller_name}-target": "dialogTitle"
125
125
  }
126
- ) { "Delete Selected Items" }
126
+ ) { t("ruby_cms.admin.bulk_action_table.delete_selected_items", default: "Delete Selected Items") }
127
127
  end
128
128
 
129
129
  def render_message
@@ -133,8 +133,8 @@ module RubyCms
133
133
  "#{@controller_name}-target": "dialogMessage"
134
134
  }
135
135
  ) do
136
- p { "Are you sure you want to delete the selected items?" }
137
- p { "This action cannot be undone." }
136
+ p { t("ruby_cms.admin.bulk_action_table.are_you_sure_you_want_to_delete_the_selected_items", default: "Are you sure you want to delete the selected items?") }
137
+ p { t("ruby_cms.admin.bulk_action_table.this_action_cannot_be_undone", default: "This action cannot be undone.") }
138
138
  end
139
139
  end
140
140
 
@@ -154,19 +154,19 @@ module RubyCms
154
154
  data: {
155
155
  action: "click->#{@controller_name}#closeDialog"
156
156
  }
157
- ) { "Cancel" }
157
+ ) { t("ruby_cms.admin.bulk_action_table.cancel", default: "Cancel") }
158
158
  end
159
159
 
160
160
  def render_confirm_button
161
161
  button(
162
162
  type: "button",
163
163
  class: "inline-flex h-9 items-center justify-center rounded-md bg-destructive px-4 " \
164
- "text-sm font-medium text-destructive-foreground shadow-sm hover:bg-destructive/90 transition-colors",
164
+ "text-sm font-medium text-white font-bold shadow-sm hover:bg-destructive/90 transition-colors",
165
165
  data: {
166
166
  "#{@controller_name}-target": "dialogConfirmButton",
167
167
  action: "click->#{@controller_name}#confirmAction"
168
168
  }
169
- ) { "Delete Selected" }
169
+ ) { t("ruby_cms.admin.bulk_action_table.delete_selected", default: "Delete Selected") }
170
170
  end
171
171
  end
172
172
  end
@@ -32,9 +32,9 @@ module RubyCms
32
32
 
33
33
  def view_template
34
34
  div(class: "px-5 py-3 border-b border-border/60 bg-white") do
35
- div(class: "flex flex-wrap items-center justify-between gap-4") do
35
+ div(class: "flex items-center justify-between gap-3") do
36
36
  render_title_group if @title.present?
37
- div(class: "flex items-center gap-2 flex-wrap") do
37
+ div(class: "ml-auto flex items-center gap-2 overflow-x-auto whitespace-nowrap") do
38
38
  render_header_filter if @header_filter.present?
39
39
  render_action_icons
40
40
  render_search_form
@@ -75,9 +75,15 @@ module RubyCms
75
75
  form_options = {
76
76
  url: @search_url,
77
77
  method: :get,
78
- class: "w-full sm:w-auto"
78
+ class: "w-full sm:w-auto",
79
+ data: { "#{default_controller_name}-target": "searchForm" }
79
80
  }
80
81
  form_options[:data] = { turbo_frame: @turbo_frame } if @turbo_frame.present?
82
+ if @turbo_frame.present?
83
+ form_options[:data] = form_options[:data].merge(
84
+ "#{default_controller_name}-target": "searchForm"
85
+ )
86
+ end
81
87
 
82
88
  form_with(**form_options) do
83
89
  div(class: "relative flex items-center") do
@@ -106,10 +112,10 @@ module RubyCms
106
112
  type: "search",
107
113
  name: @search_param,
108
114
  placeholder: "Search",
109
- class: "h-9 w-full sm:w-72 rounded-md border border-border bg-white pl-9 " \
115
+ class: "h-9 w-64 sm:w-72 rounded-md border border-border bg-white pl-9 " \
110
116
  "pr-3 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/20",
111
117
  value: search_value,
112
- data: { action: "input->turbo-frame#submit" }
118
+ data: { action: "input->#{default_controller_name}#autoSearch" }
113
119
  )
114
120
  end
115
121
 
@@ -153,6 +159,10 @@ module RubyCms
153
159
  d: icon_path)
154
160
  end
155
161
  end
162
+
163
+ def default_controller_name
164
+ "ruby-cms--bulk-action-table"
165
+ end
156
166
  end
157
167
  end
158
168
  end
@@ -51,7 +51,9 @@ module RubyCms
51
51
  def render_selection_left
52
52
  div(class: "flex items-center gap-2") do
53
53
  render_selected_count_badge
54
- span(class: "text-sm font-medium text-foreground") { "selected" }
54
+ span(class: "text-sm font-medium text-foreground") do
55
+ t("ruby_cms.admin.bulk_action_table.selected", default: "selected")
56
+ end
55
57
  end
56
58
  end
57
59
 
@@ -74,7 +76,7 @@ module RubyCms
74
76
  "#{@controller_name}-target": "selectAllButton",
75
77
  action: "click->#{@controller_name}#selectAll"
76
78
  }
77
- ) { "Select all" }
79
+ ) { t("ruby_cms.admin.bulk_action_table.select_all", default: "Select all") }
78
80
 
79
81
  button(
80
82
  type: "button",
@@ -82,7 +84,7 @@ module RubyCms
82
84
  data: {
83
85
  action: "click->#{@controller_name}#clearSelection"
84
86
  }
85
- ) { "Clear" }
87
+ ) { t("ruby_cms.admin.bulk_action_table.clear", default: "Clear") }
86
88
  end
87
89
 
88
90
  def render_action_buttons
@@ -98,19 +100,24 @@ module RubyCms
98
100
  def render_custom_action_button(config)
99
101
  label = config[:label] || config[:text] || config[:name]&.humanize || "Button"
100
102
  action_name = config[:name] || config[:action_name]
103
+ icon_path = resolve_icon_path(config, action_name)
101
104
 
102
105
  button(
103
106
  type: "button",
104
107
  class: build_button_class(config),
105
108
  data: build_button_data_attrs(config, label, action_name)
106
- ) { label }
109
+ ) do
110
+ render_button_icon(icon_path)
111
+ span { label }
112
+ end
107
113
  end
108
114
 
109
115
  def build_button_class(config)
110
- base = "inline-flex items-center justify-center rounded-md border border-border " \
111
- "bg-white px-3 py-1.5 text-sm font-medium text-foreground shadow-sm " \
116
+ color_class = resolve_button_color_class(config)
117
+ base = "inline-flex items-center justify-center gap-1.5 rounded-md border border-border " \
118
+ "bg-white px-3 py-1.5 text-sm font-medium shadow-sm " \
112
119
  "hover:bg-muted transition-colors"
113
- config[:class].present? ? "#{base} #{config[:class]}" : base
120
+ [base, color_class, config[:class]].compact.join(" ")
114
121
  end
115
122
 
116
123
  def build_button_data_attrs(config, label, action_name)
@@ -132,18 +139,56 @@ module RubyCms
132
139
  def render_delete_button
133
140
  button(
134
141
  type: "button",
135
- class: "inline-flex items-center justify-center rounded-md border border-destructive/30 " \
142
+ class: "inline-flex items-center justify-center gap-1.5 rounded-md border border-destructive/30 " \
136
143
  "bg-white px-3 py-1.5 text-sm font-medium text-destructive shadow-sm " \
137
144
  "hover:bg-destructive/10 transition-colors",
138
145
  data: {
139
146
  action: "click->#{@controller_name}#showActionDialog",
140
147
  action_name: "delete",
141
- action_label: "Delete Selected",
142
- action_confirm: "Are you sure you want to delete the selected \
143
- items? This action cannot be undone.",
148
+ action_label: t("ruby_cms.admin.bulk_action_table.delete_selected", default: "Delete Selected"),
149
+ action_confirm: t("ruby_cms.admin.bulk_action_table.delete_selected_confirm", default: "Are you sure you want to delete the selected items? This action cannot be undone."),
144
150
  action_url: @bulk_actions_url&.to_s
145
151
  }
146
- ) { "Delete Selected" }
152
+ ) do
153
+ render_button_icon("M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16")
154
+ span { t("ruby_cms.admin.bulk_action_table.delete_selected", default: "Delete Selected") }
155
+ end
156
+ end
157
+
158
+ def render_button_icon(path_d)
159
+ svg(class: "h-3.5 w-3.5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24") do |s|
160
+ s.path(stroke_linecap: "round", stroke_linejoin: "round", stroke_width: "2", d: path_d)
161
+ end
162
+ end
163
+
164
+ def resolve_button_color_class(config)
165
+ color = (config[:color] || config[:tone] || config[:variant]).to_s
166
+ return "text-foreground" if color.blank?
167
+
168
+ {
169
+ "blue" => "text-blue-700 border-blue-200 hover:bg-blue-50",
170
+ "green" => "text-emerald-700 border-emerald-200 hover:bg-emerald-50",
171
+ "emerald" => "text-emerald-700 border-emerald-200 hover:bg-emerald-50",
172
+ "orange" => "text-amber-700 border-amber-200 hover:bg-amber-50",
173
+ "amber" => "text-amber-700 border-amber-200 hover:bg-amber-50",
174
+ "red" => "text-destructive border-destructive/30 hover:bg-destructive/10",
175
+ "purple" => "text-violet-700 border-violet-200 hover:bg-violet-50"
176
+ }.fetch(color, "text-foreground")
177
+ end
178
+
179
+ def resolve_icon_path(config, action_name)
180
+ return config[:icon] if config[:icon].present?
181
+
182
+ case action_name.to_s
183
+ when "publish"
184
+ "M5 13l4 4L19 7"
185
+ when "unpublish"
186
+ "M6 18L18 6M6 6l12 12"
187
+ when "archive"
188
+ "M20 7l-1 12H5L4 7m16 0H4m3-3h10l1 3H6l1-3z"
189
+ else
190
+ "M12 4v16m8-8H4"
191
+ end
147
192
  end
148
193
  end
149
194
  end
@@ -12,6 +12,21 @@ module RubyCms
12
12
  # Sets @page_name to controller_name by default. Override in actions:
13
13
  # @page_name = "custom_page_name"
14
14
  #
15
+ # Event naming conventions (keep property keys consistent across the app):
16
+ # Page views (tracked automatically):
17
+ # ahoy.track RubyCms::Analytics::Report::EVENT_PAGE_VIEW,
18
+ # page_name: "home", request_path: request.path
19
+ #
20
+ # Conversions (tracked by host app controllers/forms):
21
+ # ahoy.track RubyCms::Analytics::Report::EVENT_CONVERSION,
22
+ # goal: "contact_form", path: request.path
23
+ # ahoy.track RubyCms::Analytics::Report::EVENT_CONVERSION,
24
+ # goal: "newsletter_signup", path: request.path
25
+ #
26
+ # Convention for property keys:
27
+ # page_view: page_name (String), request_path (String)
28
+ # conversion: goal (String, required), path (String, optional)
29
+ #
15
30
  module PageTracking
16
31
  extend ActiveSupport::Concern
17
32
 
@@ -17,6 +17,7 @@ module RubyCms
17
17
  )
18
18
  @stats = report.dashboard_stats
19
19
  @stats.each {|key, value| instance_variable_set(:"@#{key}", value) }
20
+ @active_users = active_users_count
20
21
  end
21
22
 
22
23
  def page_details
@@ -73,6 +74,17 @@ module RubyCms
73
74
  alert: "Invalid date range. Maximum range is #{max_days} days."
74
75
  end
75
76
 
77
+ def active_users_count
78
+ Ahoy::Event
79
+ .where(name: RubyCms::Analytics::Report::EVENT_PAGE_VIEW)
80
+ .where(time: 5.minutes.ago..)
81
+ .joins(:visit)
82
+ .distinct
83
+ .count(:visitor_token)
84
+ rescue StandardError
85
+ nil
86
+ end
87
+
76
88
  def sanitize_period(value)
77
89
  %w[day week month year].include?(value.to_s) ? value.to_s : nil
78
90
  end
@@ -34,7 +34,7 @@ module RubyCms
34
34
 
35
35
  respond_to do |format|
36
36
  format.html do
37
- token = persist_run_payload_for_redirect(output: output, log_tail: log_tail)
37
+ token = persist_run_payload_for_redirect(output:, log_tail:)
38
38
  redirect_to ruby_cms_admin_settings_commands_path,
39
39
  status: :see_other,
40
40
  flash: { FLASH_RUN => token }
@@ -55,14 +55,14 @@ module RubyCms
55
55
  status: :see_other,
56
56
  flash: { FLASH_RUN => token }
57
57
  end
58
- format.json { render json: { error: e.message }, status: :unprocessable_entity }
58
+ format.json { render json: { error: e.message }, status: :unprocessable_content }
59
59
  end
60
60
  end
61
61
 
62
62
  private
63
63
 
64
64
  def persist_run_payload_for_redirect(output: nil, log_tail: nil, error: nil)
65
- payload = { output: output, log_tail: log_tail, error: error }.compact
65
+ payload = { output:, log_tail:, error: }.compact
66
66
  token = SecureRandom.urlsafe_base64(32)
67
67
  if rails_cache_null_store?
68
68
  session[session_run_key(token)] = payload
@@ -77,7 +77,7 @@ module RubyCms
77
77
  return if token.blank?
78
78
 
79
79
  payload = load_run_payload(token)
80
- assign_run_ivars_from_payload(payload) if payload.is_a?(Hash)
80
+ assign_run_ivars_from_payload(payload) if payload.kind_of?(Hash)
81
81
  end
82
82
 
83
83
  def load_run_payload(token)
@@ -99,7 +99,7 @@ module RubyCms
99
99
  end
100
100
 
101
101
  def rails_cache_null_store?
102
- Rails.cache.is_a?(ActiveSupport::Cache::NullStore)
102
+ Rails.cache.kind_of?(ActiveSupport::Cache::NullStore)
103
103
  end
104
104
 
105
105
  def session_run_key(token)
@@ -115,7 +115,7 @@ module RubyCms
115
115
 
116
116
  def visible_commands
117
117
  RubyCms.registered_commands.select {|c| current_user_cms&.can?(c[:permission]) }
118
- .sort_by {|c| [ c[:label].to_s.downcase, c[:key] ] }
118
+ .sort_by {|c| [c[:label].to_s.downcase, c[:key]] }
119
119
  end
120
120
  end
121
121
  end
@@ -13,11 +13,38 @@ export default class extends Controller {
13
13
  "dialogContent",
14
14
  "dialogTitle",
15
15
  "dialogMessage",
16
+ "searchForm",
16
17
  ];
17
18
  static values = {
18
19
  csrfToken: String,
19
20
  bulkActionUrl: String,
20
21
  itemName: { type: String, default: "item" },
22
+ processingLabel: { type: String, default: "Processing..." },
23
+ confirmLabel: { type: String, default: "Confirm" },
24
+ selectAtLeastOneMessage: {
25
+ type: String,
26
+ default: "Please select at least one item.",
27
+ },
28
+ itemIdNotFoundMessage: {
29
+ type: String,
30
+ default: "Item ID not found for deletion.",
31
+ },
32
+ deletePathNotFoundMessage: {
33
+ type: String,
34
+ default: "Delete path not found.",
35
+ },
36
+ actionUrlNotConfiguredMessage: {
37
+ type: String,
38
+ default: "Action URL not configured. Please configure an action URL for this page.",
39
+ },
40
+ defaultConfirmMessage: {
41
+ type: String,
42
+ default: "Are you sure you want to proceed?",
43
+ },
44
+ genericActionErrorMessage: {
45
+ type: String,
46
+ default: "An error occurred while performing %{action}.",
47
+ },
21
48
  };
22
49
 
23
50
  connect() {
@@ -35,12 +62,29 @@ export default class extends Controller {
35
62
  // Handle ESC key to close dialog
36
63
  this.boundHandleKeydown = this.handleKeydown.bind(this);
37
64
  document.addEventListener("keydown", this.boundHandleKeydown);
65
+ this.searchDebounceTimer = null;
38
66
  }
39
67
 
40
68
  disconnect() {
41
69
  if (this.boundHandleKeydown) {
42
70
  document.removeEventListener("keydown", this.boundHandleKeydown);
43
71
  }
72
+ if (this.searchDebounceTimer) {
73
+ clearTimeout(this.searchDebounceTimer);
74
+ this.searchDebounceTimer = null;
75
+ }
76
+ }
77
+
78
+ autoSearch() {
79
+ if (!this.hasSearchFormTarget) return;
80
+
81
+ if (this.searchDebounceTimer) {
82
+ clearTimeout(this.searchDebounceTimer);
83
+ }
84
+
85
+ this.searchDebounceTimer = setTimeout(() => {
86
+ this.searchFormTarget.requestSubmit();
87
+ }, 250);
44
88
  }
45
89
 
46
90
  handleKeydown(event) {
@@ -182,7 +226,7 @@ export default class extends Controller {
182
226
  showActionDialog(event) {
183
227
  const count = this.getSelectedIds().length;
184
228
  if (count === 0) {
185
- this.showNotification("Please select at least one item.", "error");
229
+ this.showNotification(this.selectAtLeastOneMessageValue, "error");
186
230
  return;
187
231
  }
188
232
 
@@ -245,13 +289,13 @@ export default class extends Controller {
245
289
  if (actionName === "delete") {
246
290
  return "Are you sure you want to delete the selected items? This action cannot be undone.";
247
291
  }
248
- return "Are you sure you want to proceed?";
292
+ return this.defaultConfirmMessageValue;
249
293
  }
250
294
 
251
295
  redirectToBulkAction(url, actionName) {
252
296
  const selectedIds = this.getSelectedIds();
253
297
  if (selectedIds.length === 0) {
254
- this.showNotification("Please select at least one item.", "error");
298
+ this.showNotification(this.selectAtLeastOneMessageValue, "error");
255
299
  return;
256
300
  }
257
301
 
@@ -266,9 +310,8 @@ export default class extends Controller {
266
310
  }
267
311
 
268
312
  showDialog() {
269
- const label = this.currentActionLabel || "Confirm";
270
- const message =
271
- this.currentActionConfirm || "Are you sure you want to proceed?";
313
+ const label = this.currentActionLabel || this.confirmLabelValue;
314
+ const message = this.currentActionConfirm || this.defaultConfirmMessageValue;
272
315
 
273
316
  if (this.hasDialogTitleTarget) {
274
317
  this.dialogTitleTarget.textContent = label;
@@ -353,7 +396,7 @@ export default class extends Controller {
353
396
  event.params.itemId || event.params.rubyCmsBulkActionTableItemIdParam;
354
397
 
355
398
  if (!itemId) {
356
- this.showNotification("Item ID not found for deletion.", "error");
399
+ this.showNotification(this.itemIdNotFoundMessageValue, "error");
357
400
  return;
358
401
  }
359
402
 
@@ -371,7 +414,7 @@ export default class extends Controller {
371
414
  this.getFallbackPath(this.itemNameValue, itemId);
372
415
 
373
416
  if (!deletePath) {
374
- this.showNotification("Delete path not found.", "error");
417
+ this.showNotification(this.deletePathNotFoundMessageValue, "error");
375
418
  return;
376
419
  }
377
420
 
@@ -424,7 +467,7 @@ export default class extends Controller {
424
467
 
425
468
  if (this.hasDialogConfirmButtonTarget) {
426
469
  this.dialogConfirmButtonTarget.disabled = true;
427
- this.dialogConfirmButtonTarget.textContent = "Processing...";
470
+ this.dialogConfirmButtonTarget.textContent = this.processingLabelValue;
428
471
  }
429
472
 
430
473
  try {
@@ -464,7 +507,7 @@ export default class extends Controller {
464
507
  const selectedIds = itemIds || this.getSelectedIds();
465
508
 
466
509
  if (selectedIds.length === 0) {
467
- this.showNotification("Please select at least one item.", "error");
510
+ this.showNotification(this.selectAtLeastOneMessageValue, "error");
468
511
  return;
469
512
  }
470
513
 
@@ -483,7 +526,7 @@ export default class extends Controller {
483
526
 
484
527
  if (!actionUrl) {
485
528
  this.showNotification(
486
- "Action URL not configured. Please configure an action URL for this page.",
529
+ this.actionUrlNotConfiguredMessageValue,
487
530
  "error",
488
531
  );
489
532
  return;
@@ -522,7 +565,10 @@ export default class extends Controller {
522
565
  }
523
566
  } else {
524
567
  const contentType = response.headers.get("content-type");
525
- let errorMessage = `An error occurred while performing ${action}.`;
568
+ let errorMessage = this.interpolateActionMessage(
569
+ this.genericActionErrorMessageValue,
570
+ action,
571
+ );
526
572
 
527
573
  if (contentType && contentType.includes("application/json")) {
528
574
  try {
@@ -547,12 +593,18 @@ export default class extends Controller {
547
593
  } catch (error) {
548
594
  console.error(`Error performing bulk action ${action}:`, error);
549
595
  this.showNotification(
550
- `An error occurred while performing ${action}.`,
596
+ this.interpolateActionMessage(this.genericActionErrorMessageValue, action),
551
597
  "error",
552
598
  );
553
599
  }
554
600
  }
555
601
 
602
+ interpolateActionMessage(template, action) {
603
+ const normalizedTemplate =
604
+ template || "An error occurred while performing %{action}.";
605
+ return normalizedTemplate.replace("%{action}", action || "action");
606
+ }
607
+
556
608
  showNotification(message, type = "info") {
557
609
  const toast = document.createElement("div");
558
610
  const colorMap = {