panda-cms 0.8.0 → 0.10.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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +83 -4
  3. data/app/components/panda/cms/code_component.rb +117 -39
  4. data/app/components/panda/cms/grid_component.rb +26 -6
  5. data/app/components/panda/cms/menu_component.rb +66 -34
  6. data/app/components/panda/cms/page_menu_component.rb +94 -13
  7. data/app/components/panda/cms/rich_text_component.rb +198 -140
  8. data/app/components/panda/cms/text_component.rb +77 -44
  9. data/app/controllers/panda/cms/admin/base_controller.rb +19 -3
  10. data/app/controllers/panda/cms/admin/dashboard_controller.rb +3 -3
  11. data/app/controllers/panda/cms/admin/files_controller.rb +7 -0
  12. data/app/controllers/panda/cms/admin/menus_controller.rb +47 -3
  13. data/app/controllers/panda/cms/admin/pages_controller.rb +6 -1
  14. data/app/controllers/panda/cms/pages_controller.rb +2 -2
  15. data/app/helpers/panda/cms/application_helper.rb +15 -1
  16. data/app/helpers/panda/cms/asset_helper.rb +14 -3
  17. data/app/javascript/panda/cms/application_panda_cms.js +1 -1
  18. data/app/javascript/panda/cms/controllers/code_editor_controller.js +95 -0
  19. data/app/javascript/panda/cms/controllers/file_gallery_controller.js +128 -0
  20. data/app/javascript/panda/cms/controllers/index.js +48 -13
  21. data/app/javascript/panda/cms/controllers/inline_code_editor_controller.js +96 -0
  22. data/app/javascript/panda/cms/controllers/menu_form_controller.js +40 -0
  23. data/app/javascript/panda/cms/controllers/nested_form_controller.js +35 -0
  24. data/app/javascript/panda/cms/controllers/tree_controller.js +214 -0
  25. data/app/javascript/panda/cms/stimulus-loading.js +5 -7
  26. data/app/models/panda/cms/block_content.rb +9 -0
  27. data/app/models/panda/cms/page.rb +41 -0
  28. data/app/models/panda/cms/post.rb +1 -0
  29. data/app/views/panda/cms/admin/dashboard/show.html.erb +5 -5
  30. data/app/views/panda/cms/admin/files/_file_details.html.erb +45 -0
  31. data/app/views/panda/cms/admin/files/index.html.erb +11 -118
  32. data/app/views/panda/cms/admin/forms/index.html.erb +2 -2
  33. data/app/views/panda/cms/admin/forms/new.html.erb +1 -2
  34. data/app/views/panda/cms/admin/forms/show.html.erb +15 -30
  35. data/app/views/panda/cms/admin/menus/_menu_item_fields.html.erb +11 -0
  36. data/app/views/panda/cms/admin/menus/edit.html.erb +64 -0
  37. data/app/views/panda/cms/admin/menus/index.html.erb +3 -2
  38. data/app/views/panda/cms/admin/menus/new.html.erb +40 -0
  39. data/app/views/panda/cms/admin/pages/edit.html.erb +15 -9
  40. data/app/views/panda/cms/admin/pages/index.html.erb +49 -11
  41. data/app/views/panda/cms/admin/pages/new.html.erb +3 -11
  42. data/app/views/panda/cms/admin/posts/_form.html.erb +4 -14
  43. data/app/views/panda/cms/admin/posts/edit.html.erb +2 -2
  44. data/app/views/panda/cms/admin/posts/index.html.erb +3 -3
  45. data/app/views/panda/cms/admin/posts/new.html.erb +1 -1
  46. data/app/views/panda/cms/admin/settings/bulk_editor/new.html.erb +1 -1
  47. data/app/views/panda/cms/admin/settings/index.html.erb +3 -3
  48. data/config/importmap.rb +4 -6
  49. data/config/initializers/panda/cms/healthcheck_log_silencer.rb.disabled +31 -0
  50. data/config/initializers/panda/cms.rb +52 -10
  51. data/config/routes.rb +4 -2
  52. data/db/migrate/20240305000000_convert_html_content_to_editor_js.rb +9 -2
  53. data/db/migrate/20240315125421_add_nested_sets_to_panda_cms_pages.rb +6 -1
  54. data/db/migrate/20250809231125_migrate_users_to_panda_core.rb +23 -21
  55. data/db/migrate/20251104150640_add_cached_last_updated_at_to_panda_cms_pages.rb +22 -0
  56. data/db/migrate/20251104172242_add_page_type_to_panda_cms_pages.rb +6 -0
  57. data/db/migrate/20251104172638_set_page_types_for_existing_pages.rb +27 -0
  58. data/db/migrate/20251105000001_add_pending_review_status_to_pages_and_posts.panda_cms.rb +21 -0
  59. data/lib/generators/panda/cms/install_generator.rb +2 -5
  60. data/lib/panda/cms/asset_loader.rb +36 -16
  61. data/lib/panda/cms/debug.rb +29 -0
  62. data/lib/panda/cms/engine.rb +107 -48
  63. data/lib/panda/cms/features.rb +52 -0
  64. data/lib/panda-cms/version.rb +1 -1
  65. data/lib/panda-cms.rb +5 -6
  66. data/lib/tasks/assets.rake +5 -52
  67. data/lib/tasks/panda_cms_tasks.rake +16 -0
  68. metadata +22 -29
  69. data/app/components/panda/cms/admin/container_component.html.erb +0 -13
  70. data/app/components/panda/cms/admin/flash_message_component.html.erb +0 -31
  71. data/app/components/panda/cms/admin/panel_component.html.erb +0 -7
  72. data/app/components/panda/cms/admin/slideover_component.html.erb +0 -9
  73. data/app/components/panda/cms/admin/slideover_component.rb +0 -15
  74. data/app/components/panda/cms/admin/statistics_component.html.erb +0 -4
  75. data/app/components/panda/cms/admin/statistics_component.rb +0 -16
  76. data/app/components/panda/cms/admin/tab_bar_component.html.erb +0 -35
  77. data/app/components/panda/cms/admin/tab_bar_component.rb +0 -15
  78. data/app/components/panda/cms/admin/table_component.html.erb +0 -29
  79. data/app/components/panda/cms/admin/user_activity_component.html.erb +0 -7
  80. data/app/components/panda/cms/admin/user_activity_component.rb +0 -20
  81. data/app/components/panda/cms/admin/user_display_component.html.erb +0 -17
  82. data/app/components/panda/cms/admin/user_display_component.rb +0 -21
  83. data/app/components/panda/cms/grid_component.html.erb +0 -6
  84. data/app/components/panda/cms/menu_component.html.erb +0 -6
  85. data/app/components/panda/cms/page_menu_component.html.erb +0 -21
  86. data/app/components/panda/cms/rich_text_component.html.erb +0 -90
  87. data/app/views/layouts/panda/cms/application.html.erb +0 -42
  88. data/app/views/panda/cms/admin/shared/_breadcrumbs.html.erb +0 -28
  89. data/app/views/panda/cms/admin/shared/_flash.html.erb +0 -5
  90. data/app/views/panda/cms/admin/shared/_sidebar.html.erb +0 -41
  91. data/app/views/panda/cms/shared/_footer.html.erb +0 -2
  92. data/app/views/panda/cms/shared/_header.html.erb +0 -25
  93. data/config/initializers/panda/cms/healthcheck_log_silencer.rb +0 -13
