panda-cms 0.7.4 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +37 -2
  3. data/Rakefile +2 -0
  4. data/app/components/panda/cms/admin/statistics_component.rb +1 -2
  5. data/app/components/panda/cms/admin/user_activity_component.html.erb +3 -1
  6. data/app/components/panda/cms/admin/user_activity_component.rb +3 -5
  7. data/app/components/panda/cms/admin/user_display_component.html.erb +1 -1
  8. data/app/components/panda/cms/admin/user_display_component.rb +2 -2
  9. data/app/components/panda/cms/code_component.rb +8 -4
  10. data/app/components/panda/cms/menu_component.rb +7 -6
  11. data/app/components/panda/cms/page_menu_component.rb +15 -17
  12. data/app/components/panda/cms/rich_text_component.rb +10 -11
  13. data/app/components/panda/cms/text_component.rb +6 -7
  14. data/app/controllers/panda/cms/admin/base_controller.rb +18 -0
  15. data/app/controllers/panda/cms/admin/block_contents_controller.rb +1 -2
  16. data/app/controllers/panda/cms/admin/dashboard_controller.rb +14 -9
  17. data/app/controllers/panda/cms/admin/files_controller.rb +1 -3
  18. data/app/controllers/panda/cms/admin/forms_controller.rb +3 -6
  19. data/app/controllers/panda/cms/admin/menus_controller.rb +2 -3
  20. data/app/controllers/panda/cms/admin/pages_controller.rb +9 -8
  21. data/app/controllers/panda/cms/admin/posts_controller.rb +9 -11
  22. data/app/controllers/panda/cms/admin/settings/bulk_editor_controller.rb +32 -25
  23. data/app/controllers/panda/cms/admin/settings_controller.rb +13 -10
  24. data/app/controllers/panda/cms/application_controller.rb +19 -6
  25. data/app/controllers/panda/cms/errors_controller.rb +5 -2
  26. data/app/controllers/panda/cms/form_submissions_controller.rb +2 -0
  27. data/app/controllers/panda/cms/pages_controller.rb +34 -31
  28. data/app/controllers/panda/cms/posts_controller.rb +2 -0
  29. data/app/helpers/panda/cms/admin/files_helper.rb +5 -1
  30. data/app/helpers/panda/cms/admin/pages_helper.rb +5 -1
  31. data/app/helpers/panda/cms/application_helper.rb +3 -3
  32. data/app/helpers/panda/cms/asset_helper.rb +195 -0
  33. data/app/helpers/panda/cms/pages_helper.rb +2 -0
  34. data/app/helpers/panda/cms/posts_helper.rb +2 -0
  35. data/app/helpers/panda/cms/theme_helper.rb +2 -0
  36. data/app/javascript/panda/cms/application_panda_cms.js +2 -34
  37. data/app/javascript/panda/cms/controllers/editor_form_controller.js +59 -6
  38. data/app/javascript/panda/cms/controllers/index.js +8 -24
  39. data/app/javascript/panda/cms/stimulus-loading.js +39 -0
  40. data/app/javascript/panda_cms/stimulus-loading.js +39 -0
  41. data/app/jobs/panda/cms/application_job.rb +2 -0
  42. data/app/jobs/panda/cms/record_visit_job.rb +2 -0
  43. data/app/mailers/panda/cms/application_mailer.rb +2 -0
  44. data/app/mailers/panda/cms/form_mailer.rb +3 -1
  45. data/app/models/panda/cms/application_record.rb +2 -0
  46. data/app/models/panda/cms/block.rb +4 -1
  47. data/app/models/panda/cms/block_content.rb +3 -1
  48. data/app/models/panda/cms/current.rb +5 -12
  49. data/app/models/panda/cms/form.rb +2 -0
  50. data/app/models/panda/cms/form_submission.rb +2 -0
  51. data/app/models/panda/cms/menu.rb +12 -9
  52. data/app/models/panda/cms/menu_item.rb +10 -6
  53. data/app/models/panda/cms/page.rb +14 -12
  54. data/app/models/panda/cms/post.rb +12 -8
  55. data/app/models/panda/cms/redirect.rb +6 -3
  56. data/app/models/panda/cms/template.rb +12 -7
  57. data/app/models/panda/cms/visit.rb +3 -1
  58. data/app/models/panda/social/instagram_post.rb +2 -0
  59. data/app/services/panda/social/instagram_feed_service.rb +3 -1
  60. data/app/views/layouts/different_page.html.erb +6 -0
  61. data/app/views/layouts/homepage.html.erb +37 -0
  62. data/app/views/layouts/page.html.erb +18 -0
  63. data/app/views/layouts/panda/cms/application.html.erb +2 -1
  64. data/app/views/panda/cms/admin/dashboard/show.html.erb +2 -2
  65. data/app/views/panda/cms/admin/files/index.html.erb +1 -1
  66. data/app/views/panda/cms/admin/forms/index.html.erb +4 -4
  67. data/app/views/panda/cms/admin/forms/new.html.erb +2 -2
  68. data/app/views/panda/cms/admin/forms/show.html.erb +1 -1
  69. data/app/views/panda/cms/admin/menus/index.html.erb +4 -4
  70. data/app/views/panda/cms/admin/pages/edit.html.erb +6 -6
  71. data/app/views/panda/cms/admin/pages/index.html.erb +5 -5
  72. data/app/views/panda/cms/admin/pages/new.html.erb +16 -10
  73. data/app/views/panda/cms/admin/posts/_form.html.erb +1 -1
  74. data/app/views/panda/cms/admin/posts/edit.html.erb +2 -2
  75. data/app/views/panda/cms/admin/posts/index.html.erb +5 -5
  76. data/app/views/panda/cms/admin/posts/new.html.erb +1 -1
  77. data/app/views/panda/cms/admin/settings/bulk_editor/new.html.erb +1 -1
  78. data/app/views/panda/cms/admin/settings/index.html.erb +4 -4
  79. data/app/views/panda/cms/admin/shared/_breadcrumbs.html.erb +3 -3
  80. data/app/views/panda/cms/admin/shared/_flash.html.erb +1 -1
  81. data/app/views/panda/cms/admin/shared/_sidebar.html.erb +8 -8
  82. data/app/views/panda/cms/shared/_header.html.erb +10 -2
  83. data/app/views/panda/cms/shared/_importmap.html.erb +1 -1
  84. data/app/views/shared/_footer.html.erb +3 -0
  85. data/app/views/shared/_header.html.erb +11 -0
  86. data/config/importmap.rb +2 -0
  87. data/config/initializers/inflections.rb +2 -0
  88. data/config/initializers/panda/cms/form_errors.rb +20 -21
  89. data/config/initializers/panda/cms/healthcheck_log_silencer.rb +2 -0
  90. data/config/initializers/panda/cms.rb +8 -3
  91. data/config/initializers/zeitwork.rb +2 -0
  92. data/config/puma/test.rb +3 -1
  93. data/config/routes.rb +11 -19
  94. data/db/migrate/20240205223709_create_panda_cms_pages.rb +2 -0
  95. data/db/migrate/20240219213327_create_panda_cms_page_versions.rb +2 -0
  96. data/db/migrate/20240303002805_create_panda_cms_templates.rb +4 -1
  97. data/db/migrate/20240303003434_create_panda_cms_template_versions.rb +2 -0
  98. data/db/migrate/20240303022441_create_panda_cms_blocks.rb +4 -1
  99. data/db/migrate/20240303024256_create_panda_cms_block_contents.rb +2 -0
  100. data/db/migrate/20240303024746_create_panda_cms_block_content_versions.rb +2 -0
  101. data/db/migrate/20240303233238_add_panda_cms_menu_table.rb +2 -0
  102. data/db/migrate/20240303234724_add_panda_cms_menu_item_table.rb +2 -0
  103. data/db/migrate/20240304134343_add_parent_id_to_panda_cms_pages.rb +2 -0
  104. data/db/migrate/20240315125411_add_status_to_panda_cms_pages.rb +7 -5
  105. data/db/migrate/20240315125421_add_nested_sets_to_panda_cms_pages.rb +2 -0
  106. data/db/migrate/20240316212822_add_kind_to_panda_cms_menus.rb +3 -1
  107. data/db/migrate/20240316221425_add_start_page_to_panda_cms_menus.rb +2 -0
  108. data/db/migrate/20240316230706_add_nested_to_panda_cms_menu_items.rb +2 -0
  109. data/db/migrate/20240317010532_create_panda_cms_users.rb +2 -0
  110. data/db/migrate/20240317161534_add_max_uses_to_panda_cms_template.rb +2 -0
  111. data/db/migrate/20240317163053_reset_counter_cache_on_panda_cms_template.rb +2 -0
  112. data/db/migrate/20240317214827_create_panda_cms_redirects.rb +2 -0
  113. data/db/migrate/20240317230622_create_panda_cms_visits.rb +2 -0
  114. data/db/migrate/20240324205703_create_active_storage_tables.active_storage.rb +5 -2
  115. data/db/migrate/20240408084718_default_panda_cms_users_admin_to_false.rb +2 -0
  116. data/db/migrate/20240701225422_add_service_name_to_active_storage_blobs.active_storage.rb +8 -6
  117. data/db/migrate/20240701225423_create_active_storage_variant_records.active_storage.rb +2 -0
  118. data/db/migrate/20240701225424_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb +2 -0
  119. data/db/migrate/20240804235210_create_panda_cms_forms.rb +2 -0
  120. data/db/migrate/20240805013612_create_panda_cms_form_submissions.rb +2 -0
  121. data/db/migrate/20240805121123_create_panda_cms_posts.rb +3 -1
  122. data/db/migrate/20240805123104_create_panda_cms_post_versions.rb +2 -0
  123. data/db/migrate/20240806112735_fix_panda_cms_visits_column_names.rb +2 -0
  124. data/db/migrate/20240806204412_add_completion_path_to_panda_cms_forms.rb +2 -0
  125. data/db/migrate/20240820081917_change_form_submissions_to_submission_count.rb +2 -0
  126. data/db/migrate/20240923234535_add_depth_to_panda_cms_menus.rb +6 -4
  127. data/db/migrate/20241031205109_add_cached_content_to_panda_cms_block_contents.rb +2 -0
  128. data/db/migrate/20241119214548_convert_post_content_to_editor_js.rb +2 -0
  129. data/db/migrate/20241120000419_remove_post_tag_references.rb +2 -0
  130. data/db/migrate/20241120110943_add_editor_js_to_posts.rb +2 -0
  131. data/db/migrate/20241120113859_add_cached_content_to_panda_cms_posts.rb +2 -0
  132. data/db/migrate/20241123234140_remove_post_tag_id_from_posts.rb +2 -0
  133. data/db/migrate/20250106223303_add_author_id_to_panda_cms_posts.rb +5 -1
  134. data/db/migrate/20250120235542_remove_paper_trail.rb +5 -4
  135. data/db/migrate/20250126234001_create_panda_social_instagram_posts.rb +4 -0
  136. data/db/migrate/20250809231125_migrate_users_to_panda_core.rb +111 -0
  137. data/db/migrate/20250811111000_make_post_user_references_nullable.rb +11 -0
  138. data/db/seeds.rb +2 -0
  139. data/lib/generators/panda/cms/install_generator.rb +2 -0
  140. data/lib/panda/cms/asset_loader.rb +390 -0
  141. data/lib/panda/cms/bulk_editor.rb +7 -3
  142. data/lib/panda/cms/demo_site_generator.rb +2 -0
  143. data/lib/panda/cms/engine.rb +57 -116
  144. data/lib/panda/cms/exceptions_app.rb +2 -0
  145. data/lib/panda/cms/railtie.rb +2 -0
  146. data/lib/panda/cms/slug.rb +3 -1
  147. data/lib/panda-cms/version.rb +3 -1
  148. data/lib/panda-cms.rb +54 -42
  149. data/lib/tasks/assets.rake +587 -0
  150. data/lib/tasks/panda/cms/install.rake +2 -0
  151. data/lib/tasks/panda/cms/migrations.rake +13 -0
  152. data/lib/tasks/panda/social/instagram.rake +2 -0
  153. data/lib/tasks/panda_cms.rake +3 -30
  154. data/public/panda-cms-assets/manifest.json +20 -0
  155. data/public/panda-cms-assets/panda-cms-0.7.4.css +26 -0
  156. data/public/panda-cms-assets/panda-cms-0.7.4.js +150 -0
  157. metadata +186 -49
  158. data/app/builders/panda/cms/form_builder.rb +0 -217
  159. data/app/components/panda/cms/admin/button_component.rb +0 -70
  160. data/app/components/panda/cms/admin/container_component.rb +0 -13
  161. data/app/components/panda/cms/admin/flash_message_component.rb +0 -47
  162. data/app/components/panda/cms/admin/heading_component.rb +0 -45
  163. data/app/components/panda/cms/admin/panel_component.rb +0 -13
  164. data/app/components/panda/cms/admin/table_component.rb +0 -46
  165. data/app/components/panda/cms/admin/tag_component.rb +0 -35
  166. data/app/constraints/panda/cms/admin_constraint.rb +0 -18
  167. data/app/controllers/panda/cms/admin/my_profile_controller.rb +0 -43
  168. data/app/controllers/panda/cms/admin/sessions_controller.rb +0 -94
  169. data/app/javascript/panda/cms/controllers/theme_form_controller.js +0 -9
  170. data/app/javascript/panda/cms/editor/css_extractor.js +0 -80
  171. data/app/javascript/panda/cms/editor/editor_js_config.js +0 -306
  172. data/app/javascript/panda/cms/editor/editor_js_initializer.js +0 -334
  173. data/app/javascript/panda/cms/editor/plain_text_editor.js +0 -110
  174. data/app/javascript/panda/cms/editor/resource_loader.js +0 -204
  175. data/app/javascript/panda/cms/editor/rich_text_editor.js +0 -162
  176. data/app/models/panda/cms/breadcrumb.rb +0 -12
  177. data/app/models/panda/cms/user.rb +0 -31
  178. data/app/services/panda/cms/html_to_editor_js_converter.rb +0 -193
  179. data/app/views/panda/cms/admin/my_profile/edit.html.erb +0 -35
  180. data/app/views/panda/cms/admin/sessions/new.html.erb +0 -17
  181. data/db/migrate/20250504221812_add_current_theme_to_panda_cms_users.rb +0 -5
  182. data/lib/panda/cms/editor_js/blocks/alert.rb +0 -34
  183. data/lib/panda/cms/editor_js/blocks/base.rb +0 -33
  184. data/lib/panda/cms/editor_js/blocks/header.rb +0 -15
  185. data/lib/panda/cms/editor_js/blocks/image.rb +0 -36
  186. data/lib/panda/cms/editor_js/blocks/list.rb +0 -32
  187. data/lib/panda/cms/editor_js/blocks/paragraph.rb +0 -15
  188. data/lib/panda/cms/editor_js/blocks/quote.rb +0 -41
  189. data/lib/panda/cms/editor_js/blocks/table.rb +0 -50
  190. data/lib/panda/cms/editor_js/renderer.rb +0 -124
  191. data/lib/panda/cms/editor_js.rb +0 -16
  192. data/lib/panda/cms/editor_js_content.rb +0 -55
