ruby_cms 0.1.9 → 0.2.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/README.md +440 -58
  4. data/app/controllers/ruby_cms/admin/base_controller.rb +33 -12
  5. data/app/controllers/ruby_cms/admin/content_block_versions_controller.rb +62 -0
  6. data/app/controllers/ruby_cms/admin/dashboard_controller.rb +40 -0
  7. data/app/helpers/ruby_cms/admin/dashboard_helper.rb +20 -0
  8. data/app/javascript/controllers/ruby_cms/content_block_history_controller.js +91 -0
  9. data/app/javascript/controllers/ruby_cms/index.js +4 -0
  10. data/app/javascript/controllers/ruby_cms/visual_editor_controller.js +33 -29
  11. data/app/models/concerns/content_block/versionable.rb +80 -0
  12. data/app/models/content_block.rb +1 -0
  13. data/app/models/content_block_version.rb +34 -0
  14. data/app/views/admin/content_block_versions/index.html.erb +52 -0
  15. data/app/views/admin/content_block_versions/show.html.erb +37 -0
  16. data/app/views/ruby_cms/admin/content_block_versions/index.html.erb +52 -0
  17. data/app/views/ruby_cms/admin/content_block_versions/show.html.erb +37 -0
  18. data/app/views/ruby_cms/admin/content_blocks/show.html.erb +12 -0
  19. data/app/views/ruby_cms/admin/dashboard/blocks/_analytics_overview.html.erb +53 -0
  20. data/app/views/ruby_cms/admin/dashboard/blocks/_content_blocks_stats.html.erb +17 -0
  21. data/app/views/ruby_cms/admin/dashboard/blocks/_permissions_stats.html.erb +17 -0
  22. data/app/views/ruby_cms/admin/dashboard/blocks/_quick_actions.html.erb +62 -0
  23. data/app/views/ruby_cms/admin/dashboard/blocks/_recent_errors.html.erb +39 -0
  24. data/app/views/ruby_cms/admin/dashboard/blocks/_users_stats.html.erb +17 -0
  25. data/app/views/ruby_cms/admin/dashboard/blocks/_visitor_errors_stats.html.erb +24 -0
  26. data/app/views/ruby_cms/admin/dashboard/index.html.erb +22 -180
  27. data/config/routes.rb +8 -0
  28. data/db/migrate/20260328000001_create_content_block_versions.rb +22 -0
  29. data/lib/generators/ruby_cms/admin_page_generator.rb +126 -0
  30. data/lib/generators/ruby_cms/templates/admin_page/controller.rb.tt +8 -0
  31. data/lib/generators/ruby_cms/templates/admin_page/index.html.erb.tt +11 -0
  32. data/lib/ruby_cms/dashboard_blocks.rb +91 -0
  33. data/lib/ruby_cms/engine/admin_permissions.rb +69 -0
  34. data/lib/ruby_cms/engine/content_blocks_tasks.rb +66 -0
  35. data/lib/ruby_cms/engine/css.rb +14 -0
  36. data/lib/ruby_cms/engine/dashboard_registration.rb +66 -0
  37. data/lib/ruby_cms/engine/navigation_registration.rb +80 -0
  38. data/lib/ruby_cms/engine.rb +23 -278
  39. data/lib/ruby_cms/icons.rb +118 -0
  40. data/lib/ruby_cms/version.rb +1 -1
  41. data/lib/ruby_cms.rb +36 -10
  42. metadata +28 -1
@@ -2,9 +2,21 @@
2
2
 
3
3
  require_relative "settings_registry"
4
4
  require_relative "settings"
5
+ require_relative "dashboard_blocks"
6
+ require_relative "engine/css"
7
+ require_relative "engine/dashboard_registration"
8
+ require_relative "engine/navigation_registration"
9
+ require_relative "engine/admin_permissions"
10
+ require_relative "engine/content_blocks_tasks"
5
11
 