@@ -0,0 +1,64 @@
1
+ <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
+ <% component.heading(text: "Edit Menu: #{menu.name}", level: 1) %>
3
+
4
+ <%= panda_cms_form_with model: menu, url: admin_cms_menu_path(menu), method: :put do |f| %>
5
+ <%= render Panda::Core::Admin::FormErrorComponent.new(model: menu) %>
6
+
7
+ <%= f.text_field :name %>
8
+ <%= f.select :kind, options_for_select([["Static", "static"], ["Auto", "auto"]], selected: menu.kind), {}, { data: { action: "change->menu-form#kindChanged" } } %>
9
+
10
+ <div data-controller="menu-form">
11
+ <div data-menu-form-target="startPageField" class="<%= 'hidden' unless menu.kind == 'auto' %>">
12
+ <%= f.collection_select :start_page_id, Panda::CMS::Page.order(:title), :id, :title, { include_blank: "Select a page..." }, { class: "mt-1" } %>
13
+ </div>
14
+
15
+ <% if menu.kind == "static" %>
16
+ <%= render Panda::Core::Admin::PanelComponent.new do |panel| %>
17
+ <% panel.heading(text: "Menu Items") %>
18
+
19
+ <div data-controller="nested-form" data-nested-form-wrapper-selector-value=".nested-form-wrapper">
20
+ <template data-nested-form-target="template">
21
+ <%= f.fields_for :menu_items, Panda::CMS::MenuItem.new, child_index: "NEW_RECORD" do |item_form| %>
22
+ <%= render "menu_item_fields", form: item_form %>
23
+ <% end %>
24
+ </template>
25
+
26
+ <div class="space-y-4">
27
+ <% if menu.menu_items.any? %>
28
+ <%= f.fields_for :menu_items, menu.menu_items.sort_by(&:lft) do |item_form| %>
29
+ <%= render "menu_item_fields", form: item_form %>
30
+ <% end %>
31
+ <% end %>
32
+ </div>
33
+
34
+ <div data-nested-form-target="target"></div>
35
+
36
+ <div class="mt-4">
37
+ <%= render Panda::Core::Admin::ButtonComponent.new(text: "Add Menu Item", action: :add, link: "#", size: :small, data: { action: "click->nested-form#add" }) %>
38
+ </div>
39
+ </div>
40
+ <% end %>
41
+ <% else %>
42
+ <%= render Panda::Core::Admin::PanelComponent.new do |panel| %>
43
+ <% panel.heading(text: "Auto-Generated Menu Items") %>
44
+
45
+ <% if menu.menu_items.any? %>
46
+ <%= render Panda::Core::Admin::TableComponent.new(term: "menu item", rows: menu.menu_items.root.self_and_descendants) do |table| %>
47
+ <% table.column("Text") do |menu_item| %>
48
+ <div class="<%= "ml-#{menu_item.depth * 6}" %>">
49
+ <%= menu_item.text %>
50
+ </div>
51
+ <% end %>
52
+ <% table.column("Page") { |menu_item| menu_item.page&.title } %>
53
+ <% table.column("Path") { |menu_item| menu_item.page&.path } %>
54
+ <% end %>
55
+ <% else %>
56
+ <p class="text-sm text-gray-500 dark:text-gray-400">No menu items generated yet. Select a start page and save to generate menu items.</p>
57
+ <% end %>
58
+ <% end %>
59
+ <% end %>
60
+ </div>
61
+
62
+ <%= f.button "Save Menu" %>
63
+ <% end %>
64
+ <% end %>
@@ -1,8 +1,9 @@
1
1
  <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
