ruby_cms 0.1.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.
- checksums.yaml +7 -0
- data/.cursor/dhh.mdc +698 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/README.md +235 -0
- data/Rakefile +30 -0
- data/app/components/ruby_cms/admin/admin_page/admin_table_content.rb +32 -0
- data/app/components/ruby_cms/admin/admin_page.rb +345 -0
- data/app/components/ruby_cms/admin/base_component.rb +78 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table.rb +149 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_actions.rb +127 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_body.rb +15 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_cell.rb +41 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_head.rb +33 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_delete_modal.rb +174 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header.rb +59 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header_bar.rb +159 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_pagination.rb +192 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_row.rb +97 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_actions.rb +137 -0
- data/app/controllers/concerns/ruby_cms/admin_pagination.rb +120 -0
- data/app/controllers/concerns/ruby_cms/admin_turbo_table.rb +68 -0
- data/app/controllers/concerns/ruby_cms/page_tracking.rb +52 -0
- data/app/controllers/concerns/ruby_cms/visitor_error_capture.rb +39 -0
- data/app/controllers/ruby_cms/admin/analytics_controller.rb +191 -0
- data/app/controllers/ruby_cms/admin/base_controller.rb +105 -0
- data/app/controllers/ruby_cms/admin/content_blocks_controller.rb +390 -0
- data/app/controllers/ruby_cms/admin/dashboard_controller.rb +50 -0
- data/app/controllers/ruby_cms/admin/locale_controller.rb +20 -0
- data/app/controllers/ruby_cms/admin/permissions_controller.rb +66 -0
- data/app/controllers/ruby_cms/admin/settings_controller.rb +223 -0
- data/app/controllers/ruby_cms/admin/user_permissions_controller.rb +59 -0
- data/app/controllers/ruby_cms/admin/users_controller.rb +107 -0
- data/app/controllers/ruby_cms/admin/visitor_errors_controller.rb +89 -0
- data/app/controllers/ruby_cms/admin/visual_editor_controller.rb +322 -0
- data/app/controllers/ruby_cms/errors_controller.rb +35 -0
- data/app/helpers/ruby_cms/admin/admin_page_helper.rb +21 -0
- data/app/helpers/ruby_cms/admin/bulk_action_table_helper.rb +159 -0
- data/app/helpers/ruby_cms/application_helper.rb +41 -0
- data/app/helpers/ruby_cms/bulk_action_table_helper.rb +151 -0
- data/app/helpers/ruby_cms/content_blocks_helper.rb +375 -0
- data/app/helpers/ruby_cms/settings_helper.rb +160 -0
- data/app/javascript/controllers/ruby_cms/auto_save_preference_controller.js +73 -0
- data/app/javascript/controllers/ruby_cms/bulk_action_table_controller.js +553 -0
- data/app/javascript/controllers/ruby_cms/clickable_row_controller.js +28 -0
- data/app/javascript/controllers/ruby_cms/flash_messages_controller.js +29 -0
- data/app/javascript/controllers/ruby_cms/index.js +104 -0
- data/app/javascript/controllers/ruby_cms/locale_tabs_controller.js +34 -0
- data/app/javascript/controllers/ruby_cms/mobile_menu_controller.js +55 -0
- data/app/javascript/controllers/ruby_cms/nav_order_sortable_controller.js +192 -0
- data/app/javascript/controllers/ruby_cms/page_preview_controller.js +135 -0
- data/app/javascript/controllers/ruby_cms/toggle_controller.js +39 -0
- data/app/javascript/controllers/ruby_cms/visual_editor_controller.js +321 -0
- data/app/models/concerns/content_block/publishable.rb +54 -0
- data/app/models/concerns/content_block/searchable.rb +22 -0
- data/app/models/content_block.rb +155 -0
- data/app/models/ruby_cms/content_block.rb +8 -0
- data/app/models/ruby_cms/permission.rb +28 -0
- data/app/models/ruby_cms/permittable.rb +39 -0
- data/app/models/ruby_cms/preference.rb +111 -0
- data/app/models/ruby_cms/user_permission.rb +12 -0
- data/app/models/ruby_cms/visitor_error.rb +109 -0
- data/app/services/ruby_cms/analytics/report.rb +362 -0
- data/app/services/ruby_cms/security_tracker.rb +92 -0
- data/app/views/layouts/ruby_cms/_admin_flash_messages.html.erb +37 -0
- data/app/views/layouts/ruby_cms/_admin_sidebar.html.erb +121 -0
- data/app/views/layouts/ruby_cms/admin.html.erb +81 -0
- data/app/views/layouts/ruby_cms/minimal.html.erb +181 -0
- data/app/views/ruby_cms/admin/analytics/index.html.erb +160 -0
- data/app/views/ruby_cms/admin/analytics/page_details.html.erb +84 -0
- data/app/views/ruby_cms/admin/analytics/partials/_back_button.html.erb +3 -0
- data/app/views/ruby_cms/admin/analytics/partials/_browser_device.html.erb +40 -0
- data/app/views/ruby_cms/admin/analytics/partials/_daily_activity_chart.html.erb +58 -0
- data/app/views/ruby_cms/admin/analytics/partials/_hourly_activity_chart.html.erb +51 -0
- data/app/views/ruby_cms/admin/analytics/partials/_recent_activity.html.erb +31 -0
- data/app/views/ruby_cms/admin/analytics/partials/_security_alert.html.erb +4 -0
- data/app/views/ruby_cms/admin/analytics/partials/_top_referrers.html.erb +21 -0
- data/app/views/ruby_cms/admin/analytics/visitor_details.html.erb +125 -0
- data/app/views/ruby_cms/admin/content_blocks/_form.html.erb +161 -0
- data/app/views/ruby_cms/admin/content_blocks/_row.html.erb +25 -0
- data/app/views/ruby_cms/admin/content_blocks/edit.html.erb +17 -0
- data/app/views/ruby_cms/admin/content_blocks/index.html.erb +66 -0
- data/app/views/ruby_cms/admin/content_blocks/new.html.erb +5 -0
- data/app/views/ruby_cms/admin/content_blocks/show.html.erb +110 -0
- data/app/views/ruby_cms/admin/dashboard/index.html.erb +198 -0
- data/app/views/ruby_cms/admin/permissions/_row.html.erb +11 -0
- data/app/views/ruby_cms/admin/permissions/index.html.erb +62 -0
- data/app/views/ruby_cms/admin/settings/index.html.erb +220 -0
- data/app/views/ruby_cms/admin/shared/_bulk_action_table_index.html.erb +56 -0
- data/app/views/ruby_cms/admin/user_permissions/index.html.erb +55 -0
- data/app/views/ruby_cms/admin/users/_row.html.erb +14 -0
- data/app/views/ruby_cms/admin/users/index.html.erb +70 -0
- data/app/views/ruby_cms/admin/visitor_errors/_row.html.erb +35 -0
- data/app/views/ruby_cms/admin/visitor_errors/index.html.erb +57 -0
- data/app/views/ruby_cms/admin/visitor_errors/show.html.erb +147 -0
- data/app/views/ruby_cms/admin/visual_editor/index.html.erb +144 -0
- data/app/views/ruby_cms/errors/not_found.html.erb +92 -0
- data/config/database.yml +6 -0
- data/config/importmap.rb +36 -0
- data/config/locales/en.yml +101 -0
- data/config/routes.rb +65 -0
- data/db/migrate/20260125000001_create_ruby_cms_permissions.rb +14 -0
- data/db/migrate/20260125000002_create_ruby_cms_user_permissions.rb +14 -0
- data/db/migrate/20260125000003_create_ruby_cms_content_blocks.rb +19 -0
- data/db/migrate/20260125000010_add_indexes_to_ruby_cms_tables.rb +9 -0
- data/db/migrate/20260127000001_add_locale_to_ruby_cms_content_blocks.rb +34 -0
- data/db/migrate/20260129000001_create_ruby_cms_visitor_errors.rb +24 -0
- data/db/migrate/20260130000001_add_referer_and_query_to_ruby_cms_visitor_errors.rb +8 -0
- data/db/migrate/20260130000002_create_ruby_cms_preferences.rb +16 -0
- data/db/migrate/20260130000003_add_category_to_ruby_cms_preferences.rb +8 -0
- data/db/migrate/20260211000001_add_ruby_cms_analytics_fields_to_ahoy_events.rb +19 -0
- data/db/migrate/20260212000001_use_unprefixed_cms_tables.rb +146 -0
- data/exe/ruby_cms +25 -0
- data/lib/generators/ruby_cms/install_generator.rb +1062 -0
- data/lib/generators/ruby_cms/templates/admin.html.erb +82 -0
- data/lib/generators/ruby_cms/templates/ruby_cms.rb +86 -0
- data/lib/ruby_cms/app_integration.rb +82 -0
- data/lib/ruby_cms/cli.rb +169 -0
- data/lib/ruby_cms/content_blocks_grouping.rb +41 -0
- data/lib/ruby_cms/content_blocks_sync.rb +329 -0
- data/lib/ruby_cms/css_compiler.rb +35 -0
- data/lib/ruby_cms/engine.rb +498 -0
- data/lib/ruby_cms/settings.rb +145 -0
- data/lib/ruby_cms/settings_registry.rb +289 -0
- data/lib/ruby_cms/version.rb +5 -0
- data/lib/ruby_cms.rb +195 -0
- data/lib/tasks/ruby_cms.rake +27 -0
- data/log/test.log +17875 -0
- data/sig/ruby_cms.rbs +4 -0
- metadata +223 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyCms
|
|
4
|
+
module Admin
|
|
5
|
+
class VisualEditorController < BaseController
|
|
6
|
+
before_action { require_permission!(:manage_content_blocks) }
|
|
7
|
+
before_action :apply_visual_editor_locale
|
|
8
|
+
|
|
9
|
+
def index
|
|
10
|
+
@available_pages = available_pages
|
|
11
|
+
@current_page = determine_current_page
|
|
12
|
+
@edit_mode = edit_mode_enabled?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def page_preview
|
|
16
|
+
@page_key = params[:page] || "home"
|
|
17
|
+
@page = @page_key
|
|
18
|
+
@edit_mode = edit_mode_enabled?
|
|
19
|
+
@content_blocks = load_content_blocks_for_locale
|
|
20
|
+
template = template_for_page(@page_key)
|
|
21
|
+
|
|
22
|
+
return render_invalid_page unless template
|
|
23
|
+
|
|
24
|
+
load_preview_data(@page_key)
|
|
25
|
+
render template: template, layout: "ruby_cms/minimal"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def quick_update
|
|
29
|
+
block = find_or_initialize_content_block
|
|
30
|
+
update_content_block_attributes(block)
|
|
31
|
+
assign_content_block_content(block)
|
|
32
|
+
|
|
33
|
+
if block.save
|
|
34
|
+
render json: success_response(block)
|
|
35
|
+
else
|
|
36
|
+
render json: error_response(block), status: :unprocessable_content
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def apply_visual_editor_locale
|
|
43
|
+
requested = params[:locale].presence
|
|
44
|
+
return if requested.blank?
|
|
45
|
+
|
|
46
|
+
locale = requested.to_s
|
|
47
|
+
return unless available_locales.include?(locale)
|
|
48
|
+
|
|
49
|
+
session[:ruby_cms_locale] = locale
|
|
50
|
+
I18n.locale = locale.to_sym
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def available_locales
|
|
54
|
+
I18n.available_locales.map(&:to_s)
|
|
55
|
+
rescue StandardError
|
|
56
|
+
[I18n.default_locale.to_s]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def determine_current_page
|
|
60
|
+
requested = params[:page].presence
|
|
61
|
+
return requested if requested && @available_pages.key?(requested)
|
|
62
|
+
return @available_pages.keys.first if @available_pages.any?
|
|
63
|
+
|
|
64
|
+
"home"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def edit_mode_enabled?
|
|
68
|
+
params[:edit_mode].nil? || params[:edit_mode] == "true"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def template_for_page(page_key)
|
|
72
|
+
template_from_config(page_key) || template_from_auto_detect(page_key)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def template_from_config(page_key)
|
|
76
|
+
load_config_templates[page_key]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def template_from_auto_detect(page_key)
|
|
80
|
+
auto_detect_templates[page_key]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def load_config_templates
|
|
84
|
+
Rails.application.config.ruby_cms.preview_templates
|
|
85
|
+
rescue StandardError
|
|
86
|
+
{}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def render_invalid_page
|
|
90
|
+
all_templates = load_config_templates.merge(auto_detect_templates)
|
|
91
|
+
available_keys = all_templates.keys.join(", ")
|
|
92
|
+
message = if available_keys.present?
|
|
93
|
+
"Invalid page: #{@page_key}. Available pages: #{available_keys}"
|
|
94
|
+
else
|
|
95
|
+
"Invalid page: #{@page_key}. No pages found in app/views. " \
|
|
96
|
+
"Create a template like app/views/pages/home.html.erb or " \
|
|
97
|
+
"configure manually in config/initializers/ruby_cms.rb"
|
|
98
|
+
end
|
|
99
|
+
render plain: message, status: :bad_request
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def load_content_blocks_for_locale
|
|
103
|
+
blocks = ::ContentBlock.all
|
|
104
|
+
blocks_by_locale = blocks.group_by(&:locale)
|
|
105
|
+
locale_blocks = blocks_by_locale[current_locale] || []
|
|
106
|
+
default_blocks = default_locale_blocks(blocks_by_locale)
|
|
107
|
+
|
|
108
|
+
content_blocks = {}
|
|
109
|
+
default_blocks&.each {|b| content_blocks[b.key] = b }
|
|
110
|
+
locale_blocks.each {|b| content_blocks[b.key] = b }
|
|
111
|
+
content_blocks
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def default_locale_blocks(blocks_by_locale)
|
|
115
|
+
return nil if current_locale == default_locale
|
|
116
|
+
|
|
117
|
+
blocks_by_locale[default_locale] || []
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def find_or_initialize_content_block
|
|
121
|
+
key = params[:key]
|
|
122
|
+
locale = content_block_locale
|
|
123
|
+
::ContentBlock.find_or_initialize_by(key: key, locale: locale.to_s)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def content_block_locale
|
|
127
|
+
params[:locale].presence || session[:ruby_cms_locale].presence || I18n.locale.to_s
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def update_content_block_attributes(block)
|
|
131
|
+
block.content_type = params[:content_type]
|
|
132
|
+
block.updated_by = current_user_cms
|
|
133
|
+
block.published = true
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def assign_content_block_content(block)
|
|
137
|
+
if rich_text_content?
|
|
138
|
+
block.rich_content = params[:rich_content]
|
|
139
|
+
elsif params[:content].present?
|
|
140
|
+
block.content = params[:content]
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def rich_text_content?
|
|
145
|
+
params[:content_type] == "rich_text" && params[:rich_content].present?
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def success_response(block)
|
|
149
|
+
{
|
|
150
|
+
success: true,
|
|
151
|
+
message: "Content updated successfully",
|
|
152
|
+
content: content_block_content_text(block),
|
|
153
|
+
rich_content_html: rich_content_html(block),
|
|
154
|
+
content_type: block.content_type,
|
|
155
|
+
locale: block.locale,
|
|
156
|
+
updated_at: formatted_updated_at(block)
|
|
157
|
+
}
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def content_block_content_text(block)
|
|
161
|
+
block.content_type == "rich_text" ? block.rich_content.to_plain_text : block.content
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Return body HTML only (no layout/comments) so preview and Trix get clean HTML.
|
|
165
|
+
def rich_content_html(block)
|
|
166
|
+
return nil unless block.content_type == "rich_text"
|
|
167
|
+
return nil unless block.respond_to?(:rich_content)
|
|
168
|
+
return nil unless block.rich_content.respond_to?(:body) && block.rich_content.body.present?
|
|
169
|
+
|
|
170
|
+
body = block.rich_content.body
|
|
171
|
+
body.respond_to?(:to_html) ? body.to_html : body.to_s
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def formatted_updated_at(block)
|
|
175
|
+
block.updated_at.strftime("%B %d, %Y at %I:%M %p")
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def error_response(block)
|
|
179
|
+
{
|
|
180
|
+
success: false,
|
|
181
|
+
message: block.errors.full_messages.join(", ")
|
|
182
|
+
}
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def available_pages
|
|
186
|
+
pages = {}
|
|
187
|
+
add_config_pages(pages)
|
|
188
|
+
add_auto_detected_pages(pages) if pages.empty?
|
|
189
|
+
pages
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def add_auto_detected_pages(pages)
|
|
193
|
+
auto_detect_templates.each do |key, template_path|
|
|
194
|
+
next if pages.key?(key)
|
|
195
|
+
|
|
196
|
+
pages[key] = build_page_hash(key, template_path)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def add_config_pages(pages)
|
|
201
|
+
config_pages.each do |key, template_path|
|
|
202
|
+
pages[key] ||= build_page_hash(key, template_path)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def build_page_hash(key, template_path)
|
|
207
|
+
{
|
|
208
|
+
name: key.humanize,
|
|
209
|
+
url: page_url_for(key),
|
|
210
|
+
key_prefix: "#{key}_",
|
|
211
|
+
template_path: template_path
|
|
212
|
+
}
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def config_pages
|
|
216
|
+
Rails.application.config.ruby_cms.preview_templates
|
|
217
|
+
rescue StandardError
|
|
218
|
+
{}
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def page_url_for(key)
|
|
222
|
+
public_page_path(key:)
|
|
223
|
+
rescue NoMethodError, ActionController::UrlGenerationError
|
|
224
|
+
"/p/#{key}"
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def current_locale
|
|
228
|
+
(params[:locale] || session[:ruby_cms_locale] || I18n.locale.to_s).to_s
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def default_locale
|
|
232
|
+
I18n.default_locale.to_s
|
|
233
|
+
rescue StandardError
|
|
234
|
+
"en"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def auto_detect_templates
|
|
238
|
+
@auto_detect_templates ||= detect_templates_at_runtime
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def detect_templates_at_runtime
|
|
242
|
+
views_dir = Rails.root.join("app/views")
|
|
243
|
+
return {} unless Dir.exist?(views_dir)
|
|
244
|
+
|
|
245
|
+
templates = {}
|
|
246
|
+
scan_views_for_templates(views_dir, templates)
|
|
247
|
+
templates
|
|
248
|
+
rescue StandardError
|
|
249
|
+
{}
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def scan_views_for_templates(dir_path, templates, relative_path="")
|
|
253
|
+
Dir.glob(File.join(dir_path, "*")).each do |path|
|
|
254
|
+
if File.directory?(path)
|
|
255
|
+
scan_subdirectory(path, templates, relative_path)
|
|
256
|
+
else
|
|
257
|
+
process_template_file(path, templates, relative_path)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def scan_subdirectory(path, templates, relative_path)
|
|
263
|
+
dir_name = File.basename(path)
|
|
264
|
+
return if skip_directory?(dir_name, relative_path)
|
|
265
|
+
|
|
266
|
+
new_relative_path = relative_path.empty? ? dir_name : "#{relative_path}/#{dir_name}"
|
|
267
|
+
return if depth_exceeded?(new_relative_path)
|
|
268
|
+
|
|
269
|
+
scan_views_for_templates(path, templates, new_relative_path)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def skip_directory?(dir_name, relative_path)
|
|
273
|
+
%w[
|
|
274
|
+
layouts shared mailers components
|
|
275
|
+
admin
|
|
276
|
+
].include?(dir_name) || relative_path.start_with?("admin")
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def depth_exceeded?(relative_path)
|
|
280
|
+
relative_path.split("/").length > 2
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def process_template_file(file_path, templates, relative_path)
|
|
284
|
+
return unless file_path.match?(/\.(html\.erb|html\.haml|html\.slim)$/)
|
|
285
|
+
|
|
286
|
+
base_name = File.basename(file_path, ".*").sub(/\.html$/, "")
|
|
287
|
+
if base_name.start_with?("_") || relative_path == "layouts" ||
|
|
288
|
+
relative_path.start_with?("admin")
|
|
289
|
+
return
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
page_key, template_path = build_page_key_and_template_path(base_name, relative_path)
|
|
293
|
+
templates[page_key] ||= template_path
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def build_page_key_and_template_path(base_name, relative_path)
|
|
297
|
+
if relative_path.empty?
|
|
298
|
+
[base_name, base_name]
|
|
299
|
+
elsif base_name == "index"
|
|
300
|
+
key = relative_path.split("/").last
|
|
301
|
+
[key, "#{relative_path}/index"]
|
|
302
|
+
else
|
|
303
|
+
[base_name, "#{relative_path}/#{base_name}"]
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def load_preview_data(page_key)
|
|
308
|
+
@page_title = page_key.humanize
|
|
309
|
+
|
|
310
|
+
data_proc = Rails.application.config.ruby_cms.preview_data
|
|
311
|
+
return unless data_proc.respond_to?(:call)
|
|
312
|
+
|
|
313
|
+
data = data_proc.call(page_key, view_context)
|
|
314
|
+
return unless data.kind_of?(Hash)
|
|
315
|
+
|
|
316
|
+
data.each do |k, v|
|
|
317
|
+
instance_variable_set(:"@#{k}", v)
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyCms
|
|
4
|
+
# Handles 404 errors from catch-all routes.
|
|
5
|
+
# Add this to the BOTTOM of your routes.rb to capture routing errors:
|
|
6
|
+
#
|
|
7
|
+
# match "*path", to: "ruby_cms/errors#not_found", via: :all,
|
|
8
|
+
# constraints: ->(req) { !req.path.start_with?("/rails/") }
|
|
9
|
+
#
|
|
10
|
+
class ErrorsController < ApplicationController
|
|
11
|
+
def not_found
|
|
12
|
+
# Log the routing error to VisitorError (skips in development)
|
|
13
|
+
RubyCms::VisitorError.log_routing_error(request)
|
|
14
|
+
|
|
15
|
+
respond_to do |format|
|
|
16
|
+
format.html do
|
|
17
|
+
render_html_not_found
|
|
18
|
+
end
|
|
19
|
+
format.json { render json: { error: "Not found" }, status: :not_found }
|
|
20
|
+
format.any { head :not_found }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def render_html_not_found
|
|
27
|
+
static_not_found = Rails.public_path.join("404.html")
|
|
28
|
+
if static_not_found.exist?
|
|
29
|
+
render file: static_not_found, status: :not_found, layout: false
|
|
30
|
+
else
|
|
31
|
+
render :not_found, status: :not_found, layout: false
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyCms
|
|
4
|
+
module Admin
|
|
5
|
+
module AdminPageHelper
|
|
6
|
+
def admin_page(title: nil, subtitle: nil, footer: nil, **, &)
|
|
7
|
+
render RubyCms::Admin::AdminPage.new(
|
|
8
|
+
title:,
|
|
9
|
+
subtitle:,
|
|
10
|
+
footer:,
|
|
11
|
+
**,
|
|
12
|
+
&
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def admin_table_content(id: "admin_table_content", **attrs, &)
|
|
17
|
+
render RubyCms::Admin::AdminPage::AdminTableContent.new(id:, **attrs, &)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyCms
|
|
4
|
+
module Admin
|
|
5
|
+
module BulkActionTableHelper
|
|
6
|
+
# Render a complete bulk action table using Phlex components
|
|
7
|
+
#
|
|
8
|
+
# @param collection [ActiveRecord::Relation] The collection to display
|
|
9
|
+
# @param headers [Array<String, Hash>] Array of header labels or hashes with :text and :class
|
|
10
|
+
# @param turbo_frame [String, nil] Turbo Frame ID for seamless updates
|
|
11
|
+
# @param pagination [Hash, nil] Pagination hash from AdminPagination concern
|
|
12
|
+
# @param pagination_path [Proc, nil] Lambda for generating pagination URLs
|
|
13
|
+
# @param bulk_actions_url [String, nil] URL for bulk delete action
|
|
14
|
+
# @param bulk_action_buttons [Array<Hash>] Array of custom bulk action button configs
|
|
15
|
+
# @param item_name [String] Singular name for items (default: "item")
|
|
16
|
+
# @param controller_name [String] Stimulus controller identifier
|
|
17
|
+
# @param block [Proc] Block that renders table rows
|
|
18
|
+
# @return [String] Rendered HTML
|
|
19
|
+
#
|
|
20
|
+
# @example Basic usage
|
|
21
|
+
# <%= render_bulk_action_table(
|
|
22
|
+
# collection: @items,
|
|
23
|
+
# headers: ["Name", "Status", { text: "Actions", class: "text-right" }],
|
|
24
|
+
# bulk_actions_url: bulk_delete_items_path,
|
|
25
|
+
# item_name: "item"
|
|
26
|
+
# ) do %>
|
|
27
|
+
# <% @items.each do |item| %>
|
|
28
|
+
# <%= render RubyCms::Admin::BulkActionTable::BulkActionTableRow.new(
|
|
29
|
+
# data: { item_id: item.id }
|
|
30
|
+
# ) do %>
|
|
31
|
+
# <td><%= item.name %></td>
|
|
32
|
+
# <td><%= item.status %></td>
|
|
33
|
+
# <td class="text-right">
|
|
34
|
+
# <%= render RubyCms::Admin::BulkActionTable::BulkActionTableActions.new(
|
|
35
|
+
# edit_path: edit_item_path(item),
|
|
36
|
+
# delete_path: item_path(item),
|
|
37
|
+
# item_id: item.id
|
|
38
|
+
# ) %>
|
|
39
|
+
# </td>
|
|
40
|
+
# <% end %>
|
|
41
|
+
# <% end %>
|
|
42
|
+
# <% end %>
|
|
43
|
+
def render_bulk_action_table(
|
|
44
|
+
headers:,
|
|
45
|
+
turbo_frame: "admin_table_content",
|
|
46
|
+
pagination: nil,
|
|
47
|
+
pagination_path: nil,
|
|
48
|
+
bulk_actions_url: nil,
|
|
49
|
+
bulk_action_buttons: [],
|
|
50
|
+
item_name: "item",
|
|
51
|
+
controller_name: "ruby-cms--bulk-action-table",
|
|
52
|
+
&block
|
|
53
|
+
)
|
|
54
|
+
render RubyCms::Admin::BulkActionTable::BulkActionTable.new(
|
|
55
|
+
turbo_frame: turbo_frame,
|
|
56
|
+
pagination: pagination,
|
|
57
|
+
pagination_path: pagination_path,
|
|
58
|
+
bulk_actions_url: bulk_actions_url,
|
|
59
|
+
bulk_actions_buttons: bulk_action_buttons,
|
|
60
|
+
item_name: item_name,
|
|
61
|
+
controller_name: controller_name,
|
|
62
|
+
csrf_token: form_authenticity_token
|
|
63
|
+
) do
|
|
64
|
+
render RubyCms::Admin::BulkActionTable::BulkActionTableHeader.new(
|
|
65
|
+
headers: headers,
|
|
66
|
+
bulk_actions_enabled: bulk_actions_url.present? || bulk_action_buttons.any?,
|
|
67
|
+
controller_name: controller_name
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
render RubyCms::Admin::BulkActionTable::BulkActionTableBody.new(&block)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Render an admin page with consistent layout
|
|
75
|
+
#
|
|
76
|
+
# @param title [String] Page title
|
|
77
|
+
# @param subtitle [String, nil] Optional subtitle
|
|
78
|
+
# @param actions [Array<Hash>, nil] Array of action button configs
|
|
79
|
+
# @param breadcrumbs [Array<Hash>, nil] Array of breadcrumb items
|
|
80
|
+
# @param padding [Boolean] Add padding classes (default: true)
|
|
81
|
+
# @param overflow [Boolean] Allow overflow (default: true)
|
|
82
|
+
# @param turbo_frame [String, nil] Turbo Frame ID for wrapping
|
|
83
|
+
# @param turbo_frame_options [Hash, nil] Custom Turbo Frame options
|
|
84
|
+
# @param block [Proc] Block that renders page content
|
|
85
|
+
# @return [String] Rendered HTML
|
|
86
|
+
#
|
|
87
|
+
# @example
|
|
88
|
+
# <%= render_admin_page(
|
|
89
|
+
# title: "Content Blocks",
|
|
90
|
+
# actions: [
|
|
91
|
+
# { label: "New Block", url: new_content_block_path, primary: true }
|
|
92
|
+
# ],
|
|
93
|
+
# turbo_frame: "admin_table_content"
|
|
94
|
+
# ) do %>
|
|
95
|
+
# <div class="ruby_cms-card">
|
|
96
|
+
# <!-- Content here -->
|
|
97
|
+
# </div>
|
|
98
|
+
# <% end %>
|
|
99
|
+
def render_admin_page(
|
|
100
|
+
title:,
|
|
101
|
+
subtitle: nil,
|
|
102
|
+
actions: nil,
|
|
103
|
+
breadcrumbs: nil,
|
|
104
|
+
padding: true,
|
|
105
|
+
overflow: true,
|
|
106
|
+
turbo_frame: nil,
|
|
107
|
+
turbo_frame_options: nil
|
|
108
|
+
)
|
|
109
|
+
render RubyCms::Admin::AdminPage.new(
|
|
110
|
+
title:,
|
|
111
|
+
subtitle:,
|
|
112
|
+
actions:,
|
|
113
|
+
breadcrumbs:,
|
|
114
|
+
padding:,
|
|
115
|
+
overflow:,
|
|
116
|
+
turbo_frame:,
|
|
117
|
+
turbo_frame_options:
|
|
118
|
+
) do
|
|
119
|
+
yield if block_given?
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Build bulk action button configuration hash
|
|
124
|
+
#
|
|
125
|
+
# @param name [String] Action name (used internally)
|
|
126
|
+
# @param label [String] Button label text
|
|
127
|
+
# @param url [String] Action URL
|
|
128
|
+
# @param confirm [String, nil] Confirmation message
|
|
129
|
+
# @param action_type [String] "redirect" for redirect actions, nil for dialog actions
|
|
130
|
+
# @param class [String, nil] Additional CSS classes
|
|
131
|
+
# @return [Hash] Button configuration hash
|
|
132
|
+
#
|
|
133
|
+
# @example
|
|
134
|
+
# bulk_action_button(
|
|
135
|
+
# name: "publish",
|
|
136
|
+
# label: "Publish Selected",
|
|
137
|
+
# url: bulk_publish_path,
|
|
138
|
+
# confirm: "Are you sure?"
|
|
139
|
+
# )
|
|
140
|
+
def bulk_action_button(
|
|
141
|
+
name:,
|
|
142
|
+
label:,
|
|
143
|
+
url:,
|
|
144
|
+
confirm: nil,
|
|
145
|
+
action_type: nil,
|
|
146
|
+
class: nil
|
|
147
|
+
)
|
|
148
|
+
{
|
|
149
|
+
name: name,
|
|
150
|
+
label: label,
|
|
151
|
+
url: url,
|
|
152
|
+
confirm: confirm,
|
|
153
|
+
action_type: action_type,
|
|
154
|
+
class: binding.local_variable_get(:class)
|
|
155
|
+
}.compact
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyCms
|
|
4
|
+
module ApplicationHelper
|
|
5
|
+
include RubyCms::Engine.routes.url_helpers if defined?(RubyCms::Engine)
|
|
6
|
+
|
|
7
|
+
def ruby_cms_locale_display_name(locale)
|
|
8
|
+
key = "ruby_cms.admin.locales.#{locale}"
|
|
9
|
+
t(key, default: locale.to_s)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def ruby_cms_user_display(user)
|
|
13
|
+
return "—" if user.blank?
|
|
14
|
+
|
|
15
|
+
%i[email_address email username name].each do |attr|
|
|
16
|
+
return user.public_send(attr) if user.respond_to?(attr) && user.public_send(attr).present?
|
|
17
|
+
end
|
|
18
|
+
user.respond_to?(:id) ? "User ##{user.id}" : user.to_s
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def ruby_cms_nav_entries
|
|
22
|
+
RubyCms.visible_nav_registry(
|
|
23
|
+
view_context: self,
|
|
24
|
+
user: (current_user_cms if respond_to?(:current_user_cms))
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Render an SVG fragment (typically <path ...>) safely.
|
|
29
|
+
# Used for nav icons which may come from host app configuration.
|
|
30
|
+
def ruby_cms_safe_svg_fragment(fragment)
|
|
31
|
+
sanitize(
|
|
32
|
+
fragment.to_s,
|
|
33
|
+
tags: %w[svg path g circle rect line polygon polyline ellipse],
|
|
34
|
+
attributes: %w[
|
|
35
|
+
fill stroke stroke-linecap stroke-linejoin stroke-width d
|
|
36
|
+
class viewBox cx cy r x y points x1 y1 x2 y2 aria-hidden aria-label focusable
|
|
37
|
+
]
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|