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
@@ -1,56 +1,49 @@
1
- <%= render Maglev::Uikit::MenuDropdownComponent.new(icon_name: icon_name) do |dropdown| %>
2
- <% dropdown.with_item do %>
3
- <%= link_to paths[:edit], class: dropdown.item_classes, data: { turbo_frame: '_top' } do %>
4
- <%= render Maglev::Uikit::IconComponent.new(name: 'settings', class_names: 'shrink-0') %>
5
- <span class="ml-2 whitespace-nowrap truncate overflow-hidden"><%= t('maglev.editor.pages.actions.edit') %></span>
6
- <% end %>
1
+ <%= render Maglev::Uikit::MenuDropdownComponent.new(icon_name: icon_name) do |menu_dropdown| %>
2
+ <% menu_dropdown.with_item_link_to(paths[:edit], data: { turbo_frame: '_top' }) do |menu_item| %>
3
+ <% menu_item.with_icon_content('settings') %>
4
+ <% menu_item.with_label_content(t('maglev.editor.pages.actions.edit')) %>
7
5
  <% end if allow?(:edit) %>
8
6
 
9
- <% dropdown.with_item do %>
10
- <%= render Maglev::Uikit::MenuDropdownComponent.new(placement: 'right-start') do |sub_menu| %>
11
- <% sub_menu.with_trigger do %>
12
- <%= button_tag class: sub_menu.item_classes, data: { action: 'click->uikit-dropdown#toggle', 'uikit-dropdown-target': 'button' } do %>
13
- <%= render Maglev::Uikit::IconComponent.new(name: 'preview', class_names: 'shrink-0') %>
14
- <span class="ml-2 whitespace-nowrap truncate overflow-hidden"><%= t('maglev.editor.pages.actions.preview') %></span>
15
- <% end %>
16
- <% end %>
17
- <% sub_menu.with_item do %>
18
- <%= link_to t('maglev.editor.pages.actions.draft_preview'), paths[:preview], class: sub_menu.item_classes, target: '_blank' %>
19
- <% end %>
20
- <% sub_menu.with_item do %>
21
- <%= link_to t('maglev.editor.pages.actions.live_preview'), live_url, class: sub_menu.item_classes, target: '_blank' %>
22
- <% end %>
7
+ <% menu_dropdown.with_item_divider if allow?(:edit) %>
8
+
9
+ <% menu_dropdown.with_item_nested_menu(placement: 'right-start') do |menu_item| %>
10
+ <% menu_item.with_icon_content('preview') %>
11
+ <% menu_item.with_label_content(t('maglev.editor.pages.actions.preview')) %>
12
+
13
+ <% menu_item.with_item_link_to(paths[:preview], target: '_blank') do |sub_menu_item| %>
14
+ <% sub_menu_item.with_label_content(t('maglev.editor.pages.actions.draft_preview')) %>
23
15
  <% end %>
24
- <% end if allow?(:preview) %>
25
16
 
26
- <% dropdown.with_item do %>
27
- <%= button_to paths[:clone], class: dropdown.form_item_classes, form_class: dropdown.item_classes do %>
28
- <%= render Maglev::Uikit::IconComponent.new(name: 'clone', class_names: 'shrink-0') %>
29
- <span class="ml-2 whitespace-nowrap truncate overflow-hidden"><%= t('maglev.editor.pages.actions.clone') %></span>
17
+ <% menu_item.with_item_link_to(live_url, target: '_blank') do |sub_menu_item| %>
18
+ <% sub_menu_item.with_label_content(t('maglev.editor.pages.actions.live_preview')) %>
19
+ <% end if live_url.present? %>
20
+
21
+ <% menu_item.with_item_text(variant: 'subdued') do |text_item| %>
22
+ <% text_item.with_label_content(t('maglev.editor.pages.actions.no_live_preview')) %>
23
+ <% end if live_url.blank? %>
24
+ <% end %>
25
+
26
+ <% menu_dropdown.with_item_wrapper(data: { controller: 'copy-to-clipboard', copy_to_clipboard_source_value: live_url }) do |wrapper_item| %>
27
+ <% wrapper_item.with_item_button(data: { copy_to_clipboard_target: 'text', action: 'click->copy-to-clipboard#copy:prevent' }) do |menu_item| %>
28
+ <% menu_item.with_icon_content('clipboard') %>
29
+ <% menu_item.with_label_content(t('maglev.editor.pages.actions.copy_url_to_clipboard')) %>
30
30
  <% end %>
