maglevcms 3.0.0.beta3 → 3.0.1

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 (153) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +16 -53
  3. data/Rakefile +3 -1
  4. data/app/assets/builds/maglev/tailwind.css +424 -84
  5. data/app/assets/config/maglev_manifest.js +3 -2
  6. data/app/assets/javascripts/maglev/client/dom-operations.js +5 -5
  7. data/app/assets/javascripts/maglev/client/iframe-decorator.js +24 -0
  8. data/app/assets/javascripts/maglev/client/incoming-messages.js +13 -3
  9. data/app/assets/javascripts/maglev/client/index.js +3 -2
  10. data/app/assets/javascripts/maglev/client/utils.js +22 -0
  11. data/app/assets/javascripts/maglev/editor/controllers/app/forms/section_form_controller.js +4 -3
  12. data/app/assets/javascripts/maglev/editor/controllers/app/forms/style_form_controller.js +2 -1
  13. data/app/assets/javascripts/maglev/editor/controllers/app/page_preview_controller.js +96 -5
  14. data/app/assets/javascripts/maglev/editor/controllers/app/preview_notification_center_controller.js +105 -23
  15. data/app/assets/javascripts/maglev/editor/controllers/app/setting_controller.js +3 -2
  16. data/app/assets/javascripts/maglev/editor/controllers/shared/copy_to_clipboard_controller.js +2 -1
  17. data/app/assets/javascripts/maglev/editor/controllers/utils.js +22 -0
  18. data/app/assets/javascripts/maglev/editor/index.js +2 -1
  19. data/app/assets/javascripts/maglev/editor/patches/turbo_stream_patch.js +3 -3
  20. data/app/assets/stylesheets/maglev/tailwind.css.erb +8 -1
  21. data/app/components/maglev/content/link.rb +1 -1
  22. data/app/components/maglev/editor/settings/link/link_component.rb +7 -1
  23. data/app/components/maglev/section_component.rb +11 -1
  24. data/app/components/maglev/uikit/app_layout/sidebar/link_component.html.erb +1 -1
  25. data/app/components/maglev/uikit/app_layout/sidebar/link_component.rb +35 -5
  26. data/app/components/maglev/uikit/app_layout/sidebar_component.html.erb +3 -4
  27. data/app/components/maglev/uikit/app_layout/topbar_component.html.erb +1 -1
  28. data/app/components/maglev/uikit/app_layout/topbar_component.rb +1 -1
  29. data/app/components/maglev/uikit/button_group_component/button_group_component.html.erb +5 -0
  30. data/app/components/maglev/uikit/button_group_component.rb +70 -0
  31. data/app/components/maglev/uikit/device_toggler_component.rb +10 -1
  32. data/app/components/maglev/uikit/dropdown_component/dropdown_component.html.erb +2 -2
  33. data/app/components/maglev/uikit/dropdown_component.rb +6 -1
  34. data/app/components/maglev/uikit/form/color_field_component.html.erb +1 -1
  35. data/app/components/maglev/uikit/form/combobox_component.html.erb +1 -0
  36. data/app/components/maglev/uikit/form/link_component.rb +5 -1
  37. data/app/components/maglev/uikit/form/richtext_controller.js +5 -4
  38. data/app/components/maglev/uikit/form/search_form_component.html.erb +1 -0
  39. data/app/components/maglev/uikit/icon_component.rb +3 -0
  40. data/app/components/maglev/uikit/image_library/uploader_controller.js +3 -2
  41. data/app/components/maglev/uikit/list/list_item_component.html.erb +36 -19
  42. data/app/components/maglev/uikit/list/list_item_component.rb +19 -5
  43. data/app/components/maglev/uikit/locale_switcher_component/locale_switcher_component.html.erb +6 -10
  44. data/app/components/maglev/uikit/menu_dropdown_component/menu_dropdown_component.html.erb +6 -2
  45. data/app/components/maglev/uikit/menu_dropdown_component.rb +244 -7
  46. data/app/components/maglev/uikit/page_actions_dropdown_component/page_actions_dropdown_component.html.erb +39 -46
  47. data/app/components/maglev/uikit/pagination_component/pagination_component.html.erb +9 -12
  48. data/app/components/maglev/uikit/pagination_component.rb +6 -1
  49. data/app/components/maglev/uikit/section_toolbar/bottom_component.html.erb +1 -1
  50. data/app/components/maglev/uikit/tabs_component/tabs_component.html.erb +7 -4
  51. data/app/components/maglev/uikit/tabs_component.rb +23 -4
  52. data/app/components/maglev/uikit/well/simple_well_component.html.erb +15 -0
  53. data/app/components/maglev/uikit/well/simple_well_component.rb +13 -0
  54. data/app/controllers/concerns/maglev/editor/preview_urls_concern.rb +32 -0
  55. data/app/controllers/concerns/maglev/editor/turbo_concern.rb +29 -0
  56. data/app/controllers/concerns/maglev/flash_i18n_concern.rb +1 -0
  57. data/app/controllers/maglev/application_controller.rb +1 -1
  58. data/app/controllers/maglev/assets/active_storage_proxy_controller.rb +2 -1
  59. data/app/controllers/maglev/editor/assets_controller.rb +1 -1
  60. data/app/controllers/maglev/editor/base_controller.rb +6 -32
  61. data/app/controllers/maglev/editor/pages/clone_controller.rb +22 -0
  62. data/app/controllers/maglev/editor/pages/discard_draft_controller.rb +17 -0
  63. data/app/controllers/maglev/editor/pages_controller.rb +26 -7
  64. data/app/controllers/maglev/editor/section_blocks_controller.rb +13 -9
  65. data/app/controllers/maglev/editor/sections_controller.rb +26 -7
  66. data/app/controllers/maglev/published_page_preview_controller.rb +4 -0
  67. data/app/helpers/maglev/application_helper.rb +6 -6
  68. data/app/helpers/maglev/editor/section_blocks_helper.rb +2 -2
  69. data/app/models/maglev/page/publishable_concern.rb +69 -0
  70. data/app/models/maglev/page.rb +2 -13
  71. data/app/models/maglev/section/block.rb +4 -0
  72. data/app/models/maglev/section/content_concern.rb +3 -1
  73. data/app/models/maglev/section.rb +21 -1
  74. data/app/models/maglev/sections_content_store.rb +1 -3
  75. data/app/services/concerns/maglev/content/helpers_concern.rb +5 -8
  76. data/app/services/maglev/app_container.rb +4 -2
  77. data/app/services/maglev/discard_page_draft_service.rb +45 -0
  78. data/app/services/maglev/fetch_section_screenshot_url.rb +12 -1
  79. data/app/services/maglev/has_unpublished_changes.rb +21 -0
  80. data/app/services/maglev/publish_service.rb +15 -2
  81. data/app/views/layouts/maglev/editor/_sidebar.html.erb +6 -1
  82. data/app/views/layouts/maglev/editor/application.html.erb +5 -0
  83. data/app/views/layouts/maglev/editor/topbar/_page_info.html.erb +1 -1
  84. data/app/views/layouts/maglev/editor/topbar/_publish_button.html.erb +32 -10
  85. data/app/views/maglev/editor/assets/_list.html.erb +2 -1
  86. data/app/views/maglev/editor/assets/index.html.erb +1 -0
  87. data/app/views/maglev/editor/home/index.html.erb +1 -1
  88. data/app/views/maglev/editor/links/edit/_email.html.erb +1 -1
  89. data/app/views/maglev/editor/links/edit/_url.html.erb +1 -1
  90. data/app/views/maglev/editor/pages/_list.html.erb +25 -21
  91. data/app/views/maglev/editor/pages/_preview.html.erb +6 -6
  92. data/app/views/maglev/editor/pages/_preview_empty_message.html.erb +13 -10
  93. data/app/views/maglev/editor/pages/discard_draft/create.turbo_stream.erb +16 -0
  94. data/app/views/maglev/editor/pages/index.html.erb +8 -1
  95. data/app/views/maglev/editor/section_blocks/_form.html.erb +5 -13
  96. data/app/views/maglev/editor/section_blocks/_form_with_tabs.html.erb +15 -0
  97. data/app/views/maglev/editor/section_blocks/_new.html.erb +1 -1
  98. data/app/views/maglev/editor/section_blocks/edit.html.erb +2 -2
  99. data/app/views/maglev/editor/section_blocks/index/_list.html.erb +2 -2
  100. data/app/views/maglev/editor/section_blocks/index/_tree.html.erb +1 -1
  101. data/app/views/maglev/editor/section_blocks/update.turbo_stream.erb +1 -1
  102. data/app/views/maglev/editor/sections/_form.html.erb +6 -20
  103. data/app/views/maglev/editor/sections/_form_with_tabs.html.erb +21 -0
  104. data/app/views/maglev/editor/sections/_list.html.erb +3 -3
  105. data/app/views/maglev/editor/sections/edit.html.erb +4 -4
  106. data/app/views/maglev/editor/sections/index.html.erb +1 -1
  107. data/app/views/maglev/editor/sections/new.html.erb +19 -2
  108. data/app/views/maglev/editor/sections/theme/_empty_list.html.erb +3 -0
  109. data/app/views/maglev/editor/sections/theme/_list.html.erb +35 -0
  110. data/app/views/maglev/editor/sections/theme/_screenshot_placeholder.html.erb +6 -0
  111. data/app/views/maglev/editor/sections/theme/_search.html.erb +22 -0
  112. data/app/views/maglev/editor/sections/update.turbo_stream.erb +1 -1
  113. data/app/views/maglev/editor/shared/_button_label.html.erb +4 -4
  114. data/app/views/maglev/editor/style/edit.html.erb +1 -0
  115. data/config/editor_importmap.rb +13 -13
  116. data/config/locales/editor.ar.yml +12 -4
  117. data/config/locales/editor.en.yml +31 -23
  118. data/config/locales/editor.es.yml +12 -4
  119. data/config/locales/editor.fr.yml +12 -4
  120. data/config/locales/editor.pt-BR.yml +12 -4
  121. data/config/routes/maglev/assets.rb +4 -0
  122. data/config/routes/maglev/editor.rb +38 -0
  123. data/config/routes/maglev/preview.rb +8 -0
  124. data/config/routes/maglev/public_preview.rb +6 -0
  125. data/config/routes.rb +8 -47
  126. data/db/migrate/20211013210954_translate_section_content.rb +1 -0
  127. data/db/migrate/20260114112058_add_published_payload_to_pages.rb +14 -0
  128. data/exe/tailwind-cli +1 -1
  129. data/lib/generators/maglev/install_generator.rb +9 -7
  130. data/lib/generators/maglev/templates/install/config/initializers/maglev.rb +10 -3
  131. data/lib/maglev/active_storage/serving_blob.rb +29 -0
  132. data/lib/maglev/active_storage.rb +2 -0
  133. data/lib/maglev/config.rb +22 -3
  134. data/lib/maglev/engine.rb +14 -10
  135. data/lib/maglev/version.rb +1 -1
  136. data/lib/maglev.rb +18 -3
  137. data/lib/tasks/db_test_all.rake +290 -0
  138. metadata +46 -19
  139. data/app/controllers/maglev/editor/page_clone_controller.rb +0 -20
  140. data/app/views/maglev/editor/sections/_theme_list.html.erb +0 -32
  141. /data/vendor/javascript/{@floating-ui--core.js → maglev/@floating-ui--core.js} +0 -0
  142. /data/vendor/javascript/{@floating-ui--dom.js → maglev/@floating-ui--dom.js} +0 -0
  143. /data/vendor/javascript/{@floating-ui--utils--dom.js → maglev/@floating-ui--utils--dom.js} +0 -0
  144. /data/vendor/javascript/{@floating-ui--utils.js → maglev/@floating-ui--utils.js} +0 -0
  145. /data/vendor/javascript/{@hotwired--stimulus.js → maglev/@hotwired--stimulus.js} +0 -0
  146. /data/vendor/javascript/{@hotwired--turbo-rails.js → maglev/@hotwired--turbo-rails.js} +0 -0
  147. /data/vendor/javascript/{@hotwired--turbo.js → maglev/@hotwired--turbo.js} +0 -0
  148. /data/vendor/javascript/{@rails--actioncable--src.js → maglev/@rails--actioncable--src.js} +0 -0
  149. /data/vendor/javascript/{@rails--request.js.js → maglev/@rails--request.js.js} +0 -0
  150. /data/vendor/javascript/{@shopify--draggable.js → maglev/@shopify--draggable.js} +0 -0
  151. /data/vendor/javascript/{el-transition.js → maglev/el-transition.js} +0 -0
  152. /data/vendor/javascript/{stimulus-use.js → maglev/stimulus-use.js} +0 -0
  153. /data/vendor/javascript/{tiptap.bundle.js → maglev/tiptap.bundle.js} +0 -0
