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.
Files changed (131) hide show
  1. checksums.yaml +7 -0
  2. data/.cursor/dhh.mdc +698 -0
  3. data/.ruby-version +1 -0
  4. data/CHANGELOG.md +5 -0
  5. data/CODE_OF_CONDUCT.md +10 -0
  6. data/README.md +235 -0
  7. data/Rakefile +30 -0
  8. data/app/components/ruby_cms/admin/admin_page/admin_table_content.rb +32 -0
  9. data/app/components/ruby_cms/admin/admin_page.rb +345 -0
  10. data/app/components/ruby_cms/admin/base_component.rb +78 -0
  11. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table.rb +149 -0
  12. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_actions.rb +127 -0
  13. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_body.rb +15 -0
  14. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_cell.rb +41 -0
  15. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_head.rb +33 -0
  16. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_delete_modal.rb +174 -0
  17. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header.rb +59 -0
  18. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header_bar.rb +159 -0
  19. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_pagination.rb +192 -0
  20. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_row.rb +97 -0
  21. data/app/components/ruby_cms/admin/bulk_action_table/bulk_actions.rb +137 -0
  22. data/app/controllers/concerns/ruby_cms/admin_pagination.rb +120 -0
  23. data/app/controllers/concerns/ruby_cms/admin_turbo_table.rb +68 -0
  24. data/app/controllers/concerns/ruby_cms/page_tracking.rb +52 -0
  25. data/app/controllers/concerns/ruby_cms/visitor_error_capture.rb +39 -0
  26. data/app/controllers/ruby_cms/admin/analytics_controller.rb +191 -0
  27. data/app/controllers/ruby_cms/admin/base_controller.rb +105 -0
  28. data/app/controllers/ruby_cms/admin/content_blocks_controller.rb +390 -0
  29. data/app/controllers/ruby_cms/admin/dashboard_controller.rb +50 -0
  30. data/app/controllers/ruby_cms/admin/locale_controller.rb +20 -0
  31. data/app/controllers/ruby_cms/admin/permissions_controller.rb +66 -0
  32. data/app/controllers/ruby_cms/admin/settings_controller.rb +223 -0
  33. data/app/controllers/ruby_cms/admin/user_permissions_controller.rb +59 -0
  34. data/app/controllers/ruby_cms/admin/users_controller.rb +107 -0
  35. data/app/controllers/ruby_cms/admin/visitor_errors_controller.rb +89 -0
  36. data/app/controllers/ruby_cms/admin/visual_editor_controller.rb +322 -0
  37. data/app/controllers/ruby_cms/errors_controller.rb +35 -0
  38. data/app/helpers/ruby_cms/admin/admin_page_helper.rb +21 -0
  39. data/app/helpers/ruby_cms/admin/bulk_action_table_helper.rb +159 -0
  40. data/app/helpers/ruby_cms/application_helper.rb +41 -0
  41. data/app/helpers/ruby_cms/bulk_action_table_helper.rb +151 -0
  42. data/app/helpers/ruby_cms/content_blocks_helper.rb +375 -0
  43. data/app/helpers/ruby_cms/settings_helper.rb +160 -0
  44. data/app/javascript/controllers/ruby_cms/auto_save_preference_controller.js +73 -0
  45. data/app/javascript/controllers/ruby_cms/bulk_action_table_controller.js +553 -0
  46. data/app/javascript/controllers/ruby_cms/clickable_row_controller.js +28 -0
  47. data/app/javascript/controllers/ruby_cms/flash_messages_controller.js +29 -0
  48. data/app/javascript/controllers/ruby_cms/index.js +104 -0
  49. data/app/javascript/controllers/ruby_cms/locale_tabs_controller.js +34 -0
  50. data/app/javascript/controllers/ruby_cms/mobile_menu_controller.js +55 -0
  51. data/app/javascript/controllers/ruby_cms/nav_order_sortable_controller.js +192 -0
  52. data/app/javascript/controllers/ruby_cms/page_preview_controller.js +135 -0
  53. data/app/javascript/controllers/ruby_cms/toggle_controller.js +39 -0
  54. data/app/javascript/controllers/ruby_cms/visual_editor_controller.js +321 -0
  55. data/app/models/concerns/content_block/publishable.rb +54 -0
  56. data/app/models/concerns/content_block/searchable.rb +22 -0
  57. data/app/models/content_block.rb +155 -0
  58. data/app/models/ruby_cms/content_block.rb +8 -0
  59. data/app/models/ruby_cms/permission.rb +28 -0
  60. data/app/models/ruby_cms/permittable.rb +39 -0
  61. data/app/models/ruby_cms/preference.rb +111 -0
  62. data/app/models/ruby_cms/user_permission.rb +12 -0
  63. data/app/models/ruby_cms/visitor_error.rb +109 -0
  64. data/app/services/ruby_cms/analytics/report.rb +362 -0
  65. data/app/services/ruby_cms/security_tracker.rb +92 -0
  66. data/app/views/layouts/ruby_cms/_admin_flash_messages.html.erb +37 -0
  67. data/app/views/layouts/ruby_cms/_admin_sidebar.html.erb +121 -0
  68. data/app/views/layouts/ruby_cms/admin.html.erb +81 -0
  69. data/app/views/layouts/ruby_cms/minimal.html.erb +181 -0
  70. data/app/views/ruby_cms/admin/analytics/index.html.erb +160 -0
  71. data/app/views/ruby_cms/admin/analytics/page_details.html.erb +84 -0
  72. data/app/views/ruby_cms/admin/analytics/partials/_back_button.html.erb +3 -0
  73. data/app/views/ruby_cms/admin/analytics/partials/_browser_device.html.erb +40 -0
  74. data/app/views/ruby_cms/admin/analytics/partials/_daily_activity_chart.html.erb +58 -0
  75. data/app/views/ruby_cms/admin/analytics/partials/_hourly_activity_chart.html.erb +51 -0
  76. data/app/views/ruby_cms/admin/analytics/partials/_recent_activity.html.erb +31 -0
  77. data/app/views/ruby_cms/admin/analytics/partials/_security_alert.html.erb +4 -0
  78. data/app/views/ruby_cms/admin/analytics/partials/_top_referrers.html.erb +21 -0
  79. data/app/views/ruby_cms/admin/analytics/visitor_details.html.erb +125 -0
  80. data/app/views/ruby_cms/admin/content_blocks/_form.html.erb +161 -0
  81. data/app/views/ruby_cms/admin/content_blocks/_row.html.erb +25 -0
  82. data/app/views/ruby_cms/admin/content_blocks/edit.html.erb +17 -0
  83. data/app/views/ruby_cms/admin/content_blocks/index.html.erb +66 -0
  84. data/app/views/ruby_cms/admin/content_blocks/new.html.erb +5 -0
  85. data/app/views/ruby_cms/admin/content_blocks/show.html.erb +110 -0
  86. data/app/views/ruby_cms/admin/dashboard/index.html.erb +198 -0
  87. data/app/views/ruby_cms/admin/permissions/_row.html.erb +11 -0
  88. data/app/views/ruby_cms/admin/permissions/index.html.erb +62 -0
  89. data/app/views/ruby_cms/admin/settings/index.html.erb +220 -0
  90. data/app/views/ruby_cms/admin/shared/_bulk_action_table_index.html.erb +56 -0
  91. data/app/views/ruby_cms/admin/user_permissions/index.html.erb +55 -0
  92. data/app/views/ruby_cms/admin/users/_row.html.erb +14 -0
  93. data/app/views/ruby_cms/admin/users/index.html.erb +70 -0
  94. data/app/views/ruby_cms/admin/visitor_errors/_row.html.erb +35 -0
  95. data/app/views/ruby_cms/admin/visitor_errors/index.html.erb +57 -0
  96. data/app/views/ruby_cms/admin/visitor_errors/show.html.erb +147 -0
  97. data/app/views/ruby_cms/admin/visual_editor/index.html.erb +144 -0
  98. data/app/views/ruby_cms/errors/not_found.html.erb +92 -0
  99. data/config/database.yml +6 -0
  100. data/config/importmap.rb +36 -0
  101. data/config/locales/en.yml +101 -0
  102. data/config/routes.rb +65 -0
  103. data/db/migrate/20260125000001_create_ruby_cms_permissions.rb +14 -0
  104. data/db/migrate/20260125000002_create_ruby_cms_user_permissions.rb +14 -0
  105. data/db/migrate/20260125000003_create_ruby_cms_content_blocks.rb +19 -0
  106. data/db/migrate/20260125000010_add_indexes_to_ruby_cms_tables.rb +9 -0
  107. data/db/migrate/20260127000001_add_locale_to_ruby_cms_content_blocks.rb +34 -0
  108. data/db/migrate/20260129000001_create_ruby_cms_visitor_errors.rb +24 -0
  109. data/db/migrate/20260130000001_add_referer_and_query_to_ruby_cms_visitor_errors.rb +8 -0
  110. data/db/migrate/20260130000002_create_ruby_cms_preferences.rb +16 -0
  111. data/db/migrate/20260130000003_add_category_to_ruby_cms_preferences.rb +8 -0
  112. data/db/migrate/20260211000001_add_ruby_cms_analytics_fields_to_ahoy_events.rb +19 -0
  113. data/db/migrate/20260212000001_use_unprefixed_cms_tables.rb +146 -0
  114. data/exe/ruby_cms +25 -0
  115. data/lib/generators/ruby_cms/install_generator.rb +1062 -0
  116. data/lib/generators/ruby_cms/templates/admin.html.erb +82 -0
  117. data/lib/generators/ruby_cms/templates/ruby_cms.rb +86 -0
  118. data/lib/ruby_cms/app_integration.rb +82 -0
  119. data/lib/ruby_cms/cli.rb +169 -0
  120. data/lib/ruby_cms/content_blocks_grouping.rb +41 -0
  121. data/lib/ruby_cms/content_blocks_sync.rb +329 -0
  122. data/lib/ruby_cms/css_compiler.rb +35 -0
  123. data/lib/ruby_cms/engine.rb +498 -0
  124. data/lib/ruby_cms/settings.rb +145 -0
  125. data/lib/ruby_cms/settings_registry.rb +289 -0
  126. data/lib/ruby_cms/version.rb +5 -0
  127. data/lib/ruby_cms.rb +195 -0
  128. data/lib/tasks/ruby_cms.rake +27 -0
  129. data/log/test.log +17875 -0
  130. data/sig/ruby_cms.rbs +4 -0
  131. metadata +223 -0