- <% component.with_heading(text: "Menus", level: 1) do |heading| %>
2
+ <% component.heading(text: "Menus", level: 1) do |heading| %>
3
+ <% heading.button(action: :add, text: "New Menu", link: new_admin_cms_menu_path) %>
3
4
  <% end %>
4
5
  <%= render Panda::Core::Admin::TableComponent.new(term: "menu", rows: menus) do |table| %>
5
6
  <% table.column("Name") { |menu| link_to menu.name, edit_admin_cms_menu_path(menu) } %>
6
- <% table.column("Kind") { |menu| render Panda::Core::Admin::TagComponent.new(status: :active, text: menu.kind.titleize) } %>
7
+ <% table.column("Kind") { |menu| render Panda::Core::Admin::TagComponent.new(status: menu.kind.to_sym) } %>
7
8
  <% end %>
8
9
  <% end %>
@@ -0,0 +1,40 @@
1
+ <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
+ <% component.heading(text: "New Menu", level: 1) %>
3
+
4
+ <%= panda_cms_form_with model: menu, url: admin_cms_menus_path, method: :post do |f| %>
5
+ <%= render Panda::Core::Admin::FormErrorComponent.new(model: menu) %>
6
+
7
+ <%= f.text_field :name %>
8
+ <%= f.select :kind, options_for_select([["Static", "static"], ["Auto", "auto"]], selected: menu.kind || "static"), {}, { data: { action: "change->menu-form#kindChanged" } } %>
9
+
10
+ <div data-controller="menu-form">
11
+ <div data-menu-form-target="startPageField" class="hidden">
12
+ <%= f.collection_select :start_page_id, Panda::CMS::Page.order(:title), :id, :title, { include_blank: "Select a page..." }, { class: "mt-1" } %>
13
+ </div>
14
+
15
+ <div data-menu-form-target="menuItemsSection">
16
+ <%= render Panda::Core::Admin::PanelComponent.new do |panel| %>
17
+ <% panel.heading(text: "Menu Items") %>
18
+
19
+ <div data-controller="nested-form" data-nested-form-wrapper-selector-value=".nested-form-wrapper">
20
+ <template data-nested-form-target="template">
21
+ <%= f.fields_for :menu_items, Panda::CMS::MenuItem.new, child_index: "NEW_RECORD" do |item_form| %>
22
+ <%= render "menu_item_fields", form: item_form %>
23
+ <% end %>
24
+ </template>
25
+
26
+ <div class="space-y-4"></div>
27
+
28
+ <div data-nested-form-target="target"></div>
29
+
30
+ <div class="mt-4">
31
+ <%= render Panda::Core::Admin::ButtonComponent.new(text: "Add Menu Item", action: :add, link: "#", size: :small, data: { action: "click->nested-form#add" }) %>
32
+ </div>
33
+ </div>
34
+ <% end %>
35
+ </div>
36
+ </div>
37
+
38
+ <%= f.button "Create Menu" %>
39
+ <% end %>
40
+ <% end %>
@@ -1,27 +1,33 @@
1
- <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
- <% component.with_heading(text: "#{page.title}", level: 1) %>
1
+ <%= render Panda::Core::Admin::ContainerComponent.new(full_height: true) do |component| %>
2
+ <% component.heading(text: "#{page.title}", level: 1) %>
3
3
  <% component.with_slideover(title: "Page Details") do %>
4
4
  <%= panda_cms_form_with model: page, url: admin_cms_page_path, method: :put do |f| %>