data/config/routes.rb CHANGED
@@ -1,58 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Metrics/BlockLength
4
3
  Maglev::Engine.routes.draw do
5
- # convenient route to create the site in development mode from the Maglev UI
6
- resource :site, only: %i[create], controller: :site if Rails.env.local?
7
-
8
4
  # Editor
9
5
  namespace :editor do
10
- root to: 'home#index'
11
-
12
- get 'leave', to: 'home#destroy', as: :leave
13
-
14
- resources :assets, only: %i[index create destroy]
15
- resources :icons, only: %i[index]
16
- resource :link, only: %i[edit update]
17
-
18
- # combobox routes
19
- namespace :combobox do
20
- resources :pages, only: :index
21
- resources :collection_items, only: :index
22
- end
23
-
24
- # always keep the scope of the current page and locale in the url
25
- scope ':locale/:page_id' do
26
- root to: 'home#index', as: :real_root
27
-
28
- resources :pages do
29
- resource :clone, controller: :page_clone, only: :create
30
- end
31
-
32
- resources :sections do
33
- put :sort, on: :collection
34
- resources :blocks, controller: :section_blocks do
35
- put :sort, on: :collection
36
- end
37
- end
38
-
39
- resource :style, controller: :style, only: %i[edit update]
40
-
41
- resource :publication, controller: :publication, only: :create
42
- end
6
+ draw 'maglev/editor'
43
7
  end
