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.
- checksums.yaml +4 -4
- data/README.md +37 -2
- data/Rakefile +2 -0
- data/app/builders/panda/cms/form_builder.rb +13 -5
- data/app/components/panda/cms/admin/heading_component.rb +5 -4
- data/app/components/panda/cms/admin/panel_component.rb +2 -2
- data/app/components/panda/cms/admin/statistics_component.rb +1 -2
- data/app/components/panda/cms/admin/user_activity_component.html.erb +3 -1
- data/app/components/panda/cms/admin/user_activity_component.rb +1 -3
- data/app/components/panda/cms/code_component.rb +8 -4
- data/app/components/panda/cms/menu_component.rb +7 -6
- data/app/components/panda/cms/page_menu_component.rb +15 -17
- data/app/components/panda/cms/rich_text_component.rb +5 -6
- data/app/components/panda/cms/text_component.rb +6 -7
- data/app/constraints/panda/cms/admin_constraint.rb +4 -1
- data/app/controllers/panda/cms/admin/dashboard_controller.rb +13 -9
- data/app/controllers/panda/cms/admin/forms_controller.rb +0 -2
- data/app/controllers/panda/cms/admin/my_profile_controller.rb +2 -1
- data/app/controllers/panda/cms/admin/pages_controller.rb +2 -1
- data/app/controllers/panda/cms/admin/posts_controller.rb +1 -2
- data/app/controllers/panda/cms/admin/sessions_controller.rb +3 -5
- data/app/controllers/panda/cms/admin/settings/bulk_editor_controller.rb +32 -25
- data/app/controllers/panda/cms/admin/settings_controller.rb +14 -10
- data/app/controllers/panda/cms/application_controller.rb +7 -2
- data/app/controllers/panda/cms/errors_controller.rb +5 -2
- data/app/controllers/panda/cms/form_submissions_controller.rb +2 -0
- data/app/controllers/panda/cms/pages_controller.rb +32 -29
- data/app/controllers/panda/cms/posts_controller.rb +2 -0
- data/app/helpers/panda/cms/admin/files_helper.rb +5 -1
- data/app/helpers/panda/cms/admin/pages_helper.rb +5 -1
- data/app/helpers/panda/cms/asset_helper.rb +182 -0
- data/app/helpers/panda/cms/pages_helper.rb +2 -0
- data/app/helpers/panda/cms/posts_helper.rb +2 -0
- data/app/helpers/panda/cms/theme_helper.rb +2 -0
- data/app/javascript/panda/cms/controllers/editor_form_controller.js +59 -6
- data/app/javascript/panda/cms/controllers/index.js +3 -9
- data/app/javascript/panda/cms/controllers/theme_form_controller.js +16 -0
- data/app/javascript/panda/cms/stimulus-loading.js +39 -0
- data/app/javascript/panda_cms/stimulus-loading.js +39 -0
- data/app/jobs/panda/cms/application_job.rb +2 -0
- data/app/jobs/panda/cms/record_visit_job.rb +2 -0
- data/app/mailers/panda/cms/application_mailer.rb +2 -0
- data/app/mailers/panda/cms/form_mailer.rb +3 -1
- data/app/models/panda/cms/application_record.rb +2 -0
- data/app/models/panda/cms/block.rb +4 -1
- data/app/models/panda/cms/block_content.rb +2 -0
- data/app/models/panda/cms/breadcrumb.rb +2 -0
- data/app/models/panda/cms/current.rb +2 -0
- data/app/models/panda/cms/form.rb +2 -0
- data/app/models/panda/cms/form_submission.rb +2 -0
- data/app/models/panda/cms/menu.rb +12 -9
- data/app/models/panda/cms/menu_item.rb +10 -6
- data/app/models/panda/cms/page.rb +14 -12
- data/app/models/panda/cms/post.rb +9 -5
- data/app/models/panda/cms/redirect.rb +6 -3
- data/app/models/panda/cms/template.rb +12 -7
- data/app/models/panda/cms/user.rb +2 -0
- data/app/models/panda/cms/visit.rb +2 -0
- data/app/models/panda/social/instagram_post.rb +2 -0
- data/app/services/panda/cms/html_to_editor_js_converter.rb +4 -2
- data/app/services/panda/social/instagram_feed_service.rb +3 -1
- data/app/views/layouts/different_page.html.erb +6 -0
- data/app/views/layouts/homepage.html.erb +37 -0
- data/app/views/layouts/page.html.erb +18 -0
- data/app/views/layouts/panda/cms/application.html.erb +1 -0
- data/app/views/panda/cms/admin/pages/new.html.erb +14 -8
- data/app/views/panda/cms/admin/settings/index.html.erb +1 -1
- data/app/views/panda/cms/shared/_header.html.erb +10 -2
- data/app/views/panda/cms/shared/_importmap.html.erb +1 -1
- data/app/views/shared/_footer.html.erb +3 -0
- data/app/views/shared/_header.html.erb +11 -0
- data/config/importmap.rb +2 -0
- data/config/initializers/inflections.rb +2 -0
- data/config/initializers/panda/cms/form_errors.rb +20 -21
- data/config/initializers/panda/cms/healthcheck_log_silencer.rb +2 -0
- data/config/initializers/panda/cms.rb +2 -0
- data/config/initializers/zeitwork.rb +2 -0
- data/config/puma/test.rb +3 -1
- data/config/routes.rb +8 -8
- data/db/migrate/20240205223709_create_panda_cms_pages.rb +2 -0
- data/db/migrate/20240219213327_create_panda_cms_page_versions.rb +2 -0
- data/db/migrate/20240303002805_create_panda_cms_templates.rb +4 -1
- data/db/migrate/20240303003434_create_panda_cms_template_versions.rb +2 -0
- data/db/migrate/20240303022441_create_panda_cms_blocks.rb +4 -1
- data/db/migrate/20240303024256_create_panda_cms_block_contents.rb +2 -0
- data/db/migrate/20240303024746_create_panda_cms_block_content_versions.rb +2 -0
- data/db/migrate/20240303233238_add_panda_cms_menu_table.rb +2 -0
- data/db/migrate/20240303234724_add_panda_cms_menu_item_table.rb +2 -0
- data/db/migrate/20240304134343_add_parent_id_to_panda_cms_pages.rb +2 -0
- data/db/migrate/20240315125411_add_status_to_panda_cms_pages.rb +7 -5
- data/db/migrate/20240315125421_add_nested_sets_to_panda_cms_pages.rb +2 -0
- data/db/migrate/20240316212822_add_kind_to_panda_cms_menus.rb +3 -1
- data/db/migrate/20240316221425_add_start_page_to_panda_cms_menus.rb +2 -0
- data/db/migrate/20240316230706_add_nested_to_panda_cms_menu_items.rb +2 -0
- data/db/migrate/20240317010532_create_panda_cms_users.rb +2 -0
- data/db/migrate/20240317161534_add_max_uses_to_panda_cms_template.rb +2 -0
- data/db/migrate/20240317163053_reset_counter_cache_on_panda_cms_template.rb +2 -0
- data/db/migrate/20240317214827_create_panda_cms_redirects.rb +2 -0
- data/db/migrate/20240317230622_create_panda_cms_visits.rb +2 -0
- data/db/migrate/20240324205703_create_active_storage_tables.active_storage.rb +5 -2
- data/db/migrate/20240408084718_default_panda_cms_users_admin_to_false.rb +2 -0
- data/db/migrate/20240701225422_add_service_name_to_active_storage_blobs.active_storage.rb +8 -6
- data/db/migrate/20240701225423_create_active_storage_variant_records.active_storage.rb +2 -0
- data/db/migrate/20240701225424_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb +2 -0
- data/db/migrate/20240804235210_create_panda_cms_forms.rb +2 -0
- data/db/migrate/20240805013612_create_panda_cms_form_submissions.rb +2 -0
- data/db/migrate/20240805121123_create_panda_cms_posts.rb +3 -1
- data/db/migrate/20240805123104_create_panda_cms_post_versions.rb +2 -0
- data/db/migrate/20240806112735_fix_panda_cms_visits_column_names.rb +2 -0
- data/db/migrate/20240806204412_add_completion_path_to_panda_cms_forms.rb +2 -0
- data/db/migrate/20240820081917_change_form_submissions_to_submission_count.rb +2 -0
- data/db/migrate/20240923234535_add_depth_to_panda_cms_menus.rb +6 -4
- data/db/migrate/20241031205109_add_cached_content_to_panda_cms_block_contents.rb +2 -0
- data/db/migrate/20241119214548_convert_post_content_to_editor_js.rb +2 -0
- data/db/migrate/20241120000419_remove_post_tag_references.rb +2 -0
- data/db/migrate/20241120110943_add_editor_js_to_posts.rb +2 -0
- data/db/migrate/20241120113859_add_cached_content_to_panda_cms_posts.rb +2 -0
- data/db/migrate/20241123234140_remove_post_tag_id_from_posts.rb +2 -0
- data/db/migrate/20250106223303_add_author_id_to_panda_cms_posts.rb +2 -0
- data/db/migrate/20250120235542_remove_paper_trail.rb +5 -4
- data/db/migrate/20250126234001_create_panda_social_instagram_posts.rb +2 -0
- data/db/migrate/20250504221812_add_current_theme_to_panda_cms_users.rb +2 -0
- data/db/seeds.rb +2 -0
- data/lib/generators/panda/cms/install_generator.rb +2 -0
- data/lib/panda/cms/asset_loader.rb +390 -0
- data/lib/panda/cms/bulk_editor.rb +7 -3
- data/lib/panda/cms/demo_site_generator.rb +2 -0
- data/lib/panda/cms/editor_js/blocks/alert.rb +2 -0
- data/lib/panda/cms/editor_js/blocks/base.rb +2 -0
- data/lib/panda/cms/editor_js/blocks/header.rb +2 -0
- data/lib/panda/cms/editor_js/blocks/image.rb +3 -0
- data/lib/panda/cms/editor_js/blocks/list.rb +2 -0
- data/lib/panda/cms/editor_js/blocks/paragraph.rb +3 -0
- data/lib/panda/cms/editor_js/blocks/quote.rb +3 -0
- data/lib/panda/cms/editor_js/blocks/table.rb +3 -1
- data/lib/panda/cms/editor_js/renderer.rb +3 -0
- data/lib/panda/cms/editor_js.rb +2 -0
- data/lib/panda/cms/editor_js_content.rb +47 -41
- data/lib/panda/cms/engine.rb +29 -32
- data/lib/panda/cms/exceptions_app.rb +2 -0
- data/lib/panda/cms/railtie.rb +2 -0
- data/lib/panda/cms/slug.rb +3 -1
- data/lib/panda-cms/version.rb +3 -1
- data/lib/panda-cms.rb +4 -2
- data/lib/tasks/assets.rake +547 -0
- data/lib/tasks/panda/cms/install.rake +2 -0
- data/lib/tasks/panda/social/instagram.rake +2 -0
- data/lib/tasks/panda_cms.rake +3 -30
- data/public/panda-cms-assets/manifest.json +20 -0
- data/public/panda-cms-assets/panda-cms-0.7.4.css +26 -0
- data/public/panda-cms-assets/panda-cms-0.7.4.js +150 -0
- 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,
|
9
|
-
|
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: [
|
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
|
-
|
47
|
-
|
48
|
-
|
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,
|
12
|
-
|
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
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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,
|
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:
|
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
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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(
|
105
|
-
year
|
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,
|
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:
|
13
|
-
validates :destination_path, format: {with:
|
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,
|
9
|
-
|
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:
|
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[
|
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,
|
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
|
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 HtmlToEditorJsConverter
|
@@ -54,7 +56,7 @@ module Panda
|
|
54
56
|
}
|
55
57
|
when "p"
|
56
58
|
text = process_inline_elements(child)
|
57
|
-
paragraphs = text.split(
|
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(
|
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,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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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">© <%= Date.current.year %>
|
21
|
+
<p class="text-sm">© <%= 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
|
-
|
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,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
|
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
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
23
|
-
|
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.
|
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
|
data/config/puma/test.rb
CHANGED
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",
|
37
|
-
|
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
|
|