6
12
  module RubyCms
7
13
  class Engine < ::Rails::Engine
14
+ extend RubyCms::EngineCss
15
+ extend RubyCms::EngineDashboardRegistration
16
+ extend RubyCms::EngineNavigationRegistration
17
+ extend RubyCms::EngineAdminPermissions
18
+ extend RubyCms::EngineContentBlocksTasks
19
+
8
20
  # Do not isolate namespace so we can use /admin and explicit table names.
9
21
  # Engine models use unprefixed table names: content_blocks, preferences, permissions, user_permissions, visitor_errors.
10
22
 
@@ -69,6 +81,7 @@ module RubyCms
69
81
  helper RubyCms::BulkActionTableHelper
70
82
  helper RubyCms::Admin::BulkActionTableHelper
71
83
  helper RubyCms::Admin::AdminPageHelper
84
+ helper RubyCms::Admin::DashboardHelper
72
85
  end
73
86
  end
74
87
 
@@ -96,6 +109,16 @@ module RubyCms
96
109
  end
97
110
  end
98
111
 
112
+ initializer "ruby_cms.dashboard_blocks" do
113
+ RubyCms::Engine.register_default_dashboard_blocks
114
+ end
115
+
116
+ initializer "ruby_cms.versionable" do
117
+ Rails.application.config.to_prepare do
118
+ ContentBlock.include(ContentBlock::Versionable) unless ContentBlock <= ContentBlock::Versionable
119
+ end
120
+ end
121
+
99
122
  initializer "ruby_cms.settings_import", after: :load_config_initializers do
100
123
  RubyCms::Settings.import_initializer_values!
101
124
  end
@@ -108,159 +131,6 @@ module RubyCms
108
131
  Rails.logger.warn("[RubyCMS] Permission.ensure_defaults! skipped: #{e.message}")
109
132
  end
110
133
 
