panda-cms 0.8.2 → 0.10.0
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 +83 -4
- data/app/components/panda/cms/code_component.rb +117 -39
- data/app/components/panda/cms/grid_component.rb +26 -6
- data/app/components/panda/cms/menu_component.rb +66 -34
- data/app/components/panda/cms/page_menu_component.rb +94 -13
- data/app/components/panda/cms/rich_text_component.rb +198 -140
- data/app/components/panda/cms/text_component.rb +77 -44
- 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 +6 -1
- data/app/controllers/panda/cms/pages_controller.rb +2 -2
- data/app/helpers/panda/cms/application_helper.rb +15 -1
- data/app/helpers/panda/cms/asset_helper.rb +14 -3
- data/app/javascript/panda/cms/application_panda_cms.js +1 -1
- data/app/javascript/panda/cms/controllers/code_editor_controller.js +95 -0
- data/app/javascript/panda/cms/controllers/file_gallery_controller.js +128 -0
- data/app/javascript/panda/cms/controllers/index.js +48 -13
- data/app/javascript/panda/cms/controllers/inline_code_editor_controller.js +96 -0
- data/app/javascript/panda/cms/controllers/menu_form_controller.js +40 -0
- data/app/javascript/panda/cms/controllers/nested_form_controller.js +35 -0
- data/app/javascript/panda/cms/controllers/tree_controller.js +214 -0
- data/app/javascript/panda/cms/stimulus-loading.js +5 -7
- data/app/models/panda/cms/block_content.rb +9 -0
- data/app/models/panda/cms/page.rb +41 -0
- data/app/models/panda/cms/post.rb +1 -0
- 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 +64 -0
- data/app/views/panda/cms/admin/menus/index.html.erb +3 -2
- data/app/views/panda/cms/admin/menus/new.html.erb +40 -0
- data/app/views/panda/cms/admin/pages/edit.html.erb +15 -9
- 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 +4 -14
- data/app/views/panda/cms/admin/posts/edit.html.erb +2 -2
- data/app/views/panda/cms/admin/posts/index.html.erb +3 -3
- 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/config/importmap.rb +4 -6
- data/config/initializers/panda/cms/healthcheck_log_silencer.rb.disabled +31 -0
- data/config/initializers/panda/cms.rb +52 -10
- data/config/routes.rb +4 -2
- 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/lib/generators/panda/cms/install_generator.rb +2 -5
- data/lib/panda/cms/asset_loader.rb +36 -16
- data/lib/panda/cms/debug.rb +29 -0
- data/lib/panda/cms/engine.rb +107 -48
- data/lib/panda/cms/features.rb +52 -0
- data/lib/panda-cms/version.rb +1 -1
- data/lib/panda-cms.rb +5 -6
- data/lib/tasks/assets.rake +5 -52
- data/lib/tasks/panda_cms_tasks.rake +16 -0
- metadata +22 -29
- 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/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/config/initializers/panda/cms/healthcheck_log_silencer.rb +0 -13
|
@@ -2,162 +2,224 @@
|
|
|
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
|
+
div(class: "panda-cms-content", **element_attrs) do
|
|
22
|
+
if @editable_state
|
|
23
|
+
# Empty div for EditorJS to initialize into
|
|
24
|
+
else
|
|
25
|
+
raw(@rendered_content.html_safe)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def before_template
|
|
31
|
+
setup_editability
|
|
32
|
+
load_block_content
|
|
33
|
+
prepare_content
|
|
34
|
+
rescue ActiveRecord::RecordNotFound => e
|
|
35
|
+
handle_error(ComponentError.new("Database record not found: #{e.message}"))
|
|
36
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
37
|
+
handle_error(ComponentError.new("Invalid record: #{e.message}"))
|
|
38
|
+
rescue => e
|
|
39
|
+
handle_error(e)
|
|
22
40
|
end
|
|
23
41
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def setup_editability
|
|
45
|
+
@editable_state = @editable &&
|
|
46
|
+
view_context.params[:embed_id].present? &&
|
|
47
|
+
view_context.params[:embed_id] == Current.page.id &&
|
|
48
|
+
Current.user&.admin?
|
|
49
|
+
end
|
|
27
50
|
|
|
28
|
-
|
|
29
|
-
|
|
51
|
+
def load_block_content
|
|
52
|
+
block = Panda::CMS::Block.find_by(
|
|
53
|
+
kind: KIND,
|
|
54
|
+
key: @key,
|
|
55
|
+
panda_cms_template_id: Current.page.panda_cms_template_id
|
|
56
|
+
)
|
|
30
57
|
raise ComponentError, "Block not found for key: #{@key}" unless block
|
|
31
58
|
|
|
32
|
-
block_content = block.block_contents.find_by(panda_cms_page_id: Current.page.id)
|
|
33
|
-
|
|
34
|
-
|
|
59
|
+
@block_content = block.block_contents.find_by(panda_cms_page_id: Current.page.id)
|
|
60
|
+
|
|
61
|
+
if @block_content.nil?
|
|
62
|
+
@block_content = Panda::CMS::BlockContent.create!(
|
|
35
63
|
block: block,
|
|
36
64
|
panda_cms_page_id: Current.page.id,
|
|
37
65
|
content: empty_editor_js_content
|
|
38
66
|
)
|
|
39
67
|
end
|
|
40
68
|
|
|
41
|
-
|
|
69
|
+
@block_content_id = @block_content.id
|
|
70
|
+
raw_content = @block_content.cached_content || @block_content.content
|
|
42
71
|
@content = raw_content.presence || empty_editor_js_content
|
|
43
|
-
|
|
72
|
+
end
|
|
44
73
|
|
|
45
|
-
|
|
46
|
-
|
|
74
|
+
def prepare_content
|
|
75
|
+
if @editable_state
|
|
76
|
+
prepare_editable_content
|
|
77
|
+
else
|
|
78
|
+
prepare_display_content
|
|
79
|
+
end
|
|
80
|
+
end
|
|
47
81
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
82
|
+
def prepare_editable_content
|
|
83
|
+
@editor_content = if @content.blank? || @content == "{}"
|
|
84
|
+
empty_editor_js_content
|
|
85
|
+
else
|
|
86
|
+
process_content_for_editor(@content)
|
|
87
|
+
end
|
|
53
88
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
89
|
+
@encoded_data = Base64.strict_encode64(@editor_content.to_json)
|
|
90
|
+
rescue => e
|
|
91
|
+
Rails.logger.error("Content processing error: #{e.message}\nContent: #{@content.inspect}")
|
|
92
|
+
@editor_content = empty_editor_js_content
|
|
93
|
+
@encoded_data = Base64.strict_encode64(@editor_content.to_json)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def prepare_display_content
|
|
97
|
+
@rendered_content = if @content.blank? || @content == "{}"
|
|
98
|
+
"<p></p>"
|
|
99
|
+
else
|
|
100
|
+
render_content_for_display(@content)
|
|
101
|
+
end
|
|
102
|
+
rescue => e
|
|
103
|
+
Rails.logger.error("RichTextComponent render error: #{e.message}\nContent: #{@content.inspect}")
|
|
104
|
+
@rendered_content = "<p></p>"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def process_content_for_editor(content)
|
|
108
|
+
parsed = if content.is_a?(String)
|
|
109
|
+
JSON.parse(content)
|
|
110
|
+
else
|
|
111
|
+
content
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
if valid_editor_js_content?(parsed)
|
|
115
|
+
normalize_editor_content(parsed)
|
|
116
|
+
else
|
|
117
|
+
convert_html_to_editor_js(content)
|
|
118
|
+
end
|
|
119
|
+
rescue JSON::ParserError
|
|
120
|
+
convert_html_to_editor_js(content)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def normalize_editor_content(parsed)
|
|
124
|
+
{
|
|
125
|
+
"time" => parsed["time"] || Time.current.to_i * 1000,
|
|
126
|
+
"blocks" => (parsed["blocks"] || []).map { |block| normalize_block(block) },
|
|
127
|
+
"version" => parsed["version"] || "2.28.2"
|
|
128
|
+
}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def normalize_block(block)
|
|
132
|
+
case block["type"]
|
|
133
|
+
when "paragraph"
|
|
134
|
+
block.merge("data" => block["data"].merge("text" => block["data"]["text"].to_s.presence || ""))
|
|
135
|
+
when "header"
|
|
136
|
+
block.merge("data" => block["data"].merge(
|
|
137
|
+
"text" => block["data"]["text"].to_s.presence || "",
|
|
138
|
+
"level" => block["data"]["level"].to_i
|
|
139
|
+
))
|
|
140
|
+
when "list"
|
|
141
|
+
block.merge("data" => block["data"].merge(
|
|
142
|
+
"items" => (block["data"]["items"] || []).map { |item| item.to_s.presence || "" }
|
|
143
|
+
))
|
|
116
144
|
else
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
145
|
+
block
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def convert_html_to_editor_js(content)
|
|
150
|
+
editor_content = Panda::Editor::HtmlToEditorJsConverter.convert(content.to_s)
|
|
151
|
+
valid_editor_js_content?(editor_content) ? editor_content : empty_editor_js_content
|
|
152
|
+
rescue Panda::Editor::HtmlToEditorJsConverter::ConversionError => e
|
|
153
|
+
Rails.logger.error("HTML conversion error: #{e.message}")
|
|
154
|
+
empty_editor_js_content
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def render_content_for_display(content)
|
|
158
|
+
# Try to parse as JSON if it looks like EditorJS format
|
|
159
|
+
if content.is_a?(String) && content.strip.match?(/^\{.*"blocks":\s*\[.*\].*\}$/m)
|
|
160
|
+
parsed_content = JSON.parse(content)
|
|
161
|
+
if valid_editor_js_content?(parsed_content)
|
|
162
|
+
render_editor_js_content(parsed_content)
|
|
120
163
|
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
|
|
164
|
+
process_html_content(content)
|
|
148
165
|
end
|
|
166
|
+
else
|
|
167
|
+
process_html_content(content)
|
|
149
168
|
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
|
|
169
|
+
rescue JSON::ParserError
|
|
170
|
+
process_html_content(content)
|
|
158
171
|
end
|
|
159
172
|
|
|
160
|
-
|
|
173
|
+
def render_editor_js_content(parsed_content)
|
|
174
|
+
# Check if it's just an empty paragraph
|
|
175
|
+
if parsed_content["blocks"].length == 1 &&
|
|
176
|
+
parsed_content["blocks"][0]["type"] == "paragraph" &&
|
|
177
|
+
parsed_content["blocks"][0]["data"]["text"].blank?
|
|
178
|
+
"<p></p>"
|
|
179
|
+
else
|
|
180
|
+
renderer = Panda::Editor::Renderer.new(parsed_content)
|
|
181
|
+
rendered = renderer.render
|
|
182
|
+
rendered.presence || "<p></p>"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def process_html_content(content)
|
|
187
|
+
return "<p></p>" if content.blank?
|
|
188
|
+
|
|
189
|
+
# If it's already HTML, return it
|
|
190
|
+
if content.match?(/<[^>]+>/)
|
|
191
|
+
content
|
|
192
|
+
else
|
|
193
|
+
# Wrap plain text in paragraph tags
|
|
194
|
+
"<p>#{content}</p>"
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def element_attrs
|
|
199
|
+
attrs = {class: "panda-cms-content"}
|
|
200
|
+
|
|
201
|
+
if @editable_state
|
|
202
|
+
attrs.merge!(
|
|
203
|
+
id: "editor-#{@block_content_id}",
|
|
204
|
+
data: {
|
|
205
|
+
"editable-previous-data": @encoded_data,
|
|
206
|
+
"editable-content": @encoded_data,
|
|
207
|
+
"editable-initialized": "false",
|
|
208
|
+
"editable-version": "2.28.2",
|
|
209
|
+
"editable-autosave": "false",
|
|
210
|
+
"editable-tools": '{"paragraph":true,"header":true,"list":true,"quote":true,"table":true}',
|
|
211
|
+
"editable-kind": "rich_text",
|
|
212
|
+
"editable-block-content-id": @block_content_id,
|
|
213
|
+
"editable-page-id": Current.page.id,
|
|
214
|
+
controller: "editor-js",
|
|
215
|
+
"editor-js-initialized-value": "false",
|
|
216
|
+
"editor-js-content-value": @encoded_data
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
attrs
|
|
222
|
+
end
|
|
161
223
|
|
|
162
224
|
def empty_editor_js_content
|
|
163
225
|
{
|
|
@@ -173,21 +235,17 @@ module Panda
|
|
|
173
235
|
false
|
|
174
236
|
end
|
|
175
237
|
|
|
176
|
-
def
|
|
177
|
-
|
|
238
|
+
def handle_error(error)
|
|
239
|
+
Rails.logger.error("RichTextComponent error: #{error.message}\nContent: #{@content.inspect}")
|
|
178
240
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
241
|
+
if @editable_state
|
|
242
|
+
@editor_content = empty_editor_js_content
|
|
243
|
+
@encoded_data = Base64.strict_encode64(@editor_content.to_json)
|
|
182
244
|
else
|
|
183
|
-
|
|
184
|
-
"<p>#{content}</p>".html_safe
|
|
245
|
+
@rendered_content = "<p></p>"
|
|
185
246
|
end
|
|
186
|
-
end
|
|
187
247
|
|
|
188
|
-
|
|
189
|
-
def render?
|
|
190
|
-
true # Always render, we'll show empty content if needed
|
|
248
|
+
nil
|
|
191
249
|
end
|
|
192
250
|
end
|
|
193
251
|
end
|
|
@@ -2,69 +2,102 @@
|
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
def view_template
|
|
19
|
+
return unless @content
|
|
20
|
+
|
|
21
|
+
span(**element_attrs) { raw(@content.html_safe) }
|
|
22
|
+
rescue => e
|
|
23
|
+
handle_error(e)
|
|
22
24
|
end
|
|
23
25
|
|
|
24
|
-
def
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
def before_template
|
|
27
|
+
prepare_content
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def prepare_content
|
|
33
|
+
@editable_state = @editable && is_editable_context?
|
|
34
|
+
|
|
35
|
+
block = find_block
|
|
36
|
+
return false if block.nil?
|
|
37
|
+
|
|
38
|
+
block_content = find_block_content(block)
|
|
39
|
+
@plain_text = block_content&.content.to_s
|
|
40
|
+
|
|
41
|
+
if @editable_state
|
|
42
|
+
setup_editable_content(block_content)
|
|
43
|
+
else
|
|
44
|
+
@content = prepare_content_for_display(@plain_text)
|
|
29
45
|
end
|
|
46
|
+
end
|
|
30
47
|
|
|
31
|
-
|
|
48
|
+
def find_block
|
|
49
|
+
Panda::CMS::Block.find_by(
|
|
50
|
+
kind: KIND,
|
|
51
|
+
key: @key,
|
|
52
|
+
panda_cms_template_id: Current.page.panda_cms_template_id
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def find_block_content(block)
|
|
57
|
+
block.block_contents.find_by(panda_cms_page_id: Current.page.id)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def setup_editable_content(block_content)
|
|
61
|
+
@content = @plain_text
|
|
62
|
+
@block_content_id = block_content&.id
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def element_attrs
|
|
66
|
+
attrs = @attrs.merge(id: element_id)
|
|
67
|
+
|
|
68
|
+
if @editable_state
|
|
69
|
+
attrs.merge!(
|
|
70
|
+
contenteditable: "plaintext-only",
|
|
71
|
+
data: {
|
|
72
|
+
"editable-kind": "plain_text",
|
|
73
|
+
"editable-page-id": Current.page.id,
|
|
74
|
+
"editable-block-content-id": @block_content_id
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
attrs
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def element_id
|
|
83
|
+
@editable_state ? "editor-#{@block_content_id}" : "text-#{@key.to_s.dasherize}"
|
|
32
84
|
end
|
|
33
85
|
|
|
34
|
-
#
|
|
35
|
-
# Prepares content for display
|
|
36
|
-
#
|
|
37
|
-
# @usage Do not use this when rendering editable content
|
|
38
86
|
def prepare_content_for_display(content)
|
|
39
87
|
# Replace \n characters with <br> tags
|
|
40
88
|
content.gsub("\n", "<br>")
|
|
41
89
|
end
|
|
42
90
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
@editable &&= params[:embed_id].present? && params[:embed_id] == Current.page.id
|
|
47
|
-
|
|
48
|
-
block = Panda::CMS::Block.find_by(kind: KIND, key: @key,
|
|
49
|
-
panda_cms_template_id: Current.page.panda_cms_template_id)
|
|
50
|
-
|
|
51
|
-
return false if block.nil?
|
|
91
|
+
def is_editable_context?
|
|
92
|
+
view_context.params[:embed_id].present? && view_context.params[:embed_id] == Current.page.id
|
|
93
|
+
end
|
|
52
94
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
@options[:contenteditable] = "plaintext-only"
|
|
57
|
-
@options[:data] = {
|
|
58
|
-
"editable-kind": "plain_text",
|
|
59
|
-
"editable-page-id": Current.page.id,
|
|
60
|
-
"editable-block-content-id": block_content&.id
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
@options[:id] = "editor-#{block_content&.id}"
|
|
64
|
-
@content = plain_text
|
|
65
|
-
else
|
|
66
|
-
@content = prepare_content_for_display(plain_text)
|
|
95
|
+
def handle_error(error)
|
|
96
|
+
if !Rails.env.production? || defined?(Sentry)
|
|
97
|
+
raise Panda::CMS::MissingBlockError, "Block with key #{@key} not found for page #{Current.page.title}"
|
|
67
98
|
end
|
|
99
|
+
|
|
100
|
+
false
|
|
68
101
|
end
|
|
69
102
|
end
|
|
70
103
|
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
|
|
@@ -4,7 +4,8 @@ module Panda
|
|
|
4
4
|
module CMS
|
|
5
5
|
module Admin
|
|
6
6
|
class MenusController < ::Panda::CMS::Admin::BaseController
|
|
7
|
-
before_action :set_initial_breadcrumb, only: %i[index]
|
|
7
|
+
before_action :set_initial_breadcrumb, only: %i[index new edit]
|
|
8
|
+
before_action :set_menu, only: %i[edit update destroy]
|
|
8
9
|
|
|
9
10
|
# Lists all menus which can be managed by the administrator
|
|
10
11
|
# @type GET
|
|
@@ -14,10 +15,53 @@ module Panda
|
|
|
14
15
|
render :index, locals: {menus: menus}
|
|
15
16
|
end
|
|
16
17
|
|
|
18
|
+
# @type GET
|
|
19
|
+
def new
|
|
20
|
+
menu = Panda::CMS::Menu.new
|
|
21
|
+
add_breadcrumb "New Menu", new_admin_cms_menu_path
|
|
22
|
+
render :new, locals: {menu: menu}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @type POST
|
|
26
|
+
def create
|
|
27
|
+
menu = Panda::CMS::Menu.new(menu_params)
|
|
28
|
+
|
|
29
|
+
if menu.save
|
|
30
|
+
redirect_to admin_cms_menus_path, notice: "Menu was successfully created."
|
|
31
|
+
else
|
|
32
|
+
render :new, locals: {menu: menu}, status: :unprocessable_entity
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @type GET
|
|
37
|
+
def edit
|
|
38
|
+
add_breadcrumb @menu.name, edit_admin_cms_menu_path(@menu)
|
|
39
|
+
render :edit, locals: {menu: @menu}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @type PATCH/PUT
|
|
43
|
+
def update
|
|
44
|
+
if @menu.update(menu_params)
|
|
45
|
+
redirect_to admin_cms_menus_path, notice: "Menu was successfully updated."
|
|
46
|
+
else
|
|
47
|
+
render :edit, locals: {menu: @menu}, status: :unprocessable_entity
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @type DELETE
|
|
52
|
+
def destroy
|
|
53
|
+
@menu.destroy
|
|
54
|
+
redirect_to admin_cms_menus_path, notice: "Menu was successfully deleted."
|
|
55
|
+
end
|
|
56
|
+
|
|
17
57
|
private
|
|
18
58
|
|
|
19
|
-
def
|
|
20
|
-
@menu
|
|
59
|
+
def set_menu
|
|
60
|
+
@menu = Panda::CMS::Menu.find(params[:id])
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def menu_params
|
|
64
|
+
params.require(:menu).permit(:name, :kind, :start_page_id, menu_items_attributes: [:id, :text, :external_url, :panda_cms_page_id, :_destroy])
|
|
21
65
|
end
|
|
22
66
|
|
|
23
67
|
def set_initial_breadcrumb
|