ruby_cms 0.2.0.4 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table.rb +9 -1
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_actions.rb +3 -3
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_delete_modal.rb +8 -8
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header_bar.rb +15 -5
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_actions.rb +57 -12
- data/app/controllers/concerns/ruby_cms/page_tracking.rb +15 -0
- data/app/controllers/ruby_cms/admin/analytics_controller.rb +12 -0
- data/app/controllers/ruby_cms/admin/commands_controller.rb +6 -6
- data/app/javascript/controllers/ruby_cms/bulk_action_table_controller.js +65 -13
- data/app/services/ruby_cms/analytics/report.rb +119 -7
- data/app/services/ruby_cms/command_runner.rb +3 -3
- data/app/views/layouts/ruby_cms/admin.html.erb +1 -1
- data/app/views/ruby_cms/admin/analytics/index.html.erb +160 -67
- data/app/views/ruby_cms/admin/analytics/partials/_browser_device.html.erb +21 -15
- data/app/views/ruby_cms/admin/analytics/partials/_daily_activity_chart.html.erb +36 -16
- data/app/views/ruby_cms/admin/analytics/partials/_hourly_activity_chart.html.erb +29 -13
- data/app/views/ruby_cms/admin/analytics/partials/_landing_pages.html.erb +27 -15
- data/app/views/ruby_cms/admin/analytics/partials/_os_stats.html.erb +29 -18
- data/app/views/ruby_cms/admin/analytics/partials/_popular_pages.html.erb +17 -12
- data/app/views/ruby_cms/admin/analytics/partials/_recent_activity.html.erb +25 -14
- data/app/views/ruby_cms/admin/analytics/partials/_top_referrers.html.erb +27 -15
- data/app/views/ruby_cms/admin/analytics/partials/_top_visitors.html.erb +20 -10
- data/app/views/ruby_cms/admin/analytics/partials/_utm_sources.html.erb +27 -15
- data/app/views/ruby_cms/admin/content_blocks/index.html.erb +8 -1
- data/config/locales/en.yml +69 -5
- data/config/locales/nl.yml +98 -0
- data/lib/generators/ruby_cms/install_generator.rb +45 -89
- data/lib/generators/ruby_cms/templates/admin.html.erb +1 -1
- data/lib/generators/ruby_cms/templates/ruby_cms.rb +40 -3
- data/lib/ruby_cms/settings_registry.rb +35 -0
- data/lib/ruby_cms/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b2871a67fff389ef55e7c894100dcfbe9b7eeb4ccbec079986d64f6c768c9f87
|
|
4
|
+
data.tar.gz: 7658b768eed4dbc05b88312e3b25d85ccee84180441d55f91023af3c797be32c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 25b55ee8d4aa7f9a9ef0b3d861cc1238c1c97b1550703e06336c11df8ac7aa0552bcddf93c90af3dca456ee9149647501c9a37ea71c858181d70f992dd19b33d
|
|
7
|
+
data.tar.gz: 2d9aa367ab09809bde3781a654b88fa3d7b9c7eed9bf37899977c18ab57d69e7e5695f60db91c0358365a978c228129525055a1653828007178051ab22404760
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
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
|
|
25
|
+
|
|
26
|
+
- The host app no longer needs to scan de gem for tailwind
|
|
27
|
+
|
|
3
28
|
## [0.2.0.3] - 2026-04-02
|
|
4
29
|
|
|
5
30
|
- Whole repo was scanned for comiling so it was slow.
|
|
@@ -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-
|
|
49
|
-
"hover:bg-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
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-
|
|
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
|
|
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")
|
|
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
|
-
)
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
)
|
|
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
|
|
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: :
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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 ||
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 = {
|