111
- def self.register_main_nav_items
112
- RubyCms.nav_register(
113
- key: :dashboard,
114
- label: "Dashboard",
115
- path: lambda(&:ruby_cms_admin_root_path),
116
- icon: dashboard_icon_path,
117
- section: RubyCms::NAV_SECTION_MAIN,
118
- permission: :manage_admin,
119
- order: 1
120
- )
121
- RubyCms.nav_register(
122
- key: :visual_editor,
123
- label: "Visual editor",
124
- path: lambda(&:ruby_cms_admin_visual_editor_path),
125
- icon: visual_editor_icon_path,
126
- section: RubyCms::NAV_SECTION_MAIN,
127
- permission: :manage_content_blocks,
128
- order: 2
129
- )
130
- RubyCms.nav_register(
131
- key: :content_blocks,
132
- label: "Content blocks",
133
- path: lambda(&:ruby_cms_admin_content_blocks_path),
134
- icon: content_blocks_icon_path,
135
- section: RubyCms::NAV_SECTION_MAIN,
136
- permission: :manage_content_blocks,
137
- order: 3
138
- )
139
- end
140
-
141
- def self.dashboard_icon_path
142
- # Heroicons HomeIcon (outline)
143
- '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
144
- 'd="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3' \
145
- 'm-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>'
146
- end
147
-
148
- def self.content_blocks_icon_path
149
- # Heroicons DocumentDuplicateIcon (outline)
150
- '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
151
- 'd="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414' \
152
- 'a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 ' \
153
- '0 002-2v-2"></path>'
154
- end
155
-
156
- def self.visual_editor_icon_path
157
- # Heroicons PencilSquareIcon (outline)
158
- '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
159
- 'd="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 ' \
160
- '0 012.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>'
161
- end
162
-
163
- def self.register_settings_nav_items
164
- RubyCms.nav_register(
165
- key: :analytics,
166
- label: "Analytics",
167
- path: lambda(&:ruby_cms_admin_analytics_path),
168
- section: RubyCms::NAV_SECTION_BOTTOM,
169
- icon: analytics_icon_path,
170
- permission: :manage_analytics,
171
- order: 1
172
- )
173
- RubyCms.nav_register(
174
- key: :permissions,
175
- label: "Permissions",
176
- path: lambda(&:ruby_cms_admin_permissions_path),
177
- section: RubyCms::NAV_SECTION_BOTTOM,
178
- icon: permissions_icon_path,
179
- permission: :manage_permissions,
180
- order: 2
181
- )
182
- RubyCms.nav_register(
183
- key: :visitor_errors,
184
- label: "Visitor errors",
185
- path: lambda(&:ruby_cms_admin_visitor_errors_path),
186
- section: RubyCms::NAV_SECTION_BOTTOM,
187
- icon: visitor_errors_icon_path,
188
- permission: :manage_visitor_errors,
189
- order: 3
190
- )
191
- RubyCms.nav_register(
192
- key: :users,
193
- label: "Users",
194
- path: lambda(&:ruby_cms_admin_users_path),
195
- section: RubyCms::NAV_SECTION_BOTTOM,
196
- icon: users_icon_path,
197
- permission: :manage_permissions,
198
- order: 4
199
- )
200
- RubyCms.nav_register(
201
- key: :settings,
202
- label: "Settings",
203
- path: lambda(&:ruby_cms_admin_settings_path),
204
- section: RubyCms::NAV_SECTION_BOTTOM,
205
- icon: settings_icon_path,
206
- permission: :manage_admin,
207
- order: 5
208
- )
209
- end
210
-
211
- def self.settings_icon_path
212
- # Heroicons Cog6ToothIcon (outline)
213
- '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
214
- 'd="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 ' \
215
- '1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 ' \
216
- '1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 ' \
217
- '6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 ' \
218
- '0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 ' \
219
- '1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 ' \
220
- '6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 ' \
221
- '1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 ' \
222
- '1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"></path>' \
223
- '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>'
224
- end
225
-
226
- def self.visitor_errors_icon_path
227
- # Heroicons ExclamationTriangleIcon (outline)
228
- '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
229
- 'd="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 ' \
230
- '4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>'
231
- end
232
-
233
- def self.permissions_icon_path
234
- # Heroicons ShieldCheckIcon (outline)
235
- '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
236
- 'd="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01' \
237
- '-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 ' \
238
- '9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>'
239
- end
240
-
241
- def self.users_icon_path
242
- # Heroicons UserGroupIcon (outline)
243
- '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
244
- 'd="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 ' \
245
- '00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path>'
246
- end
247
-
248
- def self.analytics_icon_path
249
- '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
250
- 'd="M3 3v18h18"></path>' \
251
- '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
252
- 'd="M7 13l3-3 3 2 4-5"></path>'
253
- end
254
-
255
- def self.compile_admin_css(dest_path)
256
- gem_root = begin
257
- root
258
- rescue StandardError
259
- Pathname.new(File.expand_path("../..", __dir__))
260
- end
261
- RubyCms::CssCompiler.compile(gem_root, dest_path)
262
- end
263
-
264
134
  config.paths.add "db/migrate", with: "db/migrate"
265
135
 
266
136
  rake_tasks do # rubocop:disable Metrics/BlockLength
@@ -372,131 +242,6 @@ module RubyCms
372
242
  end
373
243
  end
374
244
 
