maglevcms 3.0.0.beta2 → 3.0.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 (171) 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 +681 -11670
  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/shared/submit_button_controller.js +29 -5
  18. data/app/assets/javascripts/maglev/editor/controllers/utils.js +38 -0
  19. data/app/assets/javascripts/maglev/editor/index.js +5 -44
  20. data/app/assets/javascripts/maglev/editor/patches/page_renderer_patch.js +47 -0
  21. data/app/assets/javascripts/maglev/editor/patches/turbo_delayed_streams.js +35 -0
  22. data/app/assets/javascripts/maglev/editor/patches/turbo_stream_patch.js +50 -0
  23. data/app/assets/stylesheets/maglev/application.css +0 -2
  24. data/app/assets/stylesheets/maglev/tailwind.css.erb +11 -2
  25. data/app/components/maglev/content/link.rb +1 -1
  26. data/app/components/maglev/editor/settings/link/link_component.rb +7 -1
  27. data/app/components/maglev/section_component.rb +11 -1
  28. data/app/components/maglev/uikit/app_layout/sidebar/link_component.html.erb +1 -1
  29. data/app/components/maglev/uikit/app_layout/sidebar/link_component.rb +35 -5
  30. data/app/components/maglev/uikit/app_layout/sidebar_component.html.erb +3 -4
  31. data/app/components/maglev/uikit/app_layout/topbar/logo_component.html.erb +1 -1
  32. data/app/components/maglev/uikit/app_layout/topbar/page_info_component.html.erb +1 -1
  33. data/app/components/maglev/uikit/app_layout/topbar_component.html.erb +1 -1
  34. data/app/components/maglev/uikit/app_layout/topbar_component.rb +1 -1
  35. data/app/components/maglev/uikit/button_group_component/button_group_component.html.erb +5 -0
  36. data/app/components/maglev/uikit/button_group_component.rb +70 -0
  37. data/app/components/maglev/uikit/device_toggler_component.rb +10 -1
  38. data/app/components/maglev/uikit/dropdown_component/dropdown_component.html.erb +2 -2
  39. data/app/components/maglev/uikit/dropdown_component/dropdown_controller.js +6 -1
  40. data/app/components/maglev/uikit/dropdown_component.rb +6 -1
  41. data/app/components/maglev/uikit/form/color_field_component.html.erb +1 -1
  42. data/app/components/maglev/uikit/form/combobox_component.html.erb +1 -0
  43. data/app/components/maglev/uikit/form/link_component.rb +5 -1
  44. data/app/components/maglev/uikit/form/richtext_controller.js +5 -4
  45. data/app/components/maglev/uikit/form/search_form_component.html.erb +1 -0
  46. data/app/components/maglev/uikit/icon_component.rb +3 -0
  47. data/app/components/maglev/uikit/image_library/uploader_controller.js +3 -2
  48. data/app/components/maglev/uikit/list/list_item_component.html.erb +36 -19
  49. data/app/components/maglev/uikit/list/list_item_component.rb +19 -5
  50. data/app/components/maglev/uikit/locale_switcher_component/locale_switcher_component.html.erb +6 -10
  51. data/app/components/maglev/uikit/menu_dropdown_component/menu_dropdown_component.html.erb +6 -2
  52. data/app/components/maglev/uikit/menu_dropdown_component.rb +244 -7
  53. data/app/components/maglev/uikit/page_actions_dropdown_component/page_actions_dropdown_component.html.erb +39 -46
  54. data/app/components/maglev/uikit/pagination_component/pagination_component.html.erb +9 -12
  55. data/app/components/maglev/uikit/pagination_component.rb +6 -1
  56. data/app/components/maglev/uikit/section_toolbar/bottom_component.html.erb +1 -1
  57. data/app/components/maglev/uikit/tabs_component/tabs_component.html.erb +7 -4
  58. data/app/components/maglev/uikit/tabs_component.rb +23 -4
  59. data/app/components/maglev/uikit/well/simple_well_component.html.erb +15 -0
  60. data/app/components/maglev/uikit/well/simple_well_component.rb +13 -0
  61. data/app/controllers/concerns/maglev/editor/errors_concern.rb +9 -9
  62. data/app/controllers/concerns/maglev/editor/preview_urls_concern.rb +32 -0
  63. data/app/controllers/concerns/maglev/editor/turbo_concern.rb +29 -0
  64. data/app/controllers/concerns/maglev/errors_concern.rb +17 -0
  65. data/app/controllers/concerns/maglev/flash_i18n_concern.rb +1 -0
  66. data/app/controllers/maglev/application_controller.rb +2 -1
  67. data/app/controllers/maglev/assets/active_storage_proxy_controller.rb +2 -1
  68. data/app/controllers/maglev/editor/assets_controller.rb +1 -1
  69. data/app/controllers/maglev/editor/base_controller.rb +6 -32
  70. data/app/controllers/maglev/editor/pages/clone_controller.rb +22 -0
  71. data/app/controllers/maglev/editor/pages/discard_draft_controller.rb +17 -0
  72. data/app/controllers/maglev/editor/pages_controller.rb +26 -7
  73. data/app/controllers/maglev/editor/section_blocks_controller.rb +13 -9
  74. data/app/controllers/maglev/editor/sections_controller.rb +26 -7
  75. data/app/controllers/maglev/published_page_preview_controller.rb +4 -0
  76. data/app/controllers/maglev/site_controller.rb +15 -0
  77. data/app/helpers/maglev/application_helper.rb +26 -8
  78. data/app/helpers/maglev/editor/section_blocks_helper.rb +2 -2
  79. data/app/models/maglev/page/publishable_concern.rb +69 -0
  80. data/app/models/maglev/page.rb +3 -0
  81. data/app/models/maglev/section/block.rb +4 -0
  82. data/app/models/maglev/section/content_concern.rb +3 -1
  83. data/app/models/maglev/section.rb +21 -1
  84. data/app/models/maglev/sections_content_store.rb +1 -3
  85. data/app/models/maglev/site.rb +5 -0
  86. data/app/services/concerns/maglev/content/helpers_concern.rb +5 -8
  87. data/app/services/maglev/app_container.rb +4 -2
  88. data/app/services/maglev/discard_page_draft_service.rb +45 -0
  89. data/app/services/maglev/fetch_section_screenshot_url.rb +12 -1
  90. data/app/services/maglev/fetch_site.rb +3 -1
  91. data/app/services/maglev/get_published_page_sections_service.rb +1 -1
  92. data/app/services/maglev/has_unpublished_changes.rb +21 -0
  93. data/app/services/maglev/publish_service.rb +18 -2
  94. data/app/views/layouts/maglev/editor/_sidebar.html.erb +8 -2
  95. data/app/views/layouts/maglev/editor/_topbar.html.erb +6 -23
  96. data/app/views/layouts/maglev/editor/application.html.erb +6 -0
  97. data/app/views/layouts/maglev/editor/topbar/_page_info.html.erb +11 -0
  98. data/app/views/layouts/maglev/editor/topbar/_publish_button.html.erb +35 -0
  99. data/app/views/maglev/editor/assets/index.html.erb +1 -0
  100. data/app/views/maglev/editor/home/index.html.erb +1 -1
  101. data/app/views/maglev/editor/links/edit/_email.html.erb +1 -1
  102. data/app/views/maglev/editor/links/edit/_url.html.erb +1 -1
  103. data/app/views/maglev/editor/pages/_list.html.erb +25 -21
  104. data/app/views/maglev/editor/pages/_preview.html.erb +6 -6
  105. data/app/views/maglev/editor/pages/_preview_empty_message.html.erb +13 -10
  106. data/app/views/maglev/editor/pages/discard_draft/create.turbo_stream.erb +16 -0
  107. data/app/views/maglev/editor/pages/index.html.erb +8 -1
  108. data/app/views/maglev/editor/publication/create.turbo_stream.erb +3 -1
  109. data/app/views/maglev/editor/section_blocks/_form.html.erb +5 -13
  110. data/app/views/maglev/editor/section_blocks/_form_with_tabs.html.erb +15 -0
  111. data/app/views/maglev/editor/section_blocks/_new.html.erb +1 -1
  112. data/app/views/maglev/editor/section_blocks/edit.html.erb +2 -2
  113. data/app/views/maglev/editor/section_blocks/index/_list.html.erb +2 -2
  114. data/app/views/maglev/editor/section_blocks/index/_tree.html.erb +1 -1
  115. data/app/views/maglev/editor/section_blocks/update.turbo_stream.erb +9 -1
  116. data/app/views/maglev/editor/sections/_form.html.erb +6 -20
  117. data/app/views/maglev/editor/sections/_form_with_tabs.html.erb +21 -0
  118. data/app/views/maglev/editor/sections/_list.html.erb +1 -1
  119. data/app/views/maglev/editor/sections/edit.html.erb +3 -3
  120. data/app/views/maglev/editor/sections/index.html.erb +1 -1
  121. data/app/views/maglev/editor/sections/new.html.erb +18 -1
  122. data/app/views/maglev/editor/sections/theme/_empty_list.html.erb +3 -0
  123. data/app/views/maglev/editor/sections/theme/_list.html.erb +35 -0
  124. data/app/views/maglev/editor/sections/theme/_screenshot_placeholder.html.erb +6 -0
  125. data/app/views/maglev/editor/sections/theme/_search.html.erb +22 -0
  126. data/app/views/maglev/editor/sections/update.turbo_stream.erb +9 -1
  127. data/app/views/maglev/editor/shared/_button_label.html.erb +4 -4
  128. data/app/views/maglev/editor/style/edit.html.erb +1 -0
  129. data/app/views/maglev/errors/site_not_found.html.erb +33 -0
  130. data/config/editor_importmap.rb +16 -13
  131. data/config/locales/editor.ar.yml +12 -4
  132. data/config/locales/editor.en.yml +31 -23
  133. data/config/locales/editor.es.yml +12 -4
  134. data/config/locales/editor.fr.yml +12 -4
  135. data/config/locales/editor.pt-BR.yml +12 -4
  136. data/config/routes/maglev/assets.rb +4 -0
  137. data/config/routes/maglev/editor.rb +38 -0
  138. data/config/routes/maglev/preview.rb +8 -0
  139. data/config/routes/maglev/public_preview.rb +6 -0
  140. data/config/routes.rb +8 -44
  141. data/db/migrate/20211013210954_translate_section_content.rb +1 -0
  142. data/db/migrate/20251116171603_add_published_at_to_sites_and_pages.rb +6 -0
  143. data/db/migrate/20260114112058_add_published_payload_to_pages.rb +14 -0
  144. data/exe/tailwind-cli +1 -1
  145. data/lib/generators/maglev/install_generator.rb +9 -7
  146. data/lib/generators/maglev/templates/install/config/initializers/maglev.rb +10 -3
  147. data/lib/maglev/active_storage/serving_blob.rb +29 -0
  148. data/lib/maglev/active_storage.rb +2 -0
  149. data/lib/maglev/config.rb +22 -3
  150. data/lib/maglev/engine.rb +15 -10
  151. data/lib/maglev/errors.rb +1 -0
  152. data/lib/maglev/version.rb +1 -1
  153. data/lib/maglev.rb +18 -3
  154. data/lib/tasks/db_test_all.rake +290 -0
  155. data/lib/tasks/maglev/tailwindcss.rake +1 -0
  156. metadata +55 -19
  157. data/app/controllers/maglev/editor/page_clone_controller.rb +0 -20
  158. data/app/views/maglev/editor/sections/_theme_list.html.erb +0 -32
  159. /data/vendor/javascript/{@floating-ui--core.js → maglev/@floating-ui--core.js} +0 -0
  160. /data/vendor/javascript/{@floating-ui--dom.js → maglev/@floating-ui--dom.js} +0 -0
  161. /data/vendor/javascript/{@floating-ui--utils--dom.js → maglev/@floating-ui--utils--dom.js} +0 -0
  162. /data/vendor/javascript/{@floating-ui--utils.js → maglev/@floating-ui--utils.js} +0 -0
  163. /data/vendor/javascript/{@hotwired--stimulus.js → maglev/@hotwired--stimulus.js} +0 -0
  164. /data/vendor/javascript/{@hotwired--turbo-rails.js → maglev/@hotwired--turbo-rails.js} +0 -0
  165. /data/vendor/javascript/{@hotwired--turbo.js → maglev/@hotwired--turbo.js} +0 -0
  166. /data/vendor/javascript/{@rails--actioncable--src.js → maglev/@rails--actioncable--src.js} +0 -0
  167. /data/vendor/javascript/{@rails--request.js.js → maglev/@rails--request.js.js} +0 -0
  168. /data/vendor/javascript/{@shopify--draggable.js → maglev/@shopify--draggable.js} +0 -0
  169. /data/vendor/javascript/{el-transition.js → maglev/el-transition.js} +0 -0
  170. /data/vendor/javascript/{stimulus-use.js → maglev/stimulus-use.js} +0 -0
  171. /data/vendor/javascript/{tiptap.bundle.js → maglev/tiptap.bundle.js} +0 -0
