panda-cms 0.7.3 → 0.7.5

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 (182) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +40 -5
  3. data/Rakefile +2 -0
  4. data/app/assets/builds/panda.cms.css +2 -6
  5. data/app/assets/tailwind/application.css +178 -0
  6. data/app/assets/tailwind/tailwind.config.js +15 -0
  7. data/app/builders/panda/cms/form_builder.rb +27 -36
  8. data/app/components/panda/cms/admin/flash_message_component.html.erb +2 -2
  9. data/app/components/panda/cms/admin/heading_component.rb +5 -4
  10. data/app/components/panda/cms/admin/panel_component.rb +2 -2
  11. data/app/components/panda/cms/admin/statistics_component.rb +1 -2
  12. data/app/components/panda/cms/admin/user_activity_component.html.erb +3 -1
  13. data/app/components/panda/cms/admin/user_activity_component.rb +8 -21
  14. data/app/components/panda/cms/code_component.rb +8 -4
  15. data/app/components/panda/cms/menu_component.rb +7 -6
  16. data/app/components/panda/cms/page_menu_component.rb +15 -17
  17. data/app/components/panda/cms/rich_text_component.rb +5 -6
  18. data/app/components/panda/cms/text_component.rb +6 -7
  19. data/app/constraints/panda/cms/admin_constraint.rb +4 -1
  20. data/app/controllers/panda/cms/admin/block_contents_controller.rb +0 -1
  21. data/app/controllers/panda/cms/admin/dashboard_controller.rb +13 -9
  22. data/app/controllers/panda/cms/admin/forms_controller.rb +0 -3
  23. data/app/controllers/panda/cms/admin/my_profile_controller.rb +44 -0
  24. data/app/controllers/panda/cms/admin/pages_controller.rb +15 -4
  25. data/app/controllers/panda/cms/admin/posts_controller.rb +6 -22
  26. data/app/controllers/panda/cms/admin/sessions_controller.rb +3 -5
  27. data/app/controllers/panda/cms/admin/settings/bulk_editor_controller.rb +32 -25
  28. data/app/controllers/panda/cms/admin/settings_controller.rb +14 -10
  29. data/app/controllers/panda/cms/application_controller.rb +7 -2
  30. data/app/controllers/panda/cms/errors_controller.rb +5 -2
  31. data/app/controllers/panda/cms/form_submissions_controller.rb +4 -0
  32. data/app/controllers/panda/cms/pages_controller.rb +40 -35
  33. data/app/controllers/panda/cms/posts_controller.rb +2 -0
  34. data/app/helpers/panda/cms/admin/files_helper.rb +5 -1
  35. data/app/helpers/panda/cms/admin/pages_helper.rb +5 -1
  36. data/app/helpers/panda/cms/asset_helper.rb +182 -0
  37. data/app/helpers/panda/cms/pages_helper.rb +2 -0
  38. data/app/helpers/panda/cms/posts_helper.rb +2 -0
  39. data/app/helpers/panda/cms/theme_helper.rb +2 -0
  40. data/app/javascript/panda/cms/controllers/editor_form_controller.js +59 -6
  41. data/app/javascript/panda/cms/controllers/index.js +5 -9
  42. data/app/javascript/panda/cms/controllers/slug_controller.js +64 -31
  43. data/app/javascript/panda/cms/controllers/theme_form_controller.js +25 -0
  44. data/app/javascript/panda/cms/stimulus-loading.js +39 -0
  45. data/app/javascript/panda_cms/stimulus-loading.js +39 -0
  46. data/app/jobs/panda/cms/application_job.rb +2 -0
  47. data/app/jobs/panda/cms/record_visit_job.rb +14 -14
  48. data/app/mailers/panda/cms/application_mailer.rb +2 -0
  49. data/app/mailers/panda/cms/form_mailer.rb +3 -1
  50. data/app/models/panda/cms/application_record.rb +3 -0
  51. data/app/models/panda/cms/block.rb +12 -17
  52. data/app/models/panda/cms/block_content.rb +7 -6
  53. data/app/models/panda/cms/breadcrumb.rb +2 -0
  54. data/app/models/panda/cms/current.rb +2 -0
  55. data/app/models/panda/cms/form.rb +2 -0
  56. data/app/models/panda/cms/form_submission.rb +2 -0
  57. data/app/models/panda/cms/menu.rb +12 -9
  58. data/app/models/panda/cms/menu_item.rb +10 -6
  59. data/app/models/panda/cms/page.rb +31 -16
  60. data/app/models/panda/cms/post.rb +12 -10
  61. data/app/models/panda/cms/redirect.rb +9 -1
  62. data/app/models/panda/cms/template.rb +17 -13
  63. data/app/models/panda/cms/user.rb +2 -0
  64. data/app/models/panda/cms/visit.rb +3 -1
  65. data/app/models/panda/social/instagram_post.rb +17 -0
  66. data/app/services/panda/cms/html_to_editor_js_converter.rb +10 -15
  67. data/app/services/panda/social/instagram_feed_service.rb +63 -0
  68. data/app/views/layouts/different_page.html.erb +6 -0
  69. data/app/views/layouts/homepage.html.erb +37 -0
  70. data/app/views/layouts/page.html.erb +18 -0
  71. data/app/views/layouts/panda/cms/application.html.erb +1 -0
  72. data/app/views/panda/cms/admin/my_profile/edit.html.erb +35 -0
  73. data/app/views/panda/cms/admin/pages/index.html.erb +1 -1
  74. data/app/views/panda/cms/admin/pages/new.html.erb +14 -8
  75. data/app/views/panda/cms/admin/posts/_form.html.erb +10 -0
  76. data/app/views/panda/cms/admin/posts/edit.html.erb +3 -2
  77. data/app/views/panda/cms/admin/posts/index.html.erb +1 -1
  78. data/app/views/panda/cms/admin/settings/index.html.erb +3 -1
  79. data/app/views/panda/cms/admin/shared/_sidebar.html.erb +1 -1
  80. data/app/views/panda/cms/shared/_header.html.erb +14 -4
  81. data/app/views/panda/cms/shared/_importmap.html.erb +2 -1
  82. data/app/views/shared/_footer.html.erb +3 -0
  83. data/app/views/shared/_header.html.erb +11 -0
  84. data/config/importmap.rb +2 -0
  85. data/config/initializers/inflections.rb +2 -0
  86. data/config/initializers/panda/cms/form_errors.rb +20 -21
  87. data/config/initializers/panda/cms/healthcheck_log_silencer.rb +2 -0
  88. data/config/initializers/panda/cms.rb +2 -0
  89. data/config/initializers/zeitwork.rb +2 -0
  90. data/config/locales/en.yml +5 -0
  91. data/config/puma/test.rb +3 -1
  92. data/config/routes.rb +11 -8
  93. data/db/migrate/20240205223709_create_panda_cms_pages.rb +2 -0
  94. data/db/migrate/20240219213327_create_panda_cms_page_versions.rb +2 -0
  95. data/db/migrate/20240303002805_create_panda_cms_templates.rb +4 -1
  96. data/db/migrate/20240303003434_create_panda_cms_template_versions.rb +2 -0
  97. data/db/migrate/20240303022441_create_panda_cms_blocks.rb +4 -1
  98. data/db/migrate/20240303024256_create_panda_cms_block_contents.rb +2 -0
  99. data/db/migrate/20240303024746_create_panda_cms_block_content_versions.rb +2 -0
  100. data/db/migrate/20240303233238_add_panda_cms_menu_table.rb +2 -0
  101. data/db/migrate/20240303234724_add_panda_cms_menu_item_table.rb +2 -0
  102. data/db/migrate/20240304134343_add_parent_id_to_panda_cms_pages.rb +2 -0
  103. data/db/migrate/20240315125411_add_status_to_panda_cms_pages.rb +7 -5
  104. data/db/migrate/20240315125421_add_nested_sets_to_panda_cms_pages.rb +2 -0
  105. data/db/migrate/20240316212822_add_kind_to_panda_cms_menus.rb +3 -1
  106. data/db/migrate/20240316221425_add_start_page_to_panda_cms_menus.rb +2 -0
  107. data/db/migrate/20240316230706_add_nested_to_panda_cms_menu_items.rb +2 -0
  108. data/db/migrate/20240317010532_create_panda_cms_users.rb +2 -0
  109. data/db/migrate/20240317161534_add_max_uses_to_panda_cms_template.rb +2 -0
  110. data/db/migrate/20240317163053_reset_counter_cache_on_panda_cms_template.rb +2 -0
  111. data/db/migrate/20240317214827_create_panda_cms_redirects.rb +2 -0
  112. data/db/migrate/20240317230622_create_panda_cms_visits.rb +2 -0
  113. data/db/migrate/20240324205703_create_active_storage_tables.active_storage.rb +5 -2
  114. data/db/migrate/20240408084718_default_panda_cms_users_admin_to_false.rb +2 -0
  115. data/db/migrate/20240701225422_add_service_name_to_active_storage_blobs.active_storage.rb +8 -6
  116. data/db/migrate/20240701225423_create_active_storage_variant_records.active_storage.rb +2 -0
  117. data/db/migrate/20240701225424_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb +2 -0
  118. data/db/migrate/20240804235210_create_panda_cms_forms.rb +2 -0
  119. data/db/migrate/20240805013612_create_panda_cms_form_submissions.rb +2 -0
  120. data/db/migrate/20240805121123_create_panda_cms_posts.rb +3 -1
  121. data/db/migrate/20240805123104_create_panda_cms_post_versions.rb +2 -0
  122. data/db/migrate/20240806112735_fix_panda_cms_visits_column_names.rb +2 -0
  123. data/db/migrate/20240806204412_add_completion_path_to_panda_cms_forms.rb +2 -0
  124. data/db/migrate/20240820081917_change_form_submissions_to_submission_count.rb +2 -0
  125. data/db/migrate/20240923234535_add_depth_to_panda_cms_menus.rb +6 -4
  126. data/db/migrate/20241031205109_add_cached_content_to_panda_cms_block_contents.rb +2 -0
  127. data/db/migrate/20241119214548_convert_post_content_to_editor_js.rb +2 -0
  128. data/db/migrate/20241120000419_remove_post_tag_references.rb +2 -0
  129. data/db/migrate/20241120110943_add_editor_js_to_posts.rb +2 -0
  130. data/db/migrate/20241120113859_add_cached_content_to_panda_cms_posts.rb +2 -0
  131. data/db/migrate/20241123234140_remove_post_tag_id_from_posts.rb +2 -0
  132. data/db/migrate/20250106223303_add_author_id_to_panda_cms_posts.rb +2 -0
  133. data/db/migrate/20250120235542_remove_paper_trail.rb +56 -0
  134. data/db/migrate/20250126234001_create_panda_social_instagram_posts.rb +16 -0
  135. data/db/migrate/20250504221812_add_current_theme_to_panda_cms_users.rb +7 -0
  136. data/db/seeds.rb +2 -0
  137. data/lib/generators/panda/cms/install_generator.rb +2 -0
  138. data/lib/panda/cms/asset_loader.rb +390 -0
  139. data/lib/panda/cms/bulk_editor.rb +7 -3
  140. data/lib/panda/cms/demo_site_generator.rb +27 -4
  141. data/lib/panda/cms/editor_js/blocks/alert.rb +2 -0
  142. data/lib/panda/cms/editor_js/blocks/base.rb +2 -0
  143. data/lib/panda/cms/editor_js/blocks/header.rb +2 -0
  144. data/lib/panda/cms/editor_js/blocks/image.rb +3 -0
  145. data/lib/panda/cms/editor_js/blocks/list.rb +2 -0
  146. data/lib/panda/cms/editor_js/blocks/paragraph.rb +3 -0
  147. data/lib/panda/cms/editor_js/blocks/quote.rb +3 -0
  148. data/lib/panda/cms/editor_js/blocks/table.rb +3 -1
  149. data/lib/panda/cms/editor_js/renderer.rb +3 -0
  150. data/lib/panda/cms/editor_js.rb +2 -0
  151. data/lib/panda/cms/editor_js_content.rb +50 -23
  152. data/lib/panda/cms/engine.rb +36 -37
  153. data/lib/panda/cms/exceptions_app.rb +2 -0
  154. data/lib/panda/cms/railtie.rb +2 -0
  155. data/lib/panda/cms/slug.rb +3 -1
  156. data/lib/panda-cms/version.rb +3 -1
  157. data/lib/panda-cms.rb +17 -2
  158. data/lib/tasks/assets.rake +547 -0
  159. data/lib/tasks/panda/cms/install.rake +25 -0
  160. data/lib/tasks/panda/social/instagram.rake +20 -0
  161. data/lib/tasks/panda_cms.rake +3 -30
  162. data/public/panda-cms-assets/editor-js/core/editorjs.min.js +83 -0
  163. data/public/panda-cms-assets/editor-js/plugins/embed.min.js +2 -0
  164. data/public/panda-cms-assets/editor-js/plugins/header.min.js +9 -0
  165. data/public/panda-cms-assets/editor-js/plugins/nested-list.min.js +2 -0
  166. data/public/panda-cms-assets/editor-js/plugins/paragraph.min.js +9 -0
  167. data/public/panda-cms-assets/editor-js/plugins/quote.min.js +2 -0
  168. data/public/panda-cms-assets/editor-js/plugins/simple-image.min.js +2 -0
  169. data/public/panda-cms-assets/editor-js/plugins/table.min.js +2 -0
  170. data/public/panda-cms-assets/manifest.json +20 -0
  171. data/public/panda-cms-assets/panda-cms-0.7.4.css +26 -0
  172. data/public/panda-cms-assets/panda-cms-0.7.4.js +150 -0
  173. metadata +71 -438
  174. data/app/models/action_text/rich_text_version.rb +0 -6
  175. data/app/models/panda/cms/block_content_version.rb +0 -8
  176. data/app/models/panda/cms/page_version.rb +0 -8
  177. data/app/models/panda/cms/post_version.rb +0 -8
  178. data/app/models/panda/cms/template_version.rb +0 -8
  179. data/app/models/panda/cms/version.rb +0 -8
  180. data/config/initializers/panda/cms/paper_trail.rb +0 -7
  181. data/db/migrate/20240904200605_create_action_text_tables.action_text.rb +0 -24
  182. data/db/migrate/20241119214549_remove_action_text_from_posts.rb +0 -9
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class CreatePandaCMSPosts < ActiveRecord::Migration[7.1]
2
4
  def change
