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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +83 -4
  3. data/app/components/panda/cms/code_component.rb +117 -39
  4. data/app/components/panda/cms/grid_component.rb +26 -6
  5. data/app/components/panda/cms/menu_component.rb +66 -34
  6. data/app/components/panda/cms/page_menu_component.rb +94 -13
  7. data/app/components/panda/cms/rich_text_component.rb +198 -140
  8. data/app/components/panda/cms/text_component.rb +77 -44
  9. data/app/controllers/panda/cms/admin/base_controller.rb +19 -3
  10. data/app/controllers/panda/cms/admin/dashboard_controller.rb +3 -3
  11. data/app/controllers/panda/cms/admin/files_controller.rb +7 -0
  12. data/app/controllers/panda/cms/admin/menus_controller.rb +47 -3
  13. data/app/controllers/panda/cms/admin/pages_controller.rb +6 -1
  14. data/app/controllers/panda/cms/pages_controller.rb +2 -2
  15. data/app/helpers/panda/cms/application_helper.rb +15 -1
  16. data/app/helpers/panda/cms/asset_helper.rb +14 -3
  17. data/app/javascript/panda/cms/application_panda_cms.js +1 -1
  18. data/app/javascript/panda/cms/controllers/code_editor_controller.js +95 -0
  19. data/app/javascript/panda/cms/controllers/file_gallery_controller.js +128 -0
  20. data/app/javascript/panda/cms/controllers/index.js +48 -13
  21. data/app/javascript/panda/cms/controllers/inline_code_editor_controller.js +96 -0
  22. data/app/javascript/panda/cms/controllers/menu_form_controller.js +40 -0
  23. data/app/javascript/panda/cms/controllers/nested_form_controller.js +35 -0
  24. data/app/javascript/panda/cms/controllers/tree_controller.js +214 -0
  25. data/app/javascript/panda/cms/stimulus-loading.js +5 -7
  26. data/app/models/panda/cms/block_content.rb +9 -0
  27. data/app/models/panda/cms/page.rb +41 -0
  28. data/app/models/panda/cms/post.rb +1 -0
  29. data/app/views/panda/cms/admin/dashboard/show.html.erb +5 -5
  30. data/app/views/panda/cms/admin/files/_file_details.html.erb +45 -0
  31. data/app/views/panda/cms/admin/files/index.html.erb +11 -118
  32. data/app/views/panda/cms/admin/forms/index.html.erb +2 -2
  33. data/app/views/panda/cms/admin/forms/new.html.erb +1 -2
  34. data/app/views/panda/cms/admin/forms/show.html.erb +15 -30
  35. data/app/views/panda/cms/admin/menus/_menu_item_fields.html.erb +11 -0
  36. data/app/views/panda/cms/admin/menus/edit.html.erb +64 -0
  37. data/app/views/panda/cms/admin/menus/index.html.erb +3 -2
  38. data/app/views/panda/cms/admin/menus/new.html.erb +40 -0
  39. data/app/views/panda/cms/admin/pages/edit.html.erb +15 -9
  40. data/app/views/panda/cms/admin/pages/index.html.erb +49 -11
  41. data/app/views/panda/cms/admin/pages/new.html.erb +3 -11
  42. data/app/views/panda/cms/admin/posts/_form.html.erb +4 -14
  43. data/app/views/panda/cms/admin/posts/edit.html.erb +2 -2
  44. data/app/views/panda/cms/admin/posts/index.html.erb +3 -3
  45. data/app/views/panda/cms/admin/posts/new.html.erb +1 -1
  46. data/app/views/panda/cms/admin/settings/bulk_editor/new.html.erb +1 -1
  47. data/app/views/panda/cms/admin/settings/index.html.erb +3 -3
  48. data/config/importmap.rb +4 -6
  49. data/config/initializers/panda/cms/healthcheck_log_silencer.rb.disabled +31 -0
  50. data/config/initializers/panda/cms.rb +52 -10
  51. data/config/routes.rb +4 -2
  52. data/db/migrate/20240305000000_convert_html_content_to_editor_js.rb +2 -2
  53. data/db/migrate/20240315125421_add_nested_sets_to_panda_cms_pages.rb +6 -1
  54. data/db/migrate/20250809231125_migrate_users_to_panda_core.rb +23 -21
  55. data/db/migrate/20251104150640_add_cached_last_updated_at_to_panda_cms_pages.rb +22 -0
  56. data/db/migrate/20251104172242_add_page_type_to_panda_cms_pages.rb +6 -0
  57. data/db/migrate/20251104172638_set_page_types_for_existing_pages.rb +27 -0
  58. data/db/migrate/20251105000001_add_pending_review_status_to_pages_and_posts.panda_cms.rb +21 -0
  59. data/lib/generators/panda/cms/install_generator.rb +2 -5
  60. data/lib/panda/cms/asset_loader.rb +36 -16
  61. data/lib/panda/cms/debug.rb +29 -0
  62. data/lib/panda/cms/engine.rb +107 -48
  63. data/lib/panda/cms/features.rb +52 -0
  64. data/lib/panda-cms/version.rb +1 -1
  65. data/lib/panda-cms.rb +5 -6
  66. data/lib/tasks/assets.rake +5 -52
  67. data/lib/tasks/panda_cms_tasks.rake +16 -0
  68. metadata +22 -29
  69. data/app/components/panda/cms/admin/container_component.html.erb +0 -13
  70. data/app/components/panda/cms/admin/flash_message_component.html.erb +0 -31
  71. data/app/components/panda/cms/admin/panel_component.html.erb +0 -7
  72. data/app/components/panda/cms/admin/slideover_component.html.erb +0 -9
  73. data/app/components/panda/cms/admin/slideover_component.rb +0 -15
  74. data/app/components/panda/cms/admin/statistics_component.html.erb +0 -4
  75. data/app/components/panda/cms/admin/statistics_component.rb +0 -16
  76. data/app/components/panda/cms/admin/tab_bar_component.html.erb +0 -35
  77. data/app/components/panda/cms/admin/tab_bar_component.rb +0 -15
  78. data/app/components/panda/cms/admin/table_component.html.erb +0 -29
  79. data/app/components/panda/cms/admin/user_activity_component.html.erb +0 -7
  80. data/app/components/panda/cms/admin/user_activity_component.rb +0 -20
  81. data/app/components/panda/cms/admin/user_display_component.html.erb +0 -17
  82. data/app/components/panda/cms/admin/user_display_component.rb +0 -21
  83. data/app/components/panda/cms/grid_component.html.erb +0 -6
  84. data/app/components/panda/cms/menu_component.html.erb +0 -6
  85. data/app/components/panda/cms/page_menu_component.html.erb +0 -21
  86. data/app/components/panda/cms/rich_text_component.html.erb +0 -90
  87. data/app/views/layouts/panda/cms/application.html.erb +0 -42
  88. data/app/views/panda/cms/admin/shared/_breadcrumbs.html.erb +0 -28
  89. data/app/views/panda/cms/admin/shared/_flash.html.erb +0 -5
  90. data/app/views/panda/cms/admin/shared/_sidebar.html.erb +0 -41
  91. data/app/views/panda/cms/shared/_footer.html.erb +0 -2
  92. data/app/views/panda/cms/shared/_header.html.erb +0 -25
  93. 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