@@ -4,6 +4,7 @@ module Maglev
4
4
  module Editor
5
5
  class SectionBlocksController < Maglev::Editor::BaseController
6
6
  helper Maglev::Editor::SettingsHelper
7
+ helper_method :source_lock_version
7
8
 
8
9
  before_action :set_section
9
10
  before_action :set_section_block, only: %i[edit update destroy]
@@ -31,7 +32,6 @@ module Maglev
31
32
 
32
33
  def update
33
34
  update_section_block
34
- refresh_lock_version
35
35
  flash.now[:notice] = flash_t(:success)
36
36
  end
37
37
 
@@ -59,12 +59,12 @@ module Maglev
59
59
 
60
60
  def set_section
61
61
  @section = current_maglev_sections.find { |section| section.id == params[:section_id] }
62
- raise ActiveRecord::StaleObjectError unless @section
62
+ redirect_to editor_sections_path_with_context unless @section
63
63
  end
64
64
 
65
65
  def set_section_block
66
66
  @section_block = @section.blocks.find(params[:id])
67
- raise ActiveRecord::StaleObjectError unless @section_block
67
+ redirect_to editor_sections_path_with_context unless @section_block
68
68
  end
69
69
 
70
70
  def update_section_block
@@ -77,12 +77,12 @@ module Maglev
77
77
  )
