panda-cms 0.7.4 → 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 (152) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +37 -2
  3. data/Rakefile +2 -0
  4. data/app/builders/panda/cms/form_builder.rb +13 -5
  5. data/app/components/panda/cms/admin/heading_component.rb +5 -4
  6. data/app/components/panda/cms/admin/panel_component.rb +2 -2
  7. data/app/components/panda/cms/admin/statistics_component.rb +1 -2
  8. data/app/components/panda/cms/admin/user_activity_component.html.erb +3 -1
  9. data/app/components/panda/cms/admin/user_activity_component.rb +1 -3
  10. data/app/components/panda/cms/code_component.rb +8 -4
  11. data/app/components/panda/cms/menu_component.rb +7 -6
  12. data/app/components/panda/cms/page_menu_component.rb +15 -17
  13. data/app/components/panda/cms/rich_text_component.rb +5 -6
  14. data/app/components/panda/cms/text_component.rb +6 -7
  15. data/app/constraints/panda/cms/admin_constraint.rb +4 -1
  16. data/app/controllers/panda/cms/admin/dashboard_controller.rb +13 -9
  17. data/app/controllers/panda/cms/admin/forms_controller.rb +0 -2
  18. data/app/controllers/panda/cms/admin/my_profile_controller.rb +2 -1
  19. data/app/controllers/panda/cms/admin/pages_controller.rb +2 -1
  20. data/app/controllers/panda/cms/admin/posts_controller.rb +1 -2
  21. data/app/controllers/panda/cms/admin/sessions_controller.rb +3 -5
  22. data/app/controllers/panda/cms/admin/settings/bulk_editor_controller.rb +32 -25
  23. data/app/controllers/panda/cms/admin/settings_controller.rb +14 -10
  24. data/app/controllers/panda/cms/application_controller.rb +7 -2
  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 +32 -29
  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/asset_helper.rb +182 -0
  32. data/app/helpers/panda/cms/pages_helper.rb +2 -0
  33. data/app/helpers/panda/cms/posts_helper.rb +2 -0
  34. data/app/helpers/panda/cms/theme_helper.rb +2 -0
  35. data/app/javascript/panda/cms/controllers/editor_form_controller.js +59 -6
  36. data/app/javascript/panda/cms/controllers/index.js +3 -9
  37. data/app/javascript/panda/cms/controllers/theme_form_controller.js +16 -0
  38. data/app/javascript/panda/cms/stimulus-loading.js +39 -0
  39. data/app/javascript/panda_cms/stimulus-loading.js +39 -0
  40. data/app/jobs/panda/cms/application_job.rb +2 -0
  41. data/app/jobs/panda/cms/record_visit_job.rb +2 -0
  42. data/app/mailers/panda/cms/application_mailer.rb +2 -0
  43. data/app/mailers/panda/cms/form_mailer.rb +3 -1
  44. data/app/models/panda/cms/application_record.rb +2 -0
  45. data/app/models/panda/cms/block.rb +4 -1
  46. data/app/models/panda/cms/block_content.rb +2 -0
  47. data/app/models/panda/cms/breadcrumb.rb +2 -0
  48. data/app/models/panda/cms/current.rb +2 -0
  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 +9 -5
  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/user.rb +2 -0
  58. data/app/models/panda/cms/visit.rb +2 -0
  59. data/app/models/panda/social/instagram_post.rb +2 -0
  60. data/app/services/panda/cms/html_to_editor_js_converter.rb +4 -2
  61. data/app/services/panda/social/instagram_feed_service.rb +3 -1
  62. data/app/views/layouts/different_page.html.erb +6 -0
  63. data/app/views/layouts/homepage.html.erb +37 -0
  64. data/app/views/layouts/page.html.erb +18 -0
  65. data/app/views/layouts/panda/cms/application.html.erb +1 -0
  66. data/app/views/panda/cms/admin/pages/new.html.erb +14 -8
  67. data/app/views/panda/cms/admin/settings/index.html.erb +1 -1
  68. data/app/views/panda/cms/shared/_header.html.erb +10 -2
  69. data/app/views/panda/cms/shared/_importmap.html.erb +1 -1
  70. data/app/views/shared/_footer.html.erb +3 -0
  71. data/app/views/shared/_header.html.erb +11 -0
  72. data/config/importmap.rb +2 -0
  73. data/config/initializers/inflections.rb +2 -0
  74. data/config/initializers/panda/cms/form_errors.rb +20 -21
  75. data/config/initializers/panda/cms/healthcheck_log_silencer.rb +2 -0
  76. data/config/initializers/panda/cms.rb +2 -0
  77. data/config/initializers/zeitwork.rb +2 -0
  78. data/config/puma/test.rb +3 -1
  79. data/config/routes.rb +8 -8
  80. data/db/migrate/20240205223709_create_panda_cms_pages.rb +2 -0
  81. data/db/migrate/20240219213327_create_panda_cms_page_versions.rb +2 -0
  82. data/db/migrate/20240303002805_create_panda_cms_templates.rb +4 -1
  83. data/db/migrate/20240303003434_create_panda_cms_template_versions.rb +2 -0
  84. data/db/migrate/20240303022441_create_panda_cms_blocks.rb +4 -1
  85. data/db/migrate/20240303024256_create_panda_cms_block_contents.rb +2 -0
  86. data/db/migrate/20240303024746_create_panda_cms_block_content_versions.rb +2 -0
  87. data/db/migrate/20240303233238_add_panda_cms_menu_table.rb +2 -0
  88. data/db/migrate/20240303234724_add_panda_cms_menu_item_table.rb +2 -0
  89. data/db/migrate/20240304134343_add_parent_id_to_panda_cms_pages.rb +2 -0
  90. data/db/migrate/20240315125411_add_status_to_panda_cms_pages.rb +7 -5
  91. data/db/migrate/20240315125421_add_nested_sets_to_panda_cms_pages.rb +2 -0
  92. data/db/migrate/20240316212822_add_kind_to_panda_cms_menus.rb +3 -1
  93. data/db/migrate/20240316221425_add_start_page_to_panda_cms_menus.rb +2 -0
  94. data/db/migrate/20240316230706_add_nested_to_panda_cms_menu_items.rb +2 -0
  95. data/db/migrate/20240317010532_create_panda_cms_users.rb +2 -0
  96. data/db/migrate/20240317161534_add_max_uses_to_panda_cms_template.rb +2 -0
  97. data/db/migrate/20240317163053_reset_counter_cache_on_panda_cms_template.rb +2 -0
  98. data/db/migrate/20240317214827_create_panda_cms_redirects.rb +2 -0
  99. data/db/migrate/20240317230622_create_panda_cms_visits.rb +2 -0
  100. data/db/migrate/20240324205703_create_active_storage_tables.active_storage.rb +5 -2
  101. data/db/migrate/20240408084718_default_panda_cms_users_admin_to_false.rb +2 -0
  102. data/db/migrate/20240701225422_add_service_name_to_active_storage_blobs.active_storage.rb +8 -6
  103. data/db/migrate/20240701225423_create_active_storage_variant_records.active_storage.rb +2 -0
  104. data/db/migrate/20240701225424_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb +2 -0
  105. data/db/migrate/20240804235210_create_panda_cms_forms.rb +2 -0
  106. data/db/migrate/20240805013612_create_panda_cms_form_submissions.rb +2 -0
  107. data/db/migrate/20240805121123_create_panda_cms_posts.rb +3 -1
  108. data/db/migrate/20240805123104_create_panda_cms_post_versions.rb +2 -0
  109. data/db/migrate/20240806112735_fix_panda_cms_visits_column_names.rb +2 -0
  110. data/db/migrate/20240806204412_add_completion_path_to_panda_cms_forms.rb +2 -0
  111. data/db/migrate/20240820081917_change_form_submissions_to_submission_count.rb +2 -0
  112. data/db/migrate/20240923234535_add_depth_to_panda_cms_menus.rb +6 -4
  113. data/db/migrate/20241031205109_add_cached_content_to_panda_cms_block_contents.rb +2 -0
  114. data/db/migrate/20241119214548_convert_post_content_to_editor_js.rb +2 -0
  115. data/db/migrate/20241120000419_remove_post_tag_references.rb +2 -0
  116. data/db/migrate/20241120110943_add_editor_js_to_posts.rb +2 -0
  117. data/db/migrate/20241120113859_add_cached_content_to_panda_cms_posts.rb +2 -0
  118. data/db/migrate/20241123234140_remove_post_tag_id_from_posts.rb +2 -0
  119. data/db/migrate/20250106223303_add_author_id_to_panda_cms_posts.rb +2 -0
  120. data/db/migrate/20250120235542_remove_paper_trail.rb +5 -4
  121. data/db/migrate/20250126234001_create_panda_social_instagram_posts.rb +2 -0
  122. data/db/migrate/20250504221812_add_current_theme_to_panda_cms_users.rb +2 -0
  123. data/db/seeds.rb +2 -0
  124. data/lib/generators/panda/cms/install_generator.rb +2 -0
  125. data/lib/panda/cms/asset_loader.rb +390 -0
  126. data/lib/panda/cms/bulk_editor.rb +7 -3
  127. data/lib/panda/cms/demo_site_generator.rb +2 -0
  128. data/lib/panda/cms/editor_js/blocks/alert.rb +2 -0
  129. data/lib/panda/cms/editor_js/blocks/base.rb +2 -0
  130. data/lib/panda/cms/editor_js/blocks/header.rb +2 -0
  131. data/lib/panda/cms/editor_js/blocks/image.rb +3 -0
  132. data/lib/panda/cms/editor_js/blocks/list.rb +2 -0
  133. data/lib/panda/cms/editor_js/blocks/paragraph.rb +3 -0
  134. data/lib/panda/cms/editor_js/blocks/quote.rb +3 -0
  135. data/lib/panda/cms/editor_js/blocks/table.rb +3 -1
  136. data/lib/panda/cms/editor_js/renderer.rb +3 -0
  137. data/lib/panda/cms/editor_js.rb +2 -0
  138. data/lib/panda/cms/editor_js_content.rb +47 -41
  139. data/lib/panda/cms/engine.rb +29 -32
  140. data/lib/panda/cms/exceptions_app.rb +2 -0
  141. data/lib/panda/cms/railtie.rb +2 -0
  142. data/lib/panda/cms/slug.rb +3 -1
  143. data/lib/panda-cms/version.rb +3 -1
  144. data/lib/panda-cms.rb +4 -2
  145. data/lib/tasks/assets.rake +547 -0
  146. data/lib/tasks/panda/cms/install.rake +2 -0
  147. data/lib/tasks/panda/social/instagram.rake +2 -0
  148. data/lib/tasks/panda_cms.rake +3 -30
  149. data/public/panda-cms-assets/manifest.json +20 -0
  150. data/public/panda-cms-assets/panda-cms-0.7.4.css +26 -0
  151. data/public/panda-cms-assets/panda-cms-0.7.4.js +150 -0
  152. metadata +168 -14
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Panda
2
4
  module CMS