44
8
 
45
- root to: 'editor/home#index'
46
-
47
9
  # Preview
48
- get 'preview/(*path)', to: 'page_preview#index',
49
- defaults: { path: 'index', rendering_mode: :editor },
50
- constraints: Maglev::PreviewConstraint.new,
51
- as: :site_preview
52
- post 'preview/(*path)', to: 'page_preview#create', defaults: { path: 'index', rendering_mode: :editor }
10
+ draw 'maglev/preview'
53
11
 
54
12
  # Public Assets
55
- get 'assets/:id(/:filename)', to: "#{Maglev.uploader_proxy_controller_name}#show",
56
- as: :public_asset
13
+ draw 'maglev/assets'
14
+
15
+ # convenient route to create the site in development mode from the Maglev UI
16
+ resource :site, only: %i[create], controller: :site if Rails.env.local?
17
+
18
+ root to: 'editor/home#index'
57
19
  end
58
- # rubocop:enable Metrics/BlockLength
@@ -1,4 +1,5 @@
1
1
  class TranslateSectionContent < ActiveRecord::Migration[6.0]
2
+ include Maglev::Migration
2
3
  def change
3
4
  remove_column :maglev_sites, :sections, :jsonb, default: [] if column_exists?(:maglev_sites, :sections)
4
5
  remove_column :maglev_pages, :sections, :jsonb, default: [] if column_exists?(:maglev_pages, :sections)
