panda-cms 0.8.2 → 0.10.2
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 +75 -5
- data/app/components/panda/cms/code_component.rb +154 -39
- data/app/components/panda/cms/grid_component.rb +26 -6
- data/app/components/panda/cms/menu_component.rb +72 -34
- data/app/components/panda/cms/page_menu_component.rb +102 -13
- data/app/components/panda/cms/rich_text_component.rb +229 -139
- data/app/components/panda/cms/text_component.rb +107 -42
- data/app/controllers/panda/cms/admin/base_controller.rb +19 -3
- data/app/controllers/panda/cms/admin/dashboard_controller.rb +3 -3
- data/app/controllers/panda/cms/admin/files_controller.rb +7 -0
- data/app/controllers/panda/cms/admin/menus_controller.rb +47 -3
- data/app/controllers/panda/cms/admin/pages_controller.rb +11 -2
- data/app/controllers/panda/cms/admin/posts_controller.rb +3 -1
- data/app/controllers/panda/cms/form_submissions_controller.rb +134 -11
- data/app/controllers/panda/cms/pages_controller.rb +7 -2
- data/app/controllers/panda/cms/posts_controller.rb +16 -0
- data/app/helpers/panda/cms/application_helper.rb +17 -4
- data/app/helpers/panda/cms/asset_helper.rb +14 -61
- data/app/helpers/panda/cms/forms_helper.rb +60 -0
- data/app/helpers/panda/cms/seo_helper.rb +85 -0
- data/app/javascript/panda/cms/{application_panda_cms.js → application.js} +5 -1
- data/app/javascript/panda/cms/controllers/code_editor_controller.js +95 -0
- data/app/javascript/panda/cms/controllers/editor_iframe_controller.js +31 -4
- data/app/javascript/panda/cms/controllers/file_gallery_controller.js +128 -0
- data/app/javascript/panda/cms/controllers/file_upload_controller.js +165 -0
- data/app/javascript/panda/cms/controllers/index.js +54 -13
- data/app/javascript/panda/cms/controllers/inline_code_editor_controller.js +96 -0
- data/app/javascript/panda/cms/controllers/menu_form_controller.js +53 -0
- data/app/javascript/panda/cms/controllers/nested_form_controller.js +35 -0
- data/app/javascript/panda/cms/controllers/page_form_controller.js +454 -0
- data/app/javascript/panda/cms/controllers/tree_controller.js +214 -0
- data/app/javascript/panda/cms/stimulus-loading.js +6 -7
- data/app/models/panda/cms/block_content.rb +9 -0
- data/app/models/panda/cms/menu.rb +12 -0
- data/app/models/panda/cms/page.rb +147 -0
- data/app/models/panda/cms/post.rb +98 -0
- data/app/views/layouts/homepage.html.erb +1 -4
- data/app/views/layouts/page.html.erb +1 -4
- data/app/views/panda/cms/admin/dashboard/show.html.erb +5 -5
- data/app/views/panda/cms/admin/files/_file_details.html.erb +45 -0
- data/app/views/panda/cms/admin/files/index.html.erb +11 -118
- data/app/views/panda/cms/admin/forms/index.html.erb +2 -2
- data/app/views/panda/cms/admin/forms/new.html.erb +1 -2
- data/app/views/panda/cms/admin/forms/show.html.erb +15 -30
- data/app/views/panda/cms/admin/menus/_menu_item_fields.html.erb +11 -0
- data/app/views/panda/cms/admin/menus/edit.html.erb +62 -0
- data/app/views/panda/cms/admin/menus/index.html.erb +3 -2
- data/app/views/panda/cms/admin/menus/new.html.erb +38 -0
- data/app/views/panda/cms/admin/pages/edit.html.erb +147 -22
- data/app/views/panda/cms/admin/pages/index.html.erb +49 -11
- data/app/views/panda/cms/admin/pages/new.html.erb +3 -11
- data/app/views/panda/cms/admin/posts/_form.html.erb +44 -15
- data/app/views/panda/cms/admin/posts/edit.html.erb +2 -2
- data/app/views/panda/cms/admin/posts/index.html.erb +6 -6
- data/app/views/panda/cms/admin/posts/new.html.erb +1 -1
- data/app/views/panda/cms/admin/settings/bulk_editor/new.html.erb +1 -1
- data/app/views/panda/cms/admin/settings/index.html.erb +3 -3
- data/app/views/shared/_header.html.erb +1 -4
- data/config/brakeman.ignore +38 -0
- data/config/importmap.rb +10 -10
- data/config/initializers/panda/cms/healthcheck_log_silencer.rb.disabled +31 -0
- data/config/initializers/panda/cms.rb +52 -10
- data/config/locales/en.yml +41 -0
- data/config/routes.rb +5 -3
- data/db/migrate/20240305000000_convert_html_content_to_editor_js.rb +2 -2
- data/db/migrate/20240315125421_add_nested_sets_to_panda_cms_pages.rb +6 -1
- data/db/migrate/20250809231125_migrate_users_to_panda_core.rb +23 -21
- data/db/migrate/20251104150640_add_cached_last_updated_at_to_panda_cms_pages.rb +22 -0
- data/db/migrate/20251104172242_add_page_type_to_panda_cms_pages.rb +6 -0
- data/db/migrate/20251104172638_set_page_types_for_existing_pages.rb +27 -0
- data/db/migrate/20251105000001_add_pending_review_status_to_pages_and_posts.panda_cms.rb +21 -0
- data/db/migrate/20251109131150_add_seo_fields_to_pages.rb +32 -0
- data/db/migrate/20251109131205_add_seo_fields_to_posts.rb +27 -0
- data/db/migrate/20251110114258_add_spam_tracking_to_form_submissions.rb +7 -0
- data/db/migrate/20251110122812_add_performance_indexes_to_pages_and_redirects.rb +13 -0
- data/lib/generators/panda/cms/install_generator.rb +2 -5
- data/lib/panda/cms/asset_loader.rb +46 -76
- data/lib/panda/cms/bulk_editor.rb +288 -12
- data/lib/panda/cms/debug.rb +29 -0
- data/lib/panda/cms/engine/asset_config.rb +49 -0
- data/lib/panda/cms/engine/autoload_config.rb +19 -0
- data/lib/panda/cms/engine/backtrace_config.rb +42 -0
- data/lib/panda/cms/engine/core_config.rb +106 -0
- data/lib/panda/cms/engine/helper_config.rb +20 -0
- data/lib/panda/cms/engine/route_config.rb +34 -0
- data/lib/panda/cms/engine/view_component_config.rb +31 -0
- data/lib/panda/cms/engine.rb +44 -162
- data/lib/panda/cms/features.rb +52 -0
- data/lib/panda/cms.rb +10 -0
- data/lib/panda-cms/version.rb +1 -1
- data/lib/panda-cms.rb +20 -7
- data/lib/tasks/panda_cms_tasks.rake +16 -0
- metadata +41 -50
- data/app/components/panda/cms/admin/container_component.html.erb +0 -13
- data/app/components/panda/cms/admin/flash_message_component.html.erb +0 -31
- data/app/components/panda/cms/admin/panel_component.html.erb +0 -7
- data/app/components/panda/cms/admin/slideover_component.html.erb +0 -9
- data/app/components/panda/cms/admin/slideover_component.rb +0 -15
- data/app/components/panda/cms/admin/statistics_component.html.erb +0 -4
- data/app/components/panda/cms/admin/statistics_component.rb +0 -16
- data/app/components/panda/cms/admin/tab_bar_component.html.erb +0 -35
- data/app/components/panda/cms/admin/tab_bar_component.rb +0 -15
- data/app/components/panda/cms/admin/table_component.html.erb +0 -29
- data/app/components/panda/cms/admin/user_activity_component.html.erb +0 -7
- data/app/components/panda/cms/admin/user_activity_component.rb +0 -20
- data/app/components/panda/cms/admin/user_display_component.html.erb +0 -17
- data/app/components/panda/cms/admin/user_display_component.rb +0 -21
- data/app/components/panda/cms/grid_component.html.erb +0 -6
- data/app/components/panda/cms/menu_component.html.erb +0 -6
- data/app/components/panda/cms/page_menu_component.html.erb +0 -21
- data/app/components/panda/cms/rich_text_component.html.erb +0 -90
- data/app/javascript/panda_cms/stimulus-loading.js +0 -39
- data/app/views/layouts/panda/cms/application.html.erb +0 -42
- data/app/views/panda/cms/admin/shared/_breadcrumbs.html.erb +0 -28
- data/app/views/panda/cms/admin/shared/_flash.html.erb +0 -5
- data/app/views/panda/cms/admin/shared/_sidebar.html.erb +0 -41
- data/app/views/panda/cms/shared/_footer.html.erb +0 -2
- data/app/views/panda/cms/shared/_header.html.erb +0 -25
- data/app/views/panda/cms/shared/_importmap.html.erb +0 -34
- data/config/initializers/inflections.rb +0 -5
- data/config/initializers/panda/cms/healthcheck_log_silencer.rb +0 -13
- data/lib/tasks/assets.rake +0 -587
|
@@ -2,162 +2,232 @@
|
|
|
2
2
|
|
|
3
3
|
module Panda
|
|
4
4
|
module CMS
|
|
5
|
-
#
|
|
6
|
-
# @param key [Symbol] The key to use for the text component
|
|
7
|
-
# @param text [String] The text to display
|
|
5
|
+
# Rich text component for EditorJS-based content editing
|
|
6
|
+
# @param key [Symbol] The key to use for the rich text component
|
|
7
|
+
# @param text [String] The default text to display
|
|
8
8
|
# @param editable [Boolean] If the text is editable or not (defaults to true)
|
|
9
|
-
|
|
10
|
-
class RichTextComponent < ViewComponent::Base
|
|
9
|
+
class RichTextComponent < Panda::Core::Base
|
|
11
10
|
class ComponentError < StandardError; end
|
|
12
11
|
|
|
13
12
|
KIND = "rich_text"
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
prop :key, Symbol, default: :text_component
|
|
15
|
+
prop :text, String, default: "Lorem ipsum..."
|
|
16
|
+
prop :editable, _Boolean, default: true
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
attr_accessor :content, :block_content_id
|
|
19
|
+
|
|
20
|
+
def view_template
|
|
21
|
+
# Russian doll caching: Cache component output at block_content level
|
|
22
|
+
# Only cache in non-editable mode (public-facing pages)
|
|
23
|
+
if should_cache?
|
|
24
|
+
raw cache_component_output
|
|
25
|
+
else
|
|
26
|
+
render_content
|
|
27
|
+
end
|
|
22
28
|
end
|
|
23
29
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
def render_content
|
|
31
|
+
div(class: "panda-cms-content", **element_attrs) do
|
|
32
|
+
if @editable_state
|
|
33
|
+
# Empty div for EditorJS to initialize into
|
|
34
|
+
else
|
|
35
|
+
raw(@rendered_content.html_safe)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
27
39
|
|
|
28
|
-
|
|
29
|
-
|
|
40
|
+
def before_template
|
|
41
|
+
setup_editability
|
|
42
|
+
load_block_content
|
|
43
|
+
prepare_content
|
|
44
|
+
rescue ActiveRecord::RecordNotFound => e
|
|
45
|
+
handle_error(ComponentError.new("Database record not found: #{e.message}"))
|
|
46
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
47
|
+
handle_error(ComponentError.new("Invalid record: #{e.message}"))
|
|
48
|
+
rescue => e
|
|
49
|
+
handle_error(e)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def setup_editability
|
|
55
|
+
@editable_state = @editable &&
|
|
56
|
+
view_context.params[:embed_id].present? &&
|
|
57
|
+
view_context.params[:embed_id] == Current.page.id &&
|
|
58
|
+
Current.user&.admin?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def load_block_content
|
|
62
|
+
block = Panda::CMS::Block.find_by(
|
|
63
|
+
kind: KIND,
|
|
64
|
+
key: @key,
|
|
65
|
+
panda_cms_template_id: Current.page.panda_cms_template_id
|
|
66
|
+
)
|
|
30
67
|
raise ComponentError, "Block not found for key: #{@key}" unless block
|
|
31
68
|
|
|
32
|
-
block_content = block.block_contents.find_by(panda_cms_page_id: Current.page.id)
|
|
33
|
-
|
|
34
|
-
|
|
69
|
+
@block_content = block.block_contents.find_by(panda_cms_page_id: Current.page.id)
|
|
70
|
+
|
|
71
|
+
if @block_content.nil?
|
|
72
|
+
@block_content = Panda::CMS::BlockContent.create!(
|
|
35
73
|
block: block,
|
|
36
74
|
panda_cms_page_id: Current.page.id,
|
|
37
75
|
content: empty_editor_js_content
|
|
38
76
|
)
|
|
39
77
|
end
|
|
40
78
|
|
|
41
|
-
|
|
79
|
+
@block_content_id = @block_content.id
|
|
80
|
+
raw_content = @block_content.cached_content || @block_content.content
|
|
42
81
|
@content = raw_content.presence || empty_editor_js_content
|
|
43
|
-
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def prepare_content
|
|
85
|
+
if @editable_state
|
|
86
|
+
prepare_editable_content
|
|
87
|
+
else
|
|
88
|
+
prepare_display_content
|
|
89
|
+
end
|
|
90
|
+
end
|
|
44
91
|
|
|
45
|
-
|
|
46
|
-
|
|
92
|
+
def prepare_editable_content
|
|
93
|
+
@editor_content = if @content.blank? || @content == "{}"
|
|
94
|
+
empty_editor_js_content
|
|
95
|
+
else
|
|
96
|
+
process_content_for_editor(@content)
|
|
97
|
+
end
|
|
47
98
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
99
|
+
@encoded_data = Base64.strict_encode64(@editor_content.to_json)
|
|
100
|
+
rescue => e
|
|
101
|
+
Rails.logger.error("Content processing error: #{e.message}\nContent: #{@content.inspect}")
|
|
102
|
+
@editor_content = empty_editor_js_content
|
|
103
|
+
@encoded_data = Base64.strict_encode64(@editor_content.to_json)
|
|
104
|
+
end
|
|
53
105
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
else
|
|
58
|
-
begin
|
|
59
|
-
if @content.is_a?(String)
|
|
60
|
-
# Try to parse as JSON first
|
|
61
|
-
begin
|
|
62
|
-
parsed = JSON.parse(@content)
|
|
63
|
-
if valid_editor_js_content?(parsed)
|
|
64
|
-
# Ensure the content is properly structured
|
|
65
|
-
{
|
|
66
|
-
"time" => parsed["time"] || Time.current.to_i * 1000,
|
|
67
|
-
"blocks" => parsed["blocks"].map do |block|
|
|
68
|
-
{
|
|
69
|
-
"type" => block["type"],
|
|
70
|
-
"data" => block["data"].merge(
|
|
71
|
-
"text" => block["data"]["text"].to_s.presence || ""
|
|
72
|
-
),
|
|
73
|
-
"tunes" => block["tunes"]
|
|
74
|
-
}.compact
|
|
75
|
-
end,
|
|
76
|
-
"version" => parsed["version"] || "2.28.2"
|
|
77
|
-
}
|
|
78
|
-
else
|
|
79
|
-
# If not valid EditorJS, try to convert from HTML
|
|
80
|
-
begin
|
|
81
|
-
editor_content = Panda::Editor::HtmlToEditorJsConverter.convert(@content)
|
|
82
|
-
if valid_editor_js_content?(editor_content)
|
|
83
|
-
editor_content
|
|
84
|
-
else
|
|
85
|
-
empty_editor_js_content
|
|
86
|
-
end
|
|
87
|
-
rescue Panda::Editor::HtmlToEditorJsConverter::ConversionError => e
|
|
88
|
-
Rails.logger.error("HTML conversion error: #{e.message}")
|
|
89
|
-
empty_editor_js_content
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
rescue JSON::ParserError => e
|
|
93
|
-
Rails.logger.error("JSON parse error: #{e.message}")
|
|
94
|
-
# Try to convert from HTML
|
|
95
|
-
begin
|
|
96
|
-
editor_content = Panda::Editor::HtmlToEditorJsConverter.convert(@content)
|
|
97
|
-
if valid_editor_js_content?(editor_content)
|
|
98
|
-
editor_content
|
|
99
|
-
else
|
|
100
|
-
empty_editor_js_content
|
|
101
|
-
end
|
|
102
|
-
rescue Panda::Editor::HtmlToEditorJsConverter::ConversionError => e
|
|
103
|
-
Rails.logger.error("HTML conversion error: #{e.message}")
|
|
104
|
-
empty_editor_js_content
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
else
|
|
108
|
-
# If it's not a string, assume it's already in the correct format
|
|
109
|
-
valid_editor_js_content?(@content) ? @content : empty_editor_js_content
|
|
110
|
-
end
|
|
111
|
-
rescue => e
|
|
112
|
-
Rails.logger.error("Content processing error: #{e.message}\nContent: #{@content.inspect}")
|
|
113
|
-
empty_editor_js_content
|
|
114
|
-
end
|
|
115
|
-
end
|
|
106
|
+
def prepare_display_content
|
|
107
|
+
@rendered_content = if @content.blank? || @content == "{}"
|
|
108
|
+
"<p></p>"
|
|
116
109
|
else
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
110
|
+
render_content_for_display(@content)
|
|
111
|
+
end
|
|
112
|
+
rescue => e
|
|
113
|
+
Rails.logger.error("RichTextComponent render error: #{e.message}\nContent: #{@content.inspect}")
|
|
114
|
+
@rendered_content = "<p></p>"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def process_content_for_editor(content)
|
|
118
|
+
parsed = if content.is_a?(String)
|
|
119
|
+
JSON.parse(content)
|
|
120
|
+
else
|
|
121
|
+
content
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
if valid_editor_js_content?(parsed)
|
|
125
|
+
normalize_editor_content(parsed)
|
|
126
|
+
else
|
|
127
|
+
convert_html_to_editor_js(content)
|
|
128
|
+
end
|
|
129
|
+
rescue JSON::ParserError
|
|
130
|
+
convert_html_to_editor_js(content)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def normalize_editor_content(parsed)
|
|
134
|
+
{
|
|
135
|
+
"time" => parsed["time"] || Time.current.to_i * 1000,
|
|
136
|
+
"blocks" => (parsed["blocks"] || []).map { |block| normalize_block(block) },
|
|
137
|
+
"version" => parsed["version"] || "2.28.2"
|
|
138
|
+
}
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def normalize_block(block)
|
|
142
|
+
case block["type"]
|
|
143
|
+
when "paragraph"
|
|
144
|
+
block.merge("data" => block["data"].merge("text" => block["data"]["text"].to_s.presence || ""))
|
|
145
|
+
when "header"
|
|
146
|
+
block.merge("data" => block["data"].merge(
|
|
147
|
+
"text" => block["data"]["text"].to_s.presence || "",
|
|
148
|
+
"level" => block["data"]["level"].to_i
|
|
149
|
+
))
|
|
150
|
+
when "list"
|
|
151
|
+
block.merge("data" => block["data"].merge(
|
|
152
|
+
"items" => (block["data"]["items"] || []).map { |item| item.to_s.presence || "" }
|
|
153
|
+
))
|
|
154
|
+
else
|
|
155
|
+
block
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def convert_html_to_editor_js(content)
|
|
160
|
+
editor_content = Panda::Editor::HtmlToEditorJsConverter.convert(content.to_s)
|
|
161
|
+
valid_editor_js_content?(editor_content) ? editor_content : empty_editor_js_content
|
|
162
|
+
rescue Panda::Editor::HtmlToEditorJsConverter::ConversionError => e
|
|
163
|
+
Rails.logger.error("HTML conversion error: #{e.message}")
|
|
164
|
+
empty_editor_js_content
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def render_content_for_display(content)
|
|
168
|
+
# Try to parse as JSON if it looks like EditorJS format
|
|
169
|
+
if content.is_a?(String) && content.strip.match?(/^\{.*"blocks":\s*\[.*\].*\}$/m)
|
|
170
|
+
parsed_content = JSON.parse(content)
|
|
171
|
+
if valid_editor_js_content?(parsed_content)
|
|
172
|
+
render_editor_js_content(parsed_content)
|
|
120
173
|
else
|
|
121
|
-
|
|
122
|
-
# Try to parse as JSON if it looks like EditorJS format
|
|
123
|
-
if @content.is_a?(String) && @content.strip.match?(/^\{.*"blocks":\s*\[.*\].*\}$/m)
|
|
124
|
-
parsed_content = JSON.parse(@content)
|
|
125
|
-
if valid_editor_js_content?(parsed_content)
|
|
126
|
-
# Check if it's just an empty paragraph
|
|
127
|
-
if parsed_content["blocks"].length == 1 &&
|
|
128
|
-
parsed_content["blocks"][0]["type"] == "paragraph" &&
|
|
129
|
-
parsed_content["blocks"][0]["data"]["text"].blank?
|
|
130
|
-
"<p></p>".html_safe
|
|
131
|
-
else
|
|
132
|
-
renderer = Panda::Editor::Renderer.new(parsed_content)
|
|
133
|
-
rendered = renderer.render
|
|
134
|
-
rendered.presence&.html_safe || "<p></p>".html_safe
|
|
135
|
-
end
|
|
136
|
-
else
|
|
137
|
-
process_html(@content)
|
|
138
|
-
end
|
|
139
|
-
else
|
|
140
|
-
process_html(@content)
|
|
141
|
-
end
|
|
142
|
-
rescue JSON::ParserError
|
|
143
|
-
process_html(@content)
|
|
144
|
-
rescue => e
|
|
145
|
-
Rails.logger.error("RichTextComponent render error: #{e.message}\nContent: #{@content.inspect}")
|
|
146
|
-
"<p></p>".html_safe
|
|
147
|
-
end
|
|
174
|
+
process_html_content(content)
|
|
148
175
|
end
|
|
176
|
+
else
|
|
177
|
+
process_html_content(content)
|
|
149
178
|
end
|
|
150
|
-
rescue
|
|
151
|
-
|
|
152
|
-
rescue ActiveRecord::RecordInvalid => e
|
|
153
|
-
raise ComponentError, "Invalid record: #{e.message}"
|
|
154
|
-
rescue => e
|
|
155
|
-
Rails.logger.error("RichTextComponent error: #{e.message}\nContent: #{@content.inspect}")
|
|
156
|
-
@content = @editable ? empty_editor_js_content : "<p></p>".html_safe
|
|
157
|
-
nil
|
|
179
|
+
rescue JSON::ParserError
|
|
180
|
+
process_html_content(content)
|
|
158
181
|
end
|
|
159
182
|
|
|
160
|
-
|
|
183
|
+
def render_editor_js_content(parsed_content)
|
|
184
|
+
# Check if it's just an empty paragraph
|
|
185
|
+
if parsed_content["blocks"].length == 1 &&
|
|
186
|
+
parsed_content["blocks"][0]["type"] == "paragraph" &&
|
|
187
|
+
parsed_content["blocks"][0]["data"]["text"].blank?
|
|
188
|
+
"<p></p>"
|
|
189
|
+
else
|
|
190
|
+
renderer = Panda::Editor::Renderer.new(parsed_content)
|
|
191
|
+
rendered = renderer.render
|
|
192
|
+
rendered.presence || "<p></p>"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def process_html_content(content)
|
|
197
|
+
return "<p></p>" if content.blank?
|
|
198
|
+
|
|
199
|
+
# If it's already HTML, return it
|
|
200
|
+
if content.match?(/<[^>]+>/)
|
|
201
|
+
content
|
|
202
|
+
else
|
|
203
|
+
# Wrap plain text in paragraph tags
|
|
204
|
+
"<p>#{content}</p>"
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def element_attrs
|
|
209
|
+
attrs = {class: "panda-cms-content"}
|
|
210
|
+
|
|
211
|
+
if @editable_state
|
|
212
|
+
attrs[:id] = "editor-#{@block_content_id}"
|
|
213
|
+
attrs[:data] = {
|
|
214
|
+
"editable-previous-data": @encoded_data,
|
|
215
|
+
"editable-content": @encoded_data,
|
|
216
|
+
"editable-initialized": "false",
|
|
217
|
+
"editable-version": "2.28.2",
|
|
218
|
+
"editable-autosave": "false",
|
|
219
|
+
"editable-tools": '{"paragraph":true,"header":true,"list":true,"quote":true,"table":true}',
|
|
220
|
+
"editable-kind": "rich_text",
|
|
221
|
+
"editable-block-content-id": @block_content_id,
|
|
222
|
+
"editable-page-id": Current.page.id,
|
|
223
|
+
controller: "editor-js",
|
|
224
|
+
"editor-js-initialized-value": "false",
|
|
225
|
+
"editor-js-content-value": @encoded_data
|
|
226
|
+
}
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
attrs
|
|
230
|
+
end
|
|
161
231
|
|
|
162
232
|
def empty_editor_js_content
|
|
163
233
|
{
|
|
@@ -173,21 +243,41 @@ module Panda
|
|
|
173
243
|
false
|
|
174
244
|
end
|
|
175
245
|
|
|
176
|
-
def
|
|
177
|
-
|
|
246
|
+
def handle_error(error)
|
|
247
|
+
Rails.logger.error("RichTextComponent error: #{error.message}\nContent: #{@content.inspect}")
|
|
178
248
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
249
|
+
if @editable_state
|
|
250
|
+
@editor_content = empty_editor_js_content
|
|
251
|
+
@encoded_data = Base64.strict_encode64(@editor_content.to_json)
|
|
182
252
|
else
|
|
183
|
-
|
|
184
|
-
"<p>#{content}</p>".html_safe
|
|
253
|
+
@rendered_content = "<p></p>"
|
|
185
254
|
end
|
|
255
|
+
|
|
256
|
+
nil
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def should_cache?
|
|
260
|
+
!@editable_state &&
|
|
261
|
+
Panda::CMS.config.performance.dig(:fragment_caching, :enabled) != false &&
|
|
262
|
+
@block_content.present?
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def cache_component_output
|
|
266
|
+
cache_key = cache_key_for_component
|
|
267
|
+
expires_in = Panda::CMS.config.performance.dig(:fragment_caching, :expires_in) || 1.hour
|
|
268
|
+
|
|
269
|
+
Rails.cache.fetch(cache_key, expires_in: expires_in) do
|
|
270
|
+
render_content_to_string
|
|
271
|
+
end.html_safe
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def cache_key_for_component
|
|
275
|
+
"panda_cms/rich_text_component/#{@block_content.cache_key_with_version}/#{@key}"
|
|
186
276
|
end
|
|
187
277
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
278
|
+
def render_content_to_string
|
|
279
|
+
# Render the component HTML to a string for caching
|
|
280
|
+
helpers.content_tag(:div, @rendered_content.html_safe, class: "panda-cms-content", **element_attrs)
|
|
191
281
|
end
|
|
192
282
|
end
|
|
193
283
|
end
|
|
@@ -2,69 +2,134 @@
|
|
|
2
2
|
|
|
3
3
|
module Panda
|
|
4
4
|
module CMS
|
|
5
|
-
# Text component
|
|
5
|
+
# Text component for editable plain text content
|
|
6
6
|
# @param key [Symbol] The key to use for the text component
|
|
7
|
-
# @param text [String] The text to display
|
|
7
|
+
# @param text [String] The default text to display
|
|
8
8
|
# @param editable [Boolean] If the text is editable or not (defaults to true)
|
|
9
|
-
|
|
10
|
-
class TextComponent < ViewComponent::Base
|
|
9
|
+
class TextComponent < Panda::Core::Base
|
|
11
10
|
KIND = "plain_text"
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
prop :key, Symbol, default: :text_component
|
|
13
|
+
prop :text, String, default: "Lorem ipsum..."
|
|
14
|
+
prop :editable, _Boolean, default: true
|
|
15
|
+
|
|
14
16
|
attr_accessor :plain_text
|
|
15
17
|
|
|
16
|
-
def
|
|
17
|
-
|
|
18
|
-
@text = text
|
|
19
|
-
@options = options || {}
|
|
20
|
-
@options[:id] ||= "text-#{key.to_s.dasherize}"
|
|
21
|
-
@editable = editable
|
|
22
|
-
end
|
|
18
|
+
def view_template
|
|
19
|
+
return unless @content
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
# Russian doll caching: Cache component output at block_content level
|
|
22
|
+
# Only cache in non-editable mode (public-facing pages)
|
|
23
|
+
if should_cache?
|
|
24
|
+
raw cache_component_output
|
|
25
|
+
else
|
|
26
|
+
render_content
|
|
29
27
|
end
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
rescue => e
|
|
29
|
+
handle_error(e)
|
|
32
30
|
end
|
|
33
31
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
#
|
|
37
|
-
# @usage Do not use this when rendering editable content
|
|
38
|
-
def prepare_content_for_display(content)
|
|
39
|
-
# Replace \n characters with <br> tags
|
|
40
|
-
content.gsub("\n", "<br>")
|
|
32
|
+
def before_template
|
|
33
|
+
prepare_content
|
|
41
34
|
end
|
|
42
35
|
|
|
43
|
-
|
|
44
|
-
# TODO: Check user permissions
|
|
45
|
-
def before_render
|
|
46
|
-
@editable &&= params[:embed_id].present? && params[:embed_id] == Current.page.id
|
|
36
|
+
private
|
|
47
37
|
|
|
48
|
-
|
|
49
|
-
|
|
38
|
+
def prepare_content
|
|
39
|
+
@editable_state = @editable && is_editable_context?
|
|
50
40
|
|
|
41
|
+
block = find_block
|
|
51
42
|
return false if block.nil?
|
|
52
43
|
|
|
53
|
-
|
|
54
|
-
plain_text =
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
@
|
|
44
|
+
find_block_content(block)
|
|
45
|
+
@plain_text = @block_content_obj&.content.to_s
|
|
46
|
+
|
|
47
|
+
if @editable_state
|
|
48
|
+
setup_editable_content(@block_content_obj)
|
|
49
|
+
else
|
|
50
|
+
@content = prepare_content_for_display(@plain_text)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def find_block
|
|
55
|
+
Panda::CMS::Block.find_by(
|
|
56
|
+
kind: KIND,
|
|
57
|
+
key: @key,
|
|
58
|
+
panda_cms_template_id: Current.page.panda_cms_template_id
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def find_block_content(block)
|
|
63
|
+
@block_content_obj = block.block_contents.find_by(panda_cms_page_id: Current.page.id)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def setup_editable_content(block_content)
|
|
67
|
+
@content = @plain_text
|
|
68
|
+
@block_content_id = block_content&.id
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def element_attrs
|
|
72
|
+
attrs = @attrs.merge(id: element_id)
|
|
73
|
+
|
|
74
|
+
if @editable_state
|
|
75
|
+
attrs[:contenteditable] = "plaintext-only"
|
|
76
|
+
attrs[:data] = {
|
|
58
77
|
"editable-kind": "plain_text",
|
|
59
78
|
"editable-page-id": Current.page.id,
|
|
60
|
-
"editable-block-content-id":
|
|
79
|
+
"editable-block-content-id": @block_content_id
|
|
61
80
|
}
|
|
81
|
+
end
|
|
62
82
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
83
|
+
attrs
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def element_id
|
|
87
|
+
@editable_state ? "editor-#{@block_content_id}" : "text-#{@key.to_s.dasherize}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def prepare_content_for_display(content)
|
|
91
|
+
# Replace \n characters with <br> tags
|
|
92
|
+
content.gsub("\n", "<br>")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def is_editable_context?
|
|
96
|
+
view_context.params[:embed_id].present? && view_context.params[:embed_id] == Current.page.id
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def handle_error(_error)
|
|
100
|
+
if !Rails.env.production? || defined?(Sentry)
|
|
101
|
+
raise Panda::CMS::MissingBlockError, "Block with key #{@key} not found for page #{Current.page.title}"
|
|
67
102
|
end
|
|
103
|
+
|
|
104
|
+
false
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def should_cache?
|
|
108
|
+
!@editable_state &&
|
|
109
|
+
Panda::CMS.config.performance.dig(:fragment_caching, :enabled) != false &&
|
|
110
|
+
@block_content_obj.present?
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def cache_component_output
|
|
114
|
+
cache_key = cache_key_for_component
|
|
115
|
+
expires_in = Panda::CMS.config.performance.dig(:fragment_caching, :expires_in) || 1.hour
|
|
116
|
+
|
|
117
|
+
Rails.cache.fetch(cache_key, expires_in: expires_in) do
|
|
118
|
+
render_content_to_string
|
|
119
|
+
end.html_safe
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def cache_key_for_component
|
|
123
|
+
"panda_cms/text_component/#{@block_content_obj.cache_key_with_version}/#{@key}"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def render_content
|
|
127
|
+
span(**element_attrs) { raw(@content.html_safe) }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def render_content_to_string
|
|
131
|
+
# Phlex doesn't have a direct way to capture output, so we render directly
|
|
132
|
+
helpers.content_tag(:span, @content.html_safe, **element_attrs)
|
|
68
133
|
end
|
|
69
134
|
end
|
|
70
135
|
end
|
|
@@ -4,11 +4,27 @@ module Panda
|
|
|
4
4
|
module CMS
|
|
5
5
|
module Admin
|
|
6
6
|
# Base controller for all CMS admin controllers
|
|
7
|
-
# Inherits from Core
|
|
8
|
-
#
|
|
9
|
-
class BaseController < ::Panda::Core::
|
|
7
|
+
# Inherits from Panda::Core::Admin::BaseController for authentication and base admin functionality
|
|
8
|
+
# Uses Core's admin layout with registered CMS navigation
|
|
9
|
+
class BaseController < ::Panda::Core::Admin::BaseController
|
|
10
|
+
# Override set_current_request_details to also set CMS-specific attributes
|
|
11
|
+
def set_current_request_details
|
|
12
|
+
super # Call Core's implementation first
|
|
13
|
+
|
|
14
|
+
# Set CMS current attributes (inherits from Core so has access to all Core attributes)
|
|
15
|
+
Panda::CMS::Current.request_id = request.uuid
|
|
16
|
+
Panda::CMS::Current.user_agent = request.user_agent
|
|
17
|
+
Panda::CMS::Current.ip_address = request.ip
|
|
18
|
+
Panda::CMS::Current.root = request.base_url
|
|
19
|
+
Panda::CMS::Current.user = Panda::Core::Current.user
|
|
20
|
+
Panda::CMS::Current.page = nil
|
|
21
|
+
|
|
22
|
+
Panda::CMS.config.url ||= Panda::Core::Current.root
|
|
23
|
+
end
|
|
24
|
+
|
|
10
25
|
# Include CMS helpers so views have access to panda_cms_form_with, etc.
|
|
11
26
|
helper Panda::CMS::ApplicationHelper
|
|
27
|
+
helper Panda::CMS::AssetHelper if defined?(Panda::CMS::AssetHelper)
|
|
12
28
|
|
|
13
29
|
# Include the helper methods in the controller as well
|
|
14
30
|
include Panda::CMS::ApplicationHelper
|
|
@@ -5,13 +5,13 @@ require "groupdate"
|
|
|
5
5
|
module Panda
|
|
6
6
|
module CMS
|
|
7
7
|
module Admin
|
|
8
|
-
class DashboardController <
|
|
8
|
+
class DashboardController < BaseController
|
|
9
9
|
before_action :set_initial_breadcrumb, only: %i[show]
|
|
10
10
|
|
|
11
|
-
#
|
|
11
|
+
# CMS-specific dashboard
|
|
12
12
|
def show
|
|
13
13
|
# Render the CMS dashboard view
|
|
14
|
-
render
|
|
14
|
+
render :show
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
private
|
|
@@ -4,6 +4,11 @@ module Panda
|
|
|
4
4
|
module CMS
|
|
5
5
|
module Admin
|
|
6
6
|
class FilesController < ::Panda::CMS::Admin::BaseController
|
|
7
|
+
def index
|
|
8
|
+
@files = ActiveStorage::Blob.order(created_at: :desc)
|
|
9
|
+
@selected_file = @files.first if @files.any?
|
|
10
|
+
end
|
|
11
|
+
|
|
7
12
|
def create
|
|
8
13
|
file = params[:image]
|
|
9
14
|
return render json: {success: 0} unless file
|
|
@@ -23,6 +28,8 @@ module Panda
|
|
|
23
28
|
}
|
|
24
29
|
}
|
|
25
30
|
end
|
|
31
|
+
|
|
32
|
+
private
|
|
26
33
|
end
|
|
27
34
|
end
|
|
28
35
|
end
|