- # Text component
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
- # @param options [Hash] The options to pass to the content_tag
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
- attr_accessor :editable, :content, :options
14
+ prop :key, Symbol, default: :text_component
15
+ prop :text, String, default: "Lorem ipsum..."
16
+ prop :editable, _Boolean, default: true
16
17
 
17
- def initialize(key: :text_component, text: "Lorem ipsum...", editable: true, **options)
18
- @key = key
19
- @text = text
20
- @options = options || {}
21
- @editable = editable
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
- # Check if the element is editable and set up the content
25
- def before_render
26
- @editable &&= params[:embed_id].present? && params[:embed_id] == Current.page.id && Current.user.admin?
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
- block = Panda::CMS::Block.find_by(kind: "rich_text", key: @key,
29
- panda_cms_template_id: Current.page.panda_cms_template_id)
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
- if block_content.nil?
34
- block_content = Panda::CMS::BlockContent.create!(
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
- raw_content = block_content.cached_content || block_content.content
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
- @options[:id] = block_content.id
72
+ end
44
73
 
45
- # Debug log the content
46
- Rails.logger.debug("RichTextComponent content before processing: #{@content.inspect}")
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
- if @editable
49
- @options[:data] = {
50
- page_id: Current.page.id,
51
- mode: "rich_text"
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
- # For editable mode, always ensure we have a valid EditorJS structure
55
- @content = if @content.blank? || @content == "{}"
56
- empty_editor_js_content
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
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
- # For non-editable mode, handle content display
118
- @content = if @content.blank? || @content == "{}"
119
- "<p></p>".html_safe
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
- begin
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 ActiveRecord::RecordNotFound => e
151
- raise ComponentError, "Database record not found: #{e.message}"
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
- private
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 process_html(content)
177
- return "<p></p>".html_safe if content.blank?
238
+ def handle_error(error)
239
+ Rails.logger.error("RichTextComponent error: #{error.message}\nContent: #{@content.inspect}")
178
240
 
179
- # If it's already HTML, just return it
180
- if content.match?(/<[^>]+>/)
181
- content.html_safe
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
- # Wrap plain text in paragraph tags
184
- "<p>#{content}</p>".html_safe
245
+ @rendered_content = "<p></p>"
185
246
  end
186
- end
187
247
 
188
- # Only render the component if there is some content set, or if the component is editable
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
- # @param options [Hash] The options to pass to the content_tag
10
- class TextComponent < ViewComponent::Base
9
+ class TextComponent < Panda::Core::Base
11
10
  KIND = "plain_text"
12
11
 
13
- # Allows accessing the plain text of the component directly
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 initialize(key: :text_component, text: "Lorem ipsum...", editable: true, **options)
17
- @key = key
18
- @text = text
19
- @options = options || {}
20
- @options[:id] ||= "text-#{key.to_s.dasherize}"
21
- @editable = editable
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 call
25
- content_tag(:span, @content, @options, false) # Don't escape the content
26
- rescue
27
- if !Rails.env.production? || is_defined?(Sentry)
28
- raise Panda::CMS::MissingBlockError, "Block with key #{@key} not found for page #{Current.page.title}"
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
- false
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
- # Check if the element is editable
44
- # TODO: Check user permissions
45
- def before_render
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
- block_content = block.block_contents.find_by(panda_cms_page_id: Current.page.id)
54
- plain_text = block_content&.content.to_s
55
- if @editable
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 AdminController for authentication
8
- # Adds CMS-specific helpers and functionality
9
- class BaseController < ::Panda::Core::AdminController
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 < ::Panda::Core::Admin::DashboardController
8
+ class DashboardController < BaseController
9
9
  before_action :set_initial_breadcrumb, only: %i[show]
10
10
 
11
- # Override the panda-core dashboard with CMS-specific dashboard
11
+ # CMS-specific dashboard
12
12
  def show
13
13
  # Render the CMS dashboard view
14
- render "panda/cms/admin/dashboard/show"
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 menu
20
- @menu ||= Panda::CMS::Menu.find(params[:id])
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