panda-cms 0.10.0 → 0.10.3
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 +79 -11
- data/app/assets/tailwind/panda/cms/_application.css +1 -0
- data/app/components/panda/cms/admin/popular_pages_component.rb +62 -0
- data/app/components/panda/cms/code_component.rb +46 -9
- data/app/components/panda/cms/menu_component.rb +18 -5
- data/app/components/panda/cms/page_menu_component.rb +9 -1
- data/app/components/panda/cms/rich_text_component.rb +49 -17
- data/app/components/panda/cms/text_component.rb +46 -14
- data/app/controllers/panda/cms/admin/menus_controller.rb +2 -2
- data/app/controllers/panda/cms/admin/pages_controller.rb +6 -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 +2 -3
- data/app/helpers/panda/cms/asset_helper.rb +14 -72
- 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} +4 -0
- data/app/javascript/panda/cms/controllers/editor_form_controller.js +3 -3
- data/app/javascript/panda/cms/controllers/editor_iframe_controller.js +35 -8
- data/app/javascript/panda/cms/controllers/file_upload_controller.js +165 -0
- data/app/javascript/panda/cms/controllers/index.js +6 -0
- data/app/javascript/panda/cms/controllers/menu_form_controller.js +14 -1
- data/app/javascript/panda/cms/controllers/page_form_controller.js +454 -0
- data/app/javascript/panda/cms/stimulus-loading.js +2 -1
- data/app/jobs/panda/cms/record_visit_job.rb +2 -1
- data/app/models/panda/cms/menu.rb +12 -0
- data/app/models/panda/cms/page.rb +106 -0
- data/app/models/panda/cms/post.rb +97 -0
- data/app/models/panda/cms/visit.rb +16 -1
- data/app/services/panda/social/instagram_feed_service.rb +54 -54
- 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 +11 -4
- data/app/views/panda/cms/admin/files/index.html.erb +1 -1
- data/app/views/panda/cms/admin/forms/new.html.erb +1 -1
- data/app/views/panda/cms/admin/forms/show.html.erb +3 -3
- data/app/views/panda/cms/admin/menus/_menu_item_fields.html.erb +3 -3
- data/app/views/panda/cms/admin/menus/edit.html.erb +12 -14
- data/app/views/panda/cms/admin/menus/index.html.erb +1 -1
- data/app/views/panda/cms/admin/menus/new.html.erb +6 -8
- data/app/views/panda/cms/admin/pages/edit.html.erb +213 -20
- data/app/views/panda/cms/admin/pages/index.html.erb +6 -6
- data/app/views/panda/cms/admin/posts/_form.html.erb +47 -8
- data/app/views/panda/cms/admin/posts/edit.html.erb +1 -1
- data/app/views/panda/cms/admin/posts/index.html.erb +4 -4
- data/app/views/panda/cms/shared/_favicons.html.erb +7 -7
- data/app/views/shared/_header.html.erb +1 -4
- data/config/brakeman.ignore +38 -0
- data/config/importmap.rb +7 -6
- data/config/initializers/groupdate.rb +5 -0
- data/config/locales/en.yml +42 -2
- data/config/routes.rb +1 -1
- data/db/migrate/20240315125421_add_nested_sets_to_panda_cms_pages.rb +0 -10
- data/db/migrate/20240316230706_add_nested_to_panda_cms_menu_items.rb +0 -6
- data/db/migrate/20240317230622_create_panda_cms_visits.rb +1 -1
- data/db/migrate/20240805121123_create_panda_cms_posts.rb +1 -1
- data/db/migrate/20240806112735_fix_panda_cms_visits_column_names.rb +1 -1
- data/db/migrate/20240923234535_add_depth_to_panda_cms_menus.rb +0 -6
- data/db/migrate/20250106223303_add_author_id_to_panda_cms_posts.rb +1 -3
- 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/db/migrate/20251117234530_add_index_to_visited_at_on_panda_cms_visits.rb +7 -0
- data/db/migrate/20251118015100_backfill_visited_at_for_existing_visits.rb +17 -0
- data/db/seeds.rb +5 -0
- data/lib/panda/cms/asset_loader.rb +42 -78
- data/lib/panda/cms/bulk_editor.rb +288 -12
- data/lib/panda/cms/engine/asset_config.rb +49 -0
- data/lib/panda/cms/engine/autoload_config.rb +37 -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 +33 -0
- data/lib/panda/cms/engine/view_component_config.rb +31 -0
- data/lib/panda/cms/engine.rb +32 -228
- data/lib/{panda-cms → panda/cms}/version.rb +1 -1
- data/lib/panda/cms.rb +12 -0
- data/lib/panda-cms.rb +24 -3
- data/lib/tasks/ci.rake +0 -0
- metadata +32 -67
- data/app/assets/builds/panda.cms.css +0 -2754
- data/app/assets/stylesheets/panda/cms/application.tailwind.css +0 -162
- data/app/assets/stylesheets/panda/cms/editor.css +0 -120
- data/app/assets/tailwind/application.css +0 -178
- data/app/assets/tailwind/tailwind.config.js +0 -15
- data/app/javascript/panda_cms/stimulus-loading.js +0 -39
- 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.disabled +0 -31
- data/db/migrate/20240317010532_create_panda_cms_users.rb +0 -14
- data/db/migrate/20240324205703_create_active_storage_tables.active_storage.rb +0 -61
- data/db/migrate/20240408084718_default_panda_cms_users_admin_to_false.rb +0 -7
- data/db/migrate/20240701225422_add_service_name_to_active_storage_blobs.active_storage.rb +0 -24
- data/db/migrate/20240701225423_create_active_storage_variant_records.active_storage.rb +0 -30
- data/db/migrate/20240701225424_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb +0 -10
- data/db/migrate/20241119214548_convert_post_content_to_editor_js.rb +0 -37
- data/db/migrate/20250809231125_migrate_users_to_panda_core.rb +0 -113
- data/lib/generators/panda/cms/install_generator.rb +0 -28
- data/lib/tasks/assets.rake +0 -540
- data/public/panda-cms-assets/editor-js/core/editorjs.min.js +0 -83
- data/public/panda-cms-assets/editor-js/plugins/embed.min.js +0 -2
- data/public/panda-cms-assets/editor-js/plugins/header.min.js +0 -9
- data/public/panda-cms-assets/editor-js/plugins/nested-list.min.js +0 -2
- data/public/panda-cms-assets/editor-js/plugins/paragraph.min.js +0 -9
- data/public/panda-cms-assets/editor-js/plugins/quote.min.js +0 -2
- data/public/panda-cms-assets/editor-js/plugins/simple-image.min.js +0 -2
- data/public/panda-cms-assets/editor-js/plugins/table.min.js +0 -2
- data/public/panda-cms-assets/favicons/android-chrome-192x192.png +0 -0
- data/public/panda-cms-assets/favicons/android-chrome-512x512.png +0 -0
- data/public/panda-cms-assets/favicons/apple-touch-icon.png +0 -0
- data/public/panda-cms-assets/favicons/browserconfig.xml +0 -9
- data/public/panda-cms-assets/favicons/favicon-16x16.png +0 -0
- data/public/panda-cms-assets/favicons/favicon-32x32.png +0 -0
- data/public/panda-cms-assets/favicons/favicon.ico +0 -0
- data/public/panda-cms-assets/favicons/mstile-150x150.png +0 -0
- data/public/panda-cms-assets/favicons/safari-pinned-tab.svg +0 -61
- data/public/panda-cms-assets/favicons/site.webmanifest +0 -14
- data/public/panda-cms-assets/manifest.json +0 -20
- data/public/panda-cms-assets/panda-cms-0.7.4.css +0 -26
- data/public/panda-cms-assets/panda-cms-0.7.4.js +0 -150
- data/public/panda-cms-assets/panda-logo-screenprint.png +0 -0
- data/public/panda-cms-assets/panda-nav.png +0 -0
- data/public/panda-cms-assets/rich_text_editor.css +0 -568
- /data/db/migrate/{20251105000001_add_pending_review_status_to_pages_and_posts.panda_cms.rb → 20251105000001_add_pending_review_status_to_pages_and_posts.rb} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 680bef797106fd65c694455aac3c55bc7799bfdfba990997a96bec44f325f780
|
|
4
|
+
data.tar.gz: a8485d6a4da3f5623531eb14ae6188226204ab8b2bce3381baa3f1c6e3b7caf4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3cba49016e04bc8ec5a2f1011897687eeb2478c451c0a9d38feef27605e4ddfe674c9d8543ee92e33b76049d3ecb08bac0bab1e6b790576c5f043683dbca606e
|
|
7
|
+
data.tar.gz: bb6c39903cdf5412c99dae06721a3d908dc0ed6d2e2c6a5c443eb96c6e6e43965029530a2206bce40bf2a8c06ddfd8fafbdf97d341c6f9739269f64693240479
|
data/README.md
CHANGED
|
@@ -32,16 +32,6 @@ The easiest way for you to get started is to visit http://localhost:3000/admin a
|
|
|
32
32
|
|
|
33
33
|
When you're ready to configure further, you can set your own configuration in `config/initializers/panda.rb`. Make sure to configure your authentication providers and update the domain restriction!
|
|
34
34
|
|
|
35
|
-
## Panda CMS Pro
|
|
36
|
-
|
|
37
|
-
Commercial features such as structured **Collections** live in the `panda-cms-pro` gem. Once the pro gem is installed you can:
|
|
38
|
-
|
|
39
|
-
- Model repeatable content with collections and items.
|
|
40
|
-
- Loop over entries inside layouts using helpers like `panda_cms_collection_items("trustees")`.
|
|
41
|
-
- Keep the feature hidden in open-source installs thanks to `Panda::CMS::Features`.
|
|
42
|
-
|
|
43
|
-
See `docs/collections.md` for the editor workflow and template examples, and `docs/private-gem-server.md` for hosting the private gem server.
|
|
44
|
-
|
|
45
35
|
### Existing applications
|
|
46
36
|
|
|
47
37
|
Add the following to `Gemfile`:
|
|
@@ -124,6 +114,7 @@ The CMS automatically loads Core's compiled stylesheet:
|
|
|
124
114
|
```
|
|
125
115
|
|
|
126
116
|
Core's Rack middleware serves this file from the gem, so:
|
|
117
|
+
|
|
127
118
|
- ✅ No CSS copying or compilation needed
|
|
128
119
|
- ✅ Styles update automatically when Core updates
|
|
129
120
|
- ✅ Consistent design across all Panda gems
|
|
@@ -136,7 +127,7 @@ For CSS compilation (when contributing to styling), see [Panda Core Asset Compil
|
|
|
136
127
|
|
|
137
128
|
This is a non-exhuastive list (there will be many more):
|
|
138
129
|
|
|
139
|
-
* To date, this has only been tested with Rails 7.1, 7.2 and 8
|
|
130
|
+
* To date, this has only been tested with Rails 7.1, 7.2 and 8
|
|
140
131
|
* There may be conflicts if you're not using Tailwind CSS on the frontend. Please report this.
|
|
141
132
|
|
|
142
133
|
## Contributing
|
|
@@ -180,6 +171,83 @@ expect(page.title).to eq("Home")
|
|
|
180
171
|
|
|
181
172
|
When testing models with file validations or complex callbacks, use the helper methods in `spec/models/panda/cms/page_spec.rb` as a reference.
|
|
182
173
|
|
|
174
|
+
## 🚀 Running CI Locally
|
|
175
|
+
|
|
176
|
+
This project uses a **deterministic CI environment**, based on a single Docker
|
|
177
|
+
image (`panda-cms-test`). This ensures:
|
|
178
|
+
|
|
179
|
+
- identical Ruby/Node/Chrome versions everywhere
|
|
180
|
+
- no drift between local / Docker / GitHub Actions / act
|
|
181
|
+
- fast, stable, reproducible tests
|
|
182
|
+
|
|
183
|
+
There are **three** supported ways to run the full CI suite locally.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
### 1. Run full CI via Docker Compose
|
|
188
|
+
|
|
189
|
+
```sh
|
|
190
|
+
bin/ci build # build the local test image
|
|
191
|
+
bin/ci local # run full CI stack locally
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
This uses `docker-compose.ci.yml` and reproduces the entire GitHub Actions environment.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
### 2. Run single RSpec execution in the CI container
|
|
199
|
+
|
|
200
|
+
```sh
|
|
201
|
+
bin/ci test
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
This mounts your project into the container and executes RSpec exactly as CI does.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
### 3. Run GitHub Actions locally using act
|
|
209
|
+
|
|
210
|
+
Install act:
|
|
211
|
+
|
|
212
|
+
```sh
|
|
213
|
+
brew install act
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Use the project’s `.actrc`:
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
-P ubuntu-latest=ghcr.io/tastybamboo/panda-cms-test:local
|
|
220
|
+
--container-options "--shm-size=2gb"
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Then run:
|
|
224
|
+
|
|
225
|
+
```sh
|
|
226
|
+
bin/ci act
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
This executes **the real GitHub Actions workflow** on your machine.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
### 4. Continuous Integration on GitHub
|
|
234
|
+
|
|
235
|
+
GitHub Actions uses the same deterministic container image.
|
|
236
|
+
See:
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
.github/workflows/ci.yml
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
### 5. Code Coverage
|
|
245
|
+
|
|
246
|
+
Coverage is produced per-suite (models, requests, libs, system) and merged
|
|
247
|
+
into a unified `coverage/` directory via SimpleCov.
|
|
248
|
+
|
|
249
|
+
Artifacts are uploaded automatically on CI.
|
|
250
|
+
|
|
183
251
|
## License
|
|
184
252
|
|
|
185
253
|
The gem is available as open source under the terms of the [BSD-3-Clause License](https://opensource.org/licenses/bsd-3-clause).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/* Panda CMS specific styles go here */
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module CMS
|
|
5
|
+
module Admin
|
|
6
|
+
class PopularPagesComponent < Panda::Core::Base
|
|
7
|
+
def initialize(popular_pages:, period_name: "All Time")
|
|
8
|
+
@popular_pages = popular_pages
|
|
9
|
+
@period_name = period_name
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def view_template
|
|
13
|
+
render Panda::Core::Admin::PanelComponent.new do |panel|
|
|
14
|
+
panel.heading(text: "Popular Pages (#{@period_name})", level: :panel)
|
|
15
|
+
|
|
16
|
+
panel.body do
|
|
17
|
+
if @popular_pages.any?
|
|
18
|
+
div(class: "overflow-y-auto max-h-96") do
|
|
19
|
+
table(class: "min-w-full divide-y divide-gray-300") do
|
|
20
|
+
thead(class: "sticky top-0 bg-white z-10") do
|
|
21
|
+
tr do
|
|
22
|
+
th(class: "py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0") { "Page" }
|
|
23
|
+
th(class: "px-3 py-3.5 text-left text-sm font-semibold text-gray-900") { "Path" }
|
|
24
|
+
th(class: "px-3 py-3.5 text-right text-sm font-semibold text-gray-900") { "Views" }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
tbody(class: "divide-y divide-gray-200") do
|
|
28
|
+
index = 0
|
|
29
|
+
@popular_pages.each do |page_data|
|
|
30
|
+
tr(class: index.even? ? "bg-indigo-50" : "bg-white") do
|
|
31
|
+
td(class: "whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0") do
|
|
32
|
+
a(
|
|
33
|
+
href: view_context.admin_cms_page_path(page_data.id),
|
|
34
|
+
class: "text-indigo-600 hover:text-indigo-900"
|
|
35
|
+
) { page_data.title }
|
|
36
|
+
end
|
|
37
|
+
td(class: "whitespace-nowrap px-3 py-4 text-sm text-gray-500") do
|
|
38
|
+
a(
|
|
39
|
+
href: page_data.path,
|
|
40
|
+
class: "text-gray-600 hover:text-gray-900",
|
|
41
|
+
target: "_blank"
|
|
42
|
+
) { page_data.path }
|
|
43
|
+
end
|
|
44
|
+
td(class: "whitespace-nowrap px-3 py-4 text-sm text-gray-500 text-right font-semibold") do
|
|
45
|
+
page_data.visit_count.to_s
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
index += 1
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
else
|
|
54
|
+
p(class: "text-sm text-gray-500") { "No page visits recorded yet." }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -14,10 +14,12 @@ module Panda
|
|
|
14
14
|
prop :editable, _Boolean, default: true
|
|
15
15
|
|
|
16
16
|
def view_template
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
# Russian doll caching: Cache component output at block_content level
|
|
18
|
+
# Only cache in non-editable mode (public-facing pages)
|
|
19
|
+
if should_cache?
|
|
20
|
+
raw cache_component_output.to_s.html_safe
|
|
19
21
|
else
|
|
20
|
-
|
|
22
|
+
render_content
|
|
21
23
|
end
|
|
22
24
|
rescue => e
|
|
23
25
|
handle_error(e)
|
|
@@ -36,9 +38,9 @@ module Panda
|
|
|
36
38
|
block = find_block
|
|
37
39
|
return false if block.nil?
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
@code_content =
|
|
41
|
-
@block_content_id =
|
|
41
|
+
@block_content_obj = find_block_content(block)
|
|
42
|
+
@code_content = @block_content_obj&.content.to_s
|
|
43
|
+
@block_content_id = @block_content_obj&.id
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
def find_block
|
|
@@ -82,7 +84,7 @@ module Panda
|
|
|
82
84
|
data: {inline_code_editor_target: "codeInput"},
|
|
83
85
|
class: "w-full h-64 p-3 font-mono text-sm border border-gray-300 rounded focus:ring-primary focus:border-primary",
|
|
84
86
|
placeholder: "Enter your HTML/embed code here..."
|
|
85
|
-
) {
|
|
87
|
+
) { plain @code_content.to_s }
|
|
86
88
|
|
|
87
89
|
div(class: "mt-3 flex justify-end space-x-2") do
|
|
88
90
|
button(type: "button",
|
|
@@ -123,8 +125,11 @@ module Panda
|
|
|
123
125
|
end
|
|
124
126
|
|
|
125
127
|
def is_embedded?
|
|
126
|
-
#
|
|
127
|
-
|
|
128
|
+
# Security: Verify embed_id matches the current page being edited
|
|
129
|
+
# This prevents unauthorized editing by ensuring the embed_id in the URL
|
|
130
|
+
# matches the actual page ID from Current.page
|
|
131
|
+
view_context.params[:embed_id].present? &&
|
|
132
|
+
Current.page&.id.to_s == view_context.params[:embed_id].to_s
|
|
128
133
|
end
|
|
129
134
|
|
|
130
135
|
def handle_error(error)
|
|
@@ -141,6 +146,38 @@ module Panda
|
|
|
141
146
|
end
|
|
142
147
|
end
|
|
143
148
|
|
|
149
|
+
def render_content
|
|
150
|
+
if @editable_state
|
|
151
|
+
render_editable_view
|
|
152
|
+
else
|
|
153
|
+
raw(@code_content.to_s.html_safe)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def should_cache?
|
|
158
|
+
!@editable_state &&
|
|
159
|
+
Panda::CMS.config.performance.dig(:fragment_caching, :enabled) != false &&
|
|
160
|
+
@block_content_obj.present?
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def cache_component_output
|
|
164
|
+
cache_key = cache_key_for_component
|
|
165
|
+
expires_in = Panda::CMS.config.performance.dig(:fragment_caching, :expires_in) || 1.hour
|
|
166
|
+
|
|
167
|
+
Rails.cache.fetch(cache_key, expires_in: expires_in) do
|
|
168
|
+
render_content_to_string
|
|
169
|
+
end.html_safe
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def cache_key_for_component
|
|
173
|
+
"panda_cms/code_component/#{@block_content_obj.cache_key_with_version}/#{@key}"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def render_content_to_string
|
|
177
|
+
# For code component, we just return the raw HTML content
|
|
178
|
+
@code_content.to_s
|
|
179
|
+
end
|
|
180
|
+
|
|
144
181
|
class BlockError < StandardError; end
|
|
145
182
|
end
|
|
146
183
|
end
|
|
@@ -6,7 +6,7 @@ module Panda
|
|
|
6
6
|
# @param name [String] The name of the menu to render
|
|
7
7
|
# @param current_path [String] The current request path for highlighting active items
|
|
8
8
|
# @param styles [Hash] CSS classes for menu items (default, active, inactive)
|
|
9
|
-
# @param overrides [Hash] Menu item overrides
|
|
9
|
+
# @param overrides [Hash] Menu item overrides - supports :hidden_items array to hide specific menu items by text
|
|
10
10
|
# @param render_page_menu [Boolean] Whether to render sub-page menus
|
|
11
11
|
# @param page_menu_styles [Hash] Styles for the page menu component
|
|
12
12
|
class MenuComponent < Panda::Core::Base
|
|
@@ -44,11 +44,24 @@ module Panda
|
|
|
44
44
|
@menu = Panda::CMS::Menu.find_by(name: @name)
|
|
45
45
|
return unless @menu
|
|
46
46
|
|
|
47
|
-
menu_items
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
# Fragment caching: Cache menu_items query results
|
|
48
|
+
# Cache key includes menu's updated_at to auto-invalidate on changes
|
|
49
|
+
cache_key = "panda_cms_menu/#{@menu.name}/#{@menu.id}/#{@menu.updated_at.to_i}/items"
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
menu_items = Rails.cache.fetch(cache_key, expires_in: 1.hour) do
|
|
52
|
+
items = @menu.menu_items
|
|
53
|
+
items = items.where("depth <= ?", @menu.depth) if @menu.depth
|
|
54
|
+
items.order(:lft).to_a # Convert to array for caching
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Filter menu items based on overrides
|
|
58
|
+
filtered_menu_items = if @overrides[:hidden_items].present?
|
|
59
|
+
menu_items.reject { |item| @overrides[:hidden_items].include?(item.text) }
|
|
60
|
+
else
|
|
61
|
+
menu_items
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
@processed_menu_items = filtered_menu_items.map do |menu_item|
|
|
52
65
|
add_css_classes_to_item(menu_item)
|
|
53
66
|
menu_item
|
|
54
67
|
end
|
|
@@ -36,7 +36,15 @@ module Panda
|
|
|
36
36
|
menu = @start_page&.page_menu
|
|
37
37
|
return if menu.nil?
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
# Fragment caching: Cache menu items for this page menu
|
|
40
|
+
# Cache key includes menu's updated_at to auto-invalidate on changes
|
|
41
|
+
cache_key = "panda_cms_page_menu/#{menu.id}/#{menu.updated_at.to_i}/items"
|
|
42
|
+
|
|
43
|
+
cached_items = Rails.cache.fetch(cache_key, expires_in: 1.hour) do
|
|
44
|
+
menu.menu_items.order(:lft).to_a
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
@menu_item = cached_items.first
|
|
40
48
|
|
|
41
49
|
# Set default styles if not already set
|
|
42
50
|
@styles[:indent_with] ||= "pl-2" if @styles
|
|
@@ -18,6 +18,16 @@ module Panda
|
|
|
18
18
|
attr_accessor :content, :block_content_id
|
|
19
19
|
|
|
20
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
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def render_content
|
|
21
31
|
div(class: "panda-cms-content", **element_attrs) do
|
|
22
32
|
if @editable_state
|
|
23
33
|
# Empty div for EditorJS to initialize into
|
|
@@ -199,23 +209,21 @@ module Panda
|
|
|
199
209
|
attrs = {class: "panda-cms-content"}
|
|
200
210
|
|
|
201
211
|
if @editable_state
|
|
202
|
-
attrs
|
|
203
|
-
|
|
204
|
-
data:
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
)
|
|
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
|
+
}
|
|
219
227
|
end
|
|
220
228
|
|
|
221
229
|
attrs
|
|
@@ -247,6 +255,30 @@ module Panda
|
|
|
247
255
|
|
|
248
256
|
nil
|
|
249
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}"
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def render_content_to_string
|
|
279
|
+
# Render the component HTML to a string for caching
|
|
280
|
+
view_context.content_tag(:div, @rendered_content.html_safe, class: "panda-cms-content", **element_attrs)
|
|
281
|
+
end
|
|
250
282
|
end
|
|
251
283
|
end
|
|
252
284
|
end
|
|
@@ -18,7 +18,13 @@ module Panda
|
|
|
18
18
|
def view_template
|
|
19
19
|
return unless @content
|
|
20
20
|
|
|
21
|
-
|
|
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
|
rescue => e
|
|
23
29
|
handle_error(e)
|
|
24
30
|
end
|
|
@@ -35,11 +41,11 @@ module Panda
|
|
|
35
41
|
block = find_block
|
|
36
42
|
return false if block.nil?
|
|
37
43
|
|
|
38
|
-
|
|
39
|
-
@plain_text =
|
|
44
|
+
find_block_content(block)
|
|
45
|
+
@plain_text = @block_content_obj&.content.to_s
|
|
40
46
|
|
|
41
47
|
if @editable_state
|
|
42
|
-
setup_editable_content(
|
|
48
|
+
setup_editable_content(@block_content_obj)
|
|
43
49
|
else
|
|
44
50
|
@content = prepare_content_for_display(@plain_text)
|
|
45
51
|
end
|
|
@@ -54,7 +60,7 @@ module Panda
|
|
|
54
60
|
end
|
|
55
61
|
|
|
56
62
|
def find_block_content(block)
|
|
57
|
-
block.block_contents.find_by(panda_cms_page_id: Current.page.id)
|
|
63
|
+
@block_content_obj = block.block_contents.find_by(panda_cms_page_id: Current.page.id)
|
|
58
64
|
end
|
|
59
65
|
|
|
60
66
|
def setup_editable_content(block_content)
|
|
@@ -66,14 +72,12 @@ module Panda
|
|
|
66
72
|
attrs = @attrs.merge(id: element_id)
|
|
67
73
|
|
|
68
74
|
if @editable_state
|
|
69
|
-
attrs
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
)
|
|
75
|
+
attrs[:contenteditable] = "plaintext-only"
|
|
76
|
+
attrs[:data] = {
|
|
77
|
+
"editable-kind": "plain_text",
|
|
78
|
+
"editable-page-id": Current.page.id,
|
|
79
|
+
"editable-block-content-id": @block_content_id
|
|
80
|
+
}
|
|
77
81
|
end
|
|
78
82
|
|
|
79
83
|
attrs
|
|
@@ -92,13 +96,41 @@ module Panda
|
|
|
92
96
|
view_context.params[:embed_id].present? && view_context.params[:embed_id] == Current.page.id
|
|
93
97
|
end
|
|
94
98
|
|
|
95
|
-
def handle_error(
|
|
99
|
+
def handle_error(_error)
|
|
96
100
|
if !Rails.env.production? || defined?(Sentry)
|
|
97
101
|
raise Panda::CMS::MissingBlockError, "Block with key #{@key} not found for page #{Current.page.title}"
|
|
98
102
|
end
|
|
99
103
|
|
|
100
104
|
false
|
|
101
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
|
+
view_context.content_tag(:span, @content.html_safe, **element_attrs)
|
|
133
|
+
end
|
|
102
134
|
end
|
|
103
135
|
end
|
|
104
136
|
end
|
|
@@ -36,7 +36,7 @@ module Panda
|
|
|
36
36
|
# @type GET
|
|
37
37
|
def edit
|
|
38
38
|
add_breadcrumb @menu.name, edit_admin_cms_menu_path(@menu)
|
|
39
|
-
render :edit
|
|
39
|
+
render :edit
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
# @type PATCH/PUT
|
|
@@ -44,7 +44,7 @@ module Panda
|
|
|
44
44
|
if @menu.update(menu_params)
|
|
45
45
|
redirect_to admin_cms_menus_path, notice: "Menu was successfully updated."
|
|
46
46
|
else
|
|
47
|
-
render :edit,
|
|
47
|
+
render :edit, status: :unprocessable_entity
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
50
|
|
|
@@ -66,7 +66,7 @@ module Panda
|
|
|
66
66
|
flash: {success: "This page was successfully updated!"}
|
|
67
67
|
else
|
|
68
68
|
flash[:error] = "There was an error updating the page."
|
|
69
|
-
render :edit, status: :unprocessable_entity
|
|
69
|
+
render :edit, locals: {page: page, template: page.template}, status: :unprocessable_entity
|
|
70
70
|
end
|
|
71
71
|
end
|
|
72
72
|
|
|
@@ -99,7 +99,11 @@ module Panda
|
|
|
99
99
|
# @type private
|
|
100
100
|
# @return ActionController::StrongParameters
|
|
101
101
|
def page_params
|
|
102
|
-
params.require(:page).permit(
|
|
102
|
+
params.require(:page).permit(
|
|
103
|
+
:title, :path, :panda_cms_template_id, :parent_id, :status, :page_type,
|
|
104
|
+
:seo_title, :seo_description, :seo_keywords, :seo_index_mode, :canonical_url,
|
|
105
|
+
:og_title, :og_description, :og_type, :og_image, :inherit_seo
|
|
106
|
+
)
|
|
103
107
|
end
|
|
104
108
|
end
|
|
105
109
|
end
|