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.
- checksums.yaml +4 -4
- data/README.md +40 -5
- data/Rakefile +2 -0
- data/app/assets/builds/panda.cms.css +2 -6
- data/app/assets/tailwind/application.css +178 -0
- data/app/assets/tailwind/tailwind.config.js +15 -0
- data/app/builders/panda/cms/form_builder.rb +27 -36
- data/app/components/panda/cms/admin/flash_message_component.html.erb +2 -2
- 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 +8 -21
- 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/block_contents_controller.rb +0 -1
- data/app/controllers/panda/cms/admin/dashboard_controller.rb +13 -9
- data/app/controllers/panda/cms/admin/forms_controller.rb +0 -3
- data/app/controllers/panda/cms/admin/my_profile_controller.rb +44 -0
- data/app/controllers/panda/cms/admin/pages_controller.rb +15 -4
- data/app/controllers/panda/cms/admin/posts_controller.rb +6 -22
- 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 +4 -0
- data/app/controllers/panda/cms/pages_controller.rb +40 -35
- 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 +5 -9
- data/app/javascript/panda/cms/controllers/slug_controller.js +64 -31
- data/app/javascript/panda/cms/controllers/theme_form_controller.js +25 -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 +14 -14
- 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 +3 -0
- data/app/models/panda/cms/block.rb +12 -17
- data/app/models/panda/cms/block_content.rb +7 -6
- 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 +31 -16
- data/app/models/panda/cms/post.rb +12 -10
- data/app/models/panda/cms/redirect.rb +9 -1
- data/app/models/panda/cms/template.rb +17 -13
- data/app/models/panda/cms/user.rb +2 -0
- data/app/models/panda/cms/visit.rb +3 -1
- data/app/models/panda/social/instagram_post.rb +17 -0
- data/app/services/panda/cms/html_to_editor_js_converter.rb +10 -15
- data/app/services/panda/social/instagram_feed_service.rb +63 -0
- 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/my_profile/edit.html.erb +35 -0
- data/app/views/panda/cms/admin/pages/index.html.erb +1 -1
- data/app/views/panda/cms/admin/pages/new.html.erb +14 -8
- data/app/views/panda/cms/admin/posts/_form.html.erb +10 -0
- data/app/views/panda/cms/admin/posts/edit.html.erb +3 -2
- data/app/views/panda/cms/admin/posts/index.html.erb +1 -1
- data/app/views/panda/cms/admin/settings/index.html.erb +3 -1
- data/app/views/panda/cms/admin/shared/_sidebar.html.erb +1 -1
- data/app/views/panda/cms/shared/_header.html.erb +14 -4
- data/app/views/panda/cms/shared/_importmap.html.erb +2 -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/locales/en.yml +5 -0
- data/config/puma/test.rb +3 -1
- data/config/routes.rb +11 -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 +56 -0
- data/db/migrate/20250126234001_create_panda_social_instagram_posts.rb +16 -0
- data/db/migrate/20250504221812_add_current_theme_to_panda_cms_users.rb +7 -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 +27 -4
- 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 +50 -23
- data/lib/panda/cms/engine.rb +36 -37
- 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 +17 -2
- data/lib/tasks/assets.rake +547 -0
- data/lib/tasks/panda/cms/install.rake +25 -0
- data/lib/tasks/panda/social/instagram.rake +20 -0
- data/lib/tasks/panda_cms.rake +3 -30
- data/public/panda-cms-assets/editor-js/core/editorjs.min.js +83 -0
- data/public/panda-cms-assets/editor-js/plugins/embed.min.js +2 -0
- data/public/panda-cms-assets/editor-js/plugins/header.min.js +9 -0
- data/public/panda-cms-assets/editor-js/plugins/nested-list.min.js +2 -0
- data/public/panda-cms-assets/editor-js/plugins/paragraph.min.js +9 -0
- data/public/panda-cms-assets/editor-js/plugins/quote.min.js +2 -0
- data/public/panda-cms-assets/editor-js/plugins/simple-image.min.js +2 -0
- data/public/panda-cms-assets/editor-js/plugins/table.min.js +2 -0
- 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 +71 -438
- data/app/models/action_text/rich_text_version.rb +0 -6
- data/app/models/panda/cms/block_content_version.rb +0 -8
- data/app/models/panda/cms/page_version.rb +0 -8
- data/app/models/panda/cms/post_version.rb +0 -8
- data/app/models/panda/cms/template_version.rb +0 -8
- data/app/models/panda/cms/version.rb +0 -8
- data/config/initializers/panda/cms/paper_trail.rb +0 -7
- data/db/migrate/20240904200605_create_action_text_tables.action_text.rb +0 -24
- data/db/migrate/20241119214549_remove_action_text_from_posts.rb +0 -9
@@ -1,33 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Panda
|
2
4
|
module CMS
|
3
5
|
class Block < ApplicationRecord
|
4
6
|
self.table_name = "panda_cms_blocks"
|
5
7
|
|
6
|
-
belongs_to :template, foreign_key: :panda_cms_template_id, class_name: "Panda::CMS::Template"
|
7
|
-
has_many :block_contents, foreign_key: :panda_cms_block_id, class_name: "Panda::CMS::BlockContent",
|
8
|
+
belongs_to :template, foreign_key: :panda_cms_template_id, class_name: "Panda::CMS::Template"
|
9
|
+
has_many :block_contents, foreign_key: :panda_cms_block_id, class_name: "Panda::CMS::BlockContent",
|
10
|
+
dependent: :destroy
|
8
11
|
|
9
|
-
validates :kind, presence: true
|
10
12
|
validates :name, presence: true
|
11
13
|
validates :key, presence: true, uniqueness: {scope: :panda_cms_template_id, case_sensitive: false}
|
14
|
+
validates :kind, presence: true
|
12
15
|
|
13
|
-
# Validation for presence on template intentionally skipped to allow global elements
|
14
|
-
|
15
|
-
# NB: Commented out values are not yet implemented
|
16
16
|
enum :kind, {
|
17
17
|
plain_text: "plain_text",
|
18
18
|
rich_text: "rich_text",
|
19
|
+
image: "image",
|
20
|
+
video: "video",
|
21
|
+
audio: "audio",
|
22
|
+
file: "file",
|
23
|
+
code: "code",
|
19
24
|
iframe: "iframe",
|
20
|
-
|
21
|
-
code: "code"
|
22
|
-
# image: "image",
|
23
|
-
# video: "video",
|
24
|
-
# audio: "audio",
|
25
|
-
# file: "file",
|
26
|
-
# iframe: "iframe",
|
27
|
-
# quote: "quote",
|
28
|
-
# list: "list"
|
29
|
-
# table: "table",
|
30
|
-
# form: "form"
|
25
|
+
quote: "quote"
|
31
26
|
}
|
32
27
|
end
|
33
28
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Panda
|
2
4
|
module CMS
|
3
5
|
class BlockContent < ApplicationRecord
|
@@ -5,14 +7,13 @@ module Panda
|
|
5
7
|
|
6
8
|
self.table_name = "panda_cms_block_contents"
|
7
9
|
|
8
|
-
|
9
|
-
|
10
|
-
}
|
11
|
-
|
12
|
-
belongs_to :page, foreign_key: :panda_cms_page_id, class_name: "Panda::CMS::Page", inverse_of: :block_contents, optional: true, touch: true
|
13
|
-
belongs_to :block, foreign_key: :panda_cms_block_id, class_name: "Panda::CMS::Block", inverse_of: :block_contents, optional: false
|
10
|
+
belongs_to :page, foreign_key: :panda_cms_page_id, class_name: "Panda::CMS::Page", touch: true
|
11
|
+
belongs_to :block, foreign_key: :panda_cms_block_id, class_name: "Panda::CMS::Block"
|
14
12
|
|
15
13
|
validates :block, presence: true, uniqueness: {scope: :page}
|
14
|
+
|
15
|
+
store_accessor :content, [], prefix: true
|
16
|
+
store_accessor :cached_content, [], prefix: true
|
16
17
|
end
|
17
18
|
end
|
18
19
|
end
|
@@ -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
|
@@ -7,15 +9,10 @@ module Panda
|
|
7
9
|
self.table_name = "panda_cms_pages"
|
8
10
|
self.implicit_order_column = "lft"
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
after_save :after_save
|
15
|
-
|
16
|
-
belongs_to :template, foreign_key: :panda_cms_template_id, class_name: "Panda::CMS::Template", inverse_of: :pages, optional: false, counter_cache: :pages_count
|
17
|
-
has_many :blocks, through: :template
|
18
|
-
has_many :block_contents, foreign_key: :panda_cms_page_id, class_name: "Panda::CMS::BlockContent", inverse_of: :page
|
12
|
+
belongs_to :template, class_name: "Panda::CMS::Template", foreign_key: :panda_cms_template_id
|
13
|
+
has_many :block_contents, class_name: "Panda::CMS::BlockContent", foreign_key: :panda_cms_page_id,
|
14
|
+
dependent: :destroy
|
15
|
+
has_many :blocks, through: :block_contents
|
19
16
|
has_many :menu_items, foreign_key: :panda_cms_page_id, class_name: "Panda::CMS::MenuItem", inverse_of: :page
|
20
17
|
has_many :menus, through: :menu_items
|
21
18
|
has_many :menus_of_parent, through: :parent, source: :menus
|
@@ -25,8 +22,9 @@ module Panda
|
|
25
22
|
|
26
23
|
validates :path,
|
27
24
|
presence: true,
|
28
|
-
|
29
|
-
|
25
|
+
format: {with: %r{\A/.*\z}, message: "must start with a forward slash"}
|
26
|
+
|
27
|
+
validate :validate_unique_path_in_scope
|
30
28
|
|
31
29
|
validates :parent,
|
32
30
|
presence: true,
|
@@ -44,6 +42,9 @@ module Panda
|
|
44
42
|
archived: "archived"
|
45
43
|
}
|
46
44
|
|
45
|
+
# Callbacks
|
46
|
+
after_save :handle_after_save
|
47
|
+
|
47
48
|
#
|
48
49
|
# Update any menus which include this page or its parent as a menu item
|
49
50
|
#
|
@@ -57,13 +58,27 @@ module Panda
|
|
57
58
|
|
58
59
|
private
|
59
60
|
|
61
|
+
def validate_unique_path_in_scope
|
62
|
+
# Skip validation if path is not present (other validations will catch this)
|
63
|
+
return if path.blank?
|
64
|
+
|
65
|
+
# Find any other pages with the same path
|
66
|
+
other_page = self.class.where(path: path).where.not(id: id).first
|
67
|
+
|
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")
|
73
|
+
end
|
74
|
+
|
60
75
|
#
|
61
76
|
# After save callbacks
|
62
77
|
#
|
63
78
|
# @return nil
|
64
79
|
# @visibility private
|
65
80
|
#
|
66
|
-
def
|
81
|
+
def handle_after_save
|
67
82
|
generate_content_blocks
|
68
83
|
update_existing_menu_items
|
69
84
|
update_auto_menus
|
@@ -75,10 +90,10 @@ module Panda
|
|
75
90
|
page_existing_block_ids = block_contents.map { |bc| bc.block.id }
|
76
91
|
required_block_ids = template_block_ids - page_existing_block_ids
|
77
92
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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: "")
|
82
97
|
end
|
83
98
|
end
|
84
99
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "awesome_nested_set"
|
2
4
|
|
3
5
|
module Panda
|
@@ -10,12 +12,10 @@ module Panda
|
|
10
12
|
|
11
13
|
self.table_name = "panda_cms_posts"
|
12
14
|
|
13
|
-
has_paper_trail versions: {
|
14
|
-
class_name: "Panda::CMS::PostVersion"
|
15
|
-
}
|
16
|
-
|
17
15
|
belongs_to :user, class_name: "Panda::CMS::User"
|
18
|
-
belongs_to :author, class_name: "Panda::CMS::User"
|
16
|
+
belongs_to :author, class_name: "Panda::CMS::User", optional: true
|
17
|
+
has_many :block_contents, as: :blockable, dependent: :destroy
|
18
|
+
has_many :blocks, through: :block_contents
|
19
19
|
|
20
20
|
validates :title, presence: true
|
21
21
|
validates :slug,
|
@@ -49,11 +49,13 @@ module Panda
|
|
49
49
|
|
50
50
|
def year
|
51
51
|
return nil unless slug.match?(%r{\A/\d{4}/})
|
52
|
+
|
52
53
|
slug.split("/")[1]
|
53
54
|
end
|
54
55
|
|
55
56
|
def month
|
56
57
|
return nil unless slug.match?(%r{\A/\d{4}/\d{2}/})
|
58
|
+
|
57
59
|
slug.split("/")[2]
|
58
60
|
end
|
59
61
|
|
@@ -98,13 +100,13 @@ module Panda
|
|
98
100
|
self.slug = CGI.unescape(slug.strip.gsub(%r{^/+|/+$}, ""))
|
99
101
|
|
100
102
|
# Handle the case where we already have a properly formatted slug
|
101
|
-
if slug.match?(%r{\A\d{4}/\d{2}/[^/]+\z})
|
102
|
-
return self.slug = "/#{slug}"
|
103
|
-
end
|
103
|
+
return self.slug = "/#{slug}" if slug.match?(%r{\A\d{4}/\d{2}/[^/]+\z})
|
104
104
|
|
105
105
|
# Handle the case where we have a date-prefixed slug (from JS)
|
106
|
-
if (match = slug.match(
|
107
|
-
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]
|
108
110
|
return self.slug = "/#{year}/#{month}/#{base_slug}"
|
109
111
|
end
|
110
112
|
|
@@ -1,11 +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
|
12
|
+
validates :origin_path, presence: true
|
13
|
+
validates :destination_path, presence: true
|
14
|
+
|
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"}
|
9
17
|
end
|
10
18
|
end
|
11
19
|
end
|
@@ -1,17 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Panda
|
2
4
|
module CMS
|
3
5
|
# Represents a template in the Panda CMS application.
|
4
6
|
class Template < ApplicationRecord
|
5
7
|
self.table_name = "panda_cms_templates"
|
6
8
|
|
7
|
-
# Enables versioning for the Template model using the `has_paper_trail` gem.
|
8
|
-
has_paper_trail versions: {
|
9
|
-
class_name: "Panda::CMS::TemplateVersion"
|
10
|
-
}
|
11
|
-
|
12
9
|
# Associations
|
13
|
-
has_many :pages, class_name: "Panda::CMS::Page", dependent: :restrict_with_error, inverse_of: :template,
|
14
|
-
|
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
|
15
14
|
has_many :block_contents, through: :blocks
|
16
15
|
|
17
16
|
# Validations
|
@@ -20,19 +19,23 @@ module Panda
|
|
20
19
|
validates :file_path,
|
21
20
|
presence: true,
|
22
21
|
uniqueness: true,
|
23
|
-
format: {with:
|
22
|
+
format: {with: %r{\Alayouts/.*\z}, message: "must be a valid layout file path"}
|
24
23
|
|
25
24
|
validate :validate_template_file_exists
|
26
25
|
|
27
26
|
# Scopes
|
28
|
-
scope :available,
|
29
|
-
where("max_uses IS NULL OR (
|
27
|
+
scope :available, lambda {
|
28
|
+
where("max_uses IS NULL OR (max_uses > 0 AND pages_count < max_uses)")
|
30
29
|
}
|
31
30
|
|
32
31
|
def self.default
|
33
32
|
find_by(file_path: "layouts/page")
|
34
33
|
end
|
35
34
|
|
35
|
+
def self.reset_counter_cache
|
36
|
+
find_each { |template| template.update_column(:pages_count, template.pages.count) }
|
37
|
+
end
|
38
|
+
|
36
39
|
# Generate missing blocks for all templates
|
37
40
|
# @return [void]
|
38
41
|
def self.generate_missing_blocks
|
@@ -44,7 +47,7 @@ module Panda
|
|
44
47
|
# Matches:
|
45
48
|
# Panda::CMS::RichTextComponent.new(key: :value)
|
46
49
|
# Panda::CMS::RichTextComponent.new key: :value, key: value
|
47
|
-
line.match(/Panda::CMS::([a-zA-Z]+)Component\.new[
|
50
|
+
line.match(/Panda::CMS::([a-zA-Z]+)Component\.new[ (]+([^)]+)\)*/) do |match|
|
48
51
|
# Extract the hash values
|
49
52
|
template_path = file.gsub("app/views/", "").gsub(".html.erb", "")
|
50
53
|
template_name = template_path.gsub("layouts/", "").titleize
|
@@ -69,7 +72,8 @@ module Panda
|
|
69
72
|
# Create the block if it doesn't exist
|
70
73
|
# TODO: +/- the output if it's created or removed
|
71
74
|
begin
|
72
|
-
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|
|
73
77
|
block.name = block_name.titleize
|
74
78
|
end
|
75
79
|
rescue ActiveRecord::RecordInvalid => e
|
@@ -109,7 +113,7 @@ module Panda
|
|
109
113
|
# Extract the file path from the Rails root
|
110
114
|
file_path = file.to_s.sub("#{Rails.root}/app/views/", "").sub(".html.erb", "")
|
111
115
|
|
112
|
-
next if
|
116
|
+
next if ["layouts/application", "layouts/mailer"].include?(file_path)
|
113
117
|
|
114
118
|
# Find or create the template based on the file path
|
115
119
|
find_or_create_by(file_path: file_path) do |t|
|
@@ -1,7 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Panda
|
2
4
|
module CMS
|
3
5
|
class Visit < ApplicationRecord
|
4
|
-
belongs_to :page, class_name: "Panda::CMS::Page", foreign_key: :
|
6
|
+
belongs_to :page, class_name: "Panda::CMS::Page", foreign_key: :panda_cms_page_id, optional: true
|
5
7
|
belongs_to :user, class_name: "Panda::CMS::User", foreign_key: :user_id, optional: true
|
6
8
|
belongs_to :redirect, class_name: "Panda::CMS::Redirect", foreign_key: :redirect_id, optional: true
|
7
9
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Panda
|
4
|
+
module Social
|
5
|
+
class InstagramPost < ApplicationRecord
|
6
|
+
self.table_name = "panda_social_instagram_posts"
|
7
|
+
|
8
|
+
has_one_attached :image
|
9
|
+
|
10
|
+
validates :instagram_id, presence: true, uniqueness: true
|
11
|
+
validates :caption, presence: true
|
12
|
+
validates :posted_at, presence: true
|
13
|
+
|
14
|
+
scope :ordered, -> { order(posted_at: :desc) }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -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
|
@@ -131,26 +133,19 @@ module Panda
|
|
131
133
|
end
|
132
134
|
end
|
133
135
|
|
134
|
-
# Add any remaining text
|
135
|
-
if current_text.present?
|
136
|
-
# Split any remaining text on double line breaks
|
137
|
-
paragraphs = current_text.split(/\n\n+/).map(&:strip)
|
138
|
-
paragraphs.each do |paragraph|
|
139
|
-
blocks << create_paragraph_block(paragraph) if paragraph.present?
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
raise ConversionError, "No valid content blocks found" if blocks.empty?
|
136
|
+
# Add any remaining text
|
137
|
+
blocks << create_paragraph_block(current_text) if current_text.present?
|
144
138
|
|
139
|
+
# Return the complete EditorJS structure
|
145
140
|
{
|
146
141
|
"time" => Time.current.to_i * 1000,
|
147
142
|
"blocks" => blocks,
|
148
143
|
"version" => "2.28.2"
|
149
144
|
}
|
150
|
-
rescue Nokogiri::SyntaxError => e
|
151
|
-
raise ConversionError, "Invalid HTML syntax: #{e.message}"
|
152
145
|
rescue => e
|
153
|
-
|
146
|
+
Rails.logger.error "HTML to EditorJS conversion failed: #{e.message}"
|
147
|
+
Rails.logger.error e.backtrace.join("\n")
|
148
|
+
raise ConversionError, "Failed to convert HTML to EditorJS format: #{e.message}"
|
154
149
|
end
|
155
150
|
end
|
156
151
|
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "http"
|
4
|
+
require "down"
|
5
|
+
|
6
|
+
module Panda
|
7
|
+
module Social
|
8
|
+
class InstagramFeedService
|
9
|
+
GRAPH_API_VERSION = "v19.0"
|
10
|
+
GRAPH_API_BASE_URL = "https://graph.instagram.com/#{GRAPH_API_VERSION}".freeze
|
11
|
+
|
12
|
+
def initialize(access_token)
|
13
|
+
@access_token = access_token
|
14
|
+
end
|
15
|
+
|
16
|
+
def sync_recent_posts
|
17
|
+
fetch_media.each do |post_data|
|
18
|
+
process_post(post_data)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def fetch_media
|
25
|
+
response = HTTP.get("#{GRAPH_API_BASE_URL}/me/media", params: {
|
26
|
+
access_token: @access_token,
|
27
|
+
fields: "id,caption,media_type,media_url,permalink,timestamp"
|
28
|
+
})
|
29
|
+
|
30
|
+
return [] unless response.status.success?
|
31
|
+
|
32
|
+
JSON.parse(response.body.to_s)["data"]
|
33
|
+
end
|
34
|
+
|
35
|
+
def process_post(post_data)
|
36
|
+
return unless post_data["media_type"] == "IMAGE"
|
37
|
+
|
38
|
+
instagram_post = InstagramPost.find_or_initialize_by(instagram_id: post_data["id"])
|
39
|
+
|
40
|
+
instagram_post.assign_attributes(
|
41
|
+
caption: post_data["caption"],
|
42
|
+
posted_at: Time.zone.parse(post_data["timestamp"]),
|
43
|
+
permalink: post_data["permalink"]
|
44
|
+
)
|
45
|
+
|
46
|
+
if instagram_post.new_record? || instagram_post.changed?
|
47
|
+
# Download and attach image
|
48
|
+
tempfile = Down.download(post_data["media_url"])
|
49
|
+
instagram_post.image.attach(
|
50
|
+
io: tempfile,
|
51
|
+
filename: File.basename(post_data["media_url"])
|
52
|
+
)
|
53
|
+
|
54
|
+
instagram_post.save!
|
55
|
+
end
|
56
|
+
rescue Down::Error => e
|
57
|
+
Rails.logger.error "Failed to download Instagram image: #{e.message}"
|
58
|
+
rescue => e
|
59
|
+
Rails.logger.error "Error processing Instagram post #{post_data["id"]}: #{e.message}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -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 %>
|