5
- <%= f.text_field :title, class: "block w-full rounded-md border-0 p-2 text-gray-900 ring-1 ring-inset ring-mid placeholder:text-gray-300 focus:ring-1 focus:ring-inset focus:ring-dark sm:leading-6 hover:pointer" %>
6
- <%= f.text_field :template, value: template.name, readonly: true, class: "read-only:bg-gray-100 block w-full rounded-md border-0 p-2 text-gray-900 ring-1 ring-inset ring-mid placeholder:text-gray-300 focus:ring-1 focus:ring-inset focus:ring-dark sm:leading-6 hover:pointer" %>
7
- <%= f.select :status, options_for_select([["Active", "active"], ["Draft", "draft"], ["Hidden", "hidden"], ["Archived", "archived"]], selected: page.status), {}, class: "block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-mid focus:ring-1 focus:ring-inset focus:ring-dark sm:leading-6 hover:pointer" %>
5
+ <%= f.text_field :title %>
6
+ <%= f.text_field :template, value: template.name, readonly: true, class: "read-only:bg-gray-100" %>
7
+ <%= f.select :status, options_for_select([["Active", "active"], ["Draft", "draft"], ["Hidden", "hidden"], ["Archived", "archived"]], selected: page.status) %>
8
+ <% if page.page_type.in?(["system", "posts"]) %>
9
+ <%= f.text_field :page_type, value: page.page_type.humanize, readonly: true, class: "read-only:bg-gray-100" %>
10
+ <% else %>
11
+ <%= f.select :page_type, options_for_select([["Active", "standard"], ["Hidden", "hidden"], ["Code", "code"]], selected: page.page_type) %>
12
+ <% end %>
8
13
  <%= f.submit "Save" %>
9
14
  <% end %>
10
15
  <% end %>
11
16
  <div id="successMessage" class="hidden">
12
- <%= render Panda::Core::Admin::FlashMessageComponent.new(kind: "success", message: "This page was successfully updated!", temporary: false) %>
17
+ <%= render Panda::Core::Admin::FlashMessageComponent.new(kind: :success, message: "This page was successfully updated!", temporary: false) %>
13
18
  </div>
14
19
  <div id="errorMessage" class="hidden">
15
- <%= render Panda::Core::Admin::FlashMessageComponent.new(kind: "error", message: "There was an error updating this page.", temporary: false) %>
20
+ <%= render Panda::Core::Admin::FlashMessageComponent.new(kind: :error, message: "There was an error updating this page.", temporary: false) %>
16
21
  </div>
17
22
  <div class="grid grid-cols-2 mb-4 -mt-5">
18
23
  <div>
19
24
  <a class="inline-block mb-2 text-sm text-black/60" target="_blank" href="<%= @page.path %>"><%= @page.path %> <i class="ml-2 fa-solid fa-arrow-up-right-from-square"></i></a>
20
25
  </div>
21
- <div class="relative -mt-5">
22
- <span class="absolute right-0"><%= render Panda::Core::Admin::ButtonComponent.new(text: "Save Changes", action: :save_inactive, icon: "check", link: "#", size: :regular, id: "saveEditableButton") %></span>
26
+ <div class="flex justify-end">
27
+ <%= render Panda::Core::Admin::ButtonComponent.new(text: "Save Changes", action: :save, icon: "check", link: "#", size: :regular, id: "saveEditableButton") %>
23
28
  </div>
24
29
  </div>
30
+
25
31
  <%= content_tag :iframe, nil,
26
32
  src: "#{page.path}?embed_id=#{page.id}",
27
33
  class: "p-0 m-0 w-full h-full border border-slate-200",
@@ -1,19 +1,57 @@
1
1
  <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
- <% component.with_heading(text: "Pages", level: 1) do |heading| %>
3
- <% heading.with_button(action: :add, text: "Add Page", link: new_admin_cms_page_path) %>
2
+ <% component.heading(text: "Pages", level: 1) do |heading| %>
3
+ <% heading.button(action: :add, text: "Add Page", link: new_admin_cms_page_path) %>
4
4
  <% end %>
5
5
 
6
6
  <% if root_page %>
7
- <%= render Panda::Core::Admin::TableComponent.new(term: "page", rows: root_page.self_and_descendants) do |table| %>
8
- <% table.column("Name") do |page| %>
9
- <div class="<%= table_indent(page) %>">
10
- <%= link_to page.title, edit_admin_cms_page_path(page), class: "block h-full w-full" %>
11
- <span class="block text-xs text-black/60"><%= page.path %></span>
12
- </div>
7
+ <div data-controller="tree" data-tree-target="container" style="opacity: 0; transition: opacity 0.2s;">
8
+ <%= render Panda::Core::Admin::TableComponent.new(term: "page", rows: root_page.self_and_descendants) do |table| %>
9
+ <% table.column("Name") do |page| %>
10
+ <div class="flex items-start" data-tree-target="row" data-page-id="<%= page.id %>" data-level="<%= page.level %>" data-parent-id="<%= page.parent_id %>">
11
+ <div class="flex-1 min-w-0">
12
+ <% # Add indentation for nested levels %>
13
+ <% indent_class = case page.level
14
+ when 0 then ""
15
+ when 1 then "ml-4"
16
+ when 2 then "ml-8"
17
+ else "ml-12"
18
+ end %>
19
+ <div class="flex items-start <%= indent_class %>">
20
+ <div class="flex-1 min-w-0">
21
+ <div class="flex items-center gap-2">
22
+ <% # Chevron/icon section %>
23
+ <% if page.children_count > 0 %>
24
+ <% if page.level > 0 %>
25
+ <% # Non-root pages with children: show chevron button only %>
26
+ <button type="button"
27
+ class="text-xs text-gray-500 hover:text-gray-700 focus:outline-none focus:ring-2 focus:ring-primary rounded px-1 w-4 flex items-center justify-center"
28
+ data-tree-target="toggle"
29
+ data-action="click->tree#toggle"
30
+ data-page-id="<%= page.id %>">
31
+ <i class="fa-solid fa-chevron-right"></i>
32
+ </button>
33
+ <% else %>
34
+ <% # Root page (Home): show folder icon %>
35
+ <i class="fa-solid fa-folder text-gray-500 mr-2"></i>
36
+ <% end %>
37
+ <% else %>
38
+ <% # Leaf pages: show file icon %>
39
+ <i class="fa-solid fa-file text-gray-400 mr-2"></i>
40
+ <% end %>
41
+ <%= link_to page.title, edit_admin_cms_page_path(page), class: "font-medium hover:text-primary #{page.children_count > 0 && page.level > 0 ? 'ml-2' : ''}" %>
42
+ </div>
43
+ <div class="text-xs text-black/60 mt-1 ml-6">
44
+ <%= page.path %>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ </div>
50
+ <% end %>
51
+ <% table.column("Type", width: "12%") { |page| render Panda::Core::Admin::TagComponent.new(page_type: page.page_type.to_sym) } %>
52
+ <% table.column("Last Updated", width: "18%") { |page| render Panda::Core::Admin::UserActivityComponent.new(at: page.last_updated_at) } %>
13
53
  <% end %>
