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
@@ -25,6 +25,11 @@ module Panda
25
25
  # Loads the page editor
26
26
  # @type GET
27
27
  def edit
28
+ # Add all ancestor pages to breadcrumbs (excluding homepage at depth 0)
29
+ page.ancestors.select { |anc| anc.depth > 0 }.each do |ancestor|
30
+ add_breadcrumb ancestor.title, edit_admin_cms_page_path(ancestor)
31
+ end
32
+
28
33
  add_breadcrumb page.title, edit_admin_cms_page_path(page)
29
34
 
30
35
  render :edit, locals: {page: page, template: page.template}
@@ -94,7 +99,7 @@ module Panda
94
99
  # @type private
95
100
  # @return ActionController::StrongParameters
96
101
  def page_params
97
- params.require(:page).permit(:title, :path, :panda_cms_template_id, :parent_id, :status)
102
+ params.require(:page).permit(:title, :path, :panda_cms_template_id, :parent_id, :status, :page_type)
98
103
  end
99
104
  end
100
105
  end
@@ -16,9 +16,9 @@ module Panda
16
16
 
17
17
  def show
18
18
  page = if @overrides&.dig(:page_path_match)
19
- Panda::CMS::Page.find_by(path: @overrides[:page_path_match])
19
+ Panda::CMS::Page.includes(:template).find_by(path: @overrides[:page_path_match])
20
20
  else
21
- Panda::CMS::Page.find_by(path: "/#{params[:path]}")
21
+ Panda::CMS::Page.includes(:template).find_by(path: "/#{params[:path]}")
22
22
  end
23
23
 
24
24
  Panda::CMS::Current.page = page || Panda::CMS::Page.find_by(path: "/404")
@@ -33,7 +33,7 @@ module Panda
33
33
  if match == :starts_with
34
34
  return request.path.starts_with?(path)
35
35
  elsif match == :exact
36
- return (request.path == path)
36
+ return request.path == path
37
37
  end
38
38
 
39
39
  false
@@ -44,6 +44,20 @@ module Panda
44
44
  link_to(name, options, html_options, &)
45
45
  end
46
46
 
47
+ def panda_cms_collection(slug, include_unpublished: false)
48
+ Panda::CMS::Features.require!(:collections)
49
+ Panda::CMS::Collections.fetch(slug, include_unpublished: include_unpublished)
50
+ end
51
+
52
+ def panda_cms_collection_items(slug, include_unpublished: false)
53
+ Panda::CMS::Features.require!(:collections)
54
+ Panda::CMS::Collections.items(slug, include_unpublished: include_unpublished)
55
+ end
56
+
57
+ def panda_cms_feature_enabled?(name)
58
+ Panda::CMS::Features.enabled?(name)
59
+ end
60
+
47
61
  def panda_cms_form_with(**options, &)
48
62
  options[:builder] = Panda::Core::FormBuilder
49
63
  options[:class] = ["block visible p-6 bg-mid/5 rounded-lg border-mid border", options[:class]].compact.join(" ")
@@ -53,9 +53,20 @@ module Panda
53
53
  javascript_include_tag(js_url, **defer_option)
54
54
  # Standalone bundle - don't use type: "module"
55
55
  else
56
- # Importmap asset - use type: "module"
57
- defer_option = (ENV["GITHUB_ACTIONS"] == "true") ? {} : {defer: true}
58
- javascript_include_tag(js_url, type: "module", **defer_option)
56
+ # Development mode - Load JavaScript with import map
57
+ # Files are served by Rack::Static middleware from engine's app/javascript
58
+ importmap_html = <<~HTML
59
+ <script type="importmap">
60
+ {
61
+ "imports": {
62
+ "@hotwired/stimulus": "/panda/core/vendor/@hotwired--stimulus.js",
63
+ "@hotwired/turbo": "/panda/core/vendor/@hotwired--turbo.js"
64
+ }
65
+ }
66
+ </script>
67
+ <script type="module" src="/panda/cms/application_panda_cms.js"></script>
68
+ HTML
69
+ importmap_html.html_safe
59
70
  end