3
5
  class Menu < ApplicationRecord
@@ -5,13 +7,16 @@ module Panda
5
7
 
6
8
  after_save :generate_auto_menu_items, if: -> { kind == "auto" }
7
9
 
8
- has_many :menu_items, -> { order(lft: :asc) }, foreign_key: :panda_cms_menu_id, class_name: "Panda::CMS::MenuItem", inverse_of: :menu
9
- belongs_to :start_page, class_name: "Panda::CMS::Page", foreign_key: "start_page_id", inverse_of: :page_menu, optional: true
10
+ has_many :menu_items, lambda {
11
+ order(lft: :asc)
12
+ }, foreign_key: :panda_cms_menu_id, class_name: "Panda::CMS::MenuItem", inverse_of: :menu
13
+ belongs_to :start_page, class_name: "Panda::CMS::Page", foreign_key: "start_page_id", inverse_of: :page_menu,
14
+ optional: true
10
15
 
11
16
  accepts_nested_attributes_for :menu_items, reject_if: :all_blank, allow_destroy: true
12
17
 
13
18
  validates :name, presence: true, uniqueness: {case_sensitive: false}
14
- validates :kind, presence: true, inclusion: {in: ["static", "auto"]}
19
+ validates :kind, presence: true, inclusion: {in: %w[static auto]}
15
20
  validate :validate_start_page