14
- <% table.column("Status") { |page| render Panda::Core::Admin::TagComponent.new(status: page.status) } %>
15
- <% table.column("Last Updated") { |page| render Panda::CMS::Admin::UserActivityComponent.new(model: page) } %>
16
- <% end %>
54
+ </div>
17
55
  <% else %>
18
56
  <div class="p-6 bg-error/10 text-error rounded-lg">
19
57
  <p class="text-base">No homepage (at <code>/</code>) found. Please create a homepage to start building your site.</p>
@@ -1,21 +1,13 @@
1
1
  <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
- <% component.with_heading(text: "Add Page", level: 1) do |heading| %>
3
- <% end %>
2
+ <% component.heading(text: "Add Page", level: 1) %>
4
3
  <%= panda_cms_form_with model: page, url: admin_cms_pages_path, method: :post do |f| %>
5
- <% if page.errors.any? %>
6
- <div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-md">
7
- <div class="text-sm text-red-600">
8
- <% page.errors.full_messages.each do |message| %>
9
- <p><%= message %></p>
10
- <% end %>
11
- </div>
12
- </div>
13
- <% end %>
4
+ <%= render Panda::Core::Admin::FormErrorComponent.new(model: page) %>
14
5
  <% options = nested_set_options(Panda::CMS::Page, page) { |i| "#{"-" * i.level} #{i.title} (#{i.path})" } %>
15
6
  <%= f.select :parent_id, options %>
16
7
  <%= f.text_field :title %>
17
8
  <%= f.text_field :path, { meta: t(".path.meta") } %>
18
9
  <%= f.collection_select :panda_cms_template_id, available_templates, :id, :name %>
10
+ <%= f.select :page_type, options_for_select([["Active", "standard"], ["Hidden", "hidden"], ["Code", "code"]], selected: "standard") %>
19
11
  <%= f.button "Create Page" %>
20
12
  <% end %>
21
13
  <% end %>
@@ -1,30 +1,20 @@
1
1
  <%= panda_cms_form_with model: post, url: url, method: post.persisted? ? :put : :post do |f| %>
2
- <% if post.errors.any? %>
3
- <div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-md">
4
- <div class="text-sm text-red-600">
5
- <% post.errors.full_messages.each do |message| %>
6
- <p><%= message %></p>
7
- <% end %>
8
- </div>
9
- </div>
10
- <% end %>
2
+ <%= render Panda::Core::Admin::FormErrorComponent.new(model: post) %>
11
3
 
12
4
  <div data-controller="slug" data-slug-add-date-prefix-value="true">
13
5
  <%= f.text_field :title,
14
- class: "block w-full rounded-md border-0 p-2 text-gray-900 ring-1 ring-inset ring-mid placeholder:text-gray-300 focus:ring-1 focus:ring-inset focus:ring-dark sm:leading-6 hover:pointer",
15
6
  data: {
16
7
  "slug-target": "input_text",
17
8
  action: "focusout->slug#generatePath"
18
9
  } %>
19
10
  <%= f.text_field :slug,
20
- class: "block w-full rounded-md border-0 p-2 text-gray-900 ring-1 ring-inset ring-mid placeholder:text-gray-300 focus:ring-1 focus:ring-inset focus:ring-dark sm:leading-6 hover:pointer",
21
11
  data: {
22
12
  "slug-target": "output_text"
23
13
  } %>
24
14
  </div>