375
- def self.grant_admin_permissions_to_admin_users
376
- return unless defined?(::User) && User.column_names.include?("admin")
377
-
378
- permission_keys = RubyCms::Permission.all_keys
379
- permissions = RubyCms::Permission.where(key: permission_keys).index_by(&:key)
380
- User.where(admin: true).find_each do |u|
381
- permission_keys.each do |key|
382
- perm = permissions[key]
383
- next if perm.nil?
384
-
385
- RubyCms::UserPermission.find_or_create_by!(user: u, permission: perm)
386
- end
387
- end
388
- end
389
-
390
- def self.extract_email_from_args(args)
391
- args[:email] || ENV["email"] || ENV.fetch("EMAIL", nil)
392
- end
393
-
394
- def self.validate_email_present(email)
395
- return if email.present?
396
-
397
- warn "Usage: rails ruby_cms:grant_manage_admin " \
398
- "email=user@example.com"
399
- raise "Email is required"
400
- end
401
-
402
- def self.find_user_by_email(email)
403
- user_class = Rails.application.config.ruby_cms.user_class_name
404
- .constantize
405
- find_user_by_email_address(user_class, email) ||
406
- find_user_by_email_column(user_class, email)
407
- end
408
-
409
- def self.find_user_by_email_address(user_class, email)
410
- return unless user_class.column_names.include?("email_address")
411
-
412
- user_class.find_by(email_address: email)
413
- end
414
-
415
- def self.find_user_by_email_column(user_class, email)
416
- return unless user_class.column_names.include?("email")
417
-
418
- user_class.find_by(email:)
419
- end
420
-
421
- def self.validate_user_found(user, email)
422
- return if user
423
-
424
- warn "User not found: #{email}"
425
- raise "User not found: #{email}"
426
- end
427
-
428
- def self.grant_manage_admin_permission(user, email)
429
- RubyCms::Permission.ensure_defaults!
430
- RubyCms::Permission.all_keys.each do |key|
431
- perm = RubyCms::Permission.find_by(key:)
432
- next unless perm
433
-
434
- RubyCms::UserPermission.find_or_create_by!(user: user, permission: perm)
435
- end
436
- puts "Granted full admin permissions to #{email}" # rubocop:disable Rails/Output
437
- end
438
-
439
- def self.parse_locales_dir(locales_dir_arg)
440
- return nil unless locales_dir_arg.presence
441
-
442
- Pathname.new(locales_dir_arg)
443
- end
444
-
445
- def self.parse_import_options
446
- {
447
- create_missing: ENV["create_missing"] != "false",
448
- update_existing: ENV["update_existing"] != "false",
449
- published: ENV["published"] == "true"
450
- }
451
- end
452
-
453
- def self.display_export_summary(summary)
454
- if summary.empty?
455
- puts "No content blocks found to export." # rubocop:disable Rails/Output
456
- else
457
- puts "Exported content blocks to locale files:" # rubocop:disable Rails/Output
458
- summary.each do |locale, count|
459
- # rubocop:disable Rails/Output
460
- puts " #{locale}: #{count} block(s) updated " \
461
- "in config/locales/#{locale}.yml"
462
- # rubocop:enable Rails/Output
463
- end
464
- end
465
- end
466
-
467
- def self.display_import_summary(summary)
468
- $stdout.puts "Import summary:"
469
- $stdout.puts " Created: #{summary[:created]}"
470
- $stdout.puts " Updated: #{summary[:updated]}"
471
- $stdout.puts " Skipped: #{summary[:skipped]}"
472
- return unless summary[:errors].any?
473
-
474
- $stdout.puts " Errors:"
475
- summary[:errors].each {|e| $stdout.puts " - #{e}" }
476
- end
477
-
478
- def self.display_sync_summary(result, import_after)
479
- display_export_results(result[:export])
480
- display_import_results(result[:import], import_after) if import_after
481
- end
482
-
483
- def self.display_export_results(export_data)
484
- $stdout.puts "Sync complete!"
485
- $stdout.puts "\nExport summary:"
486
- export_data.each do |locale, count|
487
- $stdout.puts " #{locale}: #{count} block(s) updated"
488
- end
489
- end
490
-
491
- def self.display_import_results(import_data, import_after)
492
- return unless import_after && import_data.any?
493
-
494
- $stdout.puts "\nImport summary:"
495
- $stdout.puts " Created: #{import_data[:created]}"
496
- $stdout.puts " Updated: #{import_data[:updated]}"
497
- $stdout.puts " Skipped: #{import_data[:skipped]}"
498
- end
499
-
500
245
  initializer "ruby_cms.load_migrations" do |app|