3
5
  create_table :panda_cms_post_tags, id: :uuid do |t|
@@ -20,7 +22,7 @@ class CreatePandaCMSPosts < ActiveRecord::Migration[7.1]
20
22
  t.index :slug, unique: true
21
23
  end
22
24
 
23
- create_enum :panda_cms_post_status, ["active", "draft", "hidden", "archived"]
25
+ create_enum :panda_cms_post_status, %w[active draft hidden archived]
24
26
  add_column :panda_cms_posts, :status, :panda_cms_post_status, default: "draft", null: false
25
27
  add_index :panda_cms_posts, :status
26
28
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class CreatePandaCMSPostVersions < ActiveRecord::Migration[7.1]
2
4
  def change
3
5
  create_table :panda_cms_post_versions, id: :uuid do |t|
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class FixPandaCMSVisitsColumnNames < ActiveRecord::Migration[7.1]
2
4
  def change
3
5
  change_table :panda_cms_visits do |t|
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class AddCompletionPathToPandaCMSForms < ActiveRecord::Migration[7.1]
2
4
  def change
3
5
  add_column :panda_cms_forms, :completion_path, :string
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class ChangeFormSubmissionsToSubmissionCount < ActiveRecord::Migration[7.2]
2
4
  def change
3
5
  rename_column :panda_cms_forms, :submissions, :submission_count