@@ -0,0 +1,14 @@
1
+ class AddPublishedPayloadToPages < ActiveRecord::Migration[6.0]
2
+ include Maglev::Migration
3
+ def change
4
+ change_table :maglev_pages do |t|
5
+ if t.respond_to? :jsonb
6
+ t.jsonb :published_payload, default: {}
7
+ elsif mysql?
8
+ t.json :published_payload # MySQL doesn't support default values for json columns
9
+ else
10
+ t.json :published_payload, default: {}
11
+ end
12
+ end
13
+ end
14
+ end
data/exe/tailwind-cli CHANGED
@@ -8,7 +8,7 @@ require 'bundler/inline'
8
8
 
9
9
  gemfile do
10
10
  source 'https://rubygems.org'
11
- gem 'tailwindcss-ruby', '4.1.11'
11
+ gem 'tailwindcss-ruby', '4.1.18'
12
12
  end
13
13
 
14
14
  require 'tailwindcss/ruby'
@@ -23,9 +23,11 @@ module Maglev
23
23
  def mount_engine
24
24
  inject_into_file 'config/routes.rb', before: /^end/ do
25
25
  <<-CODE
26
+ # Mount Maglev engine (mainly used for the content editor)
26
27
  mount Maglev::Engine => '/maglev'
27
- get '/sitemap', to: 'maglev/sitemap#index', defaults: { format: 'xml' }
28
- get '(*path)', to: 'maglev/published_page_preview#index', defaults: { path: 'index' }, constraints: Maglev::PreviewConstraint.new
28
+
29
+ # Handle "/:path" + "/sitemap" requests by Maglev engine
30
+ draw 'maglev/public_preview'
29
31
  CODE
30
32
  end
31
33
  end
@@ -34,13 +36,13 @@ module Maglev
34
36
  $stdout.puts <<~INFO
35
37
  Done! 🎉
36
38
 
37
- You can now tweak config/initializers/maglev.rb
38
- You can also modify your theme (in app/theme and app/views/theme)
39
- and generate new sections with rails g maglev:section.
39
+ You can now tweak the settings in `config/initializers/maglev.rb`.
40
+ You can also modify your theme (`app/theme` and `app/views/theme`)
41
+ and generate new sections with `rails g maglev:section`.
40
42
 
41
- 👉 The next step is to create a site using `rails maglev:create_site`.
43
+ 👉 The next step is to create a site with `rails maglev:create_site`.
42
44
 
43
- 🚨 Don't forget to do it in production as well!
45
+ 🚨 Don't forget to run the last command in production as well!
44
46
  INFO
45
47
  end
46
48
  end
@@ -47,9 +47,10 @@ Maglev.configure do |config|
47
47
  # config.is_authenticated = :editor_allowed? # name of any protected method from your Rails application controller
48
48
  # config.is_authenticated = ->(site) { current_user&.role == 'editor' }
49
49
 
50
- # Admin UI authentication (https://docs.maglev.dev/guides/setup-authentication)
51
- config.admin_username = Rails.env.production? ? ENV.fetch('MAGLEV_ADMIN_USERNAME') : nil
52
- config.admin_password = Rails.env.production? ? ENV.fetch('MAGLEV_ADMIN_PASSWORD') : nil
50
+ # Base class for Maglev::ApplicationController (inherit auth, layout, etc. from your app).
51
+ # Set this in an initializer that runs before Maglev controllers are loaded
52
+ # if you use something other than ApplicationController.
53
+ # config.parent_controller = 'ApplicationController'
53
54
 
54
55
  # Uploader engine (:active_storage is the default one)
55
56
  # Checkout https://github.com/MarsBased/maglevcms-shrine for an example of a custom uploader
@@ -67,6 +68,12 @@ Maglev.configure do |config|
67
68
  # }
68
69
  # }
69
70
 
71
+ # Pagination configuration
72
+ # config.pagination = {
73
+ # pages: -1 # -1 means no pagination
74
+ # assets: 16 # number of assets displayed per page in the assets modal
75
+ # }
76
+
70
77
  # Let your content editors references existing pages of your application but