78
78
  end
79
79
 
80
- def refresh_lock_version
81
- source = @section.site_scoped? ? maglev_site : current_maglev_page
82
- @section_block.lock_version = source.find_section_block_by_id(
83
- @section.id,
84
- @section_block.id
85
- )['lock_version']
80
+ def lock_source
81
+ @section.site_scoped? ? maglev_site : current_maglev_page
82
+ end
83
+
84
+ def source_lock_version
85
+ lock_source.lock_version || 0
86
86
  end
87
87
 
88
88
  def redirect_to_section_blocks_path(success: true)
@@ -96,6 +96,10 @@ module Maglev
96
96
 
97
97
  redirect_to path, status: :see_other, **flash
98
98
  end
99
+
100
+ def editor_sections_path_with_context
101
+ editor_sections_path(maglev_editing_route_context)
102
+ end
99
103
  end
100
104
  end
101
105
  end
@@ -4,7 +4,9 @@ module Maglev
4
4
  module Editor
5
5
  class SectionsController < Maglev::Editor::BaseController
6
6
  helper Maglev::Editor::SettingsHelper
7
+ helper_method :source_lock_version
7
8
 
9
+ before_action :ensure_turbo_frame_request, only: [:new]
8
10
  before_action :set_section, only: %i[edit update]
