panda-cms 0.10.0 → 0.10.2

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -11
  3. data/app/components/panda/cms/code_component.rb +45 -8
  4. data/app/components/panda/cms/menu_component.rb +9 -3
  5. data/app/components/panda/cms/page_menu_component.rb +9 -1
  6. data/app/components/panda/cms/rich_text_component.rb +49 -17
  7. data/app/components/panda/cms/text_component.rb +46 -14
  8. data/app/controllers/panda/cms/admin/menus_controller.rb +2 -2
  9. data/app/controllers/panda/cms/admin/pages_controller.rb +6 -2
  10. data/app/controllers/panda/cms/admin/posts_controller.rb +3 -1
  11. data/app/controllers/panda/cms/form_submissions_controller.rb +134 -11
  12. data/app/controllers/panda/cms/pages_controller.rb +7 -2
  13. data/app/controllers/panda/cms/posts_controller.rb +16 -0
  14. data/app/helpers/panda/cms/application_helper.rb +2 -3
  15. data/app/helpers/panda/cms/asset_helper.rb +14 -72
  16. data/app/helpers/panda/cms/forms_helper.rb +60 -0
  17. data/app/helpers/panda/cms/seo_helper.rb +85 -0
  18. data/app/javascript/panda/cms/{application_panda_cms.js → application.js} +4 -0
  19. data/app/javascript/panda/cms/controllers/editor_iframe_controller.js +31 -4
  20. data/app/javascript/panda/cms/controllers/file_upload_controller.js +165 -0
  21. data/app/javascript/panda/cms/controllers/index.js +6 -0
  22. data/app/javascript/panda/cms/controllers/menu_form_controller.js +14 -1
  23. data/app/javascript/panda/cms/controllers/page_form_controller.js +454 -0
  24. data/app/javascript/panda/cms/stimulus-loading.js +2 -1
  25. data/app/models/panda/cms/menu.rb +12 -0
  26. data/app/models/panda/cms/page.rb +106 -0
  27. data/app/models/panda/cms/post.rb +97 -0
  28. data/app/views/layouts/homepage.html.erb +1 -4
  29. data/app/views/layouts/page.html.erb +1 -4
  30. data/app/views/panda/cms/admin/dashboard/show.html.erb +1 -1
  31. data/app/views/panda/cms/admin/files/index.html.erb +1 -1
  32. data/app/views/panda/cms/admin/forms/show.html.erb +3 -3
  33. data/app/views/panda/cms/admin/menus/_menu_item_fields.html.erb +3 -3
  34. data/app/views/panda/cms/admin/menus/edit.html.erb +12 -14
  35. data/app/views/panda/cms/admin/menus/index.html.erb +1 -1
  36. data/app/views/panda/cms/admin/menus/new.html.erb +5 -7
  37. data/app/views/panda/cms/admin/pages/edit.html.erb +139 -20
  38. data/app/views/panda/cms/admin/pages/index.html.erb +6 -6
  39. data/app/views/panda/cms/admin/posts/_form.html.erb +41 -2
  40. data/app/views/panda/cms/admin/posts/edit.html.erb +1 -1
  41. data/app/views/panda/cms/admin/posts/index.html.erb +4 -4
  42. data/app/views/shared/_header.html.erb +1 -4
  43. data/config/brakeman.ignore +38 -0
  44. data/config/importmap.rb +8 -6
  45. data/config/locales/en.yml +41 -0
  46. data/config/routes.rb +1 -1
  47. data/db/migrate/20251109131150_add_seo_fields_to_pages.rb +32 -0
  48. data/db/migrate/20251109131205_add_seo_fields_to_posts.rb +27 -0
  49. data/db/migrate/20251110114258_add_spam_tracking_to_form_submissions.rb +7 -0
  50. data/db/migrate/20251110122812_add_performance_indexes_to_pages_and_redirects.rb +13 -0
  51. data/lib/panda/cms/asset_loader.rb +27 -77
  52. data/lib/panda/cms/bulk_editor.rb +288 -12
  53. data/lib/panda/cms/engine/asset_config.rb +49 -0
  54. data/lib/panda/cms/engine/autoload_config.rb +19 -0
  55. data/lib/panda/cms/engine/backtrace_config.rb +42 -0
  56. data/lib/panda/cms/engine/core_config.rb +106 -0
  57. data/lib/panda/cms/engine/helper_config.rb +20 -0
  58. data/lib/panda/cms/engine/route_config.rb +34 -0
  59. data/lib/panda/cms/engine/view_component_config.rb +31 -0
  60. data/lib/panda/cms/engine.rb +44 -221
  61. data/lib/panda/cms.rb +10 -0
  62. data/lib/panda-cms/version.rb +1 -1
  63. data/lib/panda-cms.rb +16 -2
  64. metadata +20 -22
  65. data/app/javascript/panda_cms/stimulus-loading.js +0 -39
  66. data/app/views/panda/cms/shared/_importmap.html.erb +0 -34
  67. data/config/initializers/inflections.rb +0 -5
  68. data/lib/tasks/assets.rake +0 -540