31
- <% end if allow?(:clone) %>
32
31
 
33
- <% dropdown.with_item do %>
34
- <div data-controller="copy-to-clipboard" data-copy-to-clipboard-source-value="<%= live_url %>">
35
- <%= link_to '#', class: dropdown.item_classes, data: { copy_to_clipboard_target: 'text', action: 'click->copy-to-clipboard#copy:prevent' } do %>
36
- <%= render Maglev::Uikit::IconComponent.new(name: 'clipboard', class_names: 'shrink-0') %>
37
- <span class="ml-2 whitespace-nowrap truncate overflow-hidden">
38
- <%= t('maglev.editor.pages.actions.copy_url_to_clipboard') %>
39
- </span>
40
- <% end %>
41
- <div class="<%= dropdown.item_classes %> hidden" data-copy-to-clipboard-target="success">
42
- <%= render Maglev::Uikit::IconComponent.new(name: 'checkbox_circle', class_names: 'text-green-500 shrink-0') %>
43
- <span class="ml-2 whitespace-nowrap truncate overflow-hidden text-green-500">
44
- <%= t('maglev.editor.pages.actions.copy_url_to_clipboard_success') %>
45
- </span>
46
- </div>
47
- </div>
32
+ <% wrapper_item.with_item_button(class: 'hidden text-green-500', data: { copy_to_clipboard_target: 'success' }) do |menu_item| %>
33
+ <% menu_item.with_icon_content('checkbox_circle') %>
34
+ <% menu_item.with_label_content(t('maglev.editor.pages.actions.copy_url_to_clipboard_success')) %>
35
+ <% end %>
48
36
  <% end if allow?(:copy_url_to_clipboard) %>
49
37
 
50
- <% dropdown.with_item do %>
51
- <%= button_to paths[:delete], method: :delete, class: dropdown.item_classes, form_class: dropdown.form_item_classes, data: { turbo_confirm: t('maglev.editor.pages.actions.confirm_delete'), turbo_frame: '_top' } do %>
52
- <%= render Maglev::Uikit::IconComponent.new(name: 'delete_bin', class_names: 'shrink-0') %>
53
- <span class="ml-2 whitespace-nowrap truncate overflow-hidden"><%= t('maglev.editor.pages.actions.delete') %></span>
54
- <% end %>
38
+ <% menu_dropdown.with_item_divider if allow?(:clone) || allow?(:delete) %>
39
+
40
+ <% menu_dropdown.with_item_button_to(paths[:clone], data: { turbo_frame: '_top' }) do |menu_item| %>
41
+ <% menu_item.with_icon_content('clone') %>
42
+ <% menu_item.with_label_content(t('maglev.editor.pages.actions.clone')) %>
43
+ <% end if allow?(:clone) %>
44
+
45
+ <% menu_dropdown.with_item_button_to(paths[:delete], method: :delete, variant: 'danger', data: { turbo_confirm: t('maglev.editor.pages.actions.confirm_delete'), turbo_frame: '_top' }) do |menu_item| %>
46
+ <% menu_item.with_icon_content('delete_bin') %>
47
+ <% menu_item.with_label_content(t('maglev.editor.pages.actions.delete')) %>
55
48
  <% end if allow?(:delete) %>
56
49
  <% end %>
@@ -27,20 +27,17 @@
27
27
  <% end %>
28
28
  </div>
29
29
 
30
- <div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
31
- <div>
32
- <p class="text-sm">
30
+ <div class="hidden sm:flex sm:flex-1 sm:items-center <%= show_info? ? 'sm:justify-between' : 'sm:justify-center' %>">
31
+ <p class="text-sm <%= 'hidden' unless show_info? %>">
33
32
  <%== pagy_info(pagy, item_name: item_name) %>
34
- </p>
35
- </div>
33
+ </p>
36
34
 