9
11
 
10
12
  def show
@@ -12,8 +14,11 @@ module Maglev
12
14
  end
13
15
 
14
16
  def new
15
- @grouped_sections = maglev_theme.sections.available_for(current_maglev_sections).grouped_by_category
17
+ set_query_and_category_id
16
18
  @position = (params[:position] || -1).to_i
19
+ @theme_sections = maglev_theme.sections.filter(current_maglev_sections, keyword: @query,
20
+ category_id: @category_id)
21
+ render layout: false
17
22
  end
18
23
 
19
24
  def create
@@ -34,7 +39,6 @@ module Maglev
34
39
 
35
40
  def update
36
41
  update_section
37
- refresh_lock_version
38
42
  flash.now[:notice] = flash_t(:success)
39
43
  end
40
44
 
@@ -56,7 +60,7 @@ module Maglev
56
60
 
57
61
  def set_section
58
62
  @section = current_maglev_sections.find { |section| section.id == params[:id] }
59
- raise ActiveRecord::StaleObjectError unless @section
63
+ redirect_to editor_sections_path_with_context unless @section
60
64
  end
61
65
 
62
66
  def update_section
@@ -73,6 +77,14 @@ module Maglev
73
77
  render 'index', status: :unprocessable_content
74
78
  end
75
79
 
80
+ def set_query_and_category_id
81
+ # we can't filter by both query and category_id in the same time
82
+ @query = params[:category_id].present? ? nil : params[:query]
83
+ # if no category_id is provided AND we don't have a query, we take the first category
84
+ @category_id = params[:category_id] || maglev_theme.section_categories.first.id
85
+ @category_id = nil if @query.present?
86
+ end
87
+
76
88
  def newly_added_section_to_flash
77
89
  # use flash because we can't pass directly the information to the redirect
78
90
  { section_id: @section[:id], position: current_maglev_page.position_of_section(@section[:id]) }
@@ -83,13 +95,20 @@ module Maglev
83
95
  headers['X-Section-Position'] = flash[:position]
84
96
  end
85
97
 
86
- def refresh_lock_version
87
- source = @section.site_scoped? ? maglev_site : current_maglev_page
88
- @section.lock_version = source.find_section_by_id(@section.id)['lock_version']
98
+ def lock_source
99
+ @section.site_scoped? ? maglev_site : current_maglev_page
100
+ end
101
+
102
+ def source_lock_version
103
+ lock_source.lock_version || 0
89
104
  end
90
105
 
91
106
  def redirect_to_sections_path
92
- redirect_to editor_sections_path(maglev_editing_route_context), notice: flash_t(:success), status: :see_other
107
+ redirect_to editor_sections_path_with_context, notice: flash_t(:success), status: :see_other
108
+ end
109
+
110
+ def editor_sections_path_with_context
111
+ editor_sections_path(maglev_editing_route_context)
93
112
  end
94
113
  end
95
114
  end
@@ -13,6 +13,10 @@ module Maglev
13
13
  helper Maglev::PagePreviewHelper
14
14
 
15
15
  def index
16
+ # use the title + SEO informations from the published payload
17
+ # Warning: the page may not exist even in draft
18
+ maglev_page&.apply_published_payload
19
+
16
20
  render_maglev_page
17
21
  end