@@ -24,51 +24,9 @@ module Panda
24
24
  end
25
25
 
26
26
  # Include only Panda CMS JavaScript
27
- def panda_cms_javascript
28
- js_url = Panda::CMS::AssetLoader.javascript_url
29
- return "" unless js_url
30
-
31
- if Panda::CMS::AssetLoader.use_github_assets?
32
- # GitHub-hosted assets with integrity check
33
- version = Panda::CMS::AssetLoader.send(:asset_version)
34
- integrity = asset_integrity(version, "panda-cms-#{version}.js")
35
-
36
- tag_options = {
37
- src: js_url
38
- }
39
- # In CI environment, don't use defer to ensure immediate execution
40
- tag_options[:defer] = true unless ENV["GITHUB_ACTIONS"] == "true"
41
- # Standalone bundles should NOT use type: "module" - they're regular scripts
42
- # Only use type: "module" for importmap/ES module assets
43
- if !js_url.include?("panda-cms-assets")
44
- tag_options[:type] = "module"
45
- end
46
- tag_options[:integrity] = integrity if integrity
47
- tag_options[:crossorigin] = "anonymous" if integrity
48
-
49
- content_tag(:script, "", tag_options)
50
- elsif js_url.include?("panda-cms-assets")
51
- # Development assets - check if it's a standalone bundle or importmap
52
- defer_option = (ENV["GITHUB_ACTIONS"] == "true") ? {} : {defer: true}
53
- javascript_include_tag(js_url, **defer_option)
54
- # Standalone bundle - don't use type: "module"
55
- else
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
70
- end
71
- end
27
+ #
28
+ # Delegates to Core's helper which automatically includes all registered modules.
29
+ alias_method :panda_cms_javascript, :panda_core_javascript
72
30
 
73
31
  # Include only Panda CMS CSS
74
32
  def panda_cms_stylesheet
@@ -117,39 +75,16 @@ module Panda
117
75
  version = Panda::CMS::VERSION
118
76
  js_url = Panda::CMS::AssetLoader.javascript_url
119
77
  css_url = Panda::CMS::AssetLoader.css_url
120
- using_github = Panda::CMS::AssetLoader.use_github_assets?
121
- compiled_available = Panda::CMS::AssetLoader.send(:compiled_assets_available?)
122
-
123
- # Additional CI debugging
124
- asset_file_exists = js_url && File.exist?(Rails.root.join("public#{js_url}"))
125
- ci_env = ENV["GITHUB_ACTIONS"] == "true"
126
-
127
- # Check what script tag will be generated
128
- script_tag_preview = if using_github
129
- tag_options = {src: js_url}
130
- tag_options[:defer] = true unless ci_env
131
- if !js_url.include?("panda-cms-assets")
132
- tag_options[:type] = "module"
133
- end
134
- "Script tag: <script#{tag_options.map { |k, v| (v == true) ? " #{k}" : " #{k}=\"#{v}\"" }.join}></script>"
135
- else
136
- "Using development assets"
137
- end
78
+ Panda::CMS::AssetLoader.use_github_assets?
138
79
 