501
246
  next unless app.config.respond_to?(:paths)
502
247
 
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCms
4
+ module Icons
5
+ # Named Heroicon SVG path fragments (outline style, 24x24 viewBox).
6
+ # Use symbol keys with nav_register / register_page: `icon: :home`
7
+ # Raw SVG strings are still accepted for custom icons.
8
+ REGISTRY = {
9
+ home: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
10
+ 'd="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3' \
11
+ 'm-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>',
12
+
13
+ pencil_square: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
14
+ 'd="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 ' \
15
+ '0 012.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>',
16
+
17
+ document_duplicate: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
18
+ 'd="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414' \
19
+ 'a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 ' \
20
+ '0 002-2v-2"></path>',
21
+
22
+ chart_bar: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
23
+ 'd="M3 3v18h18"></path>' \
24
+ '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
25
+ 'd="M7 13l3-3 3 2 4-5"></path>',
26
+
27
+ shield_check: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
28
+ 'd="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01' \
29
+ '-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 ' \
30
+ '9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>',
31
+
32
+ exclamation_triangle: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
33
+ 'd="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 ' \
34
+ '4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>',
35
+
36
+ user_group: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
37
+ 'd="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 ' \
38
+ '00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path>',
39
+
40
+ cog_6_tooth: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
41
+ 'd="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 ' \
42
+ '1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 ' \
43
+ '1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 ' \
44
+ '6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 ' \
45
+ '0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 ' \
46
+ '1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 ' \
47
+ '6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 ' \
48
+ '1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 ' \
49
+ '1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"></path>' \
50
+ '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>',
51
+
52
+ # Extra icons for host-app admin pages
53
+ archive_box: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
54
+ 'd="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"></path>',
55
+
56
+ folder: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
57
+ 'd="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>',
58
+
59
+ bell: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
60
+ 'd="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path>',
61
+
62
+ clock: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
63
+ 'd="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>',
64
+
65
+ tag: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
66
+ 'd="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>',
67
+
68
+ cube: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
69
+ 'd="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"></path>',
70
+
71
+ envelope: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
72
+ 'd="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>',
73
+
74
+ wrench: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
75
+ 'd="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>' \
76
+ '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>',
77
+
78
+ globe: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
79
+ 'd="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"></path>',
80
+
81
+ photograph: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
82
+ 'd="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>',
83
+
84
+ list_bullet: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
85
+ 'd="M4 6h16M4 10h16M4 14h16M4 18h16"></path>',
86
+
87
+ plus_circle: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
88
+ 'd="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"></path>',
89
+
90
+ trash: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
91
+ 'd="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"></path>',
92
+
93
+ eye: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
94
+ 'd="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>' \
95
+ '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
96
+ 'd="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>',
97
+
98
+ lock_closed: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
99
+ 'd="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>',
100
+
101
+ currency_dollar: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
102
+ 'd="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>'
103
+ }.freeze
104
+
105
+ def self.resolve(name_or_svg)
106
+ return name_or_svg if name_or_svg.kind_of?(String) && name_or_svg.include?("<")
107
+ return nil if name_or_svg.nil?
108
+
109
+ REGISTRY.fetch(name_or_svg.to_sym) do
110
+ raise ArgumentError, "Unknown icon: #{name_or_svg}. Available: #{REGISTRY.keys.join(', ')}"
111
+ end
112
+ end
113
+
114
+ def self.available
115
+ REGISTRY.keys
116
+ end
117
+ end
118
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyCms
4
- VERSION = "0.1.9"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/ruby_cms.rb CHANGED
@@ -4,6 +4,8 @@ require_relative "ruby_cms/version"
4
4
  require_relative "ruby_cms/css_compiler"