@@ -1,11 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class AddDepthToPandaCMSMenus < ActiveRecord::Migration[7.2]
2
4
  def change
3
5
  add_column :panda_cms_menus, :depth, :integer, null: true, default: nil
4
6
 
5
7
  homepage = Panda::CMS::Page.find_by(path: "/")
6
- if homepage
7
- main_menu = Panda::CMS::Menu.find_by(start_page_id: homepage.id, kind: :auto)
8
- main_menu&.update(depth: 2)
9
- end
8
+ return unless homepage
9
+
10
+ main_menu = Panda::CMS::Menu.find_by(start_page_id: homepage.id, kind: :auto)
11
+ main_menu&.update(depth: 2)
10
12
  end
11
13
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class AddCachedContentToPandaCMSBlockContents < ActiveRecord::Migration[7.2]
2
4
  def change
3
5
  add_column :panda_cms_block_contents, :cached_content, :jsonb
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class ConvertPostContentToEditorJs < ActiveRecord::Migration[7.1]
2
4
  def up
3
5
  Panda::CMS::Post.find_each do |post|
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class RemovePostTagReferences < ActiveRecord::Migration[8.0]
2
4
  def up
3
5
  remove_reference :panda_cms_posts, :post_tag, foreign_key: {to_table: :panda_cms_post_tags}, type: :uuid
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class AddEditorJsToPosts < ActiveRecord::Migration[7.0]
2
4
  def up