@@ -0,0 +1,390 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module CMS
5
+ # AssetLoader handles loading compiled assets from GitHub releases
6
+ # Falls back to local development assets when GitHub assets unavailable
7
+ class AssetLoader
8
+ class << self
9
+ # Generate HTML tags for loading Panda CMS assets
10
+ def asset_tags(options = {})
11
+ if use_github_assets?
12
+ github_asset_tags(options)
13
+ else
14
+ development_asset_tags(options)
15
+ end
16
+ end
17
+
18
+ # Get the JavaScript asset URL
19
+ def javascript_url
20
+ if use_github_assets?
21
+ github_javascript_url
22
+ else
23
+ development_javascript_url
24
+ end
25
+ end
26
+
27
+ # Get the CSS asset URL (if exists)
28
+ def css_url
29
+ if use_github_assets?
30
+ github_css_url
31
+ else
32
+ development_css_url
33
+ end
34
+ end
35
+
36
+ # Check if GitHub-hosted assets should be used
37
+ def use_github_assets?
38
+ # Use GitHub assets in production or when explicitly enabled
39
+ Rails.env.production? ||
40
+ ENV["PANDA_CMS_USE_GITHUB_ASSETS"] == "true" ||
41
+ !development_assets_available? ||
42
+ ((Rails.env.test? || in_test_environment?) && compiled_assets_available?)
43
+ end
44
+
45
+ # Download assets from GitHub to local cache
46
+ def ensure_assets_available!
47
+ return if development_assets_available? && !use_github_assets?
48
+
49
+ cache_dir = local_cache_directory
50
+ version = `git rev-parse --short HEAD`.strip
51
+
52
+ # Check if we already have cached assets for this version
53
+ if cached_assets_exist?(version)
54
+ Rails.logger.info "[Panda CMS] Using cached assets #{version}"
55
+ return
56
+ end
57
+
58
+ Rails.logger.info "[Panda CMS] Downloading assets #{version} from GitHub..."
59
+ download_github_assets(version, cache_dir)
60
+ end
61
+
62
+ private
63
+
64
+ def github_asset_tags(options = {})
65
+ version = asset_version
66
+ base_url = github_base_url(version)
67
+
68
+ tags = []
69
+
70
+ # JavaScript tag with integrity check
71
+ js_url = "#{base_url}panda-cms-#{version}.js"
72
+ integrity = asset_integrity(version, "panda-cms-#{version}.js")
73
+
74
+ js_attrs = {
75
+ src: js_url
76
+ }
77
+ # In CI environment, don't use defer to ensure immediate execution
78
+ js_attrs[:defer] = true unless ENV["GITHUB_ACTIONS"] == "true"
79
+ # Standalone bundles should NOT use type="module" - they're regular scripts
80
+ # Only use type="module" for importmap/ES module assets
81
+ if !js_url.include?("panda-cms-assets")
82
+ js_attrs[:type] = "module"
83
+ end
84
+ js_attrs[:integrity] = integrity if integrity
85
+ js_attrs[:crossorigin] = "anonymous" if integrity
86
+
87
+ tags << content_tag(:script, "", js_attrs)
88
+
89
+ # CSS tag if CSS bundle exists
90
+ css_url = "#{base_url}panda-cms-#{version}.css"
91
+ if github_asset_exists?(version, "panda-cms-#{version}.css")
92
+ css_integrity = asset_integrity(version, "panda-cms-#{version}.css")
93
+
94
+ css_attrs = {
95
+ rel: "stylesheet",
96
+ href: css_url
97
+ }
98
+ css_attrs[:integrity] = css_integrity if css_integrity
99
+ css_attrs[:crossorigin] = "anonymous" if css_integrity
100
+
101
+ tags << tag(:link, css_attrs)
102
+ end
103
+
104
+ tags.join("\n").html_safe
105
+ end
106
+
107
+ def development_asset_tags(options = {})
108
+ # In test environment with CI, always use compiled assets
109
+ if (Rails.env.test? || ENV["CI"].present?) && compiled_assets_available?
110
+ # Use the same logic as GitHub assets but with local paths
111
+ version = asset_version
112
+ js_url = "/panda-cms-assets/panda-cms-#{version}.js"
113
+ css_url = "/panda-cms-assets/panda-cms-#{version}.css"
114
+
115
+ tags = []
116
+
117
+ # JavaScript tag
118
+ tags << content_tag(:script, "", {
119
+ src: js_url,
120
+ defer: true
121
+ })
122
+
123
+ # CSS tag if exists
124
+ if cached_asset_exists?(css_url)
125
+ tags << tag(:link, {
126
+ rel: "stylesheet",
127
+ href: css_url
128
+ })
129
+ end
130
+
131
+ tags.join("\n").html_safe
132
+ else
133
+ # In development, just use a simple script tag
134
+ # The view will handle importmap tags separately
135
+ content_tag(:script, "", {
136
+ src: development_javascript_url,
137
+ type: "module",
138
+ defer: true
139
+ })
140
+ end
141
+ end
142
+
143
+ def github_javascript_url
144
+ version = asset_version
145
+ # In test environment with local compiled assets, use local URL
146
+ if Rails.env.test? && compiled_assets_available?
147
+ "/panda-cms-assets/panda-cms-#{version}.js"
148
+ else
149
+ "#{github_base_url(version)}panda-cms-#{version}.js"
150
+ end
151
+ end
152
+
153
+ def github_css_url
154
+ version = asset_version
155
+ # In test environment with local compiled assets, use local URL
156
+ if Rails.env.test? && compiled_assets_available?
157
+ "/panda-cms-assets/panda-cms-#{version}.css"
158
+ else
159
+ "#{github_base_url(version)}panda-cms-#{version}.css"
160
+ end
161
+ end
162
+
163
+ def development_javascript_url
164
+ # Try cached assets first, then importmap
165
+ version = asset_version
166
+ # Try root level first (standalone bundle), then versioned directory
167
+ root_path = "/panda-cms-assets/panda-cms-#{version}.js"
168
+ versioned_path = "/panda-cms-assets/#{version}/panda-cms-#{version}.js"
169
+
170
+ if cached_asset_exists?(root_path)
171
+ root_path
172
+ elsif cached_asset_exists?(versioned_path)
173
+ versioned_path
174
+ else
175
+ # Fallback to importmap or engine asset
176
+ "/assets/panda/cms/controllers/index.js"
177
+ end
178
+ end
179
+
180
+ def development_css_url
181
+ version = asset_version
182
+ # Try versioned directory first, then root level
183
+ versioned_path = "/panda-cms-assets/#{version}/panda-cms-#{version}.css"
184
+ root_path = "/panda-cms-assets/panda-cms-#{version}.css"
185
+
186
+ if cached_asset_exists?(versioned_path)
187
+ versioned_path
188
+ elsif cached_asset_exists?(root_path)
189
+ root_path
190
+ else
191
+ nil # No CSS in development mode typically
192
+ end
193
+ end
194
+
195
+ def github_base_url(version)
196
+ # In test environment with compiled assets, use local URLs
197
+ if (Rails.env.test? || in_test_environment?) && compiled_assets_available?
198
+ "/panda-cms-assets/"
199
+ else
200
+ "https://github.com/tastybamboo/panda-cms/releases/download/#{version}/"
201
+ end
202
+ end
203
+
204
+ def asset_version
205
+ # In test environment, use VERSION constant for consistency with compiled assets
206
+ # In other environments, use git SHA for dynamic versioning
207
+ # Also check for test environment indicators since Rails.env might be development in specs
208
+ if Rails.env.test? || ENV["CI"].present? || in_test_environment?
209
+ Panda::CMS::VERSION
210
+ else
211
+ `git rev-parse --short HEAD`.strip
212
+ end
213
+ end
214
+
215
+ def in_test_environment?
216
+ # Check if we're running specs even if Rails.env is development
217
+ defined?(RSpec) && RSpec.respond_to?(:configuration)
218
+ end
219
+
220
+ def compiled_assets_available?
221
+ # Check if compiled assets exist in test location
222
+ version = asset_version
223
+ js_file = Rails.public_path.join("panda-cms-assets", "panda-cms-#{version}.js")
224
+ css_file = Rails.public_path.join("panda-cms-assets", "panda-cms-#{version}.css")
225
+ js_file.exist? && css_file.exist?
226
+ end
227
+
228
+ def development_assets_available?
229
+ # Check if local development assets exist (importmap, etc.)
230
+ importmap_available? || engine_assets_available?
231
+ end
232
+
233
+ def importmap_available?
234
+ return false unless defined?(Rails.application.importmap)
235
+
236
+ begin
237
+ # Rails 8+ uses a different API for accessing importmap entries
238
+ if Rails.application.importmap.respond_to?(:to_json)
239
+ importmap_json = JSON.parse(Rails.application.importmap.to_json)
240
+ importmap_json.any? { |name, _| name.include?("panda") }
241
+ elsif Rails.application.importmap.respond_to?(:entries)
242
+ Rails.application.importmap.entries.any? { |entry| entry.name.include?("panda") }
243
+ else
244
+ false
245
+ end
246
+ rescue => e
247
+ Rails.logger.debug "[Panda CMS] Could not check importmap: #{e.message}"
248
+ false
249
+ end
250
+ end
251
+
252
+ def engine_assets_available?
253
+ # Check if engine's JavaScript files are available
254
+ engine_js_path = Rails.root.join("..", "..", "app", "javascript", "panda", "cms", "controllers", "index.js")
255
+ File.exist?(engine_js_path)
256
+ end
257
+
258
+ def cached_assets_exist?(version)
259
+ cache_dir = local_cache_directory.join(version)
260
+ cache_dir.exist? && cache_dir.join("panda-cms-#{version}.js").exist?
261
+ end
262
+
263
+ def cached_asset_exists?(path)
264
+ Rails.public_path.join(path.delete_prefix("/")).exist?
265
+ end
266
+
267
+ def local_cache_directory
268
+ Rails.public_path.join("panda-cms-assets")
269
+ end
270
+
271
+ def download_github_assets(version, cache_dir)
272
+ require "net/http"
273
+ require "uri"
274
+ require "json"
275
+
276
+ version_dir = cache_dir.join(version)
277
+ FileUtils.mkdir_p(version_dir)
278
+
279
+ # Download manifest first
280
+ manifest_url = "#{github_base_url(version)}manifest.json"
281
+
282
+ begin
283
+ manifest_response = fetch_url(manifest_url)
284
+ if manifest_response.success?
285
+ manifest = JSON.parse(manifest_response.body)
286
+
287
+ # Download each file
288
+ manifest["files"].each do |file_info|
289
+ filename = file_info["filename"]
290
+ file_url = "#{github_base_url(version)}#{filename}"
291
+
292
+ Rails.logger.debug "[Panda CMS] Downloading #{filename}..."
293
+
294
+ file_response = fetch_url(file_url)
295
+ if file_response.success?
296
+ File.write(version_dir.join(filename), file_response.body)
297
+ Rails.logger.debug "[Panda CMS] Downloaded #{filename}"
298
+ else
299
+ Rails.logger.warn "[Panda CMS] Failed to download #{filename}: #{file_response.code}"
300
+ end
301
+ end
302
+
303
+ # Save manifest
304
+ File.write(version_dir.join("manifest.json"), manifest_response.body)
305
+ Rails.logger.info "[Panda CMS] Assets cached locally"
306
+ else
307
+ Rails.logger.warn "[Panda CMS] Failed to download manifest: #{manifest_response.code}"
308
+ end
309
+ rescue => e
310
+ Rails.logger.error "[Panda CMS] Error downloading assets: #{e.message}"
311
+ # Fall back to development mode
312
+ end
313
+ end
314
+
315
+ def fetch_url(url)
316
+ uri = URI(url)
317
+ http = Net::HTTP.new(uri.host, uri.port)
318
+ http.use_ssl = (uri.scheme == "https")
319
+ http.open_timeout = 10
320
+ http.read_timeout = 30
321
+
322
+ request = Net::HTTP::Get.new(uri)
323
+ request["User-Agent"] = "Panda-CMS/#{`git rev-parse --short HEAD`}"
324
+
325
+ response = http.request(request)
326
+
327
+ OpenStruct.new(
328
+ success?: response.code.to_i == 200,
329
+ code: response.code,
330
+ body: response.body
331
+ )
332
+ rescue => e
333
+ Rails.logger.error "[Panda CMS] Network error: #{e.message}"
334
+ OpenStruct.new(success?: false, code: "error", body: nil)
335
+ end
336
+
337
+ def asset_integrity(version, filename)
338
+ # Try to get integrity from cached manifest
339
+ manifest_path = local_cache_directory.join(version, "manifest.json")
340
+ return nil unless manifest_path.exist?
341
+
342
+ begin
343
+ manifest = JSON.parse(File.read(manifest_path))
344
+ file_info = manifest["files"].find { |f| f["filename"] == filename }
345
+ return nil unless file_info
346
+
347
+ "sha256-#{Base64.strict_encode64([file_info["sha256"]].pack("H*"))}"
348
+ rescue => e
349
+ Rails.logger.warn "[Panda CMS] Error reading asset integrity: #{e.message}"
350
+ nil
351
+ end
352
+ end
353
+
354
+ def github_asset_exists?(version, filename)
355
+ # Quick check - assume it exists for now
356
+ # Could be enhanced to do HEAD request
357
+ true
358
+ end
359
+
360
+ def content_tag(name, content, options = {})
361
+ if defined?(ActionView::Helpers::TagHelper)
362
+ # Create a view context to render the tag
363
+ view_context = ActionView::Base.new(ActionView::LookupContext.new([]), {}, nil)
364
+ view_context.content_tag(name, content, options)
365
+ else
366
+ # Fallback implementation
367
+ attrs = options.map { |k, v| %(#{k}="#{v}") }.join(" ")
368
+ if content.present?
369
+ "<#{name} #{attrs}>#{content}</#{name}>"
370
+ else
371
+ "<#{name} #{attrs}></#{name}>"
372
+ end
373
+ end
374
+ end
375
+
376
+ def tag(name, options = {})
377
+ if defined?(ActionView::Helpers::TagHelper)
378
+ # Create a view context to render the tag
379
+ view_context = ActionView::Base.new(ActionView::LookupContext.new([]), {}, nil)
380
+ view_context.tag(name, options)
381
+ else
382
+ # Fallback implementation
383
+ attrs = options.map { |k, v| %(#{k}="#{v}") }.join(" ")
384
+ "<#{name} #{attrs}>"
385
+ end
386
+ end
387
+ end
388
+ end
389
+ end
390
+ end
@@ -1,4 +1,5 @@
1
- require "htmlentities"
1
+ # frozen_string_literal: true
2
+
2
3
  require "json"
3
4
 
4
5
  module Panda
@@ -77,11 +78,12 @@ module Panda
77
78
 
78
79
  if current_data.dig("pages", path, "contents", key).nil?
79
80
  raise "Unknown page 1" if page.nil?
81
+
80
82
  block = Panda::CMS::Block.find_or_create_by(key: key, template: page.template) do |block_meta|
81
83
  block_meta.name = key.titleize
82
84
  end
83
85
 
84
- if !block
86
+ unless block
85
87
  debug[:error] << "Error creating block '#{key.titleize}' on page '#{page.title}'"
86
88
  next
87
89
  end
@@ -104,6 +106,7 @@ module Panda
104
106
  elsif content != current_data["pages"][path]["contents"][key]["content"]
105
107
  # Content has changed
106
108
  raise "Unknown page 2" if page.nil?
109
+
107
110
  block = Panda::CMS::Block.find_by(key: key, template: page.template)
108
111
  if Panda::CMS::BlockContent.find_by(page: page, block: block)&.update(content: content)
109
112
  debug[:success] << "Updated '#{key.titleize}' content on page '#{page.title}'"
@@ -143,7 +146,8 @@ module Panda
143
146
  end
144
147
 
145
148
  # TODO: Eventually set the position of the block in the template, and then order from there rather than the name?
146
- Panda::CMS::BlockContent.includes(:block, page: [:template]).order("panda_cms_pages.lft ASC, panda_cms_blocks.key ASC").each do |block_content|
149
+ Panda::CMS::BlockContent.includes(:block,
150
+ page: [:template]).order("panda_cms_pages.lft ASC, panda_cms_blocks.key ASC").each do |block_content|
147
151
  item = data["pages"][block_content.page.path] ||= {}
148
152
  item["title"] = block_content.page.title
149
153
  item["template"] = block_content.page.template.name
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Panda
2
4
  module CMS
3
5
  class DemoSiteGenerator
@@ -1,6 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rubygems"
2
4
  require "panda/core"
3
5
  require "panda/core/engine"
6
+ require "panda/editor"
7
+ require "panda/editor/engine"
4
8
  require "panda/cms/railtie"
5
9
 
6
10
  require "invisible_captcha"
@@ -16,10 +20,8 @@ module Panda
16
20
  ]
17
21
 
18
22
  # Basic session setup only
19
- initializer "panda_cms.session", before: :load_config_initializers do |app|
20
- if app.config.middleware.frozen?
21
- app.config.middleware = app.config.middleware.dup
22
- end
23
+ initializer "panda.cms.session", before: :load_config_initializers do |app|
24
+ app.config.middleware = app.config.middleware.dup if app.config.middleware.frozen?
23
25
 
24
26
  app.config.session_store :cookie_store, key: "_panda_cms_session"
25
27
  app.config.middleware.use ActionDispatch::Cookies
@@ -28,15 +30,16 @@ module Panda
28
30
 
29
31
  config.to_prepare do
30
32
  ApplicationController.helper(::ApplicationHelper)
33
+ ApplicationController.helper(Panda::CMS::AssetHelper)
31
34
  end
32
35
 
33
36
  # Set our generators
34
37
  config.generators do |g|
35
38
  g.orm :active_record, primary_key_type: :uuid
36
39
  g.test_framework :rspec, fixture: true
37
- g.fixture_replacement :factory_bot, dir: "spec/factories"
40
+ g.fixture_replacement nil
38
41
  g.view_specs false
39
- g.templates.unshift File.expand_path("../../templates", __FILE__)
42
+ g.templates.unshift File.expand_path("../templates", __dir__)
40
43
  end
41
44
 
42
45
  # Make files in public available to the main app (e.g. /panda_cms-assets/favicon.ico)
@@ -49,7 +52,7 @@ module Panda
49
52
  # Custom error handling
50
53
  # config.exceptions_app = Panda::CMS::ExceptionsApp.new(exceptions_app: routes)
51
54
 
52
- initializer "panda_cms.assets" do |app|
55
+ initializer "panda.cms.assets" do |app|
53
56
  if Rails.configuration.respond_to?(:assets)
54
57
  # Add JavaScript paths
55
58
  app.config.assets.paths << root.join("app/javascript")
@@ -67,12 +70,10 @@ module Panda
67
70
  end
68
71
 
69
72
  # Add importmap paths from the engine
70
- initializer "panda_cms.importmap", before: "importmap" do |app|
73
+ initializer "panda.cms.importmap", before: "importmap" do |app|
71
74
  if app.config.respond_to?(:importmap)
72
75
  # Create a new array if frozen
73
- if app.config.importmap.paths.frozen?
74
- app.config.importmap.paths = app.config.importmap.paths.dup
75
- end
76
+ app.config.importmap.paths = app.config.importmap.paths.dup if app.config.importmap.paths.frozen?
76
77
 
77
78
  # Add our paths
78
79
  app.config.importmap.paths << root.join("config/importmap.rb")
@@ -96,13 +97,13 @@ module Panda
96
97
  end
97
98
  end
98
99
 
99
- initializer "#{engine_name}.backtrace_cleaner" do |app|
100
+ initializer "#{engine_name}.backtrace_cleaner" do |_app|
100
101
  engine_root_regex = Regexp.escape(root.to_s + File::SEPARATOR)
101
102
 
102
103
  # Clean those ERB lines, we don't need the internal autogenerated
103
104
  # ERB method, what we do need (line number in ERB file) is already there
104
105
  Rails.backtrace_cleaner.add_filter do |line|
105
- line.sub(/(\.erb:\d+):in `__.*$/, "\\1")
106
+ line.sub(/(\.erb:\d+):in `__.*$/, '\\1')
106
107
  end
107
108
 
108
109
  # Remove our own engine's path prefix, even if it's
@@ -124,118 +125,58 @@ module Panda
124
125
  end
125
126
  end
126
127
 
127
- # Set up ViewComponent and Lookbook
128
- # config.view_component.component_parent_class = "Panda::CMS::BaseComponent"
129
- # config.view_component.view_component_path = Panda::CMS::Engine.root.join("lib/components").to_s
130
- # config.eager_load_paths << Panda::CMS::Engine.root.join("lib/components").to_s
131
- # config.view_component.generate.sidecar = true
132
- # config.view_component.generate.preview = truexw
133
- # config.view_component.preview_paths ||= []
134
- # config.view_component.preview_paths << Panda::CMS::Engine.root.join("lib/component_previews").to_s
135
- # config.view_component.generate.preview_path = "lib/component_previews"
136
-
137
- # Set up authentication
138
- initializer "panda_cms.omniauth", before: "omniauth" do |app|
139
- app.config.session_store :cookie_store, key: "_panda_cms_session"
140
- app.config.middleware.use ActionDispatch::Cookies
141
- app.config.middleware.use ActionDispatch::Session::CookieStore, app.config.session_options
128
+ # Set up ViewComponent
129
+ initializer "panda.cms.view_component" do |app|
130
+ app.config.view_component.preview_paths ||= []
131
+ app.config.view_component.preview_paths << root.join("spec/components/previews")
132
+ app.config.view_component.generate.sidecar = true
133
+ app.config.view_component.generate.preview = true
134
+ end
142
135
 
143
- OmniAuth.config.logger = Rails.logger
144
-
145
- # TODO: Move this to somewhere more sensible?
146
- # Define the mapping of our provider "names" to the OmniAuth strategies and configuration
147
- auth_path = "#{Panda::CMS.route_namespace}/auth"
148
- callback_path = "/callback"
149
- available_providers = {
150
- microsoft: {
151
- strategy: :microsoft_graph,
152
- defaults: {
153
- name: "microsoft",
154
- # Setup at the following URL:
155
- # https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade
156
- client_id: Rails.application.credentials.dig(:microsoft, :client_id),
157
- client_secret: Rails.application.credentials.dig(:microsoft, :client_secret),
158
- # Don't change this or the sky will fall on your head
159
- # https://github.com/synth/omniauth-microsoft_graph/tree/main?tab=readme-ov-file#domain-verification
160
- skip_domain_verification: false,
161
- # If your application is single-tenanted, replace "common" with your tenant (directory) ID
162
- # from https://portal.azure.com/#settings/directory, otherwise you'll likely want to leave
163
- # these settings unchanged
164
- client_options: {
165
- site: "https://login.microsoftonline.com/",
166
- token_url: "common/oauth2/v2.0/token",
167
- authorize_url: "common/oauth2/v2.0/authorize"
168
- },
169
- # If you assign specific users or groups, you will likely want to set this to
170
- # true to enable auto-provisioning
171
- create_account_on_first_login: false,
172
- create_admin_account_on_first_login: false,
173
- # Don't hide this provider from the login page
174
- hidden: false
175
- }
176
- },
177
- google: {
178
- strategy: :google_oauth2,
179
- defaults: {
180
- name: "google",
181
- # Setup at the following URL: https://console.developers.google.com/
182
- client_id: Rails.application.credentials.dig(:google, :client_id),
183
- client_secret: Rails.application.credentials.dig(:google, :client_secret),
184
- # If you assign specific users or groups, you will likely want to set this to
185
- # true to enable auto-provisioning
186
- create_account_on_first_login: false,
187
- create_admin_account_on_first_login: false,
188
- # Options we need
189
- scope: "email, profile",
190
- image_aspect_ratio: "square",
191
- image_size: 150,
192
- # Worth setting select_account as default, as many people have multiple Google accounts now:
193
- prompt: "select_account",
194
- # You should probably also set the 'hd' option, huh?,
195
- # Don't hide this provider from the login page
196
- hidden: false
197
- }
198
- },
199
- github: {
200
- strategy: :github,
201
- defaults: {
202
- name: "github",
203
- # Setup at the following URL: https://github.com/settings/applications/new
204
- # with a callback of
205
- # In the meantime, as long as you're set to /admin as your login path, and on
206
- # http://localhost:3000, you can use these for a first login :)
207
- client_id: Rails.application.credentials.dig(:github, :client_id),
208
- client_secret: Rails.application.credentials.dig(:github, :client_secret),
209
- scope: "user:email,read:user",
210
- create_account_on_first_login: false,
211
- create_admin_account_on_first_login: false,
212
- # Don't hide this provider from the login page
213
- hidden: false
214
- }
215
- }
216
- }
136
+ # Authentication is now handled by Panda::Core::Engine
217
137
 
218
- available_providers.each do |provider, options|
219
- if Panda::CMS.config.authentication.dig(provider, :enabled)
220
- auth_path = auth_path.starts_with?("/") ? auth_path : "/#{auth_path}"
221
- options[:defaults][:path_prefix] = auth_path
138
+ # Configure Core for CMS
139
+ initializer "panda.cms.configure_core" do |app|
140
+ Panda::Core.configure do |config|
141
+ # Customize login page
142
+ config.login_logo_path = "/panda-cms-assets/panda-nav.png"
143
+ config.login_page_title = "Panda CMS Admin"
222
144
 
223
- options[:defaults][:redirect_uri] = if Rails.env.test?
224
- "#{Capybara.app_host}#{auth_path}/#{provider}#{callback_path}"
225
- else
226
- "#{Panda::CMS.config.url}#{auth_path}/#{provider}#{callback_path}"
227
- end
145
+ # Set dashboard redirect path to CMS dashboard (using Core's admin_path)
146
+ config.dashboard_redirect_path = "#{Panda::Core.configuration.admin_path}/cms"
228
147
 
229
- provider_config = options[:defaults].merge(Panda::CMS.config.authentication[provider])
148
+ # Customize initial breadcrumb
149
+ config.initial_admin_breadcrumb = ->(controller) {
150
+ # Use CMS dashboard path - just use the string path
151
+ ["Admin", "#{Panda::Core.configuration.admin_path}/cms"]
152
+ }
230
153
 
231
- app.config.middleware.use OmniAuth::Builder do
232
- provider options[:strategy], provider_config
154
+ # Dashboard widgets
155
+ config.admin_dashboard_widgets = ->(user) {
156
+ widgets = []
157
+
158
+ # Add CMS statistics widgets if CMS is available
159
+ if defined?(Panda::CMS)
160
+ widgets << Panda::CMS::Admin::StatisticsComponent.new(
161
+ metric: "Views Today",
162
+ value: Panda::CMS::Visit.group_by_day(:visited_at, last: 1).count.values.first || 0
163
+ )
164
+ widgets << Panda::CMS::Admin::StatisticsComponent.new(
165
+ metric: "Views Last Week",
166
+ value: Panda::CMS::Visit.group_by_week(:visited_at, last: 1).count.values.first || 0
167
+ )
168
+ widgets << Panda::CMS::Admin::StatisticsComponent.new(
169
+ metric: "Views Last Month",
170
+ value: Panda::CMS::Visit.group_by_month(:visited_at, last: 1).count.values.first || 0
171
+ )
233
172
  end
234
- end
173
+
174
+ widgets
175
+ }
235
176
  end
236
177
  end
237
178
 
238
- config.before_initialize do |app|
179
+ config.before_initialize do |_app|
239
180
  # Default configuration
240
181
  Panda::CMS.configure do |config|
241
182
  # Array of additional EditorJS tools to load
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # https://guides.rubyonrails.org/configuring.html#config-exceptions-app
2
4
  module Panda
3
5
  module CMS