25
- <%= f.select :author_id, Panda::Core::User.admin.map { |u| [u.name, u.id] }, {}, class: "block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-mid focus:ring-1 focus:ring-inset focus:ring-dark sm:leading-6 hover:pointer" %>
26
- <%= f.datetime_field :published_at, class: "block w-full rounded-md border-0 p-2 text-gray-900 ring-1 ring-inset ring-mid placeholder:text-gray-300 focus:ring-1 focus:ring-inset focus:ring-dark sm:leading-6 hover:pointer" %>
27
- <%= f.select :status, options_for_select([["Active", "active"], ["Draft", "draft"], ["Hidden", "hidden"], ["Archived", "archived"]], selected: post.status), {}, class: "block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-mid focus:ring-1 focus:ring-inset focus:ring-dark sm:leading-6 hover:pointer" %>
15
+ <%= f.select :author_id, Panda::Core::User.admin.map { |u| [u.name, u.id] } %>
16
+ <%= f.datetime_field :published_at %>
17
+ <%= f.select :status, options_for_select([["Active", "active"], ["Draft", "draft"], ["Hidden", "hidden"], ["Archived", "archived"]], selected: post.status) %>
28
18
 
29
19
  <% editor_id = "editor_#{dom_id(post, :content)}" %>
30
20
  <div data-controller="editor-form"
@@ -1,6 +1,6 @@
1
1
  <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
- <% component.with_heading(text: post.title, level: 1) do |heading| %>
3
- <% heading.with_button(action: :view, text: "View Post", link: post_path(post.admin_param)) %>
2
+ <% component.heading(text: post.title, level: 1) do |heading| %>
3
+ <% heading.button(action: :view, text: "View Post", link: post_path(post.admin_param)) %>
4
4
  <% end %>
5
5
 
6
6
  <%= render "form", post: post, url: admin_cms_post_path(post.admin_param), method: :patch %>
@@ -1,9 +1,9 @@
1
1
  <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
- <% component.with_heading(text: "Posts", level: 1) do |heading| %>
3
- <% heading.with_button(action: :add, text: "Add Post", link: new_admin_cms_post_path) %>
2
+ <% component.heading(text: "Posts", level: 1, icon: "fa-solid fa-newspaper") do |heading| %>
3
+ <% heading.button(action: :add, text: "Add Post", link: new_admin_cms_post_path) %>
4
4
  <% end %>
5
5
 
6
- <%= render Panda::Core::Admin::TableComponent.new(term: "post", rows: posts) do |table| %>
6
+ <%= render Panda::Core::Admin::TableComponent.new(term: "post", rows: posts, icon: "fa-solid fa-newspaper") do |table| %>
7
7
  <% table.column("Title") do |post| %>
8
8
  <div>
9
9
  <%= link_to post.title, edit_admin_cms_post_path(post.admin_param), class: "block h-full w-full" %>
@@ -1,5 +1,5 @@
1
1
  <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
- <% component.with_heading(text: "Add Post", level: 1) do |heading| %>
2
+ <% component.heading(text: "Add Post", level: 1) do |heading| %>
3
3
  <% end %>
4
4
 
5
5
  <%= render "form", post: post, url: url, method: :post %>
@@ -1,5 +1,5 @@
1
1
  <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
- <% component.with_heading(text: "Bulk Editor", level: 1) %>
2
+ <% component.heading(text: "Bulk Editor", level: 1) %>
3
3
 
4
4
  <% if @debug && @debug[:error].any? %>
5
5
  <div class="p-4 mb-4 bg-red-50 rounded-md">
@@ -1,8 +1,8 @@
1
1
  <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
- <% component.with_heading(text: "Settings", level: 1) %>
2
+ <% component.heading(text: "Settings", level: 1) %>
3
3
 
4
4
  <%= render Panda::Core::Admin::PanelComponent.new do |panel| %>
5
- <% panel.with_heading(text: "System Status") %>
5
+ <% panel.heading(text: "System Status") %>
6
6
 
7
7
  <p class="text-base leading-loose"><i class="mr-2 text-active fa fa-check-circle"></i> <span class="font-medium">Panda CMS:</span> v<%= Panda::CMS::VERSION %></p>
8
8
  <p class="text-base leading-loose"><i class="mr-2 text-active fa fa-check-circle"></i> <span class="font-medium">Rails:</span> v<%= Rails.version %></p>
@@ -11,7 +11,7 @@
11
11
  <% end %>
12
12
 
13
13
  <%= render Panda::Core::Admin::PanelComponent.new do |panel| %>
14
- <% panel.with_heading(text: "Integrations") %>
14
+ <% panel.heading(text: "Integrations") %>
15
15
 
16
16
  <p class="text-base leading-loose"><i class="mr-2 text-active fa-brands fa-instagram"></i> <span class="font-medium">Instagram:</span> <%= Panda::CMS.config.instagram[:enabled] ? "Connected (@#{Panda::CMS.config.instagram[:username]})" : "Not Connected" %></p>
17
17
  <% end %>
data/config/importmap.rb CHANGED
@@ -1,12 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- pin "application_panda_cms", to: "panda_cms/application_panda_cms.js", preload: true
3
+ # Base dependencies are now in panda-core (Stimulus, Turbo, Font Awesome, etc.)
4
+ # This file only contains CMS-specific pins
4
5
 