@@ -0,0 +1,390 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCms
4
+ module Admin
5
+ class ContentBlocksController < BaseController
6
+ include RubyCms::AdminPagination
7
+ include RubyCms::AdminTurboTable
8
+
9
+ paginates per_page: -> { RubyCms::Preference.get(:content_blocks_per_page, default: 50) },
10
+ turbo_frame: "admin_table_content"
11
+
12
+ before_action { require_permission!(:manage_content_blocks) }
13
+ before_action :set_content_block, only: %i[show edit update destroy]
14
+
15
+ def index
16
+ collection = content_blocks_collection
17
+ @content_blocks = html_index_blocks(collection)
18
+
19
+ respond_to do |format|
20
+ format.html { render_index_html }
21
+ format.json { render json: json_index_blocks(collection) }
22
+ end
23
+ end
24
+
25
+ def show
26
+ @blocks_by_locale = load_blocks_by_locale_for_show
27
+ respond_with_block(@content_block)
28
+ end
29
+
30
+ def new
31
+ @content_block = ::ContentBlock.new(new_block_params)
32
+ # Most users expect newly created blocks to show on the site immediately.
33
+ # The public helper renders only `published` blocks, while the visual editor
34
+ # preview can show unpublished ones, which is confusing.
35
+ @content_block.published = true if @content_block.published.nil?
36
+ end
37
+
38
+ def edit
39
+ @blocks_by_locale = load_blocks_by_locale_for_edit
40
+ end
41
+
42
+ def create
43
+ @content_block = ::ContentBlock.new(content_block_params)
44
+ @content_block.record_update_by(current_user_cms)
45
+ @content_block.published = true if @content_block.published.nil?
46
+ save_and_respond(@content_block, :new)
47
+ end
48
+
49
+ def update
50
+ unified_locale_params? ? update_all_locales : update_single_block
51
+ end
52
+
53
+ def destroy
54
+ @content_block.destroy
55
+ redirect_with_notice("deleted")
56
+ end
57
+
58
+ # Bulk actions
59
+ %i[bulk_delete bulk_publish bulk_unpublish].each do |action|
60
+ define_method(action) { bulk_action(action) }
61
+ end
62
+
63
+ private
64
+
65
+ def render_index_html
66
+ if turbo_frame_request?
67
+ render :index, layout: false
68
+ else
69
+ render :index
70
+ end
71
+ end
72
+
73
+ def html_index_blocks(collection)
74
+ grouped_or_paginated(collection) || []
75
+ end
76
+
77
+ def grouped_or_paginated(collection)
78
+ if params[:locale].blank? && params[:q].blank?
79
+ grouped_rows = grouped_by_key_collection(collection)
80
+ paginate_collection(grouped_rows)
81
+ else
82
+ paginate_collection(collection)
83
+ end
84
+ end
85
+
86
+ def json_index_blocks(collection)
87
+ scope = collection.limit(100)
88
+ scope = include_rich_content(scope)
89
+ { content_blocks: serialize_content_blocks(scope) }
90
+ end
91
+
92
+ def include_rich_content(scope)
93
+ return scope unless scope.respond_to?(:includes)
94
+ return scope unless ::ContentBlock.respond_to?(:reflect_on_association)
95
+ return scope unless ::ContentBlock.reflect_on_association(:rich_content)
96
+
97
+ scope.includes(:rich_content)
98
+ end
99
+
100
+ def bulk_action(action)
101
+ ids = Array(params[:item_ids]).filter_map(&:to_i).compact
102
+ count = bulk_count_for(action, ids)
103
+ turbo_redirect_with_count(action, count)
104
+ end
105
+
106
+ def bulk_count_for(action, ids)
107
+ case action
108
+ when :bulk_delete then ::ContentBlock.where(id: ids).destroy_all.size
109
+ when :bulk_publish then bulk_set_published(ids, published: true)
110
+ when :bulk_unpublish then bulk_set_published(ids, published: false)
111
+ else 0
112
+ end
113
+ end
114
+
115
+ def turbo_redirect_with_count(action, count)
116
+ action_name = action.to_s.remove("bulk_")
117
+ notice = "#{count} content block(s) #{action_name}."
118
+ turbo_redirect_to ruby_cms_admin_content_blocks_path, notice:
119
+ end
120
+
121
+ def update_all_locales
122
+ errors = build_locale_blocks_errors
123
+ errors.any? ? handle_locale_update_errors(errors) : redirect_with_notice("updated")
124
+ end
125
+
126
+ def build_locale_blocks_errors
127
+ locale_keys = content_block_permitted_params - %i[key locale]
128
+ root_params = permitted_locale_params
129
+ shared_content_type = root_params[:content_type].presence || @content_block.content_type
130
+ shared_published = root_params[:published].to_s == "1"
131
+ update_locale_blocks(root_params[:locales] || {}, locale_keys, shared_content_type,
132
+ shared_published)
133
+ end
134
+
135
+ def handle_locale_update_errors(errors)
136
+ @content_block.errors.add(:base, errors.join("; "))
137
+ @blocks_by_locale = load_blocks_by_locale_for_edit
138
+ render :edit, status: :unprocessable_content
139
+ end
140
+
141
+ def update_single_block
142
+ @content_block.record_update_by(current_user_cms)
143
+ save_and_respond(@content_block, :edit)
144
+ end
145
+
146
+ def save_and_respond(block, failure_view)
147
+ if block.save
148
+ audit_if_json(block)
149
+ respond_to_success(block)
150
+ else
151
+ respond_to_failure(block, failure_view)
152
+ end
153
+ end
154
+
155
+ def audit_if_json(block)
156
+ audit_visual_editor_edit(block.attributes) if request.format.json?
157
+ end
158
+
159
+ def respond_to_success(block)
160
+ respond_to do |f|
161
+ f.html do
162
+ redirect_to ruby_cms_admin_content_block_path(block),
163
+ notice: t("ruby_cms.admin.content_blocks.updated")
164
+ end
165
+ f.json { head :no_content }
166
+ end
167
+ end
168
+
169
+ def respond_to_failure(block, view)
170
+ respond_to do |f|
171
+ f.html { render view, status: :unprocessable_content }
172
+ f.json do
173
+ render json: { errors: block.errors.full_messages },
174
+ status: :unprocessable_content
175
+ end
176
+ end
177
+ end
178
+
179
+ def respond_with_block(block)
180
+ respond_to do |f|
181
+ f.html
182
+ f.json { render json: content_block_editor_json(block) }
183
+ end
184
+ end
185
+
186
+ def redirect_with_notice(action)
187
+ redirect_to ruby_cms_admin_content_blocks_path,
188
+ notice: t("ruby_cms.admin.content_blocks.#{action}")
189
+ end
190
+
191
+ def set_content_block
192
+ @content_block = ::ContentBlock.find(params[:id])
193
+ end
194
+
195
+ def load_blocks_by_locale_for_edit
196
+ blocks = ::ContentBlock.where(key: @content_block.key).index_by(&:locale)
197
+ I18n.available_locales.each_with_object({}) do |loc, hash|
198
+ loc_s = loc.to_s
199
+ hash[loc_s] = blocks[loc_s] || ::ContentBlock.new(
200
+ key: @content_block.key, locale: loc_s,
201
+ content_type: @content_block.content_type
202
+ )
203
+ end
204
+ end
205
+
206
+ def load_blocks_by_locale_for_show
207
+ ::ContentBlock.where(key: @content_block.key).index_by {|b| b.locale.to_s }
208
+ end
209
+
210
+ def new_block_params
211
+ return {} unless params[:content_block].kind_of?(ActionController::Parameters)
212
+
213
+ params[:content_block].permit(:key, :locale).to_h
214
+ end
215
+
216
+ def content_block_params
217
+ params.expect(content_block_param_root => [*content_block_permitted_params])
218
+ end
219
+
220
+ def content_block_param_root
221
+ if params.key?(:content_block)
222
+ :content_block
223
+ else
224
+ params.key?(:ruby_cms_content_block) ? :ruby_cms_content_block : :content_block
225
+ end
226
+ end
227
+
228
+ def update_locale_blocks(locales_params, keys, shared_content_type, shared_published)
229
+ locales_params.each.with_object([]) do |(locale_s, attrs), errors|
230
+ next if attrs.blank?
231
+
232
+ block = update_locale_block(locale_s, attrs, keys, shared_content_type, shared_published)
233
+ next if block.save
234
+
235
+ errors.concat(block.errors.full_messages.map {|m| "#{locale_s}: #{m}" })
236
+ end
237
+ end
238
+
239
+ def update_locale_block(locale_s, attrs, keys, shared_content_type, shared_published)
240
+ block = ::ContentBlock.find_or_initialize_by(key: @content_block.key, locale: locale_s)
241
+ block.record_update_by(current_user_cms)
242
+ attrs_permitted = attrs.permit(keys).to_h
243
+ attrs_permitted.delete(:published)
244
+ block.assign_attributes(
245
+ attrs_permitted.merge(
246
+ key: @content_block.key,
247
+ locale: locale_s,
248
+ content_type: shared_content_type,
249
+ published: shared_published
250
+ )
251
+ )
252
+ block
253
+ end
254
+
255
+ def permitted_locale_params
256
+ params.expect(content_block: [:content_type, :published, { locales: {} }])
257
+ end
258
+
259
+ def unified_locale_params?
260
+ params.dig(:content_block, :locales).present?
261
+ end
262
+
263
+ def bulk_set_published(ids, published:)
264
+ updated_by_id = current_user_cms&.id
265
+ # Expand to all locale variants of each selected block's key
266
+ block_ids = ::ContentBlock.where(id: ids).flat_map do |b|
267
+ ::ContentBlock.where(key: b.key).pluck(:id)
268
+ end.uniq
269
+ block_ids.reduce(0) do |sum, id|
270
+ block = ::ContentBlock.find(id)
271
+ block.updated_by_id = updated_by_id
272
+ sum + (block.update(published:) ? 1 : 0)
273
+ end
274
+ end
275
+
276
+ def content_block_editor_json(block)
277
+ {
278
+ id: block.id,
279
+ key: block.key,
280
+ title: block.title,
281
+ content: block.content.to_s,
282
+ content_type: block.content_type,
283
+ published: block.published?,
284
+ rich_content_html: rich_content_body_html(block)
285
+ }
286
+ end
287
+
288
+ def content_blocks_collection
289
+ apply_search_filter(apply_locale_filter(::ContentBlock.alphabetically.preloaded))
290
+ end
291
+
292
+ def apply_locale_filter(collection)
293
+ return collection.for_locale(params[:locale]) if params[:locale].present?
294
+ return collection if params[:search].present?
295
+
296
+ collection.for_current_locale
297
+ end
298
+
299
+ def apply_search_filter(collection)
300
+ search_term = params[:q] || params[:search]
301
+ search_term.present? ? collection.search_by_term(search_term) : collection
302
+ end
303
+
304
+ def grouped_by_key_collection(collection)
305
+ RubyCms::ContentBlocksGrouping.group_by_key(collection)
306
+ end
307
+
308
+ def serialize_content_blocks(scope)
309
+ scope.map do |block|
310
+ {
311
+ id: block.id,
312
+ key: block.key,
313
+ locale: block.locale,
314
+ title: block.title,
315
+ content: block.content.to_s,
316
+ content_type: block.content_type.to_s,
317
+ published: block.published?,
318
+ rich_content: rich_content_for_serialization(block).to_s,
319
+ updated_at: block.updated_at.strftime("%B %d, %Y at %I:%M %p")
320
+ }
321
+ end
322
+ end
323
+
324
+ # For rich_text blocks: return HTML only. Trix requires HTML in the modal (e.g. <p>...</p>), not plain text.
325
+ def rich_content_for_serialization(block)
326
+ return "" unless block.respond_to?(:rich_content)
327
+ return "" unless block.content_type.to_s == "rich_text"
328
+
329
+ html = rich_content_body_html(block).to_s.strip
330
+ return ensure_rich_content_html(html) if html.present?
331
+
332
+ # Fallback: content column (seeded or synced plain text)
333
+ return "<p>#{ERB::Util.html_escape(block.content)}</p>" if block.content.present?
334
+
335
+ # Fallback: plain text from Action Text when body HTML is blank
336
+ fallback_text = rich_content_plain_text(block)
337
+ return "<p>#{ERB::Util.html_escape(fallback_text)}</p>" if fallback_text.present?
338
+
339
+ ""
340
+ end
341
+
342
+ def rich_content_plain_text(block)
343
+ return "" unless block.rich_content.respond_to?(:to_plain_text)
344
+
345
+ block.rich_content.to_plain_text.to_s.strip
346
+ end
347
+
348
+ # Trix expects HTML. If we have plain text (no tags), wrap in <p>.
349
+ def ensure_rich_content_html(str)
350
+ return "" if str.blank?
351
+ return str if str.strip.start_with?("<")
352
+
353
+ "<p>#{ERB::Util.html_escape(str)}</p>"
354
+ end
355
+
356
+ # Body-only HTML for API/preview (no Action Text layout wrapper or comments).
357
+ def rich_content_body_html(block)
358
+ return "" unless block.respond_to?(:rich_content)
359
+ return "" unless block.rich_content.respond_to?(:body) && block.rich_content.body.present?
360
+
361
+ b = block.rich_content.body
362
+ html = b.respond_to?(:to_html) ? b.to_html : b.to_s
363
+ html.to_s.strip.presence || ""
364
+ end
365
+
366
+ def audit_visual_editor_edit(changes)
367
+ Rails.application.config.ruby_cms.audit_editor_edit&.call(
368
+ @content_block.id,
369
+ current_user_cms&.id, changes.to_h
370
+ )
371
+ end
372
+
373
+ def content_block_permitted_params
374
+ %i[key locale title content content_type published].tap do |arr|
375
+ arr << :rich_content if action_text_available?
376
+ arr << :image if active_storage_available?
377
+ end
378
+ end
379
+
380
+ def action_text_available?
381
+ ::ContentBlock.respond_to?(:action_text_available?) && ::ContentBlock.action_text_available?
382
+ end
383
+
384
+ def active_storage_available?
385
+ ::ContentBlock.respond_to?(:active_storage_available?) &&
386
+ ::ContentBlock.active_storage_available?
387
+ end
388
+ end
389
+ end
390
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCms
4
+ module Admin
5
+ class DashboardController < BaseController
6
+ def index
7
+ assign_counts
8
+ assign_recent_activity
9
+ end
10
+
11
+ private
12
+
13
+ def assign_counts
14
+ @content_blocks_count = ::ContentBlock.count
15
+ @content_blocks_published_count = ::ContentBlock.published.count
16
+ @permissions_count = RubyCms::Permission.count
17
+ @user_permissions_count = RubyCms::UserPermission.count
18
+ @users_count = safe_user_count
19
+ @visitor_errors_count = RubyCms::VisitorError.count
20
+ @unresolved_errors_count = RubyCms::VisitorError.unresolved.count
21
+ end
22
+
23
+ def assign_recent_activity
24
+ @recent_errors = RubyCms::VisitorError.order(created_at: :desc).limit(recent_errors_limit)
25
+ @recent_content_blocks =
26
+ ::ContentBlock.order(updated_at: :desc).limit(recent_content_blocks_limit)
27
+ end
28
+
29
+ def safe_user_count
30
+ user_class.count
31
+ rescue StandardError
32
+ 0
33
+ end
34
+
35
+ def user_class
36
+ Object.const_get(
37
+ Rails.application.config.ruby_cms.user_class_name.presence || "User"
38
+ )
39
+ end
40
+
41
+ def recent_errors_limit
42
+ RubyCms::Settings.get(:dashboard_recent_errors_limit, default: 5).to_i
43
+ end
44
+
45
+ def recent_content_blocks_limit
46
+ RubyCms::Settings.get(:dashboard_recent_content_blocks_limit, default: 5).to_i
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCms
4
+ module Admin
5
+ class LocaleController < BaseController
6
+ skip_before_action :require_cms_access, only: [:update]
7
+ before_action :ensure_authenticated, only: [:update]
8
+
9
+ def update
10
+ locale = params[:locale].to_sym
11
+ if I18n.available_locales.include?(locale)
12
+ session[:ruby_cms_locale] = locale
13
+ I18n.locale = locale
14
+ end
15
+
16
+ redirect_back_or_to(ruby_cms_admin_root_path)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCms
4
+ module Admin
5
+ class PermissionsController < BaseController
6
+ include RubyCms::AdminPagination
7
+ include RubyCms::AdminTurboTable
8
+
9
+ paginates per_page: -> { RubyCms::Preference.get(:permissions_per_page, default: 50) },
10
+ turbo_frame: "admin_table_content"
11
+
12
+ before_action { require_permission!(:manage_permissions) }
13
+
14
+ def index
15
+ collection = RubyCms::Permission.order(:key)
16
+
17
+ if params[:q].present?
18
+ search_term = "%#{params[:q].downcase}%"
19
+ collection = collection.where("LOWER(key) LIKE ? OR LOWER(name) LIKE ?", search_term,
20
+ search_term)
21
+ end
22
+
23
+ @permissions = paginate_collection(collection)
24
+ # Ensure @permissions is always an iterable collection, never nil
25
+ @index ||= RubyCms::Permission.none
26
+ end
27
+
28
+ def create
29
+ @permission = RubyCms::Permission.new(permission_params)
30
+ if @permission.save
31
+ redirect_to ruby_cms_admin_permissions_path,
32
+ notice: t("ruby_cms.admin.permissions.created")
33
+ else
34
+ @permissions = RubyCms::Permission.order(:key)
35
+ flash.now[:alert] =
36
+ "Could not create permission: #{@permission.errors.full_messages.to_sentence}"
37
+ render :index, status: :unprocessable_content
38
+ end
39
+ end
40
+
41
+ def destroy
42
+ @permission = RubyCms::Permission.find(params[:id])
43
+ @permission.destroy
44
+ redirect_to ruby_cms_admin_permissions_path,
45
+ notice: t("ruby_cms.admin.permissions.deleted")
46
+ end
47
+
48
+ def bulk_delete
49
+ ids = Array(params[:item_ids]).filter_map(&:to_i)
50
+ permissions = RubyCms::Permission.where(id: ids)
51
+ count = permissions.count
52
+ permissions.destroy_all
53
+ turbo_redirect_to ruby_cms_admin_permissions_path,
54
+ notice: "#{count} permission(s) #{
55
+ t('ruby_cms.admin.permissions.deleted')
56
+ }."
57
+ end
58
+
59
+ private
60
+
61
+ def permission_params
62
+ params.expect(permission: %i[key name])
63
+ end
64
+ end
65
+ end
66
+ end