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,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
|