18
22
 
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maglev
4
+ class SiteController < Maglev::ApplicationController
5
+ def create
6
+ redirect_to editor_root_path and return if Maglev::Site.exists? || !Rails.env.local?
7
+
8
+ Maglev::GenerateSite.call(
9
+ theme: Maglev.local_themes.first
10
+ )
11
+
12
+ redirect_to editor_root_path
13
+ end
14
+ end
15
+ end
@@ -3,6 +3,7 @@
3
3
  # rubocop:disable Metrics/ModuleLength
4
4
  module Maglev
5
5
  module ApplicationHelper
6
+ ## "system" helpers
6
7
  def turbo_stream
7
8
  # we don't want to pollute the global Turbo::Streams::TagBuilder
8
9
  Maglev::Turbo::Streams::TagBuilder.new(self)
@@ -26,6 +27,17 @@ module Maglev
26
27
  ], "\n"
27
28
  end
28
29
 
30
+ def maglev_delayed_stream_tag
31
+ # since we set the turbo request id before the build of the fetch request, Turbo will set
32
+ # the X-TURBO-REQUEST-ID header to a comma-separated list of request ids
33
+ # we only want to use the first request id (ours), so we split the header value and take the first part
34
+ safe_join [
35
+ tag.meta(name: 'turbo-request-id', content: ::Turbo.current_request_id.split(',').first),
36
+ tag.meta(name: 'turbo-delayed-stream', content: 'true')
37
+ ], "\n"
38
+ end
39
+
40
+ ## Editor helpers
29
41
  def maglev_editor_title
30
42
  case maglev_config.title
31
43
  when nil
@@ -93,20 +105,20 @@ module Maglev
93
105
  def maglev_button_classes(...)