5
5
  require_relative "ruby_cms/settings_registry"
6
6
  require_relative "ruby_cms/settings"
7
+ require_relative "ruby_cms/icons"
8
+ require_relative "ruby_cms/dashboard_blocks"
7
9
  require_relative "ruby_cms/engine"
8
10
  require_relative "ruby_cms/app_integration"
9
11
  require_relative "ruby_cms/content_blocks_sync"
@@ -45,11 +47,6 @@ module RubyCms
45
47
  NAV_SECTION_MAIN = "main"
46
48
  NAV_SECTION_BOTTOM = "Settings"
47
49
 
48
- ANALYTICS_ICON_PATH = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
49
- 'd="M3 3v18h18"></path>' \
50
- '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
51
- 'd="M7 13l3-3 3 2 4-5"></path>'
52
-
53
50
  def self.configure
54
51
  yield(Rails.application.config.ruby_cms)
55
52
  end
@@ -64,8 +61,8 @@ module RubyCms
64
61
  # Options:
65
62
  # - key: required Symbol/String
66
63
  # - label: required String
67
- # - path: required String or callable(view_context) -> path
68
- # - icon: optional SVG path fragment
64
+ # - path: String, Symbol (route helper name, auto-wrapped via main_app), or callable(view_context) -> path
65
+ # - icon: Symbol (named icon from RubyCms::Icons) or raw SVG path fragment string
69
66
  # - section: "main" | "Settings" (or nil => main)
70
67
  # - order: optional Integer for sorting within section
71
68
  # - permission: optional permission key (e.g., :manage_analytics)
@@ -74,11 +71,13 @@ module RubyCms
74
71
  def self.nav_register(key:, label:, path:, icon: nil, section: nil, order: nil, permission: nil, default_visible: true, **options)
75
72
  normalized_key = key.to_sym
76
73
  normalized_section = section.presence || NAV_SECTION_MAIN