3
5
  # First, add a temporary column
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class AddCachedContentToPandaCMSPosts < ActiveRecord::Migration[8.0]
2
4
  def change
3
5
  add_column :panda_cms_posts, :cached_content, :text
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class RemovePostTagIdFromPosts < ActiveRecord::Migration[8.0]
2
4
  def change
3
5
  remove_column :panda_cms_posts, :post_tag_id if column_exists?(:panda_cms_posts, :post_tag_id)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class AddAuthorIdToPandaCMSPosts < ActiveRecord::Migration[8.0]
2
4
  def change
3
5
  add_reference :panda_cms_posts, :author, type: :uuid, foreign_key: {to_table: :panda_cms_users}
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RemovePaperTrail < ActiveRecord::Migration[7.1]
4
+ def up
5
+ version_tables = %w[
6
+ panda_cms_versions
7
+ panda_cms_version_associations
8
+ panda_cms_template_versions
9
+ panda_cms_page_versions
10
+ panda_cms_block_content_versions
11
+ panda_cms_post_versions
12
+ action_text_rich_text_versions
13
+ ]
14
+
15
+ version_tables.each do |table|
16
+ drop_table table if table_exists?(table)
17
+ end
18
+ end
19
+
20
+ def down
21
+ create_table :panda_cms_versions do |t|
22
+ t.string :item_type, null: false
23
+ t.uuid :item_id, null: false
24
+ t.string :event, null: false
25
+ t.string :whodunnit
26
+ t.jsonb :object
27
+ t.jsonb :object_changes
28
+ t.datetime :created_at
29
+ end
30
+ add_index :panda_cms_versions, %i[item_type item_id]
31
+
32
+ create_table :panda_cms_version_associations do |t|
33
+ t.integer :version_id
34
+ t.string :foreign_key_name, null: false
35
+ t.integer :foreign_key_id
36
+ t.string :foreign_type
37
+ end
38
+ add_index :panda_cms_version_associations, [:version_id]
39
+ add_index :panda_cms_version_associations, %i[foreign_key_name foreign_key_id foreign_type],
40
+ name: "index_version_associations_on_foreign_key"
41
+
42
+ # Model-specific version tables
43
+ %w[template page block_content post action_text_rich_text].each do |model|
44
+ create_table "panda_cms_#{model}_versions" do |t|
45
+ t.string :item_type, null: false
46
+ t.uuid :item_id, null: false
47
+ t.string :event, null: false
48
+ t.string :whodunnit
49
+ t.jsonb :object
50
+ t.jsonb :object_changes
51
+ t.datetime :created_at
52
+ end
53
+ add_index "panda_cms_#{model}_versions", %i[item_type item_id]
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreatePandaSocialInstagramPosts < ActiveRecord::Migration[7.1]
4
+ def change
5
+ create_table :panda_social_instagram_posts, id: :uuid do |t|
6
+ t.string :instagram_id, null: false
7
+ t.text :caption
8
+ t.datetime :posted_at, null: false
9
+ t.string :permalink
10
+ t.timestamps
11
+
12
+ t.index :instagram_id, unique: true
13
+ t.index :posted_at
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddCurrentThemeToPandaCMSUsers < ActiveRecord::Migration[8.0]
4
+ def change
5
+ add_column :panda_cms_users, :current_theme, :string, default: "default"
6
+ end
7
+ end
data/db/seeds.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  generator = Panda::CMS::DemoSiteGenerator.new
2
4
  generator.create_templates