5
- pin "@hotwired/turbo", to: "@hotwired--turbo.js", preload: true # @8.0.12
6
- pin "@rails/actioncable/src", to: "@rails--actioncable--src.js", preload: true # @7.2.101
7
- pin "@hotwired/stimulus", to: "@hotwired--stimulus.js" # @3.2.2
8
- pin "@hotwired/stimulus-loading", to: "panda_cms/stimulus-loading.js", preload: true
9
- pin "tailwindcss-stimulus-components" # @6.1.2
6
+ pin "application_panda_cms", to: "panda/cms/application_panda_cms.js", preload: true
7
+ pin "@hotwired/stimulus-loading", to: "panda/cms/stimulus-loading.js", preload: true
10
8
  pin "@editorjs/editorjs", to: "panda/cms/editor/editorjs.js" # @2.30.6
11
9
 
12
10
  # Pin the controllers directory
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "silencer/rails/logger"
4
+
5
+ # Don't log requests to the healthcheck endpoint
6
+ Rails.application.configure do
7
+ # Rails 8 renamed Rails::Rack::Logger to ActionDispatch::Request::Logger
8
+ # Determine which logger middleware class to use
9
+ logger_class = if Object.const_defined?("ActionDispatch::Request::Logger")
10
+ ActionDispatch::Request::Logger
11
+ elsif Object.const_defined?("Rails::Rack::Logger")
12
+ Rails::Rack::Logger
13
+ else
14
+ # If neither exists, skip the middleware swap
15
+ nil
16
+ end
17
+
18
+ if logger_class
19
+ begin
20
+ config.middleware.swap(
21
+ logger_class,
22
+ Silencer::Logger,
23
+ config.log_tags,
24
+ silence: ["/up"]
25
+ )
26
+ rescue RuntimeError => e
27
+ # Silently fail if middleware doesn't exist in stack
28
+ raise unless e.message.include?("No such middleware")
29
+ end
30
+ end
31
+ end
@@ -1,15 +1,57 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Panda::CMS.configure do |config|
4
- # The main title of your website
5
- config.title = "Demo Site"
6
-
7
- # Site access control
8
- config.require_login_to_view = false
9
- end
10
-
11
- # Admin path is now configured via Panda::Core
3
+ # This file is an example of Panda configuration.
4
+ # In your application, this should be at config/initializers/panda.rb
12
5
  Panda::Core.configure do |config|
13
- # The path to the administration panel
14
6
  config.admin_path = "/admin"
7
+
8
+ config.login_page_title = "Panda Admin"
9
+
10
+ # Configure authentication providers
11
+ # Uncomment and configure the providers you want to use
12
+ # Don't forget to add the corresponding gems (e.g., omniauth-google-oauth2)
13
+ #
14
+ # config.authentication_providers = {
15
+ # google_oauth2: {
16
+ # enabled: true,
17
+ # name: "Google", # Display name for the button
18
+ # client_id: Rails.application.credentials.dig(:google, :client_id),
19
+ # client_secret: Rails.application.credentials.dig(:google, :client_secret),
20
+ # options: {
21
+ # scope: "email,profile",
22
+ # prompt: "select_account",
23
+ # hd: "yourdomain.com" # Specify your domain here if you want to restrict admin logins
24
+ # }
25
+ # }
26
+ # }
27
+
28
+ # Configure the session token cookie name
29
+ config.session_token_cookie = :panda_session
30
+
31
+ # Configure the user class for the application
32
+ config.user_class = "Panda::Core::User"
33
+
34
+ # Configure the user identity class for the application
35
+ config.user_identity_class = "Panda::Core::UserIdentity"
36
+
37
+ # Configure the storage provider (default: :active_storage)
38
+ # config.storage_provider = :active_storage
39
+
40
+ # Configure the cache store (default: :memory_store)
41
+ # config.cache_store = :memory_store
15
42
  end
43
+
44
+ # Optional CMS-specific configuration
45
+ # Panda::CMS.configure do |config|
46
+ # # Site access control
47
+ # config.require_login_to_view = false
48
+ # end
49
+
50
+ # Optional EditorJS configuration
51
+ # Panda::Editor.configure do |config|
52
+ # # Additional EditorJS tools to load
53
+ # # config.editor_js_tools = []
54
+ #
55
+ # # EditorJS tool configurations
56
+ # # config.editor_js_tool_config = {}
57
+ # end
data/config/routes.rb CHANGED
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Panda::CMS::Engine.routes.draw do
4
- constraints Panda::Core::AdminConstraint.new(&:present?) do
4
+ # Test authentication endpoint moved to panda-core at /admin/test_login/:user_id
5
+
6
+ constraints Panda::Core::AdminConstraint.new do
5
7
  # CMS-specific dashboard (using Core's admin_path)
6
- admin_path = Panda::Core.configuration.admin_path
8
+ admin_path = Panda::Core.config.admin_path
7
9
  get "#{admin_path}/cms", to: "admin/dashboard#show", as: :admin_cms_dashboard
8
10
 
9
11
  namespace admin_path.delete_prefix("/").to_sym, path: "#{admin_path}/cms", as: :admin_cms, module: :admin do