37
- <div class="<%= 'hidden' if pagy.series.size == 1 %>">
38
35
  <nav class="isolate inline-flex gap-1" aria-label="Pages">
39
36
  <%# Previous page link %>
40
37
  <% if pagy.prev %>
41
- <%== a.(pagy.prev, left_arrow, aria_label: 'Previous', classes: 'relative inline-flex items-center rounded-sm px-1 py-1 text-gray-400 hover:bg-gray-100 focus:z-20 focus:outline-offset-0') %>
38
+ <%== a.(pagy.prev, left_arrow, aria_label: 'Previous', classes: 'relative inline-flex items-center rounded-xs px-1 py-1 text-gray-400 hover:bg-gray-100 focus:z-20 focus:outline-offset-0') %>
42
39
  <% else %>
43
- <a role="link" aria-disabled="true" aria-label="Previous" class="relative inline-flex items-center rounded-sm px-1 py-1 text-gray-400 hover:bg-gray-100 focus:z-20 focus:outline-offset-0">
40
+ <a role="link" aria-disabled="true" aria-label="Previous" class="relative inline-flex items-center rounded-xs px-1 py-1 text-gray-400 hover:bg-gray-100 focus:z-20 focus:outline-offset-0">
44
41
  <%= left_arrow %>
45
42
  </a>
46
43
  <% end %>
@@ -48,9 +45,9 @@
48
45
  <%# Page links (series example: [1, :gap, 7, 8, "9", 10, 11, :gap, 36]) %>
49
46
  <% pagy.series.each do |item| %>
50
47
  <% if item.is_a?(Integer) %>
51
- <%== a.(item, classes: 'relative inline-flex items-center px-2.5 py-1 text-sm font-semibold text-gray-900 hover:bg-gray-100 focus:z-20 focus:outline-offset-0') %>
48
+ <%== a.(item, classes: 'relative inline-flex items-center px-2.5 py-1 text-sm font-semibold text-gray-900 rounded-xs hover:bg-gray-100 focus:z-20 focus:outline-offset-0') %>
52
49
  <% elsif item.is_a?(String) %>
53
- <a role="link" aria-disabled="true" aria-current="page" class="relative z-10 inline-flex items-center bg-editor-primary px-2.5 py-1 text-sm font-semibold text-white focus:z-20"><%= item %></a>
50
+ <a role="link" aria-disabled="true" aria-current="page" class="relative z-10 inline-flex items-center rounded-xs bg-editor-primary px-2.5 py-1 text-sm font-semibold text-white focus:z-20"><%= item %></a>
54
51
  <% elsif item == :gap %>
55
52
  <a role="link" aria-disabled="true" class="relative inline-flex items-center px-2.5 py-1 text-sm font-semibold text-gray-700 focus:outline-offset-0">&hellip;</a>
56
53
  <% end %>
@@ -58,9 +55,9 @@
58
55
 
59
56
  <%# Next page link %>
60
57
  <% if pagy.next %>
61
- <%== a.(pagy.next, right_arrow, aria_label: 'Next', classes: 'relative inline-flex items-center rounded-sm px-1 py-1 text-gray-400 hover:bg-gray-100 focus:z-20 focus:outline-offset-0') %>
58
+ <%== a.(pagy.next, right_arrow, aria_label: 'Next', classes: 'relative inline-flex items-center rounded-xs px-1 py-1 text-gray-400 hover:bg-gray-100 focus:z-20 focus:outline-offset-0') %>
62
59
  <% else %>
63
- <a role="link" aria-disabled="true" aria-label="Next" class="relative inline-flex items-center rounded-sm px-1 py-1 text-gray-400 hover:bg-gray-100 focus:z-20 focus:outline-offset-0">
60
+ <a role="link" aria-disabled="true" aria-label="Next" class="relative inline-flex items-center rounded-xs px-1 py-1 text-gray-400 hover:bg-gray-100 focus:z-20 focus:outline-offset-0">
64
61
  <%= right_arrow %>
65
62
  </a>
66
63
  <% end %>
@@ -7,10 +7,11 @@ module Maglev
7
7
 
