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.
- checksums.yaml +4 -4
- data/README.md +2 -11
- data/app/components/panda/cms/code_component.rb +45 -8
- data/app/components/panda/cms/menu_component.rb +9 -3
- data/app/components/panda/cms/page_menu_component.rb +9 -1
- data/app/components/panda/cms/rich_text_component.rb +49 -17
- data/app/components/panda/cms/text_component.rb +46 -14
- data/app/controllers/panda/cms/admin/menus_controller.rb +2 -2
- data/app/controllers/panda/cms/admin/pages_controller.rb +6 -2
- data/app/controllers/panda/cms/admin/posts_controller.rb +3 -1
- data/app/controllers/panda/cms/form_submissions_controller.rb +134 -11
- data/app/controllers/panda/cms/pages_controller.rb +7 -2
- data/app/controllers/panda/cms/posts_controller.rb +16 -0
- data/app/helpers/panda/cms/application_helper.rb +2 -3
- data/app/helpers/panda/cms/asset_helper.rb +14 -72
- data/app/helpers/panda/cms/forms_helper.rb +60 -0
- data/app/helpers/panda/cms/seo_helper.rb +85 -0
- data/app/javascript/panda/cms/{application_panda_cms.js → application.js} +4 -0
- data/app/javascript/panda/cms/controllers/editor_iframe_controller.js +31 -4
- data/app/javascript/panda/cms/controllers/file_upload_controller.js +165 -0
- data/app/javascript/panda/cms/controllers/index.js +6 -0
- data/app/javascript/panda/cms/controllers/menu_form_controller.js +14 -1
- data/app/javascript/panda/cms/controllers/page_form_controller.js +454 -0
- data/app/javascript/panda/cms/stimulus-loading.js +2 -1
- data/app/models/panda/cms/menu.rb +12 -0
- data/app/models/panda/cms/page.rb +106 -0
- data/app/models/panda/cms/post.rb +97 -0
- data/app/views/layouts/homepage.html.erb +1 -4
- data/app/views/layouts/page.html.erb +1 -4
- data/app/views/panda/cms/admin/dashboard/show.html.erb +1 -1
- data/app/views/panda/cms/admin/files/index.html.erb +1 -1
- data/app/views/panda/cms/admin/forms/show.html.erb +3 -3
- data/app/views/panda/cms/admin/menus/_menu_item_fields.html.erb +3 -3
- data/app/views/panda/cms/admin/menus/edit.html.erb +12 -14
- data/app/views/panda/cms/admin/menus/index.html.erb +1 -1
- data/app/views/panda/cms/admin/menus/new.html.erb +5 -7
- data/app/views/panda/cms/admin/pages/edit.html.erb +139 -20
- data/app/views/panda/cms/admin/pages/index.html.erb +6 -6
- data/app/views/panda/cms/admin/posts/_form.html.erb +41 -2
- data/app/views/panda/cms/admin/posts/edit.html.erb +1 -1
- data/app/views/panda/cms/admin/posts/index.html.erb +4 -4
- data/app/views/shared/_header.html.erb +1 -4
- data/config/brakeman.ignore +38 -0
- data/config/importmap.rb +8 -6
- data/config/locales/en.yml +41 -0
- data/config/routes.rb +1 -1
- data/db/migrate/20251109131150_add_seo_fields_to_pages.rb +32 -0
- data/db/migrate/20251109131205_add_seo_fields_to_posts.rb +27 -0
- data/db/migrate/20251110114258_add_spam_tracking_to_form_submissions.rb +7 -0
- data/db/migrate/20251110122812_add_performance_indexes_to_pages_and_redirects.rb +13 -0
- data/lib/panda/cms/asset_loader.rb +27 -77
- data/lib/panda/cms/bulk_editor.rb +288 -12
- data/lib/panda/cms/engine/asset_config.rb +49 -0
- data/lib/panda/cms/engine/autoload_config.rb +19 -0
- data/lib/panda/cms/engine/backtrace_config.rb +42 -0
- data/lib/panda/cms/engine/core_config.rb +106 -0
- data/lib/panda/cms/engine/helper_config.rb +20 -0
- data/lib/panda/cms/engine/route_config.rb +34 -0
- data/lib/panda/cms/engine/view_component_config.rb +31 -0
- data/lib/panda/cms/engine.rb +44 -221
- data/lib/panda/cms.rb +10 -0
- data/lib/panda-cms/version.rb +1 -1
- data/lib/panda-cms.rb +16 -2
- metadata +20 -22
- data/app/javascript/panda_cms/stimulus-loading.js +0 -39
- data/app/views/panda/cms/shared/_importmap.html.erb +0 -34
- data/config/initializers/inflections.rb +0 -5
- data/lib/tasks/assets.rake +0 -540
|
@@ -7,6 +7,14 @@ module Panda
|
|
|
7
7
|
#
|
|
8
8
|
# Bulk editor for site content in JSON format
|
|
9
9
|
#
|
|
10
|
+
# IMPORTANT: When adding new fields to Page, Post, or Menu models via migrations:
|
|
11
|
+
# 1. Update the `extract_current_data` method to export the new fields
|
|
12
|
+
# 2. Update the `import` method to import the new fields
|
|
13
|
+
# 3. Run the spec at spec/lib/panda/cms/bulk_editor_spec.rb to verify completeness
|
|
14
|
+
# 4. The spec will fail if database columns are not being exported
|
|
15
|
+
#
|
|
16
|
+
# This ensures content can be properly exported and imported between environments.
|
|
17
|
+
#
|
|
10
18
|
class BulkEditor
|
|
11
19
|
#
|
|
12
20
|
# Export all site content to a JSON string
|
|
@@ -45,8 +53,20 @@ module Panda
|
|
|
45
53
|
page = Panda::CMS::Page.create!(
|
|
46
54
|
path: path,
|
|
47
55
|
title: page_data["title"],
|
|
56
|
+
status: page_data["status"] || "active",
|
|
57
|
+
page_type: page_data["page_type"] || "standard",
|
|
48
58
|
template: Panda::CMS::Template.find_by(name: page_data["template"]),
|
|
49
|
-
parent: Panda::CMS::Page.find_by(path: page_data["parent"])
|
|
59
|
+
parent: Panda::CMS::Page.find_by(path: page_data["parent"]),
|
|
60
|
+
# SEO fields
|
|
61
|
+
seo_title: page_data["seo_title"],
|
|
62
|
+
seo_description: page_data["seo_description"],
|
|
63
|
+
seo_keywords: page_data["seo_keywords"],
|
|
64
|
+
seo_index_mode: page_data["seo_index_mode"] || "visible",
|
|
65
|
+
canonical_url: page_data["canonical_url"],
|
|
66
|
+
# Open Graph fields
|
|
67
|
+
og_type: page_data["og_type"] || "website",
|
|
68
|
+
og_title: page_data["og_title"],
|
|
69
|
+
og_description: page_data["og_description"]
|
|
50
70
|
)
|
|
51
71
|
rescue => e
|
|
52
72
|
debug[:error] << "Failed to create page '#{path}': #{e.message}"
|
|
@@ -62,18 +82,37 @@ module Panda
|
|
|
62
82
|
else
|
|
63
83
|
page = Panda::CMS::Page.find_by(path: path)
|
|
64
84
|
|
|
65
|
-
if
|
|
66
|
-
page.update(title: page_data["title"])
|
|
67
|
-
debug[:success] << "Updated: page '#{path}' title from '#{current_data["pages"][path]["title"]}' to '#{page_data["title"]}'"
|
|
68
|
-
end
|
|
69
|
-
|
|
85
|
+
# Check if template changed
|
|
70
86
|
if page_data["template"] != current_data["pages"][path]["template"]
|
|
71
87
|
# TODO: Handle page template changes
|
|
72
88
|
debug[:error] << "Page '#{path}' template is '#{current_data["pages"][path]["template"]}' and cannot be changed to '#{page_data["template"]}' without manual intervention"
|
|
89
|
+
else
|
|
90
|
+
# Update page fields
|
|
91
|
+
begin
|
|
92
|
+
page.update!(
|
|
93
|
+
title: page_data["title"],
|
|
94
|
+
status: page_data["status"] || page.status,
|
|
95
|
+
page_type: page_data["page_type"] || page.page_type,
|
|
96
|
+
parent: Panda::CMS::Page.find_by(path: page_data["parent"]) || page.parent,
|
|
97
|
+
# SEO fields
|
|
98
|
+
seo_title: page_data["seo_title"],
|
|
99
|
+
seo_description: page_data["seo_description"],
|
|
100
|
+
seo_keywords: page_data["seo_keywords"],
|
|
101
|
+
seo_index_mode: page_data["seo_index_mode"] || page.seo_index_mode,
|
|
102
|
+
canonical_url: page_data["canonical_url"],
|
|
103
|
+
# Open Graph fields
|
|
104
|
+
og_type: page_data["og_type"] || page.og_type,
|
|
105
|
+
og_title: page_data["og_title"],
|
|
106
|
+
og_description: page_data["og_description"]
|
|
107
|
+
)
|
|
108
|
+
debug[:success] << "Updated page '#{path}'"
|
|
109
|
+
rescue => e
|
|
110
|
+
debug[:error] << "Failed to update page '#{path}': #{e.message}"
|
|
111
|
+
end
|
|
73
112
|
end
|
|
74
113
|
end
|
|
75
114
|
|
|
76
|
-
page_data["contents"]
|
|
115
|
+
page_data["contents"]&.each do |key, block_data|
|
|
77
116
|
content = block_data["content"]
|
|
78
117
|
|
|
79
118
|
if current_data.dig("pages", path, "contents", key).nil?
|
|
@@ -117,12 +156,144 @@ module Panda
|
|
|
117
156
|
end
|
|
118
157
|
end
|
|
119
158
|
|
|
120
|
-
|
|
159
|
+
# Posts
|
|
160
|
+
new_data["posts"]&.each do |post_data|
|
|
161
|
+
slug = post_data["slug"]
|
|
162
|
+
post = Panda::CMS::Post.find_by(slug: slug)
|
|
163
|
+
|
|
164
|
+
# Find or create user by email
|
|
165
|
+
user = if post_data["user_email"]
|
|
166
|
+
Panda::Core::User.find_by(email: post_data["user_email"]) || Panda::Core::User.first
|
|
167
|
+
else
|
|
168
|
+
Panda::Core::User.first # Fallback to first user
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
author = if post_data["author_email"]
|
|
172
|
+
Panda::Core::User.find_by(email: post_data["author_email"])
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
if post.nil?
|
|
176
|
+
begin
|
|
177
|
+
post = Panda::CMS::Post.create!(
|
|
178
|
+
slug: slug,
|
|
179
|
+
title: post_data["title"],
|
|
180
|
+
status: post_data["status"] || "draft",
|
|
181
|
+
published_at: post_data["published_at"],
|
|
182
|
+
user: user,
|
|
183
|
+
author: author,
|
|
184
|
+
content: post_data["content"] || "",
|
|
185
|
+
cached_content: post_data["cached_content"] || "",
|
|
186
|
+
# SEO fields
|
|
187
|
+
seo_title: post_data["seo_title"],
|
|
188
|
+
seo_description: post_data["seo_description"],
|
|
189
|
+
seo_keywords: post_data["seo_keywords"],
|
|
190
|
+
seo_index_mode: post_data["seo_index_mode"] || "visible",
|
|
191
|
+
canonical_url: post_data["canonical_url"],
|
|
192
|
+
# Open Graph fields
|
|
193
|
+
og_type: post_data["og_type"] || "article",
|
|
194
|
+
og_title: post_data["og_title"],
|
|
195
|
+
og_description: post_data["og_description"]
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
debug[:success] << "Created post '#{post.title}' (#{slug})"
|
|
199
|
+
rescue => e
|
|
200
|
+
debug[:error] << "Failed to create post '#{slug}': #{e.message}"
|
|
201
|
+
end
|
|
202
|
+
else
|
|
203
|
+
# Update existing post
|
|
204
|
+
begin
|
|
205
|
+
post.update!(
|
|
206
|
+
title: post_data["title"],
|
|
207
|
+
status: post_data["status"] || post.status,
|
|
208
|
+
published_at: post_data["published_at"] || post.published_at,
|
|
209
|
+
author: author || post.author,
|
|
210
|
+
content: post_data["content"] || post.content || "",
|
|
211
|
+
cached_content: post_data["cached_content"] || post.cached_content || "",
|
|
212
|
+
# SEO fields
|
|
213
|
+
seo_title: post_data["seo_title"],
|
|
214
|
+
seo_description: post_data["seo_description"],
|
|
215
|
+
seo_keywords: post_data["seo_keywords"],
|
|
216
|
+
seo_index_mode: post_data["seo_index_mode"] || post.seo_index_mode,
|
|
217
|
+
canonical_url: post_data["canonical_url"],
|
|
218
|
+
# Open Graph fields
|
|
219
|
+
og_type: post_data["og_type"] || post.og_type,
|
|
220
|
+
og_title: post_data["og_title"],
|
|
221
|
+
og_description: post_data["og_description"]
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
debug[:success] << "Updated post '#{post.title}' (#{slug})"
|
|
225
|
+
rescue => e
|
|
226
|
+
debug[:error] << "Failed to update post '#{slug}': #{e.message}"
|
|
227
|
+
end
|
|
228
|
+
end
|
|
121
229
|
end
|
|
122
230
|
|
|
123
|
-
|
|
231
|
+
# Menus
|
|
232
|
+
new_data["menus"]&.each do |menu_data|
|
|
233
|
+
menu = Panda::CMS::Menu.find_by(name: menu_data["name"])
|
|
234
|
+
|
|
235
|
+
if menu.nil?
|
|
236
|
+
begin
|
|
237
|
+
if menu_data["kind"] == "auto"
|
|
238
|
+
start_page = Panda::CMS::Page.find_by(path: menu_data["start_page_path"])
|
|
239
|
+
menu = Panda::CMS::Menu.create!(
|
|
240
|
+
name: menu_data["name"],
|
|
241
|
+
kind: "auto",
|
|
242
|
+
start_page: start_page
|
|
243
|
+
)
|
|
244
|
+
debug[:success] << "Created auto menu '#{menu.name}'"
|
|
245
|
+
else
|
|
246
|
+
menu = Panda::CMS::Menu.create!(
|
|
247
|
+
name: menu_data["name"],
|
|
248
|
+
kind: "static"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Create menu items
|
|
252
|
+
menu_data["items"]&.each do |item_data|
|
|
253
|
+
page = Panda::CMS::Page.find_by(path: item_data["page_path"]) if item_data["page_path"]
|
|
254
|
+
menu.menu_items.create!(
|
|
255
|
+
text: item_data["text"],
|
|
256
|
+
page: page,
|
|
257
|
+
external_url: item_data["external_url"]
|
|
258
|
+
)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
debug[:success] << "Created static menu '#{menu.name}' with #{menu_data["items"]&.length || 0} items"
|
|
262
|
+
end
|
|
263
|
+
rescue => e
|
|
264
|
+
debug[:error] << "Failed to create menu '#{menu_data["name"]}': #{e.message}"
|
|
265
|
+
end
|
|
266
|
+
else
|
|
267
|
+
# Update existing menu
|
|
268
|
+
if menu.kind != menu_data["kind"]
|
|
269
|
+
debug[:warning] << "Menu '#{menu.name}' kind mismatch (#{menu.kind} vs #{menu_data["kind"]}). Skipping update."
|
|
270
|
+
next
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
if menu_data["kind"] == "auto"
|
|
274
|
+
start_page = Panda::CMS::Page.find_by(path: menu_data["start_page_path"])
|
|
275
|
+
if menu.start_page != start_page
|
|
276
|
+
menu.update(start_page: start_page)
|
|
277
|
+
debug[:success] << "Updated auto menu '#{menu.name}' start page"
|
|
278
|
+
end
|
|
279
|
+
elsif menu_data["kind"] == "static"
|
|
280
|
+
# Update static menu items
|
|
281
|
+
menu.menu_items.destroy_all
|
|
282
|
+
menu_data["items"]&.each do |item_data|
|
|
283
|
+
page = Panda::CMS::Page.find_by(path: item_data["page_path"]) if item_data["page_path"]
|
|
284
|
+
menu.menu_items.create!(
|
|
285
|
+
text: item_data["text"],
|
|
286
|
+
page: page,
|
|
287
|
+
external_url: item_data["external_url"]
|
|
288
|
+
)
|
|
289
|
+
end
|
|
290
|
+
debug[:success] << "Updated static menu '#{menu.name}' with #{menu_data["items"]&.length || 0} items"
|
|
291
|
+
end
|
|
292
|
+
end
|
|
124
293
|
end
|
|
125
294
|
|
|
295
|
+
# Templates - skip as they are code-based
|
|
296
|
+
|
|
126
297
|
debug
|
|
127
298
|
end
|
|
128
299
|
|
|
@@ -135,7 +306,8 @@ module Panda
|
|
|
135
306
|
def self.extract_current_data
|
|
136
307
|
data = {
|
|
137
308
|
"pages" => {},
|
|
138
|
-
"
|
|
309
|
+
"posts" => [],
|
|
310
|
+
"menus" => [],
|
|
139
311
|
"templates" => {},
|
|
140
312
|
"settings" => {}
|
|
141
313
|
}
|
|
@@ -148,10 +320,32 @@ module Panda
|
|
|
148
320
|
# TODO: Eventually set the position of the block in the template, and then order from there rather than the name?
|
|
149
321
|
Panda::CMS::BlockContent.includes(:block,
|
|
150
322
|
page: [:template]).order("panda_cms_pages.lft ASC, panda_cms_blocks.key ASC").each do |block_content|
|
|
323
|
+
# Skip block contents without a page (orphaned data)
|
|
324
|
+
next unless block_content.page
|
|
325
|
+
|
|
151
326
|
item = data["pages"][block_content.page.path] ||= {}
|
|
327
|
+
item["id"] = block_content.page.id
|
|
328
|
+
item["path"] = block_content.page.path
|
|
152
329
|
item["title"] = block_content.page.title
|
|
153
330
|
item["template"] = block_content.page.template.name
|
|
154
331
|
item["parent"] = block_content.page.parent&.path
|
|
332
|
+
item["status"] = block_content.page.status
|
|
333
|
+
item["page_type"] = block_content.page.page_type
|
|
334
|
+
# SEO fields
|
|
335
|
+
item["seo_title"] = block_content.page.seo_title if block_content.page.seo_title.present?
|
|
336
|
+
item["seo_description"] = block_content.page.seo_description if block_content.page.seo_description.present?
|
|
337
|
+
item["seo_keywords"] = block_content.page.seo_keywords if block_content.page.seo_keywords.present?
|
|
338
|
+
item["seo_index_mode"] = block_content.page.seo_index_mode
|
|
339
|
+
item["canonical_url"] = block_content.page.canonical_url if block_content.page.canonical_url.present?
|
|
340
|
+
# Open Graph fields
|
|
341
|
+
item["og_type"] = block_content.page.og_type
|
|
342
|
+
item["og_title"] = block_content.page.og_title if block_content.page.og_title.present?
|
|
343
|
+
item["og_description"] = block_content.page.og_description if block_content.page.og_description.present?
|
|
344
|
+
item["og_image_url"] = active_storage_url(block_content.page.og_image) if block_content.page.og_image.attached?
|
|
345
|
+
# Panda CMS Pro fields (if present)
|
|
346
|
+
item["contributor_count"] = block_content.page.contributor_count if block_content.page.respond_to?(:contributor_count)
|
|
347
|
+
item["workflow_status"] = block_content.page.workflow_status if block_content.page.respond_to?(:workflow_status)
|
|
348
|
+
item["inherit_seo"] = block_content.page.inherit_seo if block_content.page.respond_to?(:inherit_seo)
|
|
155
349
|
item["contents"] ||= {}
|
|
156
350
|
item["contents"][block_content.block.key] = {
|
|
157
351
|
kind: block_content.block.kind, # We need the kind to recreate the block
|
|
@@ -160,16 +354,98 @@ module Panda
|
|
|
160
354
|
data["pages"][block_content.page.path] = item
|
|
161
355
|
end
|
|
162
356
|
|
|
357
|
+
# Posts
|
|
358
|
+
Panda::CMS::Post.order("published_at DESC").each do |post|
|
|
359
|
+
post_data = {
|
|
360
|
+
"id" => post.id,
|
|
361
|
+
"slug" => post.slug,
|
|
362
|
+
"title" => post.title,
|
|
363
|
+
"status" => post.status,
|
|
364
|
+
"published_at" => post.published_at&.iso8601,
|
|
365
|
+
"user_email" => post.user&.email,
|
|
366
|
+
"author_email" => post.author&.email,
|
|
367
|
+
"content" => post.content,
|
|
368
|
+
"cached_content" => post.cached_content
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
# SEO fields
|
|
372
|
+
post_data["seo_title"] = post.seo_title if post.seo_title.present?
|
|
373
|
+
post_data["seo_description"] = post.seo_description if post.seo_description.present?
|
|
374
|
+
post_data["seo_keywords"] = post.seo_keywords if post.seo_keywords.present?
|
|
375
|
+
post_data["seo_index_mode"] = post.seo_index_mode
|
|
376
|
+
post_data["canonical_url"] = post.canonical_url if post.canonical_url.present?
|
|
377
|
+
|
|
378
|
+
# Open Graph fields
|
|
379
|
+
post_data["og_type"] = post.og_type
|
|
380
|
+
post_data["og_title"] = post.og_title if post.og_title.present?
|
|
381
|
+
post_data["og_description"] = post.og_description if post.og_description.present?
|
|
382
|
+
post_data["og_image_url"] = active_storage_url(post.og_image) if post.og_image.attached?
|
|
383
|
+
|
|
384
|
+
# Panda CMS Pro fields (if present)
|
|
385
|
+
post_data["contributor_count"] = post.contributor_count if post.respond_to?(:contributor_count)
|
|
386
|
+
post_data["workflow_status"] = post.workflow_status if post.respond_to?(:workflow_status)
|
|
387
|
+
|
|
388
|
+
data["posts"] << post_data
|
|
389
|
+
end
|
|
390
|
+
|
|
163
391
|
# Menus
|
|
164
|
-
|
|
392
|
+
Panda::CMS::Menu.includes(menu_items: :page).order(:name).each do |menu|
|
|
393
|
+
menu_data = {
|
|
394
|
+
"name" => menu.name,
|
|
395
|
+
"kind" => menu.kind
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if menu.kind == "auto" && menu.start_page
|
|
399
|
+
menu_data["start_page_path"] = menu.start_page.path
|
|
400
|
+
elsif menu.kind == "static"
|
|
401
|
+
menu_data["items"] = serialize_menu_items(menu.menu_items)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
data["menus"] << menu_data
|
|
405
|
+
end
|
|
165
406
|
|
|
166
407
|
# Templates
|
|
167
|
-
#
|
|
408
|
+
# Skipping templates for now as they are code-based
|
|
168
409
|
|
|
169
410
|
data["settings"] = {}
|
|
170
411
|
|
|
171
412
|
data.with_indifferent_access
|
|
172
413
|
end
|
|
414
|
+
|
|
415
|
+
#
|
|
416
|
+
# Serialize menu items recursively
|
|
417
|
+
#
|
|
418
|
+
# @param menu_items [ActiveRecord::Relation<Panda::CMS::MenuItem>]
|
|
419
|
+
# @return [Array<Hash>]
|
|
420
|
+
# @visibility private
|
|
421
|
+
def self.serialize_menu_items(menu_items)
|
|
422
|
+
menu_items.map do |item|
|
|
423
|
+
{
|
|
424
|
+
"text" => item.text,
|
|
425
|
+
"page_path" => item.page&.path,
|
|
426
|
+
"external_url" => item.external_url
|
|
427
|
+
}.compact
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
#
|
|
432
|
+
# Get URL for Active Storage attachment
|
|
433
|
+
#
|
|
434
|
+
# @param attachment [ActiveStorage::Attached::One]
|
|
435
|
+
# @return [String, nil]
|
|
436
|
+
# @visibility private
|
|
437
|
+
def self.active_storage_url(attachment)
|
|
438
|
+
return nil unless attachment.attached?
|
|
439
|
+
|
|
440
|
+
if Rails.application.routes.url_helpers.respond_to?(:rails_blob_url)
|
|
441
|
+
Rails.application.routes.url_helpers.rails_blob_url(attachment, only_path: false)
|
|
442
|
+
else
|
|
443
|
+
# Fallback: return the key which can be used to reconstruct the URL
|
|
444
|
+
attachment.key
|
|
445
|
+
end
|
|
446
|
+
rescue
|
|
447
|
+
nil
|
|
448
|
+
end
|
|
173
449
|
end
|
|
174
450
|
end
|
|
175
451
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module CMS
|
|
5
|
+
class Engine < ::Rails::Engine
|
|
6
|
+
# Asset pipeline and importmap configuration
|
|
7
|
+
module AssetConfig
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
# Asset pipeline configuration
|
|
12
|
+
initializer "panda.cms.assets" do |app|
|
|
13
|
+
if Rails.configuration.respond_to?(:assets)
|
|
14
|
+
# Add JavaScript paths
|
|
15
|
+
app.config.assets.paths << root.join("app/javascript")
|
|
16
|
+
app.config.assets.paths << root.join("app/javascript/panda")
|
|
17
|
+
app.config.assets.paths << root.join("app/javascript/panda/cms")
|
|
18
|
+
app.config.assets.paths << root.join("app/javascript/panda/cms/controllers")
|
|
19
|
+
|
|
20
|
+
# Make sure these files are precompiled
|
|
21
|
+
app.config.assets.precompile += %w[
|
|
22
|
+
panda_cms_manifest.js
|
|
23
|
+
panda/cms/controllers/**/*.js
|
|
24
|
+
panda/cms/application.js
|
|
25
|
+
]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Add importmap paths from the engine
|
|
30
|
+
initializer "panda.cms.importmap", before: "importmap" do |app|
|
|
31
|
+
if app.config.respond_to?(:importmap)
|
|
32
|
+
# Create a new array if frozen
|
|
33
|
+
app.config.importmap.paths = app.config.importmap.paths.dup if app.config.importmap.paths.frozen?
|
|
34
|
+
|
|
35
|
+
# Add our paths
|
|
36
|
+
app.config.importmap.paths << root.join("config/importmap.rb")
|
|
37
|
+
|
|
38
|
+
# Handle cache sweepers similarly
|
|
39
|
+
if app.config.importmap.cache_sweepers.frozen?
|
|
40
|
+
app.config.importmap.cache_sweepers = app.config.importmap.cache_sweepers.dup
|
|
41
|
+
end
|
|
42
|
+
app.config.importmap.cache_sweepers << root.join("app/javascript")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module CMS
|
|
5
|
+
class Engine < ::Rails::Engine
|
|
6
|
+
# Autoload paths configuration
|
|
7
|
+
module AutoloadConfig
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
# Add services directory to autoload paths
|
|
12
|
+
config.autoload_paths += %W[
|
|
13
|
+
#{root}/app/services
|
|
14
|
+
]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module CMS
|
|
5
|
+
class Engine < ::Rails::Engine
|
|
6
|
+
# Backtrace cleaner configuration for better error messages
|
|
7
|
+
module BacktraceConfig
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
initializer "#{engine_name}.backtrace_cleaner" do |_app|
|
|
12
|
+
engine_root_regex = Regexp.escape(root.to_s + File::SEPARATOR)
|
|
13
|
+
|
|
14
|
+
# Clean those ERB lines, we don't need the internal autogenerated
|
|
15
|
+
# ERB method, what we do need (line number in ERB file) is already there
|
|
16
|
+
Rails.backtrace_cleaner.add_filter do |line|
|
|
17
|
+
line.sub(/(\.erb:\d+):in `__.*$/, '\\1')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Remove our own engine's path prefix, even if it's
|
|
21
|
+
# being used from a local path rather than the gem directory.
|
|
22
|
+
Rails.backtrace_cleaner.add_filter do |line|
|
|
23
|
+
line.sub(/^#{engine_root_regex}/, "#{engine_name} ")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Keep Umlaut's own stacktrace in the backtrace -- we have to remove Rails
|
|
27
|
+
# silencers and re-add them how we want.
|
|
28
|
+
Rails.backtrace_cleaner.remove_silencers!
|
|
29
|
+
|
|
30
|
+
# Silence what Rails silenced, UNLESS it looks like
|
|
31
|
+
# it's from Umlaut engine
|
|
32
|
+
Rails.backtrace_cleaner.add_silencer do |line|
|
|
33
|
+
(line !~ Rails::BacktraceCleaner::APP_DIRS_PATTERN) &&
|
|
34
|
+
(line !~ /^#{engine_root_regex}/) &&
|
|
35
|
+
(line !~ /^#{engine_name} /)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module CMS
|
|
5
|
+
class Engine < ::Rails::Engine
|
|
6
|
+
# Configuration for Panda::Core integration (navigation, breadcrumbs, widgets)
|
|
7
|
+
module CoreConfig
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
# Configure Core for CMS (runs before app initializers so apps can override)
|
|
12
|
+
initializer "panda.cms.configure_core", before: :load_config_initializers do |app|
|
|
13
|
+
Panda::Core.configure do |config|
|
|
14
|
+
# Core now provides the admin interface foundation
|
|
15
|
+
# Apps using CMS can customize login_logo_path, login_page_title, etc. in their own initializers
|
|
16
|
+
|
|
17
|
+
# Register CMS navigation items with nested structure
|
|
18
|
+
config.admin_navigation_items = ->(user) {
|
|
19
|
+
items = []
|
|
20
|
+
|
|
21
|
+
# Dashboard
|
|
22
|
+
items << {
|
|
23
|
+
path: "#{config.admin_path}/cms",
|
|
24
|
+
label: "Dashboard",
|
|
25
|
+
icon: "fa-solid fa-house"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Content group - Pages, Posts, Collections
|
|
29
|
+
content_children = [
|
|
30
|
+
{label: "Pages", path: "#{config.admin_path}/cms/pages"},
|
|
31
|
+
{label: "Posts", path: "#{config.admin_path}/cms/posts"}
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
# Add Collections if enabled
|
|
35
|
+
if Panda::CMS::Features.enabled?(:collections)
|
|
36
|
+
content_children << {label: "Collections", path: "#{config.admin_path}/cms/collections"}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
items << {
|
|
40
|
+
label: "Content",
|
|
41
|
+
icon: "fa-solid fa-file-lines",
|
|
42
|
+
children: content_children
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Forms & Files group
|
|
46
|
+
items << {
|
|
47
|
+
label: "Forms & Files",
|
|
48
|
+
icon: "fa-solid fa-folder",
|
|
49
|
+
children: [
|
|
50
|
+
{label: "Forms", path: "#{config.admin_path}/cms/forms"},
|
|
51
|
+
{label: "Files", path: "#{config.admin_path}/cms/files"}
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Menus (standalone)
|
|
56
|
+
items << {
|
|
57
|
+
path: "#{config.admin_path}/cms/menus",
|
|
58
|
+
label: "Menus",
|
|
59
|
+
icon: "fa-solid fa-bars"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Tools group
|
|
63
|
+
items << {
|
|
64
|
+
label: "Tools",
|
|
65
|
+
icon: "fa-solid fa-wrench",
|
|
66
|
+
children: [
|
|
67
|
+
{label: "Import/Export", path: "#{config.admin_path}/cms/tools/import-export"}
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Settings (standalone)
|
|
72
|
+
items << {
|
|
73
|
+
path: "#{config.admin_path}/cms/settings",
|
|
74
|
+
label: "Settings",
|
|
75
|
+
icon: "fa-solid fa-gear"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
items
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Redirect to CMS dashboard after login
|
|
82
|
+
# Apps can override this if they want different behavior
|
|
83
|
+
config.dashboard_redirect_path = -> { "#{Panda::Core.config.admin_path}/cms" }
|
|
84
|
+
|
|
85
|
+
# Customize initial breadcrumb
|
|
86
|
+
config.initial_admin_breadcrumb = ->(controller) {
|
|
87
|
+
# Use CMS dashboard path - just use the string path
|
|
88
|
+
["Admin", "#{config.admin_path}/cms"]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Dashboard widgets
|
|
92
|
+
config.admin_dashboard_widgets = ->(user) {
|
|
93
|
+
widgets = []
|
|
94
|
+
|
|
95
|
+
# TODO: Add CMS statistics widgets when StatisticsComponent is implemented
|
|
96
|
+
# This was removed along with Pro code migration
|
|
97
|
+
|
|
98
|
+
widgets
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module CMS
|
|
5
|
+
class Engine < ::Rails::Engine
|
|
6
|
+
# Helper configuration
|
|
7
|
+
module HelperConfig
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
# Make helpers available to ApplicationController
|
|
12
|
+
config.to_prepare do
|
|
13
|
+
ApplicationController.helper(::ApplicationHelper)
|
|
14
|
+
ApplicationController.helper(Panda::CMS::AssetHelper)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module CMS
|
|
5
|
+
class Engine < ::Rails::Engine
|
|
6
|
+
# Route mounting and configuration
|
|
7
|
+
module RouteConfig
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
# Auto-mount CMS routes
|
|
12
|
+
config.after_initialize do |app|
|
|
13
|
+
# Append routes to the routes file
|
|
14
|
+
app.routes.append do
|
|
15
|
+
mount Panda::CMS::Engine => "/", :as => "panda_cms"
|
|
16
|
+
post "/_forms/:id", to: "panda/cms/form_submissions#create", as: :panda_cms_form_submit
|
|
17
|
+
get "/_maintenance", to: "panda/cms/errors#error_503", as: :panda_cms_maintenance
|
|
18
|
+
|
|
19
|
+
# Catch-all route for CMS pages, but exclude admin paths and assets
|
|
20
|
+
admin_path = Panda::Core.config.admin_path.delete_prefix("/")
|
|
21
|
+
constraints = ->(request) {
|
|
22
|
+
!request.path.start_with?("/#{admin_path}") &&
|
|
23
|
+
!request.path.start_with?("/panda-cms-assets/")
|
|
24
|
+
}
|
|
25
|
+
get "/*path", to: "panda/cms/pages#show", as: :panda_cms_page, constraints: constraints
|
|
26
|
+
|
|
27
|
+
root to: "panda/cms/pages#root"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module CMS
|
|
5
|
+
class Engine < ::Rails::Engine
|
|
6
|
+
# ViewComponent configuration
|
|
7
|
+
module ViewComponentConfig
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
# Set up ViewComponent
|
|
12
|
+
initializer "panda.cms.view_component" do |app|
|
|
13
|
+
app.config.view_component.preview_paths ||= []
|
|
14
|
+
app.config.view_component.preview_paths << root.join("spec/components/previews")
|
|
15
|
+
app.config.view_component.generate.sidecar = true
|
|
16
|
+
app.config.view_component.generate.preview = true
|
|
17
|
+
|
|
18
|
+
# Add preview directories to autoload paths in development
|
|
19
|
+
if Rails.env.development?
|
|
20
|
+
# Handle frozen autoload_paths array
|
|
21
|
+
if app.config.autoload_paths.frozen?
|
|
22
|
+
app.config.autoload_paths = app.config.autoload_paths.dup
|
|
23
|
+
end
|
|
24
|
+
app.config.autoload_paths << root.join("spec/components/previews")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|