16
21
 
17
22
  def generate_auto_menu_items
@@ -30,9 +35,7 @@ module Panda
30
35
  def generate_menu_items(parent_menu_item:, parent_page:)
31
36
  parent_page.children.where(status: [:active]).each do |page|
32
37
  menu_item = menu_items.create(text: page.title, panda_cms_page_id: page.id, parent: parent_menu_item)
33
- if page.children
34
- generate_menu_items(parent_menu_item: menu_item, parent_page: page)
35
- end
38
+ generate_menu_items(parent_menu_item: menu_item, parent_page: page) if page.children
36
39
  end
37
40
  end
38
41
 
@@ -43,9 +46,9 @@ module Panda
43
46
  # @visibility private
44
47
  #
45
48
  def validate_start_page
46
- if kind == "auto" && start_page.nil?
47
- errors.add(:start_page, "can't be blank")
48
- end
49
+ return unless kind == "auto" && start_page.nil?
50
+
51
+ errors.add(:start_page, "can't be blank")
49
52
  end
50
53
  end
51
54
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "awesome_nested_set"
2
4
 
3
5
  module Panda
@@ -8,8 +10,10 @@ module Panda
8
10
  self.implicit_order_column = "lft"
9
11
  self.table_name = "panda_cms_menu_items"
10
12
 
11
- belongs_to :menu, foreign_key: :panda_cms_menu_id, class_name: "Panda::CMS::Menu", inverse_of: :menu_items, touch: true
12
- belongs_to :page, foreign_key: :panda_cms_page_id, class_name: "Panda::CMS::Page", inverse_of: :menu_items, optional: true
13
+ belongs_to :menu, foreign_key: :panda_cms_menu_id, class_name: "Panda::CMS::Menu", inverse_of: :menu_items,
14
+ touch: true
15
+ belongs_to :page, foreign_key: :panda_cms_page_id, class_name: "Panda::CMS::Page", inverse_of: :menu_items,
16
+ optional: true
13
17
 
14
18
  validates :text, presence: true, uniqueness: {scope: :panda_cms_menu_id, case_sensitive: false}
15
19
  validates :page, presence: true, unless: -> { external_url.present? }
@@ -48,10 +52,10 @@ module Panda
48
52
  errors.add(:external_url, "must be a valid page or external link, neither are set")
49
53
  end
50
54
 