8
8
  attr_reader :pagy, :hidden_if_single_page
9
9
 
10
- def initialize(pagy:, item_name: 'item', hidden_if_single_page: true)
10
+ def initialize(pagy:, item_name: 'item', hidden_if_single_page: true, show_info: true)
11
11
  @pagy = pagy
12
12
  @hidden_if_single_page = hidden_if_single_page
13
13
  @item_name = item_name
14
+ @show_info = show_info
14
15
  end
15
16
 
16
17
  def item_name
@@ -20,6 +21,10 @@ module Maglev
20
21
  def render?
21
22
  !(hidden_if_single_page && pagy.pages < 2)
22
23
  end
24
+
25
+ def show_info?
26
+ @show_info
27
+ end
23
28
  end
24
29
  end
25
30
  end
@@ -7,7 +7,7 @@
7
7
  data-transition-leave-start="translate-y-0 opacity-100"
8
8
  data-transition-leave-end="translate-y-[50%] opacity-0"
9
9
  >
10
- <%= link_to paths[:add], class: button_classes, data: { turbo_frame: '_top' } do %>
10
+ <%= link_to paths[:add], class: button_classes, data: { turbo_frame: 'modal' } do %>
11
11
  <%= render Maglev::Uikit::IconComponent.new(name: 'add') %>
12
12
  <% end if insert_button? %>
13
13
  </div>
@@ -8,9 +8,9 @@
8
8
  <input type="hidden" name="active_tab" value="<%= active_tab_index %>" data-uikit-tabs-target="hiddenInput" />
9
9
 
10
10
  <nav>
11
- <ul class="flex flex-col sm:flex-row">
11
+ <ul class="flex flex-row">
12
12
  <% tabs.each_with_index do |tab, index| %>
13
- <li>
13
+ <li class="shrink-0">
14
14
  <% if tab.link? %>
15
15
  <%= link_to tab.label, tab.link[:url], class: label_classes(active: tab.active?), **tab.link_html_options %>
16
16
  <% else %>
@@ -25,11 +25,14 @@
25
25
  </ul>
26
26
  </nav>
27
27
 
28
- <div class="relative -mt-0.5"><div class="w-full border-gray-200 border-t-2 h-0"></div></div>
28
+ <% if variant == :default %>
29
+ <div class="relative -mt-0.5"><div class="w-full border-gray-200 border-t-2 h-0"></div></div>
30
+ <% end %>
29
31
 
30
32
  <% tabs.each_with_index do |tab, index| %>
33
+ <% next unless tab.block? %>
31
34
  <div data-uikit-tabs-target="tabContent" class="flex-1 mt-4 pt-2 pb-4 overflow-y-auto overflow-x-visible <%= 'hidden' unless tab.active? %>">
32
- <%= capture(&tab.block) if tab.block? %>
35
+ <%= capture(&tab.block) %>
33
36
  </div>
34
37
  <% end %>
35
38
  </div>
@@ -7,16 +7,21 @@ module Maglev
7
7
  Tab.new(self, label: label, active: active, link: link, block: block)
8
8
  }
9
9
 
10
- attr_reader :container_classes, :active_tab, :active_tab_index, :disable_inputs
10
+ attr_reader :container_classes, :active_tab, :active_tab_index, :disable_inputs, :variant
11
11
 
12
12
  # disable_inputs: true | false (default: false) if true, the inputs of hidden tabs will not be submitted
13
- def initialize(container_classes: nil, active_tab_index: nil, disable_inputs: false)
13
+ def initialize(container_classes: nil, active_tab_index: nil, disable_inputs: false, variant: :default)
14
14
  @container_classes = container_classes
15
15
  @active_tab_index = active_tab_index
16
16
  @disable_inputs = disable_inputs
17
+ @variant = variant
17
18
  end
18
19
 
19
20
  def label_classes(...)