139
80
  debug_info = [
140
81
  "<!-- Panda CMS Asset Debug Info -->",
141
82
  "<!-- Version: #{version} -->",
142
- "<!-- Using GitHub assets: #{using_github} -->",
143
- "<!-- Compiled assets available: #{compiled_available} -->",
83
+ "<!-- Using importmaps: true (no compilation) -->",
144
84
  "<!-- JavaScript URL: #{js_url} -->",
145
- "<!-- CSS URL: #{css_url || "none"} -->",
85
+ "<!-- CSS URL: #{css_url || "CSS from panda-core"} -->",
146
86
  "<!-- Rails environment: #{Rails.env} -->",
147
- "<!-- Asset file exists: #{asset_file_exists} -->",
148
- "<!-- Rails root: #{Rails.root} -->",
149
- "<!-- CI environment: #{ci_env} -->",
150
- "<!-- #{script_tag_preview} -->",
151
- "<!-- Params embed_id: #{params[:embed_id] if respond_to?(:params)} -->",
152
- "<!-- Compiled at: #{Time.now.utc.iso8601} -->"
87
+ "<!-- Rails root: #{Rails.root} -->"
153
88
  ]
154
89
 
155
90
  debug_info.join("\n").html_safe
@@ -194,6 +129,13 @@ module Panda
194
129
  ].join("\n").html_safe
195
130
  end
196
131
 
132
+ # Returns asset HTML for injection into iframe
133
+ # Used by editor_iframe_controller to inject assets dynamically
134
+ # Returns the raw HTML string (not JSON-encoded)
135
+ def panda_cms_injectable_assets
136
+ panda_cms_complete_assets.to_s
137
+ end
138
+
197
139
  private
198
140
 