51
- if !page.nil? && !external_url.nil?
52
- errors.add(:page, "must be a valid page or external link, both are set")
53
- errors.add(:external_url, "must be a valid page or external link, both are set")
54
- end
55
+ return unless !page.nil? && !external_url.nil?
56
+
57
+ errors.add(:page, "must be a valid page or external link, both are set")
58
+ errors.add(:external_url, "must be a valid page or external link, both are set")
55
59
  end
56
60
  end
57
61
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "awesome_nested_set"
2
4
 
3
5
  module Panda
@@ -8,7 +10,8 @@ module Panda
8
10
  self.implicit_order_column = "lft"
9
11
 
10
12
  belongs_to :template, class_name: "Panda::CMS::Template", foreign_key: :panda_cms_template_id
11
- has_many :block_contents, class_name: "Panda::CMS::BlockContent", foreign_key: :panda_cms_page_id, dependent: :destroy
13
+ has_many :block_contents, class_name: "Panda::CMS::BlockContent", foreign_key: :panda_cms_page_id,
14
+ dependent: :destroy
12
15
  has_many :blocks, through: :block_contents
13
16
  has_many :menu_items, foreign_key: :panda_cms_page_id, class_name: "Panda::CMS::MenuItem", inverse_of: :page
14
17
  has_many :menus, through: :menu_items
@@ -19,7 +22,7 @@ module Panda
19
22
 
20
23
  validates :path,
21
24
  presence: true,
22
- format: {with: /\A\/.*\z/, message: "must start with a forward slash"}
25
+ format: {with: %r{\A/.*\z}, message: "must start with a forward slash"}
23
26
 
24
27
  validate :validate_unique_path_in_scope
25
28
 
@@ -62,12 +65,11 @@ module Panda
62
65
  # Find any other pages with the same path
63
66
  other_page = self.class.where(path: path).where.not(id: id).first
64
67
 
65
- if other_page
66
- # If there's another page with the same path, check if it has a different parent
67
- if other_page.parent_id == parent_id
68
- errors.add(:path, "has already been taken in this section")
69
- end
70
- end
68
+ return unless other_page
69
+ # If there's another page with the same path, check if it has a different parent
70
+ return unless other_page.parent_id == parent_id
71
+
72
+ errors.add(:path, "has already been taken in this section")
71
73
  end
72
74
 
73
75
  #
@@ -88,10 +90,10 @@ module Panda
88
90
  page_existing_block_ids = block_contents.map { |bc| bc.block.id }
89
91
  required_block_ids = template_block_ids - page_existing_block_ids
90
92
 
91
- if required_block_ids.count > 0
92
- required_block_ids.each do |block_id|
93
- Panda::CMS::BlockContent.find_or_create_by!(page: self, panda_cms_block_id: block_id, content: "")
94
- end
93
+ return unless required_block_ids.count.positive?
94
+
95
+ required_block_ids.each do |block_id|
96
+ Panda::CMS::BlockContent.find_or_create_by!(page: self, panda_cms_block_id: block_id, content: "")
95
97
  end
96
98
  end
97
99
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "awesome_nested_set"
2
4
 
3
5
  module Panda
@@ -47,11 +49,13 @@ module Panda
47
49
 
48
50
  def year
49
51
  return nil unless slug.match?(%r{\A/\d{4}/})
52
+
50
53
  slug.split("/")[1]
51
54
  end
52
55
 
53
56
  def month
54
57
  return nil unless slug.match?(%r{\A/\d{4}/\d{2}/})
58
+
55
59
  slug.split("/")[2]
56
60
  end
57
61
 
@@ -96,13 +100,13 @@ module Panda
96
100
  self.slug = CGI.unescape(slug.strip.gsub(%r{^/+|/+$}, ""))
97
101
 
98
102
  # Handle the case where we already have a properly formatted slug
99
- if slug.match?(%r{\A\d{4}/\d{2}/[^/]+\z})
100
- return self.slug = "/#{slug}"
101
- end
103
+ return self.slug = "/#{slug}" if slug.match?(%r{\A\d{4}/\d{2}/[^/]+\z})
102
104
 
103
105
  # Handle the case where we have a date-prefixed slug (from JS)
104
- if (match = slug.match(%r{\A(\d{4})-(\d{2})-(.+)\z}))
105
- year, month, base_slug = match[1], match[2], match[3]
106
+ if (match = slug.match(/\A(\d{4})-(\d{2})-(.+)\z/))
107
+ year = match[1]
108
+ month = match[2]
109
+ base_slug = match[3]
106
110
  return self.slug = "/#{year}/#{month}/#{base_slug}"
107
111
  end
108
112
 
@@ -1,16 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Panda
2
4
  module CMS
3
5
  class Redirect < ApplicationRecord
4
6
  belongs_to :origin_page, class_name: "Panda::CMS::Page", foreign_key: :origin_panda_cms_page_id, optional: true
5
- belongs_to :destination_page, class_name: "Panda::CMS::Page", foreign_key: :destination_panda_cms_page_id, optional: true
7
+ belongs_to :destination_page, class_name: "Panda::CMS::Page", foreign_key: :destination_panda_cms_page_id,
8
+ optional: true
6
9
 