21
+ variant == :pills ? pills_label_classes(...) : default_label_classes(...)
22
+ end
23
+
24
+ def default_label_classes(...)
20
25
  class_variants(
21
26
  base: %(
22
27
  relative py-1 pb-0 px-4 block focus:outline-none border-b-2
@@ -29,9 +34,23 @@ module Maglev
29
34
  ).render(...)
30
35
  end
31
36
 
37
+ def pills_label_classes(...)
38
+ class_variants(
39
+ base: %(rounded-md px-3 py-2 text-sm),
40
+ variants: {
41
+ active: 'rounded-md bg-editor-primary/10 font-semibold text-editor-primary',
42
+ '!active': 'text-gray-500 hover:text-gray-700 font-medium'
43
+ }
44
+ ).render(...)
45
+ end
46
+
32
47
  def before_render
33
- @active_tab = tabs.find(&:active?) || tabs[active_tab_index.to_i]
34
- @active_tab.active = true
48
+ @active_tab = tabs.find(&:active?)
49
+
50
+ return unless !@active_tab && active_tab_index != -1
51
+
52
+ @active_tab = tabs[active_tab_index.to_i]
53
+ @active_tab&.active = true
35
54
  end
36
55
 
37
56
  def disable_inputs?
@@ -0,0 +1,15 @@
1
+ <div class="bg-gray-50 sm:rounded-lg">
2
+ <div class="px-4 py-5 sm:p-6">
3
+ <h3 class="text-base font-semibold text-gray-900">
4
+ <%= title %>
5
+ </h3>
6
+ <div class="mt-2 max-w-xl text-sm text-gray-500 space-y-2">
7
+ <%= description %>
8
+ </div>
9
+ <% if action? %>
10
+ <div class="mt-5">
11
+ <%= action %>
12
+ </div>
13
+ <% end %>
14
+ </div>
15
+ </div>
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maglev
4
+ module Uikit
5
+ module Well
6
+ class SimpleWellComponent < ViewComponent::Base
7
+ renders_one :title
8
+ renders_one :description
9
+ renders_one :action
10
+ end
11
+ end
12
+ end
13
+ end
@@ -7,23 +7,16 @@ module Maglev
7
7
 
8
8
  included do
9
9
  around_action :handle_editor_errors
10
-
11
- rescue_from ActiveRecord::StaleObjectError, with: :handle_stale_object
12
10
  end
13
11
 
14
12
  private
15
13
 
16
- def handle_stale_object
17
- respond_to do |format|
18
- format.turbo_stream { render 'maglev/editor/shared/errors/stale_object_error' }
19
- format.html { redirect_to editor_root_path }
20
- end
21
- end
22
-
23
14
  def handle_editor_errors
24
15
  yield
25
16
  rescue Maglev::Errors::NotAuthorized
26
17
  raise # Let the main app handle this one
18
+ rescue ActiveRecord::StaleObjectError
19
+ handle_stale_object
27
20
  rescue StandardError => e
28
21
  respond_to do |format|
29
22
  format.turbo_stream { render_turbo_stream_standard_error(e) }
@@ -31,6 +24,13 @@ module Maglev
31
24
  end
32
25
  end
33
26
 
27
+ def handle_stale_object
28
+ respond_to do |format|
29
+ format.turbo_stream { render 'maglev/editor/shared/errors/stale_object_error' }
30
+ format.html { redirect_to editor_root_path }
31
+ end
32
+ end
33
+
34
34
  def render_turbo_stream_standard_error(error)
35
35
  track_maglev_error(error)
36
36
  render 'maglev/editor/shared/errors/standard_error', status: :internal_server_error
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maglev
4
+ module Editor
5
+ module PreviewUrlsConcern
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ helper_method :current_maglev_page_urls, :maglev_page_live_url, :maglev_page_preview_url
10
+ end
11
+
12
+ private
13
+
14
+ def current_maglev_page_urls
15
+ {
16
+ path: maglev_services.get_page_fullpath.call(page: current_maglev_page, preview_mode: false,
17
+ locale: content_locale),
18
+ preview: maglev_page_preview_url(current_maglev_page),
19
+ live: maglev_page_live_url(current_maglev_page)
20
+ }
21
+ end
22
+
23
+ def maglev_page_live_url(page)
24
+ maglev_services.get_page_fullpath.call(page: page, preview_mode: false, locale: content_locale)
25
+ end
26
+
27
+ def maglev_page_preview_url(page)
28
+ maglev_services.get_page_fullpath.call(page: page, preview_mode: true, locale: content_locale)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maglev
4
+ module Editor
5
+ module TurboConcern
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ helper_method :maglev_disable_turbo_cache?
10
+ end
11
+
12
+ private
13
+
14
+ def maglev_disable_turbo_cache
15
+ @maglev_disable_turbo_cache = true
16
+ end
17
+
18
+ def maglev_disable_turbo_cache?
19
+ !!@maglev_disable_turbo_cache
20
+ end
21
+
22
+ def ensure_turbo_frame_request
23
+ return if turbo_frame_request?
24
+
25
+ redirect_to editor_root_path
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maglev
4
+ module ErrorsConcern
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ rescue_from ::Maglev::Errors::SiteNotFound, with: :handle_site_not_found if Rails.env.development?
9
+ end
10
+
11
+ private
12
+
13
+ def handle_site_not_found
14
+ render 'maglev/errors/site_not_found', layout: nil, status: :not_found
15
+ end
16
+ end
17
+ end
@@ -5,6 +5,7 @@ module Maglev
5
5
  private
6
6
 
7
7
  def flash_t(type, **opts)
8
+ controller_name = controller_path.gsub('maglev/editor/', '').gsub('/', '.')
8
9
  t("maglev.editor.flash.#{controller_name}.#{action_name}.#{type}", **opts)
9
10
  end
10
11
  end
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Maglev
4
- class ApplicationController < ::ApplicationController
4
+ class ApplicationController < Maglev.parent_controller_class
5
5
  include Maglev::ServicesConcern
6
6
  include Maglev::ResourceIdConcern
7
+ include Maglev::ErrorsConcern
7
8
 
8
9
  protect_from_forgery with: :exception
9
10
 
@@ -8,7 +8,8 @@ module Maglev
8
8
  private
9
9
 
10
10
  def set_blob
11
- @blob = Maglev::Asset.find(resource_id).file
11
+ blob = Maglev::Asset.find(resource_id).file.blob
12
+ @blob = Maglev::ActiveStorage::ServingBlob.new(blob)
12
13
  rescue ActiveRecord::RecordNotFound
13
14
  head :not_found
14
15
  end
@@ -36,7 +36,7 @@ module Maglev
36
36
  end
37
37
 
38
38
  def per_page
39
- 16 # TODO: make this configurable
39
+ maglev_config.per_page(:assets)
40
40
  end
41
41
 
42
42
  def resources
@@ -13,6 +13,8 @@ module Maglev
13
13
  include Maglev::ServicesConcern
14
14
  include Maglev::FlashI18nConcern
15
15
  include Maglev::Editor::ErrorsConcern
16
+ include Maglev::Editor::TurboConcern
17
+ include Maglev::Editor::PreviewUrlsConcern
16
18
 
17
19
  before_action :fetch_maglev_site
18
20
  before_action :fetch_maglev_page
@@ -20,9 +22,8 @@ module Maglev
20
22
 
21
23
  helper Maglev::ApplicationHelper
22
24
  helper_method :maglev_site, :maglev_theme,
23
- :current_maglev_page, :current_maglev_sections, :current_maglev_page_urls,
24
- :maglev_editing_route_context, :maglev_disable_turbo_cache?,
25
- :maglev_page_live_url, :maglev_page_preview_url
25
+ :current_maglev_page, :current_maglev_sections,
26
+ :maglev_editing_route_context, :unpublished_changes?
26
27
 
27
28
  private
28
29
 
@@ -61,21 +62,8 @@ module Maglev
61
62
  )