60
71
  end
61
72
 
@@ -1,6 +1,6 @@
1
1
  import "@hotwired/turbo"
2
2
  console.debug("[Panda CMS] Controllers loading...");
3
- import "controllers"
3
+ import "./controllers/index.js"
4
4
  console.debug("[Panda CMS] Controllers loaded...");
5
5
 
6
6
  // Editor resources are now handled by panda-editor gem
@@ -0,0 +1,95 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["previewTab", "codeTab", "previewView", "codeView", "codeInput", "saveMessage"]
5
+ static values = {
6
+ pageId: String,
7
+ blockContentId: String
8
+ }
9
+
10
+ connect() {
11
+ // Preview is active by default
12
+ }
13
+
14
+ showPreview(event) {
15
+ event.preventDefault()
16
+
17
+ // Toggle tabs
18
+ this.previewTabTarget.classList.remove("border-transparent", "text-gray-500", "hover:border-gray-300", "hover:text-gray-700")
19
+ this.previewTabTarget.classList.add("border-primary", "text-primary")
20
+
21
+ this.codeTabTarget.classList.remove("border-primary", "text-primary")
22
+ this.codeTabTarget.classList.add("border-transparent", "text-gray-500", "hover:border-gray-300", "hover:text-gray-700")
23
+
24
+ // Toggle views
25
+ this.previewViewTarget.classList.remove("hidden")
26
+ this.codeViewTarget.classList.add("hidden")
27
+ }
28
+
29
+ showCode(event) {
30
+ event.preventDefault()
31
+
32
+ // Toggle tabs
33
+ this.codeTabTarget.classList.remove("border-transparent", "text-gray-500", "hover:border-gray-300", "hover:text-gray-700")
34
+ this.codeTabTarget.classList.add("border-primary", "text-primary")
35
+
36
+ this.previewTabTarget.classList.remove("border-primary", "text-primary")
37
+ this.previewTabTarget.classList.add("border-transparent", "text-gray-500", "hover:border-gray-300", "hover:text-gray-700")
38
+
39
+ // Toggle views
40
+ this.codeViewTarget.classList.remove("hidden")
41
+ this.previewViewTarget.classList.add("hidden")
42
+ }
43
+
44
+ async saveCode(event) {
45
+ event.preventDefault()
46
+
47
+ const blockContentId = this.codeInputTarget.dataset.codeEditorBlockContentIdValue
48
+ const code = this.codeInputTarget.value
49
+ const pageId = this.pageIdValue
50
+
51
+ try {
52
+ const response = await fetch(`/admin/cms/pages/${pageId}/block_contents/${blockContentId}`, {
53
+ method: 'PATCH',
54
+ headers: {
55
+ 'Content-Type': 'application/json',
56
+ 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
57
+ },
58
+ body: JSON.stringify({
59
+ content: code
60
+ })
61
+ })
62
+
63
+ if (response.ok) {
64
+ this.showSaveMessage('Code saved successfully!', 'success')
65
+
66
+ // Reload the preview iframe
67
+ const iframe = document.getElementById('editablePageFrame')
68
+ if (iframe) {
69
+ iframe.src = iframe.src
70
+ }
71
+ } else {
72
+ this.showSaveMessage('Error saving code', 'error')
73
+ }
74
+ } catch (error) {
75
+ console.error('Error saving code:', error)
76
+ this.showSaveMessage('Error saving code', 'error')
77
+ }
78
+ }
79
+
80
+ showSaveMessage(message, type) {
81
+ const messageEl = this.saveMessageTarget
82
+ messageEl.textContent = message
83
+ messageEl.classList.remove('hidden')
84
+
85
+ if (type === 'success') {
86
+ messageEl.className = 'mt-2 p-3 rounded-md bg-green-50 text-green-800 dark:bg-green-900/20 dark:text-green-400'
87
+ } else {
88
+ messageEl.className = 'mt-2 p-3 rounded-md bg-red-50 text-red-800 dark:bg-red-900/20 dark:text-red-400'
89
+ }
90
+
91
+ setTimeout(() => {
92
+ messageEl.classList.add('hidden')
93
+ }, 3000)
94
+ }
95
+ }
@@ -0,0 +1,128 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["slideoverContent"]
5
+
6
+ selectFile(event) {
7
+ const button = event.currentTarget
8
+ const fileData = {
9
+ id: button.dataset.fileId,
10
+ url: button.dataset.fileUrl,
11
+ name: button.dataset.fileName,
12
+ size: button.dataset.fileSize,
13
+ type: button.dataset.fileType,
14
+ created: button.dataset.fileCreated
15
+ }
16
+
17
+ // Update slideover content
18
+ this.updateSlideover(fileData)
19
+
20
+ // Update selected state
21
+ this.updateSelectedState(button)
22
+ }
23
+
24
+ updateSlideover(fileData) {
25
+ if (!this.hasSlideoverContentTarget) return
26
+
27
+ // Build the slideover content HTML
28
+ const html = this.buildSlideoverHTML(fileData)
29
+ this.slideoverContentTarget.innerHTML = html
30
+
31
+ // Show the slideover if it's hidden
32
+ const slideover = document.getElementById("slideover")
33
+ if (slideover && slideover.classList.contains("hidden")) {
34
+ slideover.classList.remove("hidden")
35
+ }
36
+
37
+ // Trigger the toggle controller to show the slideover
38
+ const toggleController = this.application.getControllerForElementAndIdentifier(
39
+ document.getElementById("panda-inner-container"),
40
+ "toggle"
41
+ )
42
+ if (toggleController) {
43
+ toggleController.show()
44
+ }
45
+ }
46
+
47
+ updateSelectedState(selectedButton) {
48
+ // Remove selected state from all file items
49
+ const allFileItems = this.element.querySelectorAll('[data-action*="file-gallery#selectFile"]')
50
+ allFileItems.forEach(button => {
51
+ const container = button.closest('.relative').querySelector('.group')
52
+ if (container) {
53
+ container.classList.remove('outline', 'outline-2', 'outline-offset-2', 'outline-panda-dark', 'dark:outline-panda-light')
54
+ container.classList.add('focus-within:outline-2', 'focus-within:outline-offset-2', 'focus-within:outline-indigo-600', 'dark:focus-within:outline-indigo-500')
55
+ }
56
+ })
57
+
58
+ // Add selected state to clicked item
59
+ const container = selectedButton.closest('.relative').querySelector('.group')
60
+ if (container) {
61
+ container.classList.add('outline', 'outline-2', 'outline-offset-2', 'outline-panda-dark', 'dark:outline-panda-light')
62
+ container.classList.remove('focus-within:outline-2', 'focus-within:outline-offset-2', 'focus-within:outline-indigo-600', 'dark:focus-within:outline-indigo-500')
63
+ }
64
+ }
65
+
66
+ buildSlideoverHTML(fileData) {
67
+ const isImage = fileData.type && fileData.type.startsWith('image/')
68
+ const humanSize = this.humanFileSize(parseInt(fileData.size))
69
+ const createdDate = new Date(fileData.created).toLocaleDateString('en-US', {
70
+ year: 'numeric',
71
+ month: 'long',
72
+ day: 'numeric'
73
+ })
74
+
75
+ return `
76
+ <div>
77
+ <div class="block overflow-hidden w-full rounded-lg aspect-h-7 aspect-w-10">
78
+ ${isImage ?
79
+ `<img src="${fileData.url}" alt="${fileData.name}" class="object-cover">` :
80
+ `<div class="flex items-center justify-center h-full bg-gray-100">
81
+ <div class="text-center">
82
+ <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
83
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
84
+ </svg>
85
+ <p class="mt-1 text-xs text-gray-500 uppercase">${fileData.type ? fileData.type.split('/')[1] : 'file'}</p>
86
+ </div>
87
+ </div>`
88
+ }
89
+ </div>
90
+ <div class="flex justify-between items-start mt-4">
91
+ <div>
92
+ <h2 class="text-lg font-medium text-gray-900">
93
+ <span class="sr-only">Details for </span>${fileData.name}
94
+ </h2>
95
+ <p class="text-sm font-medium text-gray-500">${humanSize}</p>
96
+ </div>
97
+ </div>
98
+ </div>
99
+
100
+ <div>
101
+ <h3 class="font-medium text-gray-900">Information</h3>
102
+ <dl class="mt-2 border-t border-b border-gray-200 divide-y divide-gray-200">
103
+ <div class="flex justify-between py-3 text-sm font-medium">
104
+ <dt class="text-gray-500">Created</dt>
105
+ <dd class="text-gray-900 whitespace-nowrap">${createdDate}</dd>
106
+ </div>
107
+ <div class="flex justify-between py-3 text-sm font-medium">
108
+ <dt class="text-gray-500">Content Type</dt>
109
+ <dd class="text-gray-900 whitespace-nowrap">${fileData.type || 'Unknown'}</dd>
110
+ </div>
111
+ </dl>
112
+ </div>
113
+
114
+ <div class="flex gap-x-3">
115
+ <a href="${fileData.url}?disposition=attachment" class="flex-1 py-2 px-3 text-sm font-semibold text-white bg-black rounded-md shadow-sm hover:bg-gray-800 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-panda-dark text-center">Download</a>
116
+ <button type="button" class="flex-1 py-2 px-3 text-sm font-semibold text-gray-900 bg-white rounded-md ring-1 ring-inset shadow-sm hover:bg-gray-50 ring-mid">Delete</button>
117
+ </div>
118
+ `
119
+ }
120
+
121
+ humanFileSize(bytes) {
122
+ if (bytes === 0) return '0 Bytes'
123
+ const k = 1024
124
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
125
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
126
+ return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]
127
+ }
128
+ }
@@ -1,6 +1,6 @@
1
1
  console.debug("[Panda CMS] Importing Panda CMS Stimulus Controller...")