7
10
  validates :status_code, presence: true
8
11
  validates :visits, presence: true
9
12
  validates :origin_path, presence: true
10
13
  validates :destination_path, presence: true
11
14
 
12
- validates :origin_path, format: {with: /\A\/.*\z/, message: "must start with a forward slash"}
13
- validates :destination_path, format: {with: /\A\/.*\z/, message: "must start with a forward slash"}
15
+ validates :origin_path, format: {with: %r{\A/.*\z}, message: "must start with a forward slash"}
16
+ validates :destination_path, format: {with: %r{\A/.*\z}, message: "must start with a forward slash"}
14
17
  end
15
18
  end
16
19
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Panda
2
4
  module CMS
3
5
  # Represents a template in the Panda CMS application.
@@ -5,8 +7,10 @@ module Panda
5
7
  self.table_name = "panda_cms_templates"
6
8
 
7
9
  # Associations
8
- has_many :pages, class_name: "Panda::CMS::Page", dependent: :restrict_with_error, inverse_of: :template, foreign_key: :panda_cms_template_id, counter_cache: :pages_count
9
- has_many :blocks, class_name: "Panda::CMS::Block", dependent: :restrict_with_error, inverse_of: :template, foreign_key: :panda_cms_template_id
10
+ has_many :pages, class_name: "Panda::CMS::Page", dependent: :restrict_with_error, inverse_of: :template,
11
+ foreign_key: :panda_cms_template_id, counter_cache: :pages_count
12
+ has_many :blocks, class_name: "Panda::CMS::Block", dependent: :restrict_with_error, inverse_of: :template,
13
+ foreign_key: :panda_cms_template_id
10
14
  has_many :block_contents, through: :blocks
11
15
 
12
16
  # Validations
@@ -15,12 +19,12 @@ module Panda
15
19
  validates :file_path,
16
20
  presence: true,
17
21
  uniqueness: true,
18
- format: {with: /\Alayouts\/.*\z/, message: "must be a valid layout file path"}
22
+ format: {with: %r{\Alayouts/.*\z}, message: "must be a valid layout file path"}
19
23
 
20
24
  validate :validate_template_file_exists
21
25
 
22
26
  # Scopes
23
- scope :available, -> {
27
+ scope :available, lambda {
24
28
  where("max_uses IS NULL OR (max_uses > 0 AND pages_count < max_uses)")
25
29
  }
26
30
 
@@ -43,7 +47,7 @@ module Panda
43
47
  # Matches:
44
48
  # Panda::CMS::RichTextComponent.new(key: :value)
45
49
  # Panda::CMS::RichTextComponent.new key: :value, key: value
46
- line.match(/Panda::CMS::([a-zA-Z]+)Component\.new[ \(]+([^\)]+)[\)]*/) do |match|
50
+ line.match(/Panda::CMS::([a-zA-Z]+)Component\.new[ (]+([^)]+)\)*/) do |match|
47
51
  # Extract the hash values
48
52
  template_path = file.gsub("app/views/", "").gsub(".html.erb", "")
49
53
  template_name = template_path.gsub("layouts/", "").titleize
@@ -68,7 +72,8 @@ module Panda
68
72
  # Create the block if it doesn't exist
69
73
  # TODO: +/- the output if it's created or removed
70
74
  begin
71
- block = Panda::CMS::Block.find_or_create_by!(template: template, kind: block_kind, key: block_name) do |block|
75
+ block = Panda::CMS::Block.find_or_create_by!(template: template, kind: block_kind,
76
+ key: block_name) do |block|
72
77
  block.name = block_name.titleize
73
78
  end
74
79
  rescue ActiveRecord::RecordInvalid => e
@@ -108,7 +113,7 @@ module Panda
108
113
  # Extract the file path from the Rails root
109
114
  file_path = file.to_s.sub("#{Rails.root}/app/views/", "").sub(".html.erb", "")
110
115
 
111
- next if file_path == "layouts/application" || file_path == "layouts/mailer"
116
+ next if ["layouts/application", "layouts/mailer"].include?(file_path)
112
117
 
113
118
  # Find or create the template based on the file path
114
119
  find_or_create_by(file_path: file_path) do |t|
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Panda
2
4
  module CMS
3
5
  class User < ApplicationRecord
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Panda
2
4
  module CMS
3
5
  class Visit < ApplicationRecord
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Panda
2
4
  module Social
3
5
  class InstagramPost < ApplicationRecord
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Panda
2
4
  module CMS
3
5
  class HtmlToEditorJsConverter
@@ -54,7 +56,7 @@ module Panda
54
56
  }
55
57
  when "p"
56
58
  text = process_inline_elements(child)
57
- paragraphs = text.split(/<br\s*\/?>\s*<br\s*\/?>/).map(&:strip)
59
+ paragraphs = text.split(%r{<br\s*/?>\s*<br\s*/?>}).map(&:strip)
58
60
  paragraphs.each do |paragraph|