94
106
  ClassVariants.build(
95
107
  base: %(
96
- rounded-xs transition-colors transition-background duration-200 text-center cursor-pointer
97
- group-[.is-success]/form:bg-green-500/95 group-[.is-success]/form:hover:bg-green-500/100
108
+ rounded-sm transition-colors transition-background duration-200 text-center cursor-pointer
109
+ group-[.is-success]/form:bg-green-500 group-[.is-success]/form:hover:bg-green-500/90
98
110
  group-[.is-success]/form:disabled:bg-green-500/75
99
- group-[.is-error]/form:bg-red-500/95 group-[.is-error]/form:hover:bg-red-500/100
111
+ group-[.is-error]/form:bg-red-500 group-[.is-error]/form:hover:bg-red-500/90
100
112
  group-[.is-error]/form:disabled:bg-red-500/75
101
113
  ),
102
114
  variants: {
103
115
  color: {
104
- primary: 'text-white bg-editor-primary/95 hover:bg-editor-primary/100 disabled:bg-editor-primary/75',
116
+ primary: 'text-white bg-editor-primary hover:bg-editor-primary/90 disabled:bg-editor-primary/75',
105
117
  secondary: 'text-gray-800 hover:bg-gray-100'
106
118
  },
107
119
  size: {
108
- big: 'flex items-center justify-center w-full px-6 py-4',
109
- medium: 'inline-flex items-center justify-center px-4 py-2'
120
+ big: 'flex items-center justify-center w-full px-6 h-14',
121
+ medium: 'inline-flex items-center justify-center px-4 h-10'
110
122
  }
111
123
  },
112
124
  defaults: {
@@ -139,9 +151,15 @@ module Maglev
139
151
  disappear_after: 3.seconds).with_content(message)
140
152
  end
141
153
 
142
- def maglev_page_icon(page, size: '1.15rem')
154
+ def maglev_page_icon(page, size: '1.15rem', wrapper_class_names: nil)
143
155
  icon_name = page.index? ? 'home' : 'file'
144
- render Maglev::Uikit::IconComponent.new(name: icon_name, size: size, class_names: 'shrink-0')
156
+ content_tag :span, class: class_names('shrink-0 relative', wrapper_class_names) do
157
+ if page.need_to_be_published?
158
+ concat(content_tag(:span, '',
159
+ class: 'absolute -bottom-0.25 right-0 bg-yellow-600 rounded-full w-1.5 h-1.5'))
160
+ end
161
+ concat render(Maglev::Uikit::IconComponent.new(name: icon_name, size: size))
162
+ end
145
163
  end
146
164
 
147
165
  def maglev_page_preview_reload_data
@@ -39,7 +39,7 @@ module Maglev
39
39
  def add_section_block_no_dropdown_button(section, block_type)
40
40
  button_to(
41
41
  t('maglev.editor.section_blocks.index.add_button'),
42
- editor_section_blocks_path(section, block_type: block_type, lock_version: section.lock_version,
42
+ editor_section_blocks_path(section, block_type: block_type, lock_version: source_lock_version,
43
43
  **maglev_editing_route_context),
44
44
  class: maglev_button_classes(size: :big),
45
45
  **section_block_button_common_attributes(section)
@@ -72,7 +72,7 @@ module Maglev
72
72
  def add_child_section_block_no_dropdown_button(section, parent_id, block_type)
73
73
  button_to(
74
74
  editor_section_blocks_path(section, block_type: block_type, parent_id: parent_id,
75
- lock_version: section.lock_version,
75
+ lock_version: source_lock_version,
76
76
  **maglev_editing_route_context),
77
77
  class: maglev_icon_button_classes,
78
78
  name: 'add_child_section_block',
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Style/ClassAndModuleChildren
4
+ module Maglev::Page::PublishableConcern
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ # force JSON column for MariaDB
9
+ attribute :published_payload, :json
10
+ end
11
+
12
+ def published?
13
+ published_at.present?
14
+ end
15
+
16
+ def need_to_be_published?
17
+ !published? || updated_at.blank? || updated_at > published_at
18
+ end
19
+
20
+ # opposite of #need_to_be_published?
21
+ def published_and_up_to_date?
22
+ published? && updated_at <= published_at
23
+ end
24
+
25
+ def discard_draft?
26
+ published? && updated_at > published_at
27
+ end
28
+
29
+ # called when a publishedpage is being previewed
30
+ def apply_published_payload
31
+ return if !published? || published_payload.blank?
32
+
33
+ published_payload_attributes.each do |attribute|
34
+ send("#{attribute}=", published_payload[attribute])
35
+ end
36
+ end
37
+
38
+ # called when a page is being published
39
+ def update_published_payload
40
+ # in MySQL, default values for json columns are not supported, so we need to set an empty hash if the value is nil
41
+ self.published_payload ||= {}
42
+
43
+ published_payload_attributes.each do |attribute|
44
+ value = send(attribute.to_sym)
45
+
46
+ # in MySQL, default values for json columns are not supported, so we need to set an empty hash if the value is nil
47
+ value = {} if attribute.ends_with?('_translations') && value.nil?
48
+
49
+ self.published_payload[attribute] = value
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def published_payload_attributes
56
+ published_payload_core_attributes + published_payload_additional_attributes
57
+ end
58
+
59
+ def published_payload_additional_attributes
60
+ # override this method to add additional attributes to the published payload
61
+ []
62
+ end
63
+
64
+ def published_payload_core_attributes
65
+ %w[title_translations seo_title_translations meta_description_translations og_title_translations
66
+ og_description_translations og_image_url_translations]
67
+ end
68
+ end
69
+ # rubocop:enable Style/ClassAndModuleChildren
@@ -10,6 +10,8 @@
10
10
  # og_description_translations :jsonb
11
11
  # og_image_url_translations :jsonb
12
12
  # og_title_translations :jsonb
13
+ # published_at :datetime
14
+ # published_payload :jsonb
13
15
  # sections_translations :jsonb
14
16
  # seo_title_translations :jsonb
15
17
  # title_translations :jsonb
@@ -24,6 +26,7 @@ module Maglev
24
26
  include Maglev::SectionsConcern
25
27
  include Maglev::Page::PathConcern
26
28
  include Maglev::Page::SearchConcern
29
+ include Maglev::Page::PublishableConcern
27
30
 
28
31
  ## associations ##
29
32
  has_many :sections_content_stores, as: :container, dependent: :destroy
@@ -30,6 +30,10 @@ class Maglev::Section::Block
30
30
  settings.select(&:advanced?)
31
31
  end
32
32
 
33
+ def empty?
34
+ settings.none?
35
+ end
36
+
33
37
  def as_json
34
38
  super(only: %i[name type settings root accept])
35
39
  end
@@ -85,13 +85,15 @@ module Maglev::Section::ContentConcern
85
85
  return [] if blocks.blank?
86
86
 
87
87
  blocks.map do |block|
88
+ next unless block.root?
89
+
88
90
  3.times.to_a.map do
89
91
  {
90
92
  type: block.type,
91
93
  settings: fallback_build_default_settings_content(block.settings)
92
94
  }
93
95
  end
94
- end.flatten
96
+ end.compact.flatten
95
97
  end
96
98
  end
97
99
  # rubocop:enable Style/ClassAndModuleChildren
@@ -78,6 +78,14 @@ module Maglev
78
78
  "maglev.themes.#{theme.id}.sections.#{id}"
79
79
  end
80
80
 
81
+ def empty?
82
+ settings.none? && blocks.none?
83
+ end
84
+
85
+ def local_screenshot?
86
+ screenshot_timestamp.present?
87
+ end
88
+
81
89
  ## class methods ##
82
90
 
83
91
  def self.build(hash)
@@ -104,7 +112,7 @@ module Maglev
104
112
 
105
113
  class Store
106
114
  extend Forwardable
107
- def_delegators :@array, :all, :first, :last, :count, :each, :each_with_index, :map, :group_by
115
+ def_delegators :@array, :all, :first, :last, :count, :each, :each_with_index, :map, :group_by, :any?
108
116
 
109
117
  attr_reader :array
110
118
 
@@ -134,6 +142,18 @@ module Maglev
134
142
  self.class.new(new_array)
135
143
  end
136
144
 
145
+ def filter(sections_content, keyword: nil, category_id: nil)
146
+ new_array = if keyword.present?
147
+ @array.select { |section| section.name.downcase.include?(keyword.downcase) }
148
+ elsif category_id.present?
149
+ @array.select { |section| section.category == category_id }
150
+ else
151
+ @array
152
+ end
153
+
154
+ self.class.new(new_array).available_for(sections_content)
155
+ end
156
+
137
157
  def as_json(**_options)
138
158
  @array.as_json
139
159
  end
@@ -14,9 +14,7 @@
14
14
  #
15
15
  # Indexes
16
16
  #
17
- # index_maglev_sections_content_stores_on_published (published)
18
- # maglev_sections_content_stores_container (container_id,container_type)
19
- # maglev_sections_content_stores_container_and_published (container_id,container_type,published)
17
+ # maglev_sections_content_stores_container_and_published (container_id,container_type,published) UNIQUE
20
18
  #
21
19
  module Maglev
22
20
  class SectionsContentStore < ApplicationRecord
@@ -8,6 +8,7 @@
8
8
  # locales :jsonb
9
9
  # lock_version :integer
10
10
  # name :string
11
+ # published_at :datetime
11
12
  # sections_translations :jsonb
12
13
  # style :jsonb
13
14
  # created_at :datetime not null
@@ -35,6 +36,10 @@ module Maglev
35
36
 
36
37
  ## methods ##
37
38
 
39
+ def published?
40
+ published_at.present?
41
+ end
42
+
38
43
  def api_attributes
39
44
  %i[id name]
40
45
  end
@@ -68,22 +68,19 @@ module Maglev
68
68
  end
69
69
 
70
70
  def check_section_lock_version!(source)
71
- check_lock_version!(source, find_section(source), 'update_section')
71
+ check_lock_version!(source, 'update_section')
72
72
  end
73
73
 
74
74
  def check_block_lock_version!(source)
75
- check_lock_version!(source, find_block(source), 'update_block')
75
+ check_lock_version!(source, 'update_block')
76
76
  end
77
77
 
78
- def check_lock_version!(source, section_or_block, action_name)
78
+ def check_lock_version!(source, action_name)
79
79
  return if lock_version.blank? # without a lock version, we disable the lock version check
80
80
 
81
- current_lock_version = section_or_block['lock_version'].to_i
81
+ current_lock_version = source.lock_version.to_i
82
+ source.lock_version = lock_version.to_i
82
83
 
83
- # always increment the lock version
84
- section_or_block['lock_version'] = lock_version.to_i + 1
85
-
86
- # if the lock version is the same, we don't need to raise an error
87
84
  return if current_lock_version == lock_version.to_i
88
85
 
89
86
  raise ActiveRecord::StaleObjectError.new(source, action_name)
@@ -8,7 +8,7 @@ module Maglev
8
8
  Maglev.config
9
9
  end
10
10
 
11
- # hold the Rails request context
11
+ # hold the Rails HTTP request context
12
12
  dependency :context
13
13
 
14
14
  dependency :fetch_site, class: Maglev::FetchSite, depends_on: %i[config context]
@@ -19,7 +19,7 @@ module Maglev
19
19
  dependency :fetch_section_screenshot_path, class: Maglev::FetchSectionScreenshotPath,
20
20
  depends_on: :fetch_sections_path
21
21
  dependency :fetch_section_screenshot_url, class: Maglev::FetchSectionScreenshotUrl,
22
- depends_on: :fetch_section_screenshot_path
22
+ depends_on: %i[fetch_section_screenshot_path context]
23
23
 
24
24
  dependency :fetch_collection_items, class: Maglev::FetchCollectionItems, depends_on: %i[fetch_site config]
25
25
  dependency :fetch_static_pages, class: Maglev::FetchStaticPages, depends_on: %i[config]
@@ -67,7 +67,9 @@ module Maglev
67
67
  dependency :sort_section_blocks, class: Maglev::Content::SortSectionBlocksService,
68
68
  depends_on: %i[fetch_site fetch_theme]
69
69
 
70
+ dependency :has_unpublished_changes, class: Maglev::HasUnpublishedChanges
70
71
  dependency :publish, class: Maglev::PublishService
72
+ dependency :discard_page_draft, class: Maglev::DiscardPageDraftService
71
73
 
72
74
  def call
73
75
  self
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maglev
4
+ class DiscardPageDraftService
5
+ include Injectable
6
+
7
+ argument :site
8
+ argument :page
9
+
10
+ def call
11
+ ActiveRecord::Base.transaction do
12
+ revert_container_sections!(site)
13
+ revert_container_sections!(page)
14
+ revert_page_information!
15
+ end
16
+ true
17
+ end
18
+
19
+ private
20
+
21
+ def revert_container_sections!(container)
22
+ store = find_published_store(container)
23
+
24
+ raise Maglev::Errors::UnpublishedPage if store.blank?
25
+
26
+ container.sections_translations_will_change!
27
+ container.sections_translations = store.sections_translations
28
+ container.save!
29
+
30
+ # Update updated_at to be before published_at to mark as up-to-date
31
+ # rubocop:disable Rails/SkipsModelValidations
32
+ container.update_column(:updated_at, container.published_at) if container.published_at.present?
33
+ # rubocop:enable Rails/SkipsModelValidations
34
+ end
35
+
36
+ def find_published_store(container)
37
+ container.sections_content_stores.published.first
38
+ end
39
+
40
+ def revert_page_information!
41
+ page.apply_published_payload
42
+ page.save!
43
+ end
44
+ end
45
+ end
@@ -5,9 +5,13 @@ module Maglev
5
5
  include Injectable
6
6
 
7
7
  dependency :fetch_section_screenshot_path
8
+ dependency :context
9
+
8
10
  argument :section
9
11
 
10
12
  def call
13
+ return nil unless section.local_screenshot?
14
+
11
15
  screenshot_path = fetch_section_screenshot_path.call(section: section) + query_string
12
16
  asset_host ? URI.join(asset_host, screenshot_path).to_s : screenshot_path
13
17
  end
@@ -15,11 +19,18 @@ module Maglev
15
19
  private
16
20
 
17
21
  def asset_host
18
- Rails.application.config.asset_host
22
+ host = Rails.application.config.asset_host
23
+ return nil if host.blank?
24
+
25
+ host.start_with?('http://', 'https://') ? host : "#{request_protocol}#{host}"
19
26
  end
20
27
 
21
28
  def query_string
22
29
  "?#{section.screenshot_timestamp}"
23
30
  end
31
+
32
+ def request_protocol
33
+ context.controller.request.protocol
34
+ end
24
35
  end
25
36
  end
@@ -14,7 +14,9 @@ module Maglev
14
14
  private
15
15
 
16
16
  def site
17
- @site ||= Maglev::Site.first
17
+ @site ||= Maglev::Site.first.tap do |site|
18
+ raise Maglev::Errors::SiteNotFound unless site
19
+ end
18
20
  end
19
21
 
20
22
  def change_default_locales(site)
@@ -15,7 +15,7 @@ module Maglev
15
15
  argument :locale, default: nil
16
16
 
17
17
  def call
18
- fetch_container_store(page).sections.map do |section|
18
+ (fetch_container_store(page).sections || []).map do |section|
19
19
  transform_section(section.dup)
20
20
  end.compact
21
21
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maglev
4
+ class HasUnpublishedChanges
5
+ include Injectable
6
+
7
+ argument :site
8
+ argument :page
9
+ argument :theme
10
+
11
+ def call
12
+ page.need_to_be_published? || site_need_to_be_published?
13
+ end
14
+
15
+ private
16
+
17
+ def site_need_to_be_published?
18
+ !site.published? || site.updated_at.blank? || site.updated_at > site.published_at
19
+ end
20
+ end
21
+ end
@@ -9,22 +9,38 @@ module Maglev
9
9
 
10
10
  def call
11
11
  ActiveRecord::Base.transaction do
12
- publish_container_sections!(site)
13
- publish_container_sections!(page)
12
+ unsafe_call
14
13
  end
15
14
  true
16
15
  end
17
16
 
18
17
  private
19
18
 
19
+ def unsafe_call
20
+ # copy content from the containers (site and page) to the published stores
21
+ publish_container_sections!(site)
22
+ publish_container_sections!(page)
23
+
24
+ # copy the page information to the page published payload
25
+ publish_page_information!
26
+ end
27
+
20
28
  def publish_container_sections!(container)
21
29
  store = find_or_build_published_store(container)
22
30
  store.sections_translations = container.sections_translations
23
31
  store.save!
32
+ # mark the container as published.
33
+ # We need to add a delay to ensure that published_at will be posterior to the native updated_at of the container.
34
+ container.update(published_at: Time.current + 0.2.seconds)
24
35
  end
25
36
 
26
37
  def find_or_build_published_store(container)
27
38
  container.sections_content_stores.find_or_initialize_by(container: container, published: true)
28
39
  end
40
+
41
+ def publish_page_information!
42
+ page.update_published_payload
43
+ page.save!
44
+ end
29
45
  end
30
46
  end