2
2
 
3
- import { application } from "@hotwired/stimulus-loading"
3
+ import { application } from "../stimulus-loading.js"
4
4
 
5
5
  console.debug("[Panda CMS] Using shared Stimulus application...")
6
6
 
@@ -8,24 +8,59 @@ const pandaCmsApplication = application
8
8
 
9
9
  console.debug("[Panda CMS] Registering controllers...")
10
10
 
11
- // Use the same paths as defined in _importmap.html.erb
12
- import DashboardController from "panda/cms/controllers/dashboard_controller"
11
+ // Use relative imports with .js extensions for proper module resolution
12
+ import DashboardController from "./dashboard_controller.js"
13
13
  pandaCmsApplication.register("dashboard", DashboardController)
14
14
 
15
- import EditorFormController from "panda/cms/controllers/editor_form_controller"
16
- pandaCmsApplication.register("editor-form", EditorFormController)
17
-
18
- import SlugController from "panda/cms/controllers/slug_controller"
15
+ import SlugController from "./slug_controller.js"
19
16
  pandaCmsApplication.register("slug", SlugController)
20
17
 
21
- import EditorIframeController from "panda/cms/controllers/editor_iframe_controller"
22
- pandaCmsApplication.register("editor-iframe", EditorIframeController)
18
+ import TreeController from "./tree_controller.js"
19
+ pandaCmsApplication.register("tree", TreeController)
20
+
21
+ import FileGalleryController from "./file_gallery_controller.js"
22
+ pandaCmsApplication.register("file-gallery", FileGalleryController)
23
+
24
+ import NestedFormController from "./nested_form_controller.js"
25
+ pandaCmsApplication.register("nested-form", NestedFormController)
26
+
27
+ import MenuFormController from "./menu_form_controller.js"
28
+ pandaCmsApplication.register("menu-form", MenuFormController)
29
+
30
+ // Lazy load editor controllers only when needed
31
+ // These will only be loaded when the data-controller attribute is present in the DOM
32
+ class EditorFormLazyController {
33
+ connect() {
34
+ // Only import the editor controller when it's actually needed in the DOM
35
+ import("./editor_form_controller.js").then(module => {
36
+ const Controller = module.default
37
+ // Replace this lazy controller with the real one
38
+ pandaCmsApplication.register("editor-form", Controller)
39
+ }).catch(err => {
40
+ console.error("[Panda CMS] Failed to load editor-form controller:", err)
41
+ })
42
+ }
43
+ }
44
+
45
+ class EditorIframeLazyController {
46
+ connect() {
47
+ // Only import the editor controller when it's actually needed in the DOM
48
+ import("./editor_iframe_controller.js").then(module => {
49
+ const Controller = module.default
50
+ // Replace this lazy controller with the real one
51
+ pandaCmsApplication.register("editor-iframe", Controller)
52
+ }).catch(err => {
53
+ console.error("[Panda CMS] Failed to load editor-iframe controller:", err)
54
+ })
55
+ }
56
+ }
23
57
 