74
+ resolved_path = path.kind_of?(Symbol) ? ->(v) { v.main_app.send(path) } : path
75
+ resolved_icon = icon.nil? ? nil : RubyCms::Icons.resolve(icon)
77
76
  entry = {
78
77
  key: normalized_key,
79
78
  label: label.to_s,
80
- path: path,
81
- icon: icon,
79
+ path: resolved_path,
80
+ icon: resolved_icon,
82
81
  section: normalized_section,
83
82
  order: order,
84
83
  permission: permission&.to_s,
@@ -94,8 +93,35 @@ module RubyCms
94
93
  entry
95
94
  end
96
95
 
96
+ VALID_PAGE_SECTIONS = %i[main settings].freeze
97
+
98
+ # Unified API to register an admin page: nav item + permission key in one call.
99
+ # Accepts :main or :settings for section (resolved to NAV_SECTION_MAIN / NAV_SECTION_BOTTOM).
100
+ # Automatically registers the permission key if provided.
101
+ def self.register_page(
102
+ key:, label:, path:, icon: nil, section: :main, order: nil,
103
+ permission: nil, default_visible: true, **
104
+ )
105
+ raise ArgumentError, "register_page section must be :main or :settings, got #{section.inspect}" unless VALID_PAGE_SECTIONS.include?(section.to_s.to_sym)
106
+
107
+ register_permission_keys(permission) if permission.present?
108
+ resolved_section = section.to_s == "settings" ? NAV_SECTION_BOTTOM : NAV_SECTION_MAIN
109
+ nav_register(
110
+ key: key,
111
+ label: label,
112
+ path: path,
113
+ icon: icon,
114
+ section: resolved_section,
115
+ order: order,
116
+ permission: permission,
117
+ default_visible: default_visible,
118
+ **
119
+ )
120
+ end
121
+
97
122
  # Returns only entries that pass settings + permissions + conditional checks,
98
123
  # sorted by saved nav_order (Settings → Navigation drag-and-drop) when set, else by section + order.
124
+ # Fail-closed: returns empty array on error (never exposes unfiltered items).
99
125
  def self.visible_nav_registry(view_context: nil, user: nil)
100
126
  list = nav_registry
101
127
  .select {|item| nav_entry_visible?(item, view_context:, user:) }
@@ -103,7 +129,7 @@ module RubyCms
103
129
  localize_nav_labels(apply_nav_order(list))
104
130
  rescue StandardError => e
105
131
  Rails.logger.error("[RubyCMS] Error filtering navigation: #{e.message}") if defined?(Rails.logger)
106
- nav_registry
132
+ []
107
133
  end
108
134
 
109
135
  module Nav
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_cms
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.9
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Codebyjob
@@ -96,6 +96,7 @@ files:
96
96
  - app/controllers/concerns/ruby_cms/visitor_error_capture.rb
97
97
  - app/controllers/ruby_cms/admin/analytics_controller.rb
98
98
  - app/controllers/ruby_cms/admin/base_controller.rb
99
+ - app/controllers/ruby_cms/admin/content_block_versions_controller.rb
99
100
  - app/controllers/ruby_cms/admin/content_blocks_controller.rb
100
101
  - app/controllers/ruby_cms/admin/dashboard_controller.rb
101
102
  - app/controllers/ruby_cms/admin/locale_controller.rb
@@ -108,6 +109,7 @@ files:
108
109
  - app/controllers/ruby_cms/errors_controller.rb
109
110
  - app/helpers/ruby_cms/admin/admin_page_helper.rb
110
111
  - app/helpers/ruby_cms/admin/bulk_action_table_helper.rb
112
+ - app/helpers/ruby_cms/admin/dashboard_helper.rb
111
113
  - app/helpers/ruby_cms/application_helper.rb
112
114
  - app/helpers/ruby_cms/bulk_action_table_helper.rb
113
115
  - app/helpers/ruby_cms/content_blocks_helper.rb
@@ -115,6 +117,7 @@ files:
115
117
  - app/javascript/controllers/ruby_cms/auto_save_preference_controller.js
116
118
  - app/javascript/controllers/ruby_cms/bulk_action_table_controller.js
117
119
  - app/javascript/controllers/ruby_cms/clickable_row_controller.js
120
+ - app/javascript/controllers/ruby_cms/content_block_history_controller.js
118
121
  - app/javascript/controllers/ruby_cms/flash_messages_controller.js
119
122
  - app/javascript/controllers/ruby_cms/index.js
120
123
  - app/javascript/controllers/ruby_cms/locale_tabs_controller.js
@@ -125,7 +128,9 @@ files:
125
128
  - app/javascript/controllers/ruby_cms/visual_editor_controller.js
126
129
  - app/models/concerns/content_block/publishable.rb
127
130
  - app/models/concerns/content_block/searchable.rb
131
+ - app/models/concerns/content_block/versionable.rb
128
132
  - app/models/content_block.rb
133
+ - app/models/content_block_version.rb
129
134
  - app/models/ruby_cms/content_block.rb
130
135
  - app/models/ruby_cms/permission.rb
131
136
  - app/models/ruby_cms/permittable.rb
@@ -134,6 +139,8 @@ files:
134
139
  - app/models/ruby_cms/visitor_error.rb
135
140
  - app/services/ruby_cms/analytics/report.rb
136
141
  - app/services/ruby_cms/security_tracker.rb
142
+ - app/views/admin/content_block_versions/index.html.erb
143
+ - app/views/admin/content_block_versions/show.html.erb
137
144
  - app/views/layouts/ruby_cms/_admin_flash_messages.html.erb
138
145
  - app/views/layouts/ruby_cms/_admin_sidebar.html.erb
139
146
  - app/views/layouts/ruby_cms/admin.html.erb
@@ -153,11 +160,20 @@ files:
153
160
  - app/views/ruby_cms/admin/analytics/partials/_top_visitors.html.erb
154
161
  - app/views/ruby_cms/admin/analytics/partials/_utm_sources.html.erb
155
162
  - app/views/ruby_cms/admin/analytics/visitor_details.html.erb
163
+ - app/views/ruby_cms/admin/content_block_versions/index.html.erb
164
+ - app/views/ruby_cms/admin/content_block_versions/show.html.erb
156
165
  - app/views/ruby_cms/admin/content_blocks/_form.html.erb
157
166
  - app/views/ruby_cms/admin/content_blocks/_row.html.erb
158
167
  - app/views/ruby_cms/admin/content_blocks/index.html.erb
159
168
  - app/views/ruby_cms/admin/content_blocks/new.html.erb
160
169
  - app/views/ruby_cms/admin/content_blocks/show.html.erb
170
+ - app/views/ruby_cms/admin/dashboard/blocks/_analytics_overview.html.erb
171
+ - app/views/ruby_cms/admin/dashboard/blocks/_content_blocks_stats.html.erb
172
+ - app/views/ruby_cms/admin/dashboard/blocks/_permissions_stats.html.erb
173
+ - app/views/ruby_cms/admin/dashboard/blocks/_quick_actions.html.erb
174
+ - app/views/ruby_cms/admin/dashboard/blocks/_recent_errors.html.erb
175
+ - app/views/ruby_cms/admin/dashboard/blocks/_users_stats.html.erb
176
+ - app/views/ruby_cms/admin/dashboard/blocks/_visitor_errors_stats.html.erb
161
177
  - app/views/ruby_cms/admin/dashboard/index.html.erb
162
178
  - app/views/ruby_cms/admin/permissions/_row.html.erb
163
179
  - app/views/ruby_cms/admin/permissions/index.html.erb
@@ -187,10 +203,14 @@ files:
187
203
  - db/migrate/20260130000003_add_category_to_ruby_cms_preferences.rb
188
204
  - db/migrate/20260211000001_add_ruby_cms_analytics_fields_to_ahoy_events.rb
189
205
  - db/migrate/20260212000001_use_unprefixed_cms_tables.rb
206
+ - db/migrate/20260328000001_create_content_block_versions.rb
190
207
  - docs/assets/ruby_cms_logo.png
191
208
  - exe/ruby_cms
209
+ - lib/generators/ruby_cms/admin_page_generator.rb
192
210
  - lib/generators/ruby_cms/install_generator.rb
193
211
  - lib/generators/ruby_cms/templates/admin.html.erb
212
+ - lib/generators/ruby_cms/templates/admin_page/controller.rb.tt
213
+ - lib/generators/ruby_cms/templates/admin_page/index.html.erb.tt
194
214
  - lib/generators/ruby_cms/templates/ruby_cms.rb
195
215
  - lib/ruby_cms.rb
196
216
  - lib/ruby_cms/app_integration.rb
@@ -198,7 +218,14 @@ files:
198
218
  - lib/ruby_cms/content_blocks_grouping.rb
199
219
  - lib/ruby_cms/content_blocks_sync.rb
200
220
  - lib/ruby_cms/css_compiler.rb
221
+ - lib/ruby_cms/dashboard_blocks.rb
201
222
  - lib/ruby_cms/engine.rb
223
+ - lib/ruby_cms/engine/admin_permissions.rb
224
+ - lib/ruby_cms/engine/content_blocks_tasks.rb
225
+ - lib/ruby_cms/engine/css.rb
226
+ - lib/ruby_cms/engine/dashboard_registration.rb
227
+ - lib/ruby_cms/engine/navigation_registration.rb
228
+ - lib/ruby_cms/icons.rb
202
229
  - lib/ruby_cms/settings.rb
203
230
  - lib/ruby_cms/settings_registry.rb
204
231
  - lib/ruby_cms/version.rb