panda-cms 0.8.2 → 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.
- checksums.yaml +4 -4
- data/README.md +83 -4
- data/app/components/panda/cms/code_component.rb +117 -39
- data/app/components/panda/cms/grid_component.rb +26 -6
- data/app/components/panda/cms/menu_component.rb +66 -34
- data/app/components/panda/cms/page_menu_component.rb +94 -13
- data/app/components/panda/cms/rich_text_component.rb +198 -140
- data/app/components/panda/cms/text_component.rb +77 -44
- data/app/controllers/panda/cms/admin/base_controller.rb +19 -3
- data/app/controllers/panda/cms/admin/dashboard_controller.rb +3 -3
- data/app/controllers/panda/cms/admin/files_controller.rb +7 -0
- data/app/controllers/panda/cms/admin/menus_controller.rb +47 -3
- data/app/controllers/panda/cms/admin/pages_controller.rb +6 -1
- data/app/controllers/panda/cms/pages_controller.rb +2 -2
- data/app/helpers/panda/cms/application_helper.rb +15 -1
- data/app/helpers/panda/cms/asset_helper.rb +14 -3
- data/app/javascript/panda/cms/application_panda_cms.js +1 -1
- data/app/javascript/panda/cms/controllers/code_editor_controller.js +95 -0
- data/app/javascript/panda/cms/controllers/file_gallery_controller.js +128 -0
- data/app/javascript/panda/cms/controllers/index.js +48 -13
- data/app/javascript/panda/cms/controllers/inline_code_editor_controller.js +96 -0
- data/app/javascript/panda/cms/controllers/menu_form_controller.js +40 -0
- data/app/javascript/panda/cms/controllers/nested_form_controller.js +35 -0
- data/app/javascript/panda/cms/controllers/tree_controller.js +214 -0
- data/app/javascript/panda/cms/stimulus-loading.js +5 -7
- data/app/models/panda/cms/block_content.rb +9 -0
- data/app/models/panda/cms/page.rb +41 -0
- data/app/models/panda/cms/post.rb +1 -0
- data/app/views/panda/cms/admin/dashboard/show.html.erb +5 -5
- data/app/views/panda/cms/admin/files/_file_details.html.erb +45 -0
- data/app/views/panda/cms/admin/files/index.html.erb +11 -118
- data/app/views/panda/cms/admin/forms/index.html.erb +2 -2
- data/app/views/panda/cms/admin/forms/new.html.erb +1 -2
- data/app/views/panda/cms/admin/forms/show.html.erb +15 -30
- data/app/views/panda/cms/admin/menus/_menu_item_fields.html.erb +11 -0
- data/app/views/panda/cms/admin/menus/edit.html.erb +64 -0
- data/app/views/panda/cms/admin/menus/index.html.erb +3 -2
- data/app/views/panda/cms/admin/menus/new.html.erb +40 -0
- data/app/views/panda/cms/admin/pages/edit.html.erb +15 -9
- data/app/views/panda/cms/admin/pages/index.html.erb +49 -11
- data/app/views/panda/cms/admin/pages/new.html.erb +3 -11
- data/app/views/panda/cms/admin/posts/_form.html.erb +4 -14
- data/app/views/panda/cms/admin/posts/edit.html.erb +2 -2
- data/app/views/panda/cms/admin/posts/index.html.erb +3 -3
- data/app/views/panda/cms/admin/posts/new.html.erb +1 -1
- data/app/views/panda/cms/admin/settings/bulk_editor/new.html.erb +1 -1
- data/app/views/panda/cms/admin/settings/index.html.erb +3 -3
- data/config/importmap.rb +4 -6
- data/config/initializers/panda/cms/healthcheck_log_silencer.rb.disabled +31 -0
- data/config/initializers/panda/cms.rb +52 -10
- data/config/routes.rb +4 -2
- data/db/migrate/20240305000000_convert_html_content_to_editor_js.rb +2 -2
- data/db/migrate/20240315125421_add_nested_sets_to_panda_cms_pages.rb +6 -1
- data/db/migrate/20250809231125_migrate_users_to_panda_core.rb +23 -21
- data/db/migrate/20251104150640_add_cached_last_updated_at_to_panda_cms_pages.rb +22 -0
- data/db/migrate/20251104172242_add_page_type_to_panda_cms_pages.rb +6 -0
- data/db/migrate/20251104172638_set_page_types_for_existing_pages.rb +27 -0
- data/db/migrate/20251105000001_add_pending_review_status_to_pages_and_posts.panda_cms.rb +21 -0
- data/lib/generators/panda/cms/install_generator.rb +2 -5
- data/lib/panda/cms/asset_loader.rb +36 -16
- data/lib/panda/cms/debug.rb +29 -0
- data/lib/panda/cms/engine.rb +107 -48
- data/lib/panda/cms/features.rb +52 -0
- data/lib/panda-cms/version.rb +1 -1
- data/lib/panda-cms.rb +5 -6
- data/lib/tasks/assets.rake +5 -52
- data/lib/tasks/panda_cms_tasks.rake +16 -0
- metadata +22 -29
- data/app/components/panda/cms/admin/container_component.html.erb +0 -13
- data/app/components/panda/cms/admin/flash_message_component.html.erb +0 -31
- data/app/components/panda/cms/admin/panel_component.html.erb +0 -7
- data/app/components/panda/cms/admin/slideover_component.html.erb +0 -9
- data/app/components/panda/cms/admin/slideover_component.rb +0 -15
- data/app/components/panda/cms/admin/statistics_component.html.erb +0 -4
- data/app/components/panda/cms/admin/statistics_component.rb +0 -16
- data/app/components/panda/cms/admin/tab_bar_component.html.erb +0 -35
- data/app/components/panda/cms/admin/tab_bar_component.rb +0 -15
- data/app/components/panda/cms/admin/table_component.html.erb +0 -29
- data/app/components/panda/cms/admin/user_activity_component.html.erb +0 -7
- data/app/components/panda/cms/admin/user_activity_component.rb +0 -20
- data/app/components/panda/cms/admin/user_display_component.html.erb +0 -17
- data/app/components/panda/cms/admin/user_display_component.rb +0 -21
- data/app/components/panda/cms/grid_component.html.erb +0 -6
- data/app/components/panda/cms/menu_component.html.erb +0 -6
- data/app/components/panda/cms/page_menu_component.html.erb +0 -21
- data/app/components/panda/cms/rich_text_component.html.erb +0 -90
- data/app/views/layouts/panda/cms/application.html.erb +0 -42
- data/app/views/panda/cms/admin/shared/_breadcrumbs.html.erb +0 -28
- data/app/views/panda/cms/admin/shared/_flash.html.erb +0 -5
- data/app/views/panda/cms/admin/shared/_sidebar.html.erb +0 -41
- data/app/views/panda/cms/shared/_footer.html.erb +0 -2
- data/app/views/panda/cms/shared/_header.html.erb +0 -25
- 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.
|
|
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:
|
|
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.
|
|
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
|
|
6
|
-
<%= f.text_field :template, value: template.name,
|
|
7
|
-
<%= f.select :status, options_for_select([["Active", "active"], ["Draft", "draft"], ["Hidden", "hidden"], ["Archived", "archived"]], selected: page.status)
|
|
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:
|
|
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:
|
|
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="
|
|
22
|
-
|
|
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.
|
|
3
|
-
<% heading.
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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] }
|
|
26
|
-
<%= f.datetime_field :published_at
|
|
27
|
-
<%= f.select :status, options_for_select([["Active", "active"], ["Draft", "draft"], ["Hidden", "hidden"], ["Archived", "archived"]], selected: post.status)
|
|
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.
|
|
3
|
-
<% heading.
|
|
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.
|
|
3
|
-
<% heading.
|
|
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.
|
|
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.
|
|
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.
|
|
2
|
+
<% component.heading(text: "Settings", level: 1) %>
|
|
3
3
|
|
|
4
4
|
<%= render Panda::Core::Admin::PanelComponent.new do |panel| %>
|
|
5
|
-
<% panel.
|
|
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.
|
|
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
|
-
|
|
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 "
|
|
6
|
-
pin "@
|
|
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
|
|
4
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
@@ -4,12 +4,12 @@ class ConvertHtmlContentToEditorJs < ActiveRecord::Migration[7.1]
|
|
|
4
4
|
def up
|
|
5
5
|
# First, let's check if the converter service exists
|
|
6
6
|
converter_path = Panda::CMS::Engine.root.join("app/services/panda/cms/html_to_editor_js_converter.rb")
|
|
7
|
-
|
|
7
|
+
|
|
8
8
|
unless File.exist?(converter_path)
|
|
9
9
|
Rails.logger.info "HtmlToEditorJsConverter service not found. Skipping HTML to EditorJS conversion."
|
|
10
10
|
return
|
|
11
11
|
end
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
require converter_path
|
|
14
14
|
|
|
15
15
|
# Check if we have any existing valid EditorJS 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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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,27 @@
|
|
|
1
|
+
class SetPageTypesForExistingPages < ActiveRecord::Migration[8.0]
|
|
2
|
+
def up
|
|
3
|
+
# Set system type for error pages
|
|
4
|
+
execute <<-SQL
|
|
5
|
+
UPDATE panda_cms_pages
|
|
6
|
+
SET page_type = 'system'
|
|
7
|
+
WHERE path IN ('/404', '/500')
|
|
8
|
+
SQL
|
|
9
|
+
|
|
10
|
+
# Set posts type for news/blog pages
|
|
11
|
+
execute <<-SQL
|
|
12
|
+
UPDATE panda_cms_pages
|
|
13
|
+
SET page_type = 'posts'
|
|
14
|
+
WHERE path LIKE '%news%' OR path LIKE '%blog%' OR path LIKE '%updates%'
|
|
15
|
+
SQL
|
|
16
|
+
|
|
17
|
+
# All other pages remain as 'standard' (the default)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def down
|
|
21
|
+
# Reset all to standard
|
|
22
|
+
execute <<-SQL
|
|
23
|
+
UPDATE panda_cms_pages
|
|
24
|
+
SET page_type = 'standard'
|
|
25
|
+
SQL
|
|
26
|
+
end
|
|
27
|
+
end
|