59
61
  blocks << create_paragraph_block(paragraph) if paragraph.present?
60
62
  end
@@ -86,7 +88,7 @@ module Panda
86
88
  else
87
89
  # Handle p with nested content
88
90
  text = process_inline_elements(node)
89
- paragraphs = text.split(/<br\s*\/?>\s*<br\s*\/?>/).map(&:strip)
91
+ paragraphs = text.split(%r{<br\s*/?>\s*<br\s*/?>}).map(&:strip)
90
92
  paragraphs.each do |paragraph|
91
93
  blocks << create_paragraph_block(paragraph) if paragraph.present?
92
94
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "http"
2
4
  require "down"
3
5
 
@@ -5,7 +7,7 @@ module Panda
5
7
  module Social
6
8
  class InstagramFeedService
7
9
  GRAPH_API_VERSION = "v19.0"
8
- GRAPH_API_BASE_URL = "https://graph.instagram.com/#{GRAPH_API_VERSION}"
10
+ GRAPH_API_BASE_URL = "https://graph.instagram.com/#{GRAPH_API_VERSION}".freeze
9
11
 
10
12
  def initialize(access_token)
11
13
  @access_token = access_token
@@ -0,0 +1,6 @@
1
+ <%= render "shared/header" %>
2
+ <h1><%= @page.title %></h1>
3
+ <h2>Different Page Layout</h2>
4
+
5
+ <%= yield %>
6
+ <%= render "shared/footer" %>
@@ -0,0 +1,37 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Test Homepage</title>
5
+ <% if params[:embed_id].present? %>
6
+ <!-- Include Panda CMS assets for editor functionality when in edit mode -->
7
+ <%= panda_cms_complete_assets %>
8
+ <% end %>
9
+ </head>
10
+ <body>
11
+ <h1><%= @page.title %></h1>
12
+ <h2>Homepage Layout</h2>
13
+
14
+ <div class="prose">
15
+ <p>On this page we expect:</p>
16
+ <ul>
17
+ <li>Header</li>
18
+ <li>Footer</li>
19
+ <li>Some content</li>
20
+ <li>Tailwind-styled content</li>
21
+ <li>JS injected content through vanilla JS:
22
+ <div class="mt-0 font-bold" id="vanilla-injected-content">
23
+ Hello, Stimulus!
24
+ </div>
25
+ </li>
26
+ <li>JS injected content through <code>hello-controller</code>:
27
+ <div class="mt-0 font-bold" data-controller="hello" id="stimulus-injected-content">
28
+ Hello, Stimulus!
29
+ </div>
30
+ </li>
31
+ </ul>
32
+ </div>
33
+
34
+ <%= render Panda::CMS::RichTextComponent.new(key: :hero_content) %>
35
+ <%= yield %>
36
+ </body>
37
+ </html>
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Test Page</title>
5
+ <% if params[:embed_id].present? %>
6
+ <!-- Include Panda CMS assets for editor functionality when in edit mode -->
7
+ <%= panda_cms_complete_assets %>
8
+ <% end %>
9
+ </head>
10
+ <body>
11
+ <h1><%= @page.title %></h1>
12
+ <h2>Basic Page Layout</h2>
13
+ <%= render Panda::CMS::TextComponent.new(key: :plain_text) %>
14
+ <%= render Panda::CMS::CodeComponent.new(key: :html_code) %>
15
+ <%= render Panda::CMS::RichTextComponent.new(key: :main_content) %>
16
+ <%= yield %>
17
+ </body>
18
+ </html>
@@ -9,6 +9,7 @@
9
9
  <section id="panda-main" class="flex flex-row h-full">
10
10
  <div class="flex-1 h-full" id="panda-cms-primary-content">
11
11
  <%= render "panda/cms/admin/shared/breadcrumbs" %>
12
+ <%= render "panda/cms/admin/shared/flash" %>
12
13
  <%= yield %>
13
14
  </div>
14
15
  <% if content_for :sidebar %>
@@ -2,14 +2,20 @@
2
2
  <% component.with_heading(text: "Add Page", level: 1) do |heading| %>
3
3
  <% end %>
4
4
  <%= panda_cms_form_with model: page, url: admin_pages_path, method: :post do |f| %>
5
+ <% if page.errors.any? %>
6
+ <div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-md">
7
+ <div class="text-sm text-red-600">
8
+ <% page.errors.full_messages.each do |message| %>
9
+ <p><%= message %></p>
10
+ <% end %>
11
+ </div>
12
+ </div>
13
+ <% end %>
5
14
  <% options = nested_set_options(Panda::CMS::Page, page) { |i| "#{"-" * i.level} #{i.title} (#{i.path})" } %>