@@ -2,8 +2,15 @@
2
2
 
3
3
  class ConvertHtmlContentToEditorJs < ActiveRecord::Migration[7.1]
4
4
  def up
5
- # First, let's ensure we have the converter available in the migration
6
- require Panda::CMS::Engine.root.join("app/services/panda/cms/html_to_editor_js_converter")
5
+ # First, let's check if the converter service exists
6
+ converter_path = Panda::CMS::Engine.root.join("app/services/panda/cms/html_to_editor_js_converter.rb")
7
+
8
+ unless File.exist?(converter_path)
9
+ Rails.logger.info "HtmlToEditorJsConverter service not found. Skipping HTML to EditorJS conversion."
10
+ return
11
+ end
12
+
13
+ require converter_path
7
14
 
8
15
  # Check if we have any existing valid EditorJS content
9
16
  existing_editor_js = Panda::CMS::BlockContent.find_each.any? do |block_content|
@@ -8,7 +8,12 @@ class AddNestedSetsToPandaCMSPages < ActiveRecord::Migration[7.1]
8
8
 
9
9
  # This is necessary to update :lft and :rgt columns
10
10
  Panda::CMS::Page.reset_column_information
11
- Panda::CMS::Page.rebuild!
11
+
12
+ # Only rebuild if there are existing pages
13
+ # On fresh installs, there won't be any pages yet
14
+ if Panda::CMS::Page.any?
15
+ Panda::CMS::Page.rebuild!
16
+ end
12
17
  end
13
18
 
14
19
  def self.down
@@ -7,27 +7,29 @@ class MigrateUsersToPandaCore < ActiveRecord::Migration[8.0]
7
7
  if table_exists?(:panda_cms_users) && table_exists?(:panda_core_users)
8
8
  # Check if there's any data to migrate
9
9
  cms_user_count = ActiveRecord::Base.connection.select_value("SELECT COUNT(*) FROM panda_cms_users")
10
- return if cms_user_count == 0
11
- # Copy all user data
12
- execute <<-SQL
13
- INSERT INTO panda_core_users (
14
- id, name, email, image_url, is_admin, created_at, updated_at
15
- )
16
- SELECT
17
- id,
18
- COALESCE(name, CONCAT(firstname, ' ', lastname), 'Unknown User'),
19
- email,
20
- image_url,
21
- COALESCE(admin, false),
22
- created_at,
23
- updated_at
24
- FROM panda_cms_users
25
- WHERE NOT EXISTS (
26
- SELECT 1 FROM panda_core_users WHERE panda_core_users.id = panda_cms_users.id
27
- )
28
- SQL
29
10
 
30
- # Update foreign key references in other tables
11
+ if cms_user_count > 0
12
+ # Copy all user data
13
+ execute <<-SQL
14
+ INSERT INTO panda_core_users (
15
+ id, name, email, image_url, is_admin, created_at, updated_at
16
+ )
17
+ SELECT
18
+ id,
19
+ COALESCE(name, CONCAT(firstname, ' ', lastname), 'Unknown User'),
20
+ email,
21
+ image_url,
22
+ COALESCE(admin, false),
23
+ created_at,
24
+ updated_at
25
+ FROM panda_cms_users
26
+ WHERE NOT EXISTS (
27
+ SELECT 1 FROM panda_core_users WHERE panda_core_users.id = panda_cms_users.id
28
+ )
29
+ SQL
30
+ end
31
+
32
+ # Update foreign key references in other tables (always do this, even if no data)
31
33
 
32
34
  # Posts author_id
33
35
  if column_exists?(:panda_cms_posts, :author_id)
@@ -47,7 +49,7 @@ class MigrateUsersToPandaCore < ActiveRecord::Migration[8.0]
47
49
  add_foreign_key :panda_cms_visits, :panda_core_users, column: :user_id, primary_key: :id
48
50
  end
49
51
 
50
- # Drop the old table
52
+ # Always drop the old table if it exists (even if it was empty)
51
53
  drop_table :panda_cms_users
52
54
  end
53
55
  end
@@ -0,0 +1,22 @@
1
+ class AddCachedLastUpdatedAtToPandaCMSPages < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :panda_cms_pages, :cached_last_updated_at, :datetime
4
+ add_index :panda_cms_pages, :cached_last_updated_at
5
+
6
+ # Backfill existing pages
7
+ reversible do |dir|
8
+ dir.up do
9
+ execute <<-SQL
10
+ UPDATE panda_cms_pages
11
+ SET cached_last_updated_at = GREATEST(
12
+ updated_at,
13
+ COALESCE(
14
+ (SELECT MAX(updated_at) FROM panda_cms_block_contents WHERE panda_cms_page_id = panda_cms_pages.id),
15
+ updated_at
16
+ )
17
+ )
18
+ SQL
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,6 @@
1
+ class AddPageTypeToPandaCMSPages < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :panda_cms_pages, :page_type, :string, default: "standard", null: false
4
+ add_index :panda_cms_pages, :page_type
5
+ end
6
+ end