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
@@ -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 page_data["title"] != current_data["pages"][path]["title"]
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"].each do |key, block_data|
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
- new_data["menus"].each do |menu_data|
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
- new_data["templates"].each do |template_data|
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
- "menus" => {},
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
- # item = data["menus"][] ||= {}
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
- # item = data["templates"][] ||= {}
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