71
78
  # not served/rendered by Maglev.
72
79
  # config.static_pages = [
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maglev
4
+ module ActiveStorage
5
+ # Since assets are uploaed by editors, we can assume they are always safe to serve.
6
+ # Wraps ActiveStorage::Blob only for Maglev's proxy: Active Storage maps image/svg+xml to
7
+ # application/octet-stream globally; we serve SVG as image/svg+xml without changing the host app's config.
8
+ class ServingBlob < SimpleDelegator
9
+ def content_type_for_serving
10
+ return 'image/svg+xml' if svg?
11
+
12
+ __getobj__.content_type_for_serving
13
+ end
14
+
15
+ def forced_disposition_for_serving
16
+ return nil if svg?
17
+
18
+ __getobj__.forced_disposition_for_serving
19
+ end
20
+
21
+ private
22
+
23
+ def svg?
24
+ filename.to_s.downcase.end_with?('.svg') ||
25
+ content_type.to_s == 'image/svg+xml'
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'active_storage/serving_blob'
4
+
3
5
  module Maglev
4
6
  module ActiveStorage
5
7
  extend ActiveSupport::Concern
data/lib/maglev/config.rb CHANGED
@@ -1,10 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Maglev
4
+ DEFAULT_PAGINATION = { pages: -1, assets: 16 }.freeze
5
+
4
6
  Config = Struct.new(:primary_color, :title, :favicon, :logo, :back_action,
5
7
  :site_publishable, :uploader, :preview_host, :asset_host, :services,
6
8
  :collections, :is_authenticated, :ui_locale, :default_site_locales,
7
- :static_pages, :reserved_paths,
8
- :admin_username, :admin_password,
9
- :tailwindcss_folders)
9
+ :static_pages, :reserved_paths, :parent_controller,
10
+ :admin_username, :admin_password, # legacy config
11
+ :tailwindcss_folders, :pagination) do
12
+ # custom setter + helpers for the pagination config
13
+ def pagination=(value)
14
+ self[:pagination] = if value.is_a?(Hash)
15
+ ::Maglev::DEFAULT_PAGINATION.merge(value)
16
+ else
17
+ value
18
+ end
19
+ end
20
+
21
+ def pagination?(key)
22
+ self[:pagination][key].present? && self[:pagination][key] != -1
23
+ end
24
+
25
+ def per_page(key)
26
+ self[:pagination][key]
27
+ end
28
+ end
10
29
  end
data/lib/maglev/engine.rb CHANGED
@@ -15,7 +15,13 @@ module Maglev
15
15
  g.factory_bot dir: 'spec/factories'
16
16
  end
17
17
 
18
+ initializer 'maglev.i18n' do |_app|
19
+ Rails.application.config.i18n.load_path += Dir["#{config.root}/config/locales/**/*.yml"]
20
+ end
21
+
18
22
  initializer 'maglev.theme_reloader' do |app|
23
+ next unless Maglev.theme_reloader_enabled?
24
+
19
25
  require_relative './theme_filesystem_loader'
20
26
  theme_path = Rails.root.join('app/theme')
21
27
  theme_reloader = app.config.file_watcher.new([], { theme_path.to_s => ['.yml', 'yml'] }) do
@@ -37,10 +43,6 @@ module Maglev
37
43
  end
38
44
  end
39
45
 
40
- initializer 'maglev.i18n' do |_app|
41
- Rails.application.config.i18n.load_path += Dir["#{config.root}/config/locales/**/*.yml"]
42
- end
43
-
44
46
  def self.importmaps