3
5
  generator.create_pages
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Generators
2
4
  module Panda
3
5
  module CMS
@@ -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
@@ -34,10 +36,31 @@ module Panda
34
36
  #
35
37
  # @return [Hash] A hash containing the created pages
36
38
  def create_pages
37
- @pages[:home] = Panda::CMS::Page.find_or_create_by({path: "/", title: "Home", template: @templates[:homepage]})
38
- @pages[:about] = Panda::CMS::Page.find_or_create_by({path: "/about", title: "About", template: @templates[:page], parent: @pages[:home]})
39
- @pages[:not_found] = Panda::CMS::Page.find_or_create_by({path: "/404", title: "Page Not Found", template: @templates[:page], parent: @pages[:home], status: "hidden"})
40
- @pages[:internal_error] = Panda::CMS::Page.find_or_create_by({path: "/500", title: "Internal Server Error", template: @templates[:page], parent: @pages[:home], status: "hidden"})
39
+ @pages[:home] = Panda::CMS::Page.find_or_create_by({
40
+ path: "/",
41
+ title: "Home",
42
+ panda_cms_template_id: @templates[:homepage].id
43
+ })
44
+ @pages[:about] = Panda::CMS::Page.find_or_create_by({
45
+ path: "/about",
46
+ title: "About",
47
+ panda_cms_template_id: @templates[:page].id,
48
+ parent: @pages[:home]
49
+ })
50
+ @pages[:not_found] = Panda::CMS::Page.find_or_create_by({
51
+ path: "/404",
52
+ title: "Page Not Found",
53
+ panda_cms_template_id: @templates[:page].id,
54
+ parent: @pages[:home],
55
+ status: "hidden"
56
+ })
57
+ @pages[:internal_error] = Panda::CMS::Page.find_or_create_by({
58
+ path: "/500",
59
+ title: "Internal Server Error",
60
+ panda_cms_template_id: @templates[:page].id,
61
+ parent: @pages[:home],
62
+ status: "hidden"
63
+ })
41
64
 
42
65
  Panda::CMS::Page.reset_column_information
43
66
  Panda::CMS::Page.rebuild!