62
63
  end
63
64
 
64
- def current_maglev_page_urls
65
- {
66
- path: maglev_services.get_page_fullpath.call(page: current_maglev_page, preview_mode: false,
67
- locale: content_locale),
68
- preview: maglev_page_preview_url(current_maglev_page),
69
- live: maglev_page_live_url(current_maglev_page)
70
- }
71
- end
72
-
73
- def maglev_page_live_url(page)
74
- maglev_services.get_page_fullpath.call(page: page, preview_mode: false, locale: content_locale)
75
- end
76
-
77
- def maglev_page_preview_url(page)
78
- maglev_services.get_page_fullpath.call(page: page, preview_mode: true, locale: content_locale)
65
+ def unpublished_changes?
66
+ maglev_services.has_unpublished_changes.call(site: maglev_site, page: current_maglev_page, theme: maglev_theme)
79
67
  end
80
68
 
81
69
  def maglev_theme
@@ -89,20 +77,6 @@ module Maglev
89
77
  }.compact_blank
90
78
  end
91
79
 
92
- def maglev_disable_turbo_cache
93
- @maglev_disable_turbo_cache = true
94
- end
95
-
96
- def maglev_disable_turbo_cache?
97
- !!@maglev_disable_turbo_cache
98
- end
99
-
100
- def ensure_turbo_frame_request
101
- return if turbo_frame_request?
102
-
103
- redirect_to editor_root_path
104
- end
105
-
106
80
  def redirect_to_real_root