6
- <div data-controller="slug">
7
- <input type="hidden" value="<%= Panda::CMS::Current.root %>" data-slug-target="existing_root">
8
- <%= f.select :parent_id, options, {}, { "data-slug-target": "input_select", "data-action": "change->slug#setPrePath" } %>
9
- <%= f.text_field :title, { data: { "slug-target": "input_text", action: "input->slug#generatePath focusout->slug#generatePath" } } %>
10
- <%= f.text_field :path, { meta: t(".path.meta"), data: { prefix: Panda::CMS::Current.root, "slug-target": "output_text", action: "input->slug#handlePathInput" } } %>
11
- <%= f.collection_select :panda_cms_template_id, available_templates, :id, :name %>
12
- <%= f.button "Create Page" %>
13
- </div>
15
+ <%= f.select :parent_id, options %>
16
+ <%= f.text_field :title %>
17
+ <%= f.text_field :path, { meta: t(".path.meta") } %>
18
+ <%= f.collection_select :panda_cms_template_id, available_templates, :id, :name %>
19
+ <%= f.button "Create Page" %>
14
20
  <% end %>
15
21
  <% end %>
@@ -18,6 +18,6 @@
18
18
 
19
19
  <div class="text-center mt-6 space-y-2">
20
20
  <p class="text-sm font-semibold">🐼 Panda CMS version: <%= Panda::CMS::VERSION %></p>
21
- <p class="text-sm">&copy; <%= Date.current.year %> Panda Software Limited. All rights reserved.</p>
21
+ <p class="text-sm">&copy; <%= Date.current.year %> Otaina Limited. All rights reserved.</p>
22
22
  </div>
23
23
  <% end %>
@@ -7,9 +7,17 @@
7
7
  <%= csrf_meta_tags %>
8
8
  <%= csp_meta_tag %>
9
9
  <script src="https://kit.fontawesome.com/7835d81e75.js" defer="true" crossorigin="anonymous"></script>
10
- <script async src="https://ga.jspm.io/npm:es-module-shims@1.10.0/dist/es-module-shims.js" defer="true" crossorigin="anonymous"></script>
11
10
  <%= stylesheet_link_tag "panda.cms", "data-turbo-track": "reload", media: "all" %>
12
- <%= render "panda/cms/shared/importmap" %>
11
+
12
+ <% if Panda::CMS::AssetLoader.use_github_assets? || Rails.env.test? %>
13
+ <!-- Using compiled Panda CMS assets -->
14
+ <%= panda_cms_complete_assets %>
15
+ <% else %>
16
+ <!-- Using development importmap assets -->
17
+ <script async src="https://ga.jspm.io/npm:es-module-shims@1.10.0/dist/es-module-shims.js" defer="true" crossorigin="anonymous"></script>
18
+ <%= render "panda/cms/shared/importmap" %>
19
+ <% end %>
20
+
13
21
  <%= render "panda/cms/shared/favicons" %>
14
22
  <%= yield :head %>
15
23
  </head>
@@ -8,7 +8,7 @@
8
8
  # Vendored
9
9
  "@hotwired/turbo": asset_path("panda/cms/@hotwired--turbo.js"),
10
10
  "@hotwired/stimulus": asset_path("panda/cms/@hotwired--stimulus.js"),
11
- "@hotwired/stimulus-loading": asset_path("stimulus-loading.js"),
11
+ "@hotwired/stimulus-loading": asset_path("panda_cms/stimulus-loading.js"),
12
12
  "@editorjs/editorjs": asset_path("panda/cms/@editorjs--editorjs.js"),
13
13
  "tailwindcss-stimulus-components": asset_path("panda/cms/tailwindcss-stimulus-components.js"),
14
14
  # Our page editor
@@ -0,0 +1,3 @@
1
+ <h1>Test Footer</h1>
2
+ </body>
3
+ </html>
@@ -0,0 +1,11 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Panda CMS Page</title>
5
+ <% if params[:embed_id].present? %>
6
+ <!-- Include Panda CMS assets for editor functionality when in edit mode -->
7
+ <%= panda_cms_complete_assets %>
8
+ <% end %>
9
+ </head>
10
+ <body>
11
+ <h1>Test Header</h1>
data/config/importmap.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  pin "application_panda_cms", to: "panda_cms/application_panda_cms.js", preload: true
2
4
 
3
5
  pin "@hotwired/turbo", to: "@hotwired--turbo.js", preload: true # @8.0.12
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  ActiveSupport::Inflector.inflections(:en) do |inflect|
2
4
  inflect.acronym "CMS"
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  ActionView::Base.field_error_proc = proc do |html_tag, instance|
2
4
  html = ""
3
5
  form_fields = %w[input select textarea trix-editor label].join(", ")
@@ -6,30 +8,27 @@ ActionView::Base.field_error_proc = proc do |html_tag, instance|
6
8
  autofocused = false
7
9
 
8
10
  Nokogiri::HTML::DocumentFragment.parse(html_tag).css(form_fields).each do |element|
9
- if form_fields.include?(element.node_name)
10
- if !autofocused
11
- # element.attribute("autofocus", "true")
12
- autofocused = true
13
- end
11
+ next unless form_fields.include?(element.node_name)
14
12
 
