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
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCms
4
+ module Generators
5
+ class AdminPageGenerator < ::Rails::Generators::NamedBase
6
+ source_root File.expand_path("templates/admin_page", __dir__)
7
+
8
+ class_option :permission, type: :string, default: nil,
9
+ desc: "Permission key (default: manage_<name>)"
10
+ class_option :icon, type: :string, default: "folder",
11
+ desc: "Named icon from RubyCms::Icons (e.g. archive_box, bell, clock). " \
12
+ "Run RubyCms::Icons.available for the full list."
13
+ class_option :section, type: :string, default: "main",
14
+ desc: "Navigation section: main or settings"
15
+ class_option :order, type: :numeric, default: 10,
16
+ desc: "Sort order within the nav section"
17
+
18
+ def validate_options
19
+ raise ArgumentError, "--section must be 'main' or 'settings', got '#{section_name}'" unless %w[main settings].include?(section_name)
20
+
21
+ icon_sym = icon_name.to_sym
22
+ return if RubyCms::Icons::REGISTRY.key?(icon_sym)
23
+
24
+ say "Warning: icon '#{icon_name}' is not in RubyCms::Icons::REGISTRY. " \
25
+ "Available: #{RubyCms::Icons.available.join(', ')}", :yellow
26
+ end
27
+
28
+ def create_controller
29
+ template "controller.rb.tt",
30
+ File.join("app/controllers/admin", "#{file_name}_controller.rb")
31
+ end
32
+
33
+ def create_view
34
+ template "index.html.erb.tt",
35
+ File.join("app/views/admin", file_name, "index.html.erb")
36
+ end
37
+
38
+ def add_route
39
+ routes_path = Rails.root.join("config/routes.rb")
40
+ return unless routes_path.exist?
41
+
42
+ content = File.read(routes_path)
43
+ route_line = " resources :#{plural_name}, only: [:index], controller: \"admin/#{file_name}\""
44
+
45
+ if content.include?("resources :#{plural_name}") && content.include?("admin/#{file_name}")
46
+ say "Route already exists, skipping.", :yellow
47
+ return
48
+ end
49
+
50
+ if content.match?(/namespace\s+:admin\s+do/)
51
+ inject_into_file routes_path.to_s, after: /namespace\s+:admin\s+do\s*\n/ do
52
+ "#{route_line}\n"
53
+ end
54
+ else
55
+ route <<~RUBY
56
+ namespace :admin do
57
+ #{route_line}
58
+ end
59
+ RUBY
60
+ end
61
+ say "Route: added admin/#{plural_name}", :green
62
+ end
63
+
64
+ def add_page_registration
65
+ pages_file = Rails.root.join("config/initializers/ruby_cms_pages.rb")
66
+
67
+ registration = register_page_code
68
+
69
+ if pages_file.exist?
70
+ content = File.read(pages_file)
71
+ if content.include?("key: :#{file_name}")
72
+ say "Page registration already exists, skipping.", :yellow
73
+ return
74
+ end
75
+ append_to_file pages_file.to_s, "\n#{registration}\n"
76
+ else
77
+ create_file pages_file.to_s, "# frozen_string_literal: true\n\n#{registration}\n"
78
+ end
79
+ say "Registered page :#{file_name} in config/initializers/ruby_cms_pages.rb", :green
80
+ end
81
+
82
+ def show_next_steps
83
+ say "\nAdmin page '#{file_name}' created.", :green
84
+ say "Next: rails ruby_cms:seed_permissions", :cyan
85
+ end
86
+
87
+ private
88
+
89
+ def permission_key
90
+ options[:permission].presence || "manage_#{file_name}"
91
+ end
92
+
93
+ def icon_name
94
+ options[:icon]
95
+ end
96
+
97
+ def section_name
98
+ options[:section]
99
+ end
100
+
101
+ def order_value
102
+ options[:order]
103
+ end
104
+
105
+ def path_helper_name
106
+ "admin_#{plural_name}_path"
107
+ end
108
+
109
+ def register_page_code
110
+ <<~RUBY
111
+ Rails.application.config.to_prepare do
112
+ RubyCms.register_page(
113
+ key: :#{file_name},
114
+ label: "#{human_name}",
115
+ path: :#{path_helper_name},
116
+ icon: :#{icon_name},
117
+ section: :#{section_name},
118
+ permission: :#{permission_key},
119
+ order: #{order_value}
120
+ )
121
+ end
122
+ RUBY
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Admin::<%= class_name %>Controller < RubyCms::Admin::BaseController
4
+ cms_page :<%= file_name %>
5
+
6
+ def index
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ <%% content_for :title, "<%= human_name %>" %>
2
+
3
+ <div class="px-4 py-6 sm:px-6 lg:px-8">
4
+ <div class="sm:flex sm:items-center sm:justify-between">
5
+ <h1 class="text-2xl font-bold text-gray-900"><%= human_name %></h1>
6
+ </div>
7
+
8
+ <div class="mt-6">
9
+ <p class="text-gray-500">Start building your <%= human_name.downcase %> page here.</p>
10
+ </div>
11
+ </div>
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Loaded from lib/ruby_cms.rb and from lib/ruby_cms/engine.rb so dashboard API exists even when
4
+ # the host only requires "ruby_cms/engine" (without loading lib/ruby_cms.rb first).
5
+ module RubyCms
6
+ mattr_accessor :dashboard_registry
7
+ self.dashboard_registry = []
8
+
9
+ # Register a dashboard block (stats row or main row). Host apps can add blocks or replace defaults by key.
10
+ def self.dashboard_register(
11
+ key:, label:, section:, order:, partial: nil, render: nil, permission: nil,
12
+ enabled: true, default_visible: true, span: :single, data: nil
13
+ )
14
+ normalized_key = key.to_sym
15
+ normalized_section = section.to_sym
16
+ raise ArgumentError, "section must be :stats or :main" unless %i[stats main].include?(normalized_section)
17
+
18
+ raise ArgumentError, "partial or render is required" if partial.blank? && !render.respond_to?(:call)
19
+
20
+ entry = {
21
+ key: normalized_key,
22
+ label: label.to_s,
23
+ section: normalized_section,
24
+ order: order.to_i,
25
+ partial: partial,
26
+ render: render,
27
+ permission: permission&.to_sym,
28
+ enabled: enabled ? true : false,
29
+ default_visible: default_visible ? true : false,
30
+ span: span.to_sym == :double ? :double : :single,
31
+ data: data
32
+ }
33
+
34
+ self.dashboard_registry = dashboard_registry.reject {|e| e[:key] == normalized_key }
35
+ self.dashboard_registry += [entry]
36
+
37
+ register_dashboard_setting!(entry)
38
+ entry
39
+ end
40
+
41
+ def self.visible_dashboard_blocks(user: nil)
42
+ dashboard_registry
43
+ .select {|e| e[:enabled] }
44
+ .select {|e| dashboard_block_visible?(e, user:) }
45
+ .sort_by {|e| [e[:section] == :stats ? 0 : 1, e[:order], e[:label]] }
46
+ rescue StandardError => e
47
+ Rails.logger.error("[RubyCMS] Error filtering dashboard blocks: #{e.message}") if defined?(Rails.logger)
48
+ []
49
+ end
50
+
51
+ class << self
52
+ private
53
+
54
+ def register_dashboard_setting!(entry)
55
+ RubyCms::SettingsRegistry.register(
56
+ key: :"dashboard_show_#{entry[:key]}",
57
+ type: :boolean,
58
+ default: entry.fetch(:default_visible, true),
59
+ category: :dashboard,
60
+ description: "Show #{entry[:label]} on the admin dashboard"
61
+ )
62
+ rescue StandardError => e
63
+ Rails.logger.warn("[RubyCMS] Failed to register dashboard setting for #{entry[:key]}: #{e.message}") if defined?(Rails.logger)
64
+ end
65
+
66
+ def dashboard_block_visible?(entry, user:)
67
+ return false unless setting_enabled_for_dashboard_block?(entry)
68
+ return false unless permission_allows_dashboard_block?(entry, user:)
69
+
70
+ true
71
+ end
72
+
73
+ def setting_enabled_for_dashboard_block?(entry)
74
+ pref_key = :"dashboard_show_#{entry[:key]}"
75
+ RubyCms::Settings.get(pref_key, default: entry.fetch(:default_visible, true))
76
+ rescue StandardError
77
+ entry.fetch(:default_visible, true)
78
+ end
79
+
80
+ def permission_allows_dashboard_block?(entry, user:)
81
+ permission_key = entry[:permission]
82
+ return true if permission_key.blank?
83
+ return false if user.nil?
84
+ return true unless user.respond_to?(:can?)
85
+
86
+ user.can?(permission_key)
87
+ rescue StandardError
88
+ false
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCms
4
+ module EngineAdminPermissions
5
+ def grant_admin_permissions_to_admin_users
6
+ return unless defined?(::User) && User.column_names.include?("admin")
7
+
8
+ permission_keys = RubyCms::Permission.all_keys
9
+ permissions = RubyCms::Permission.where(key: permission_keys).index_by(&:key)
10
+ User.where(admin: true).find_each do |u|
11
+ permission_keys.each do |key|
12
+ perm = permissions[key]
13
+ next if perm.nil?
14
+
15
+ RubyCms::UserPermission.find_or_create_by!(user: u, permission: perm)
16
+ end
17
+ end
18
+ end
19
+
20
+ def extract_email_from_args(args)
21
+ args[:email] || ENV["email"] || ENV.fetch("EMAIL", nil)
22
+ end
23
+
24
+ def validate_email_present(email)
25
+ return if email.present?
26
+
27
+ warn "Usage: rails ruby_cms:grant_manage_admin " \
28
+ "email=user@example.com"
29
+ raise "Email is required"
30
+ end
31
+
32
+ def find_user_by_email(email)
33
+ user_class = Rails.application.config.ruby_cms.user_class_name
34
+ .constantize
35
+ find_user_by_email_address(user_class, email) ||
36
+ find_user_by_email_column(user_class, email)
37
+ end
38
+
39
+ def find_user_by_email_address(user_class, email)
40
+ return unless user_class.column_names.include?("email_address")
41
+
42
+ user_class.find_by(email_address: email)
43
+ end
44
+
45
+ def find_user_by_email_column(user_class, email)
46
+ return unless user_class.column_names.include?("email")
47
+
48
+ user_class.find_by(email:)
49
+ end
50
+
51
+ def validate_user_found(user, email)
52
+ return if user
53
+
54
+ warn "User not found: #{email}"
55
+ raise "User not found: #{email}"
56
+ end
57
+
58
+ def grant_manage_admin_permission(user, email)
59
+ RubyCms::Permission.ensure_defaults!
60
+ RubyCms::Permission.all_keys.each do |key|
61
+ perm = RubyCms::Permission.find_by(key:)
62
+ next unless perm
63
+
64
+ RubyCms::UserPermission.find_or_create_by!(user: user, permission: perm)
65
+ end
66
+ puts "Granted full admin permissions to #{email}" # rubocop:disable Rails/Output
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCms
4
+ module EngineContentBlocksTasks
5
+ def parse_locales_dir(locales_dir_arg)
6
+ return nil unless locales_dir_arg.presence
7
+
8
+ Pathname.new(locales_dir_arg)
9
+ end
10
+
11
+ def parse_import_options
12
+ {
13
+ create_missing: ENV["create_missing"] != "false",
14
+ update_existing: ENV["update_existing"] != "false",
15
+ published: ENV["published"] == "true"
16
+ }
17
+ end
18
+
19
+ def display_export_summary(summary)
20
+ if summary.empty?
21
+ puts "No content blocks found to export." # rubocop:disable Rails/Output
22
+ else
23
+ puts "Exported content blocks to locale files:" # rubocop:disable Rails/Output
24
+ summary.each do |locale, count|
25
+ # rubocop:disable Rails/Output
26
+ puts " #{locale}: #{count} block(s) updated " \
27
+ "in config/locales/#{locale}.yml"
28
+ # rubocop:enable Rails/Output
29
+ end
30
+ end
31
+ end
32
+
33
+ def display_import_summary(summary)
34
+ $stdout.puts "Import summary:"
35
+ $stdout.puts " Created: #{summary[:created]}"
36
+ $stdout.puts " Updated: #{summary[:updated]}"
37
+ $stdout.puts " Skipped: #{summary[:skipped]}"
38
+ return unless summary[:errors].any?
39
+
40
+ $stdout.puts " Errors:"
41
+ summary[:errors].each {|e| $stdout.puts " - #{e}" }
42
+ end
43
+
44
+ def display_sync_summary(result, import_after)
45
+ display_export_results(result[:export])
46
+ display_import_results(result[:import], import_after) if import_after
47
+ end
48
+
49
+ def display_export_results(export_data)
50
+ $stdout.puts "Sync complete!"
51
+ $stdout.puts "\nExport summary:"
52
+ export_data.each do |locale, count|
53
+ $stdout.puts " #{locale}: #{count} block(s) updated"
54
+ end
55
+ end
56
+
57
+ def display_import_results(import_data, import_after)
58
+ return unless import_after && import_data.any?
59
+
60
+ $stdout.puts "\nImport summary:"
61
+ $stdout.puts " Created: #{import_data[:created]}"
62
+ $stdout.puts " Updated: #{import_data[:updated]}"
63
+ $stdout.puts " Skipped: #{import_data[:skipped]}"
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCms
4
+ module EngineCss
5
+ def compile_admin_css(dest_path)
6
+ gem_root = begin
7
+ root
8
+ rescue StandardError
9
+ Pathname.new(File.expand_path("../..", __dir__))
10
+ end
11
+ RubyCms::CssCompiler.compile(gem_root, dest_path)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCms
4
+ module EngineDashboardRegistration
5
+ def register_default_dashboard_blocks
6
+ RubyCms.dashboard_register(
7
+ key: :content_blocks_stats,
8
+ label: "Content blocks",
9
+ section: :stats,
10
+ order: 1,
11
+ partial: "ruby_cms/admin/dashboard/blocks/content_blocks_stats",
12
+ permission: :manage_content_blocks
13
+ )
14
+ RubyCms.dashboard_register(
15
+ key: :users_stats,
16
+ label: "Users",
17
+ section: :stats,
18
+ order: 2,
19
+ partial: "ruby_cms/admin/dashboard/blocks/users_stats",
20
+ permission: :manage_permissions
21
+ )
22
+ RubyCms.dashboard_register(
23
+ key: :permissions_stats,
24
+ label: "Permissions",
25
+ section: :stats,
26
+ order: 3,
27
+ partial: "ruby_cms/admin/dashboard/blocks/permissions_stats",
28
+ permission: :manage_permissions
29
+ )
30
+ RubyCms.dashboard_register(
31
+ key: :visitor_errors_stats,
32
+ label: "Visitor errors",
33
+ section: :stats,
34
+ order: 4,
35
+ partial: "ruby_cms/admin/dashboard/blocks/visitor_errors_stats",
36
+ permission: :manage_visitor_errors
37
+ )
38
+ RubyCms.dashboard_register(
39
+ key: :quick_actions,
40
+ label: "Quick actions",
41
+ section: :main,
42
+ order: 1,
43
+ span: :single,
44
+ partial: "ruby_cms/admin/dashboard/blocks/quick_actions"
45
+ )
46
+ RubyCms.dashboard_register(
47
+ key: :recent_errors,
48
+ label: "Recent errors",
49
+ section: :main,
50
+ order: 2,
51
+ span: :single,
52
+ partial: "ruby_cms/admin/dashboard/blocks/recent_errors",
53
+ permission: :manage_visitor_errors
54
+ )
55
+ RubyCms.dashboard_register(
56
+ key: :analytics_overview,
57
+ label: "Analytics",
58
+ section: :main,
59
+ order: 3,
60
+ span: :single,
61
+ partial: "ruby_cms/admin/dashboard/blocks/analytics_overview",
62
+ permission: :manage_analytics
63
+ )
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCms
4
+ module EngineNavigationRegistration
5
+ def register_main_nav_items
6
+ RubyCms.register_page(
7
+ key: :dashboard,
8
+ label: "Dashboard",
9
+ path: lambda(&:ruby_cms_admin_root_path),
10
+ icon: :home,
11
+ permission: :manage_admin,
12
+ order: 1
13
+ )
14
+ RubyCms.register_page(
15
+ key: :visual_editor,
16
+ label: "Visual editor",
17
+ path: lambda(&:ruby_cms_admin_visual_editor_path),
18
+ icon: :pencil_square,
19
+ permission: :manage_content_blocks,
20
+ order: 2
21
+ )
22
+ RubyCms.register_page(
23
+ key: :content_blocks,
24
+ label: "Content blocks",
25
+ path: lambda(&:ruby_cms_admin_content_blocks_path),
26
+ icon: :document_duplicate,
27
+ permission: :manage_content_blocks,
28
+ order: 3
29
+ )
30
+ end
31
+
32
+ def register_settings_nav_items
33
+ RubyCms.register_page(
34
+ key: :analytics,
35
+ label: "Analytics",
36
+ path: lambda(&:ruby_cms_admin_analytics_path),
37
+ icon: :chart_bar,
38
+ section: :settings,
39
+ permission: :manage_analytics,
40
+ order: 1
41
+ )
42
+ RubyCms.register_page(
43
+ key: :permissions,
44
+ label: "Permissions",
45
+ path: lambda(&:ruby_cms_admin_permissions_path),
46
+ icon: :shield_check,
47
+ section: :settings,
48
+ permission: :manage_permissions,
49
+ order: 2
50
+ )
51
+ RubyCms.register_page(
52
+ key: :visitor_errors,
53
+ label: "Visitor errors",
54
+ path: lambda(&:ruby_cms_admin_visitor_errors_path),
55
+ icon: :exclamation_triangle,
56
+ section: :settings,
57
+ permission: :manage_visitor_errors,
58
+ order: 3
59
+ )
60
+ RubyCms.register_page(
61
+ key: :users,
62
+ label: "Users",
63
+ path: lambda(&:ruby_cms_admin_users_path),
64
+ icon: :user_group,
65
+ section: :settings,
66
+ permission: :manage_permissions,
67
+ order: 4
68
+ )
69
+ RubyCms.register_page(
70
+ key: :settings,
71
+ label: "Settings",
72
+ path: lambda(&:ruby_cms_admin_settings_path),
73
+ icon: :cog_6_tooth,
74
+ section: :settings,
75
+ permission: :manage_admin,
76
+ order: 5
77
+ )
78
+ end
79
+ end
80
+ end