199
141
  def asset_integrity(version, filename)
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module CMS
5
+ module FormsHelper
6
+ # Generates a hidden timing field for spam protection
7
+ # This should be included in all forms that submit to Panda::CMS::FormSubmissionsController
8
+ #
9
+ # @example In your form
10
+ # <%= form_with url: form_submissions_path(form.id), method: :post do |f| %>
11
+ # <%= panda_cms_form_timestamp %>
12
+ # <%= f.text_field :name %>
13
+ # <%= f.submit "Submit" %>
14
+ # <% end %>
15
+ #
16
+ # @return [String] HTML hidden input with current timestamp
17
+ def panda_cms_form_timestamp
18
+ hidden_field_tag "_form_timestamp", Time.current.to_i
19
+ end
20
+
21
+ # Generates a complete spam-protected form wrapper
22
+ # Includes timing protection and invisible captcha honeypot
23
+ #
24
+ # @param form [Panda::CMS::Form] The form model
25
+ # @param options [Hash] Additional options for form_with
26
+ # @yield [FormBuilder] The form builder
27
+ #
28
+ # @example
29
+ # <%= panda_cms_protected_form(form) do |f| %>
30
+ # <%= f.text_field :name %>
31
+ # <%= f.email_field :email %>
32
+ # <%= f.text_area :message %>
33
+ # <%= f.submit "Send Message" %>
34
+ # <% end %>
35
+ def panda_cms_protected_form(form, options = {}, &block)
36
+ default_options = {
37
+ url: "/forms/#{form.id}",
38
+ method: :post,
39
+ data: {turbo: false}
40
+ }
41
+
42
+ form_with(**default_options.merge(options)) do |f|
43
+ concat panda_cms_form_timestamp
44
+ concat invisible_captcha_field
45
+ yield f
46
+ end
47
+ end
48
+
49
+ # Generates the invisible captcha honeypot field
50
+ # This is a hidden field that bots typically fill out but humans don't
51
+ #
52
+ # @return [String] HTML for invisible captcha field
53
+ def invisible_captcha_field
54
+ # invisible_captcha gem automatically adds this, but we can add it manually if needed
55
+ # The field name "spinner" is configured in invisible_captcha initializer
56
+ text_field_tag :spinner, nil, style: "position: absolute; left: -9999px; width: 1px; height: 1px;", tabindex: -1, autocomplete: "off", aria_hidden: true
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module CMS
5
+ module SEOHelper
6
+ #
7
+ # Renders all SEO meta tags for a given page or post
8
+ #
9
+ # @param resource [Panda::CMS::Page, Panda::CMS::Post] The page or post to render meta tags for
10
+ # @return [String] HTML meta tags
11
+ # @visibility public
12
+ #
13
+ def render_seo_meta_tags(resource)
14
+ return "" if resource.blank?
15
+
16
+ tags = []
17
+
18
+ # Basic SEO tags
19
+ tags << tag.meta(name: "description", content: resource.effective_seo_description) if resource.effective_seo_description.present?
20
+ tags << tag.meta(name: "keywords", content: resource.seo_keywords) if resource.seo_keywords.present?
21
+ tags << tag.meta(name: "robots", content: resource.robots_meta_content)
22
+ tags << tag.link(rel: "canonical", href: canonical_url_for(resource))
23
+
24
+ # Open Graph tags
25
+ tags << tag.meta(property: "og:title", content: resource.effective_og_title)
26
+ tags << tag.meta(property: "og:description", content: resource.effective_og_description) if resource.effective_og_description.present?
27
+ tags << tag.meta(property: "og:type", content: resource.og_type)
28
+ tags << tag.meta(property: "og:url", content: canonical_url_for(resource))
29
+
30
+ # Open Graph image
31
+ if resource.og_image.attached?
32
+ og_image_url = url_for(resource.og_image.variant(:og_share))
33
+ tags << tag.meta(property: "og:image", content: og_image_url)
34
+ tags << tag.meta(property: "og:image:width", content: "1200")
35
+ tags << tag.meta(property: "og:image:height", content: "630")
36
+ end
37
+
38
+ # Twitter Card tags (with fallback to OG)
39
+ tags << tag.meta(name: "twitter:card", content: "summary_large_image")
40
+ tags << tag.meta(name: "twitter:title", content: resource.effective_og_title)
41
+ tags << tag.meta(name: "twitter:description", content: resource.effective_og_description) if resource.effective_og_description.present?
42
+
43
+ # Twitter image (same as OG)
44
+ if resource.og_image.attached?
45
+ tags << tag.meta(name: "twitter:image", content: url_for(resource.og_image.variant(:og_share)))
46
+ end
47
+
48
+ safe_join(tags, "\n")
49
+ end
50
+
51
+ #
52
+ # Renders just the page title with SEO optimization
53
+ #
54
+ # @param resource [Panda::CMS::Page, Panda::CMS::Post] The page or post
55
+ # @param separator [String] Separator between page title and site name
56
+ # @param site_name [String] The site name (optional)
57
+ # @return [String] Formatted page title
58
+ # @visibility public
59
+ #
60
+ def seo_title(resource, separator: " · ", site_name: nil)
61
+ parts = [resource.effective_seo_title]
62
+ parts << site_name if site_name.present?
63
+ safe_join(parts, separator)
64
+ end
65
+
66
+ private
67
+
68
+ #
69
+ # Generates the full canonical URL for a resource
70
+ #
71
+ # @param resource [Panda::CMS::Page, Panda::CMS::Post] The page or post
72
+ # @return [String] Full canonical URL
73
+ # @visibility private
74
+ #
75
+ def canonical_url_for(resource)
76
+ # If canonical_url is a full URL, use it as-is
77
+ return resource.canonical_url if resource.canonical_url&.match?(%r{\Ahttps?://})
78
+
79
+ # Otherwise, construct from the path
80
+ path = resource.effective_canonical_url
81
+ "#{request.protocol}#{request.host_with_port}#{path}"
82
+ end
83
+ end
84
+ end
85
+ end
@@ -3,5 +3,9 @@ console.debug("[Panda CMS] Controllers loading...");
3
3
  import "./controllers/index.js"
4
4
  console.debug("[Panda CMS] Controllers loaded...");
5
5
 
6
+ // Mark that Panda CMS JavaScript has loaded
7
+ window.pandaCmsLoaded = true
8
+ console.debug("[Panda CMS] Ready!");
9
+
6
10
  // Editor resources are now handled by panda-editor gem
7
11
  // The panda-editor gem will load its own resources when needed
@@ -8,7 +8,8 @@ export default class extends Controller {
8
8
  static values = {
9
9
  pageId: Number,
10
10
  adminPath: String,
11
- autosave: Boolean
11
+ autosave: Boolean,
12
+ assets: String
12
13
  }
13
14
 
14
15
  connect() {
@@ -65,6 +66,33 @@ export default class extends Controller {
65
66
  this.body = this.frameDocument.body
66
67
  this.head = this.frameDocument.head
67
68
 
69
+ // Inject CMS assets into the iframe head
70
+ if (this.hasAssetsValue && this.assetsValue) {
71
+ console.debug("[Panda CMS] Injecting assets into iframe", {
72
+ assetsLength: this.assetsValue.length,
73
+ assetsPreview: this.assetsValue.substring(0, 200)
74
+ })
75
+ const assetsHTML = this.assetsValue
76
+ const tempDiv = document.createElement('div')
77
+ tempDiv.innerHTML = assetsHTML
78
+
79
+ // Append each node to the iframe's head
80
+ Array.from(tempDiv.childNodes).forEach(node => {
81
+ if (node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.TEXT_NODE) {
82
+ const importedNode = this.frameDocument.importNode(node, true)
83
+ this.head.appendChild(importedNode)
84
+ console.debug("[Panda CMS] Injected node:", node.nodeName)
85
+ }
86
+ })
87
+
88
+ console.debug("[Panda CMS] Assets injected successfully - head element count:", this.head.children.length)
89
+ } else {
90
+ console.warn("[Panda CMS] No assets to inject", {
91
+ hasAssetsValue: this.hasAssetsValue,
92
+ assetsValue: this.assetsValue
93
+ })
94
+ }
95
+
68
96
  // Ensure iframe content is properly positioned but doesn't block UI
69
97
  this.body.style.position = "relative"
70
98
  this.body.style.zIndex = "1"
@@ -545,11 +573,10 @@ export default class extends Controller {
545
573
  }
546
574
 
547
575
  setupSlideoverHandling() {
548
- // Watch for slideover toggle
549
- const slideoverToggle = document.getElementById('slideover-toggle')
576
+ // Watch for slideover visibility changes
550
577
  const slideover = document.getElementById('slideover')
551
578
 
552
- if (slideoverToggle && slideover) {
579
+ if (slideover) {
553
580
  const observer = new MutationObserver((mutations) => {
554
581
  mutations.forEach((mutation) => {
555
582
  if (mutation.attributeName === 'class') {
@@ -0,0 +1,165 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["input", "preview", "dropzone", "fileInfo", "removeButton"]
5
+
6
+ connect() {
7
+ // Setup drag and drop handlers
8
+ if (this.hasDropzoneTarget) {
9
+ this.setupDragAndDrop()
10
+ }
11
+ }
12
+
13
+ setupDragAndDrop() {
14
+ const dropzone = this.dropzoneTarget
15
+
16
+ // Prevent default drag behaviors
17
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
18
+ dropzone.addEventListener(eventName, this.preventDefaults.bind(this), false)
19
+ document.body.addEventListener(eventName, this.preventDefaults.bind(this), false)
20
+ })
21
+
22
+ // Highlight drop zone when item is dragged over it
23
+ ['dragenter', 'dragover'].forEach(eventName => {
24
+ dropzone.addEventListener(eventName, this.highlight.bind(this), false)
25
+ })
26
+
27
+ ;['dragleave', 'drop'].forEach(eventName => {
28
+ dropzone.addEventListener(eventName, this.unhighlight.bind(this), false)
29
+ })
30
+
31
+ // Handle dropped files
32
+ dropzone.addEventListener('drop', this.handleDrop.bind(this), false)
33
+ }
34
+
35
+ preventDefaults(e) {
36
+ e.preventDefault()
37
+ e.stopPropagation()
38
+ }
39
+
40
+ highlight(e) {
41
+ this.dropzoneTarget.classList.add('border-indigo-600', 'bg-indigo-50', 'dark:bg-indigo-950', 'dark:border-indigo-400')
42
+ }
43
+
44
+ unhighlight(e) {
45
+ this.dropzoneTarget.classList.remove('border-indigo-600', 'bg-indigo-50', 'dark:bg-indigo-950', 'dark:border-indigo-400')
46
+ }
47
+
48
+ handleDrop(e) {
49
+ const dt = e.dataTransfer
50
+ const files = dt.files
51
+
52
+ if (files.length > 0) {
53
+ // Update the file input with the dropped file
54
+ this.inputTarget.files = files
55
+ this.handleFileSelect()
56
+ }
57
+ }
58
+
59
+ // Triggered when user selects file via input or drag-drop
60
+ handleFileSelect() {
61
+ const file = this.inputTarget.files[0]
62
+
63
+ if (!file) {
64
+ return
65
+ }
66
+
67
+ // Show file preview if it's an image
68
+ if (file.type.startsWith('image/')) {
69
+ this.showImagePreview(file)
70
+ } else {
71
+ this.showFileInfo(file)
72
+ }
73
+ }
74
+
75
+ showImagePreview(file) {
76
+ const reader = new FileReader()
77
+
78
+ reader.onload = (e) => {
79
+ if (this.hasPreviewTarget) {
80
+ // Create or update preview image
81
+ const existingImage = this.previewTarget.querySelector('img')
82
+ if (existingImage) {
83
+ existingImage.src = e.target.result
84
+ } else {
85
+ const img = document.createElement('img')
86
+ img.src = e.target.result
87
+ img.className = 'max-w-xs rounded border border-gray-300 dark:border-gray-600'
88
+ this.previewTarget.innerHTML = ''
89
+ this.previewTarget.appendChild(img)
90
+ }
91
+ this.previewTarget.classList.remove('hidden')
92
+ }
93
+
94
+ // Show file info with remove button
95
+ this.showFileInfo(file, true)
96
+ }
97
+
98
+ reader.readAsDataURL(file)
99
+ }
100
+
101
+ showFileInfo(file, withPreview = false) {
102
+ if (!this.hasFileInfoTarget) {
103
+ return
104
+ }
105
+
106
+ const fileSize = this.humanFileSize(file.size)
107
+ const fileName = file.name
108
+
109
+ this.fileInfoTarget.innerHTML = `
110
+ <div class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
111
+ <div class="flex items-center gap-x-3 flex-1 min-w-0">
112
+ <svg class="size-8 text-gray-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
113
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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" />
114
+ </svg>
115
+ <div class="flex-1 min-w-0">
116
+ <p class="text-sm font-medium text-gray-900 dark:text-white truncate">${fileName}</p>
117
+ <p class="text-xs text-gray-500 dark:text-gray-400">${fileSize}</p>
118
+ </div>
119
+ </div>
120
+ <button type="button"
121
+ data-action="click->file-upload#removeFile"
122
+ class="flex-shrink-0 ml-3 text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300">
123
+ <svg class="size-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
124
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
125
+ </svg>
126
+ </button>
127
+ </div>
128
+ `
129
+ this.fileInfoTarget.classList.remove('hidden')
130
+
131
+ // Hide the dropzone upload area
132
+ if (this.hasDropzoneTarget) {
133
+ this.dropzoneTarget.classList.add('hidden')
134
+ }
135
+ }
136
+
137
+ removeFile() {
138
+ // Clear the file input
139
+ this.inputTarget.value = ''
140
+
141
+ // Hide preview and file info
142
+ if (this.hasPreviewTarget) {
143
+ this.previewTarget.classList.add('hidden')
144
+ this.previewTarget.innerHTML = ''
145
+ }
146
+
147
+ if (this.hasFileInfoTarget) {
148
+ this.fileInfoTarget.classList.add('hidden')
149
+ this.fileInfoTarget.innerHTML = ''
150
+ }
151
+
152
+ // Show the dropzone again
153
+ if (this.hasDropzoneTarget) {
154
+ this.dropzoneTarget.classList.remove('hidden')
155
+ }
156
+ }
157
+
158
+ humanFileSize(bytes) {
159
+ if (bytes === 0) return '0 Bytes'
160
+ const k = 1024
161
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
162
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
163
+ return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]
164
+ }
165
+ }
@@ -21,6 +21,12 @@ pandaCmsApplication.register("tree", TreeController)
21
21
  import FileGalleryController from "./file_gallery_controller.js"
22
22
  pandaCmsApplication.register("file-gallery", FileGalleryController)
23
23
 
24
+ import FileUploadController from "./file_upload_controller.js"
25
+ pandaCmsApplication.register("file-upload", FileUploadController)
26
+
27
+ import PageFormController from "./page_form_controller.js"
28
+ pandaCmsApplication.register("page-form", PageFormController)
29
+
24
30
  import NestedFormController from "./nested_form_controller.js"
25
31
  pandaCmsApplication.register("nested-form", NestedFormController)
26
32
 
@@ -10,25 +10,38 @@ export default class extends Controller {
10
10
  }
11
11
 
12
12
  kindChanged(event) {
13
+ console.log("[menu-form] kindChanged called", event)
13
14
  this.updateFieldsVisibility()
14
15
  }
15
16
 
16
17
  updateFieldsVisibility() {
18
+ console.log("[menu-form] updateFieldsVisibility called")
17
19
  const kindSelect = this.element.querySelector('select[name*="[kind]"]')
18
- if (!kindSelect) return
20
+ console.log("[menu-form] kindSelect found:", !!kindSelect)
21
+
22
+ if (!kindSelect) {
23
+ console.error("[menu-form] Could not find kind select!")
24
+ return
25
+ }
19
26
 
20
27
  const selectedKind = kindSelect.value
28
+ console.log("[menu-form] selectedKind:", selectedKind)
21
29
 
22
30
  if (selectedKind === "auto") {
23
31
  // Show start page field, hide menu items section
32
+ console.log("[menu-form] AUTO - Showing start page field")
24
33
  if (this.hasStartPageFieldTarget) {
34
+ console.log("[menu-form] Removing hidden from start page field")
25
35
  this.startPageFieldTarget.classList.remove("hidden")
36
+ } else {
37
+ console.error("[menu-form] Start page field target not found!")
26
38
  }
27
39
  if (this.hasMenuItemsSectionTarget) {
28
40
  this.menuItemsSectionTarget.classList.add("hidden")
29
41
  }
30
42
  } else {
31
43
  // Hide start page field, show menu items section
44
+ console.log("[menu-form] STATIC - Hiding start page field")
32
45
  if (this.hasStartPageFieldTarget) {
33
46
  this.startPageFieldTarget.classList.add("hidden")
34
47
  }