45
47
  @importmaps ||= {
46
48
  editor: ::Importmap::Map.new,
@@ -49,13 +51,15 @@ module Maglev
49
51
  end
50
52
 
51
53
  initializer 'maglev.assets' do |app|
52
- app.config.assets.paths << Engine.root.join('app/assets/builds')
53
- app.config.assets.paths << Engine.root.join('app/components')
54
- app.config.assets.paths << Engine.root.join('app/assets/javascripts')
55
- app.config.assets.paths << Engine.root.join('vendor/javascript')
54
+ if app.config.respond_to?(:assets) && app.config.assets.respond_to?(:paths)
55
+ app.config.assets.paths << Engine.root.join('app/assets/builds')
56
+ app.config.assets.paths << Engine.root.join('app/components')
57
+ app.config.assets.paths << Engine.root.join('app/assets/javascripts')
58
+ app.config.assets.paths << Engine.root.join('vendor/javascript')
56
59
 
57
- # required by Sprockets (if used by the main app)
58
- app.config.assets.precompile += %w[maglev_manifest]
60
+ # required by Sprockets (if used by the main app)
61
+ app.config.assets.precompile += %w[maglev_manifest] if defined?(Sprockets)
62
+ end
59
63
  end
60
64
 
61
65
  initializer 'maglev.importmap', after: 'importmap' do |app|
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Maglev
4
- VERSION = '3.0.0.beta3'
4
+ VERSION = '3.0.1'
5
5
  end
data/lib/maglev.rb CHANGED
@@ -18,7 +18,7 @@ module Maglev
18
18
  ServiceContext = Struct.new(:rendering_mode, :controller, :site, keyword_init: true)
19
19
 
20
20
  class << self
21
- attr_accessor :local_themes
21
+ attr_accessor :local_themes, :theme_reloader_disabled
22
22
 
23
23
  # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
24
24
  def config
@@ -36,11 +36,13 @@ module Maglev
36
36
  c.services = {}
37
37
  c.default_site_locales = [{ label: 'English', prefix: 'en' }]
38
38
  c.is_authenticated = ->(_site) { !Rails.env.production? }
39
- c.admin_username = nil
40
- c.admin_password = nil
39
+ c.admin_username = nil # legacy config
40
+ c.admin_password = nil # legacy config
41
41
  c.static_pages = []
42
42
  c.reserved_paths = []
43
+ c.parent_controller = 'ApplicationController'
43
44
  c.tailwindcss_folders = []
45
+ c.pagination = ::Maglev::DEFAULT_PAGINATION.dup
44
46
  end
45
47
  end
46
48
  # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
@@ -53,6 +55,10 @@ module Maglev
53
55
  end
54
56
  end
55
57
 
58
+ def parent_controller_class
59
+ (config.parent_controller.presence || 'ApplicationController').constantize
60
+ end
61
+
56
62
  def uploader
57
63
  case config.uploader
58
64
  when Symbol then const_get("::Maglev::#{config.uploader.to_s.classify}")
@@ -78,6 +84,15 @@ module Maglev
78
84
  @uuid_as_primary_key = config.options[config.orm][:primary_key_type] == :uuid
79
85
  end
80
86
 
87
+ # disable the theme reloader which is required by the Maglev SaaS plugin
88
+ def disable_theme_reloader
89
+ @theme_reloader_disabled = true
90
+ end
91
+
92
+ def theme_reloader_enabled?
93
+ !@theme_reloader_disabled
94
+ end
95
+
81
96
  def config_klass
82
97
  ::Maglev::Config
83
98
  end
@@ -0,0 +1,290 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Database Migration Testing Automation
4
+ #
5
+ # This Rake task automates the process of testing migrations against multiple
6
+ # databases and Rails versions, generating schema files for each combination.
7
+ #
8
+ # Usage:
9
+ # # Test all databases with all Rails versions
10
+ # bundle exec rake db:test_all
11
+ #
12
+ # # Test all databases with current Gemfile
13
+ # bundle exec rake db:test:all
14
+ #
15
+ # # Test specific database with current Gemfile
16
+ # bundle exec rake db:test:pg
17
+ # bundle exec rake db:test:sqlite
18
+ # bundle exec rake db:test:mysql
19
+ # bundle exec rake db:test:mariadb
20
+ #
21
+ # # Test with specific Rails version
22
+ # BUNDLE_GEMFILE=Gemfile.rails_7_2 bundle exec rake db:test:all
23
+ #
24
+ # Environment variables:
25
+ # CONTINUE_ON_ERROR=1 - Continue testing other databases even if one fails
26
+ # DEBUG=1 - Show detailed error backtraces
27
+ #
28
+ # The task will:
29
+ # 1. Backup schema.rb (PG schema) before switching databases
30
+ # 2. Restore appropriate schema file for each database
31
+ # 3. Run migrations
32
+ # 4. Generate new schema files
33
+ # 5. Save schemas with appropriate prefixes (schema.sqlite.rb, etc.)
34
+ # 6. Run tests
35
+ # 7. Restore schema.rb to PG version (for Git)
36
+
37
+ require 'fileutils'
38
+ require 'open3'
39
+
40
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Naming/VariableNumber, Metrics/CyclomaticComplexity
41
+
42
+ DATABASES = {
43
+ pg: {
44
+ env: {
45
+ MAGLEV_APP_DATABASE_USERNAME: ENV.fetch('MAGLEV_APP_DATABASE_USERNAME', ''),
46
+ MAGLEV_APP_DATABASE_PASSWORD: ENV.fetch('MAGLEV_APP_DATABASE_PASSWORD', '')
47
+ },
48
+ schema_file: 'schema.rb',
49
+ backup_file: 'schema.pg.rb'
50
+ },
51
+ sqlite: {
52
+ env: { USE_SQLITE: '1' },
53
+ schema_file: 'schema.sqlite.rb',
54
+ backup_file: 'schema.pg.rb'
55
+ },
56
+ mysql: {
57
+ env: {
58
+ USE_MYSQL: '1',
59
+ MAGLEV_APP_DATABASE_HOST: ENV.fetch('MAGLEV_APP_DATABASE_HOST', '127.0.0.1'),
60
+ MAGLEV_APP_DATABASE_USERNAME: ENV.fetch('MAGLEV_APP_DATABASE_USERNAME', 'start'),
61
+ MAGLEV_APP_DATABASE_PASSWORD: ENV.fetch('MAGLEV_APP_DATABASE_PASSWORD', 'start')
62
+ },
63
+ schema_file: 'schema.mysql.rb',
64
+ backup_file: 'schema.pg.rb'
65
+ },
66
+ mariadb: {
67
+ env: {
68
+ USE_MYSQL: '1',
69
+ MAGLEV_APP_DATABASE_HOST: ENV.fetch('MAGLEV_APP_DATABASE_HOST', '127.0.0.1'),
70
+ MAGLEV_APP_DATABASE_PORT: ENV.fetch('MAGLEV_APP_DATABASE_PORT', '3307'),
71
+ MAGLEV_APP_DATABASE_USERNAME: ENV.fetch('MAGLEV_APP_DATABASE_USERNAME', 'start'),
72
+ MAGLEV_APP_DATABASE_PASSWORD: ENV.fetch('MAGLEV_APP_DATABASE_PASSWORD', 'start')
73
+ },
74
+ schema_file: 'schema.mariadb.rb',
75
+ backup_file: 'schema.pg.rb'
76
+ }
77
+ }.freeze
78
+
79
+ GEMFILES = {
80
+ Gemfile: 'spec/dummy',
81
+ "Gemfile.rails_7_2": 'spec/legacy_dummy'
82
+ }.with_indifferent_access.freeze
83
+
84
+ namespace :db do
85
+ desc 'Test all databases with all Rails versions'
86
+ task test_all: :environment do
87
+ all_passed = true
88
+
89
+ GEMFILES.each do |gemfile, dummy_path|
90
+ next unless File.exist?(gemfile)
91
+
92
+ puts "\n#{'=' * 70}"
93
+ puts "Testing with #{gemfile} (#{dummy_path})"
94
+ puts '=' * 70
95
+
96
+ # Ensure bundle is installed for this Gemfile
97
+ unless ensure_bundle_installed(gemfile)
98
+ puts "❌ Failed to install bundle for #{gemfile}"
99
+ all_passed = false
100
+ next
101
+ end
102
+
103
+ DATABASES.each_key do |db_name|
104
+ success = test_database(db_name, gemfile, dummy_path)
105
+ all_passed = false unless success
106
+
107
+ abort "Failed testing #{db_name} with #{gemfile}" if !ENV['CONTINUE_ON_ERROR'] && !success
108
+ end
109
+ end
110
+
111
+ unless all_passed
112
+ puts "\n❌ Some tests failed. Use CONTINUE_ON_ERROR=1 to continue despite errors."
113
+ exit 1
114
+ end
115
+
116
+ puts "\n✅ All tests passed!"
117
+ end
118
+
119
+ namespace :test do
120
+ DATABASES.each_key do |db_name|
121
+ desc "Test #{db_name} database with current Gemfile"
122
+ task db_name.to_sym => :environment do
123
+ gemfile = File.basename(ENV['BUNDLE_GEMFILE'] || 'Gemfile')
124
+ dummy_path = GEMFILES[gemfile] || GEMFILES['Gemfile']
125
+ success = test_database(db_name, gemfile, dummy_path)
126
+ exit 1 unless success
127
+ end
128
+ end
129
+
130
+ desc 'Test all databases with current Gemfile'
131
+ task all: DATABASES.keys.map(&:to_sym) do
132
+ puts "\n✅ All database tests passed!"
133
+ end
134
+ end
135
+
136
+ def test_database(db_name, gemfile, dummy_path)
137
+ puts "\n#{'-' * 70}"
138
+ puts "Testing #{db_name} with #{gemfile}"
139
+ puts '-' * 70
140
+
141
+ db_config = DATABASES[db_name]
142
+
143
+ root_dir = File.expand_path('../..', __dir__)
144
+
145
+ db_dir = File.join(root_dir, dummy_path, 'db')
146
+ schema_rb = File.join(db_dir, 'schema.rb')
147
+ schema_backup = File.join(db_dir, db_config[:backup_file])
148
+ schema_target = File.join(db_dir, db_config[:schema_file])
149
+
150
+ # Setup environment
151
+ env = {
152
+ BUNDLE_GEMFILE: File.join(root_dir, gemfile),
153
+ RAILS_ENV: 'test'
154
+ }.merge(db_config[:env].reject { |_k, v| v.blank? })
155
+
156
+ begin
157
+ # Step 1: Backup current schema.rb (the PG schema) before switching databases
158
+ if File.exist?(schema_rb) && !File.exist?(schema_backup)
159
+ FileUtils.cp(schema_rb, schema_backup)
160
+ puts "✓ Backed up schema.rb to #{db_config[:backup_file]}"
161
+ end
162
+
163
+ # Step 2: Restore appropriate schema file
164
+ if db_name == :pg
165
+ # For PG, restore from backup if exists, otherwise keep current schema.rb
166
+ if File.exist?(schema_backup)
167
+ FileUtils.cp(schema_backup, schema_rb)
168
+ puts "✓ Restored schema from #{db_config[:backup_file]}"
169
+ end
170
+ elsif File.exist?(schema_target)
171
+ # For other DBs, copy the specific schema file to schema.rb (if it exists)
172
+ FileUtils.cp(schema_target, schema_rb)
173
+ puts "✓ Restored schema from #{db_config[:schema_file]}"
174
+ elsif File.exist?(schema_backup)
175
+ # Fallback to PG schema if specific schema doesn't exist
176
+ FileUtils.cp(schema_backup, schema_rb)
177
+ puts "✓ Restored schema from #{db_config[:backup_file]} (fallback)"
178
+ end
179
+
180
+ # Step 3: Run migrations
181
+ puts 'Running migrations...'
182
+ unless run_command('./bin/rails db:migrate', env, root_dir)
183
+ puts "❌ Migrations failed for #{db_name}"
184
+ return false
185
+ end
186
+
187
+ # Step 4: Generate schema
188
+ puts 'Generating schema...'
189
+ unless run_command('bin/rails db:schema:dump', env, root_dir)
190
+ puts "❌ Schema generation failed for #{db_name}"
191
+ return false
192
+ end
193
+
194
+ # Step 5: Save generated schema to the appropriate file
195
+ if db_name == :pg
196
+ # For PG, keep as schema.rb (the committed version) and also backup to schema.pg.rb
197
+ if File.exist?(schema_rb)
198
+ FileUtils.cp(schema_rb, schema_backup)
199
+ puts "✓ Saved schema to #{db_config[:backup_file]}"
200
+ end
201
+ elsif File.exist?(schema_rb)
202
+ # For other DBs, copy generated schema.rb to the specific schema file
203
+ FileUtils.cp(schema_rb, schema_target)
204
+ puts "✓ Saved schema to #{db_config[:schema_file]}"
205
+ end
206
+
207
+ # Step 6: Run tests
208
+ puts 'Running tests...'
209
+ unless run_command('bundle exec rspec', env, root_dir)
210
+ puts "❌ Tests failed for #{db_name}"
211
+ return false
212
+ end
213
+
214
+ puts "✅ #{db_name} tests passed!"
215
+ true
216
+ rescue StandardError => e
217
+ puts "❌ Error testing #{db_name}: #{e.message}"
218
+ puts e.backtrace.first(5).join("\n") if ENV['DEBUG']
219
+ false
220
+ ensure
221
+ # Step 7: Restore schema.rb from PG backup (for Git)
222
+ if db_name != 'pg' && File.exist?(schema_backup)
223
+ FileUtils.cp(schema_backup, schema_rb)
224
+ puts '✓ Restored schema.rb from PG backup (for Git)'
225
+ end
226
+ end
227
+ end
228
+
229
+ def ensure_bundle_installed(gemfile)
230
+ root_dir = File.expand_path('../..', __dir__)
231
+ gemfile_path = File.join(root_dir, gemfile)
232
+
233
+ env = {
234
+ BUNDLE_GEMFILE: gemfile_path
235
+ }
236
+ full_env = ENV.to_h.merge(env).deep_stringify_keys
237
+
238
+ puts "Checking bundle for #{gemfile}..."
239
+ stdout, stderr, status = Open3.capture3(full_env, 'bundle check', chdir: root_dir)
240
+
241
+ if status.success?
242
+ puts "✓ Bundle is up to date for #{gemfile}"
243
+ true
244
+ else
245
+ puts "Installing bundle for #{gemfile}..."
246
+ stdout, stderr, status = Open3.capture3(full_env, 'bundle install', chdir: root_dir)
247
+
248
+ if status.success?
249
+ puts "✓ Bundle installed for #{gemfile}"
250
+ true
251
+ else
252
+ puts "❌ Failed to install bundle for #{gemfile}"
253
+ puts stderr unless stderr.empty?
254
+ puts stdout unless stdout.empty?
255
+ false
256
+ end
257
+ end
258
+ end
259
+
260
+ def run_command(command, env, cwd)
261
+ full_env = ENV.to_h.merge(env).deep_stringify_keys
262
+
263
+ stdout, stderr, status = Open3.capture3(full_env, command, chdir: cwd)
264
+
265
+ unless status.success?
266
+ puts "\n#{'=' * 70}"
267
+ puts "Command failed: #{command}"
268
+ puts "Working directory: #{cwd}"
269
+ puts('=' * 70)
270
+
271
+ unless stdout.empty?
272
+ puts "\nSTDOUT:"
273
+ puts stdout
274
+ end
275
+
276
+ unless stderr.empty?
277
+ puts "\nSTDERR:"
278
+ puts stderr
279
+ end
280
+
281
+ puts "\nExit status: #{status.exitstatus}"
282
+ puts "#{'=' * 70}\n"
283
+ return false
284
+ end
285
+
286
+ true
287
+ end
288
+ end
289
+
290
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Naming/VariableNumber, Metrics/CyclomaticComplexity