15
- message = "#{instance.object.class.human_attribute_name(instance.send(:sanitized_method_name))} "
16
- message += if instance.error_message.respond_to?(:each)
17
- "#{instance.error_message.uniq.to_sentence}."
18
- else
19
- "#{instance.error_message}."
20
- end
13
+ autofocused ||= true
14
+
15
+ message = "#{instance.object.class.human_attribute_name(instance.send(:sanitized_method_name))} "
16
+ message += if instance.error_message.respond_to?(:each)
17
+ "#{instance.error_message.uniq.to_sentence}."
18
+ else
19
+ "#{instance.error_message}."
20
+ end
21
21
 
22
- if element.node_name.eql?("label")
23
- html = element.to_s
22
+ if element.node_name.eql?("label")
23
+ html = element.to_s
24
+ else
25
+ element.add_class(error_class)
26
+ html = if element.get_attribute("data-prefix")
27
+ "#{element}</div><div class=\"#{message_class}\">#{message}"
28
+ elsif element.get_attribute("type") != "checkbox"
29
+ "#{element}<div class=\"#{message_class}\">#{message}</div>"
24
30
  else
25
- element.add_class(error_class)
26
- html = if element.get_attribute("data-prefix")
27
- "#{element}</div><div class=\"#{message_class}\">#{message}"
28
- elsif element.get_attribute("type") != "checkbox"
29
- "#{element}<div class=\"#{message_class}\">#{message}</div>"
30
- else
31
- element.to_s
32
- end
31
+ element.to_s
33
32
  end
34
33
  end
35
34
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "silencer/rails/logger"
2
4
 
3
5
  # Don't log requests to the healthcheck endpoint
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  Panda::CMS.configure do |config|
2
4
  # The main title of your website
3
5
  config.title = "Demo Site"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  Rails.autoloaders.main.inflector.inflect(
2
4
  "panda-cms" => "Panda::CMS"
3
5
  )
data/config/puma/test.rb CHANGED
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Puma configuration for test environment
2
4
  workers 0
3
5
  threads 1, 1
4
6
 
5
- port ENV.fetch("PORT") { 3001 }
7
+ port ENV.fetch("PORT", 3001)
6
8
  environment "test"
7
9
 
8
10
  # Allow for proper shutdown
data/config/routes.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../app/constraints/panda/cms/admin_constraint"
2
4
 
3
5
  Panda::CMS::Engine.routes.draw do
@@ -19,10 +21,6 @@ Panda::CMS::Engine.routes.draw do
19
21
  get "bulk_editor", to: "bulk_editor#new"
20
22
  post "bulk_editor", to: "bulk_editor#create"
21
23
  end
22
-
23
- if Rails.env.development?
24
- mount Lookbook::Engine, at: "/lookbook"
25
- end
26
24
  end
27
25
 
28
26
  get Panda::CMS.route_namespace, to: "admin/dashboard#show", as: :admin_dashboard
@@ -33,8 +31,10 @@ Panda::CMS::Engine.routes.draw do
33
31
  # Authentication routes
34
32
  get Panda::CMS.route_namespace, to: "admin/sessions#new", as: :admin_login
35
33
  # Get and post options here are for OmniAuth coming back in, not going out
36
- match "#{Panda::CMS.route_namespace}/auth/:provider/callback", to: "admin/sessions#create", as: :admin_login_callback, via: %i[get post]
37
- match "#{Panda::CMS.route_namespace}/auth/failure", to: "admin/sessions#failure", as: :admin_login_failure, via: %i[get post]
34
+ match "#{Panda::CMS.route_namespace}/auth/:provider/callback", to: "admin/sessions#create",
35
+ as: :admin_login_callback, via: %i[get post]
36
+ match "#{Panda::CMS.route_namespace}/auth/failure", to: "admin/sessions#failure", as: :admin_login_failure,
37
+ via: %i[get post]
38
38
  # OmniAuth additionally adds a GET route for "#{Panda::CMS.route_namespace}/auth/:provider" but doesn't name it
39
39
  delete Panda::CMS.route_namespace, to: "admin/sessions#destroy", as: :admin_logout
40
40
 
@@ -51,7 +51,7 @@ Panda::CMS::Engine.routes.draw do
51
51
  constraints: {
52
52
  year: /\d{4}/,
53
53
  month: /\d{2}/,
54
- slug: /[^\/]+/,
54
+ slug: %r{[^/]+},
55
55
  format: /html|json|xml/
56
56
  }
57
57
 
@@ -60,7 +60,7 @@ Panda::CMS::Engine.routes.draw do
60
60
  to: "posts#show",
61
61
  as: :post,
62
62
  constraints: {
63
- slug: /[^\/]+/,
63
+ slug: %r{[^/]+},
64
64
  format: /html|json|xml/
65
65
  }
66
66
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class CreatePandaCMSPages < ActiveRecord::Migration[7.1]
2
4
  def change
3
5
  create_table :panda_cms_pages, id: :uuid do |t|