107
81
  redirect_to editor_real_root_path(maglev_editing_route_context)
108
82
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maglev
4
+ module Editor
5
+ module Pages
6
+ class CloneController < Maglev::Editor::BaseController
7
+ def create
8
+ @source_page = maglev_page_resources.find(params[:id])
9
+ @page = clone_page(@source_page)
10
+ edit_cloned_page_path = edit_editor_page_path(@page, maglev_editing_route_context(page: @page))
11
+ redirect_to edit_cloned_page_path, notice: flash_t(:success), status: :see_other
12
+ end
13
+
14
+ private
15
+
16
+ def clone_page(page)
17
+ services.clone_page.call(page: page)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maglev
4
+ module Editor
5
+ module Pages
6
+ class DiscardDraftController < Maglev::Editor::BaseController
7
+ def create
8
+ page = maglev_page_resources.find(params[:id])
9
+ services.discard_page_draft.call(
10
+ site: maglev_site,
11
+ page: page
12
+ )
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -3,16 +3,16 @@
3
3
  module Maglev
4
4
  module Editor
5
5
  class PagesController < Maglev::Editor::BaseController
6
+ include ::Pagy::Backend
7
+
6
8
  before_action :set_page, only: %i[edit update destroy]
7
9
  before_action :maglev_disable_turbo_cache, only: %i[edit update new create]
8
10
 
9
11
  helper_method :query_params
10
12
 
11
13
  def index
12
- @pages = services.search_pages.call(q: params[:q], content_locale: content_locale,
13
- default_locale: default_content_locale,
14
- with_static_pages: false,
15
- index_first: true)
14
+ @pages = fetch_pages
15
+ @pagy, @pages = pagy(@pages, limit: per_page, page_param: 'offset') if pagination_enabled?
16
16
  end
17
17
 
18
18
  def new
@@ -58,6 +58,10 @@ module Maglev
58
58
  @page = maglev_page_resources.find(params[:id])
59
59
  end
60
60
 
61
+ def build_page_resource
62
+ maglev_page_resources.build(page_params)
63
+ end
64
+
61
65
  def page_params
62
66
  params.require(:page).permit(:title, :path,
63
67
  :seo_title, :meta_description,
@@ -66,11 +70,26 @@ module Maglev
66
70
  end
67
71
 
68
72
  def query_params(from_list: false)
69
- { q: params[:q], from_list: params[:from_list] || from_list }.compact_blank
73
+ base = { q: params[:q], from_list: params[:from_list] || from_list }
74
+ (pagination_enabled? ? base.merge(offset: params[:offset]) : base).compact_blank
70
75
  end
71
76
 
72
- def build_page_resource
73
- maglev_page_resources.build(page_params)
77
+ def fetch_pages
78
+ services.search_pages.call(
79
+ q: params[:q],
80
+ content_locale: content_locale,
81
+ default_locale: default_content_locale,
82
+ with_static_pages: false,
83
+ index_first: true
84
+ )
85
+ end
86
+
87
+ def pagination_enabled?
88
+ maglev_config.pagination?(:pages)
89
+ end
90
+
91
+ def per_page
92
+ maglev_config.per_page(:pages)
74
93
  end
75
94
  end
76
95
  end