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.
- 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 +9 -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
|
@@ -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
|
|
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
|
-
#
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
|
@@ -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 "
|
|
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
|
|
12
|
-
import DashboardController from "
|
|
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
|
|
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
|
|
22
|
-
pandaCmsApplication.register("
|
|
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
|
-
//
|
|
25
|
-
|
|
26
|
-
pandaCmsApplication.register("
|
|
58
|
+
// Register the lazy-loading proxy controllers
|
|
59
|
+
pandaCmsApplication.register("editor-form", EditorFormLazyController)
|
|
60
|
+
pandaCmsApplication.register("editor-iframe", EditorIframeLazyController)
|
|
27
61
|
|
|
28
|
-
|
|
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
|
+
}
|