24
- // Import and register TailwindCSS Stimulus Components needed by CMS
25
- import { Toggle } from "tailwindcss-stimulus-components"
26
- pandaCmsApplication.register("toggle", Toggle)
58
+ // Register the lazy-loading proxy controllers
59
+ pandaCmsApplication.register("editor-form", EditorFormLazyController)
60
+ pandaCmsApplication.register("editor-iframe", EditorIframeLazyController)
27
61
 
28
- console.debug("[Panda CMS] Registered Toggle controller for slideover functionality")
62
+ // Note: Toggle, Slideover, and other TailwindCSS Stimulus Components
63
+ // are now registered by Panda Core since the admin layout lives there
29
64
 
30
65
  console.debug("[Panda CMS] Components registered...")
31
66
 
@@ -0,0 +1,96 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["previewTab", "codeTab", "previewView", "codeView", "codeInput", "saveMessage"]
5
+ static values = {
6
+ pageId: String,
7
+ blockContentId: String
8
+ }
9
+
10
+ connect() {
11
+ // Preview is active by default
12
+ }
13
+
14
+ showPreview(event) {
15
+ event.preventDefault()
16
+
17
+ // Toggle tabs
18
+ this.previewTabTarget.classList.remove("border-transparent", "text-gray-500", "hover:border-gray-300", "hover:text-gray-700")
19
+ this.previewTabTarget.classList.add("border-primary", "text-primary")
20
+
21
+ this.codeTabTarget.classList.remove("border-primary", "text-primary")
22
+ this.codeTabTarget.classList.add("border-transparent", "text-gray-500", "hover:border-gray-300", "hover:text-gray-700")
23
+
24
+ // Toggle views
25
+ this.previewViewTarget.classList.remove("hidden")
26
+ this.codeViewTarget.classList.add("hidden")
27
+ }
28
+
29
+ showCode(event) {
30
+ event.preventDefault()
31
+
32
+ // Toggle tabs
33
+ this.codeTabTarget.classList.remove("border-transparent", "text-gray-500", "hover:border-gray-300", "hover:text-gray-700")
34
+ this.codeTabTarget.classList.add("border-primary", "text-primary")
35
+
36
+ this.previewTabTarget.classList.remove("border-primary", "text-primary")
37
+ this.previewTabTarget.classList.add("border-transparent", "text-gray-500", "hover:border-gray-300", "hover:text-gray-700")
38
+
39
+ // Toggle views
40
+ this.codeViewTarget.classList.remove("hidden")
41
+ this.previewViewTarget.classList.add("hidden")
42
+ }
43
+
44
+ async saveCode(event) {
45
+ event.preventDefault()
46
+
47
+ const code = this.codeInputTarget.value
48
+ const pageId = this.pageIdValue
49
+ const blockContentId = this.blockContentIdValue
50
+
51
+ try {
52
+ const response = await fetch(`/admin/cms/pages/${pageId}/block_contents/${blockContentId}`, {
53
+ method: 'PATCH',
54
+ headers: {
55
+ 'Content-Type': 'application/json',
56
+ 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
57
+ },
58
+ body: JSON.stringify({
59
+ content: code
60
+ })
61
+ })
62
+
63
+ if (response.ok) {
64
+ this.showSaveMessage('Code saved successfully!', 'success')
65
+
66
+ // Reload the page to show updated preview
67
+ setTimeout(() => {
68
+ window.location.reload()
69
+ }, 1000)
70
+ } else {
71
+ this.showSaveMessage('Error saving code', 'error')
72
+ }
73
+ } catch (error) {
74
+ console.error('Error saving code:', error)
75
+ this.showSaveMessage('Error saving code', 'error')
76
+ }
77
+ }
78
+
79
+ showSaveMessage(message, type) {
80
+ const messageEl = this.saveMessageTarget
81
+ messageEl.textContent = message
82
+ messageEl.classList.remove('hidden')
83
+
84
+ if (type === 'success') {
85
+ messageEl.className = 'mt-2 p-3 rounded-md bg-green-50 text-green-800 border border-green-200'
86
+ } else {
87
+ messageEl.className = 'mt-2 p-3 rounded-md bg-red-50 text-red-800 border border-red-200'
88
+ }
89
+
90
+ setTimeout(() => {
91
+ if (type !== 'success') { // Keep success message visible until reload
92
+ messageEl.classList.add('hidden')
93
+ }
94
+ }, 3000)
95
+ }
96
+ }
@@ -0,0 +1,40 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["startPageField", "menuItemsSection"]
5
+
6
+ connect() {
7
+ console.log("Menu form controller connected")
8
+ // Initialize visibility based on current kind selection
9
+ this.updateFieldsVisibility()
10
+ }
11
+
12
+ kindChanged(event) {
13
+ this.updateFieldsVisibility()
14
+ }
15
+
16
+ updateFieldsVisibility() {
17
+ const kindSelect = this.element.querySelector('select[name*="[kind]"]')
18
+ if (!kindSelect) return
19
+
20
+ const selectedKind = kindSelect.value
21
+
22
+ if (selectedKind === "auto") {
23
+ // Show start page field, hide menu items section
24
+ if (this.hasStartPageFieldTarget) {
25
+ this.startPageFieldTarget.classList.remove("hidden")
26
+ }
27
+ if (this.hasMenuItemsSectionTarget) {
28
+ this.menuItemsSectionTarget.classList.add("hidden")
29
+ }
30
+ } else {
31
+ // Hide start page field, show menu items section
32
+ if (this.hasStartPageFieldTarget) {
33
+ this.startPageFieldTarget.classList.add("hidden")
34
+ }
35
+ if (this.hasMenuItemsSectionTarget) {
36
+ this.menuItemsSectionTarget.classList.remove("hidden")
37
+ }
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,35 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["template", "target"]
5
+ static values = {
6
+ wrapperSelector: String
7
+ }
8
+
9
+ connect() {
10
+ console.log("Nested form controller connected")
11
+ }
12
+
13
+ add(event) {
14
+ event.preventDefault()
15
+
16
+ const content = this.templateTarget.innerHTML.replace(/NEW_RECORD/g, new Date().getTime())
17
+ this.targetTarget.insertAdjacentHTML('beforebegin', content)
18
+ }
19
+
20
+ remove(event) {
21
+ event.preventDefault()
22
+
23
+ const wrapper = event.target.closest(this.wrapperSelectorValue)
24
+
25
+ if (wrapper.dataset.newRecord === "true") {
26
+ wrapper.remove()
27
+ } else {
28
+ wrapper.style.display = 'none'
29
+ const destroyInput = wrapper.querySelector("input[name*='_destroy']")
30
+ if (destroyInput) {
31
+ destroyInput.value = '1'
32
+ }
33
+ }
34
+ }
35
+ }