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
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
// Stimulus loading utilities for Panda CMS
|
|
2
2
|
// This provides the loading functionality that would normally come from stimulus-rails
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// Import the shared Stimulus application from Panda Core
|
|
5
|
+
// This ensures all controllers (Core and CMS) are registered in the same application
|
|
6
|
+
// Use the importmap module name, not an absolute path
|
|
7
|
+
import { application } from "panda/core/application"
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
// Configure debug mode based on environment
|
|
9
|
-
const railsEnv = document.body?.dataset?.environment || "production";
|
|
10
|
-
application.debug = railsEnv === "development"
|
|
11
|
-
window.Stimulus = application
|
|
9
|
+
// The application is already started and configured in Core
|
|
10
|
+
// No need to start it again or configure debug mode
|
|
12
11
|
|
|
13
12
|
// Auto-registration functionality
|
|
14
13
|
function eagerLoadControllersFrom(context) {
|
|
@@ -12,8 +12,17 @@ module Panda
|
|
|
12
12
|
|
|
13
13
|
validates :block, presence: true, uniqueness: {scope: :page}
|
|
14
14
|
|
|
15
|
+
after_save :refresh_page_cached_timestamp
|
|
16
|
+
after_destroy :refresh_page_cached_timestamp
|
|
17
|
+
|
|
15
18
|
store_accessor :content, [], prefix: true
|
|
16
19
|
store_accessor :cached_content, [], prefix: true
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def refresh_page_cached_timestamp
|
|
24
|
+
page&.refresh_last_updated_at!
|
|
25
|
+
end
|
|
17
26
|
end
|
|
18
27
|
end
|
|
19
28
|
end
|
|
@@ -6,6 +6,7 @@ module Panda
|
|
|
6
6
|
self.table_name = "panda_cms_menus"
|
|
7
7
|
|
|
8
8
|
after_save :generate_auto_menu_items, if: -> { kind == "auto" }
|
|
9
|
+
after_commit :clear_menu_cache
|
|
9
10
|
|
|
10
11
|
has_many :menu_items, lambda {
|
|
11
12
|
order(lft: :asc)
|
|
@@ -50,6 +51,17 @@ module Panda
|
|
|
50
51
|
|
|
51
52
|
errors.add(:start_page, "can't be blank")
|
|
52
53
|
end
|
|
54
|
+
|
|
55
|
+
#
|
|
56
|
+
# Clear fragment cache when menu is updated
|
|
57
|
+
# This ensures menu changes appear immediately on the front-end
|
|
58
|
+
#
|
|
59
|
+
# @return nil
|
|
60
|
+
# @visibility private
|
|
61
|
+
#
|
|
62
|
+
def clear_menu_cache
|
|
63
|
+
Rails.cache.delete("panda_cms_menu/#{name}/#{id}")
|
|
64
|
+
end
|
|
53
65
|
end
|
|
54
66
|
end
|
|
55
67
|
end
|
|
@@ -38,12 +38,47 @@ module Panda
|
|
|
38
38
|
enum :status, {
|
|
39
39
|
active: "active",
|
|
40
40
|
draft: "draft",
|
|
41
|
+
pending_review: "pending_review",
|
|
41
42
|
hidden: "hidden",
|
|
42
43
|
archived: "archived"
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
enum :page_type, {
|
|
47
|
+
standard: "standard",
|
|
48
|
+
hidden_type: "hidden",
|
|
49
|
+
system: "system",
|
|
50
|
+
posts: "posts",
|
|
51
|
+
code: "code"
|
|
52
|
+
}, prefix: :type
|
|
53
|
+
|
|
54
|
+
enum :seo_index_mode, {
|
|
55
|
+
visible: "visible",
|
|
56
|
+
invisible: "invisible"
|
|
57
|
+
}, prefix: :seo
|
|
58
|
+
|
|
59
|
+
enum :og_type, {
|
|
60
|
+
website: "website",
|
|
61
|
+
article: "article",
|
|
62
|
+
profile: "profile",
|
|
63
|
+
video: "video",
|
|
64
|
+
book: "book"
|
|
65
|
+
}, prefix: :og
|
|
66
|
+
|
|
67
|
+
# Active Storage attachment for Open Graph image
|
|
68
|
+
has_one_attached :og_image do |attachable|
|
|
69
|
+
attachable.variant :og_share, resize_to_limit: [1200, 630]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# SEO validations
|
|
73
|
+
validates :seo_title, length: {maximum: 70}, allow_blank: true
|
|
74
|
+
validates :seo_description, length: {maximum: 160}, allow_blank: true
|
|
75
|
+
validates :og_title, length: {maximum: 60}, allow_blank: true
|
|
76
|
+
validates :og_description, length: {maximum: 200}, allow_blank: true
|
|
77
|
+
validates :canonical_url, format: {with: URI::DEFAULT_PARSER.make_regexp(%w[http https])}, allow_blank: true
|
|
78
|
+
|
|
45
79
|
# Callbacks
|
|
46
80
|
after_save :handle_after_save
|
|
81
|
+
before_save :update_cached_last_updated_at
|
|
47
82
|
|
|
48
83
|
#
|
|
49
84
|
# Update any menus which include this page or its parent as a menu item
|
|
@@ -56,6 +91,110 @@ module Panda
|
|
|
56
91
|
menus_of_parent.find_each(&:generate_auto_menu_items)
|
|
57
92
|
end
|
|
58
93
|
|
|
94
|
+
#
|
|
95
|
+
# Returns the most recent update time between the page and its block contents
|
|
96
|
+
# Uses cached value for performance
|
|
97
|
+
#
|
|
98
|
+
# @return [Time] The most recent updated_at timestamp
|
|
99
|
+
# @visibility public
|
|
100
|
+
#
|
|
101
|
+
def last_updated_at
|
|
102
|
+
cached_last_updated_at || updated_at
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
#
|
|
106
|
+
# Refresh the cached last updated timestamp
|
|
107
|
+
# Used when block contents are updated
|
|
108
|
+
#
|
|
109
|
+
# @return [Time] The updated timestamp
|
|
110
|
+
# @visibility public
|
|
111
|
+
#
|
|
112
|
+
def refresh_last_updated_at!
|
|
113
|
+
block_content_updated_at = block_contents.maximum(:updated_at)
|
|
114
|
+
new_timestamp = [updated_at, block_content_updated_at].compact.max
|
|
115
|
+
update_column(:cached_last_updated_at, new_timestamp)
|
|
116
|
+
new_timestamp
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
#
|
|
120
|
+
# Returns the effective SEO title for this page
|
|
121
|
+
# Falls back to page title if not set, with optional inheritance
|
|
122
|
+
#
|
|
123
|
+
# @return [String] The SEO title to use
|
|
124
|
+
# @visibility public
|
|
125
|
+
#
|
|
126
|
+
def effective_seo_title
|
|
127
|
+
return seo_title if seo_title.present?
|
|
128
|
+
return title unless inherit_seo
|
|
129
|
+
|
|
130
|
+
# Traverse up tree to find inherited value
|
|
131
|
+
self_and_ancestors.reverse.find { |p| p.seo_title.present? }&.seo_title || title
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
#
|
|
135
|
+
# Returns the effective SEO description for this page
|
|
136
|
+
# With optional inheritance from parent pages
|
|
137
|
+
#
|
|
138
|
+
# @return [String, nil] The SEO description to use
|
|
139
|
+
# @visibility public
|
|
140
|
+
#
|
|
141
|
+
def effective_seo_description
|
|
142
|
+
return seo_description if seo_description.present?
|
|
143
|
+
return nil unless inherit_seo
|
|
144
|
+
|
|
145
|
+
self_and_ancestors.reverse.find { |p| p.seo_description.present? }&.seo_description
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
#
|
|
149
|
+
# Returns the effective Open Graph title
|
|
150
|
+
# Falls back to SEO title, then page title
|
|
151
|
+
#
|
|
152
|
+
# @return [String] The OG title to use
|
|
153
|
+
# @visibility public
|
|
154
|
+
#
|
|
155
|
+
def effective_og_title
|
|
156
|
+
og_title.presence || effective_seo_title
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
#
|
|
160
|
+
# Returns the effective Open Graph description
|
|
161
|
+
# Falls back to SEO description
|
|
162
|
+
#
|
|
163
|
+
# @return [String, nil] The OG description to use
|
|
164
|
+
# @visibility public
|
|
165
|
+
#
|
|
166
|
+
def effective_og_description
|
|
167
|
+
og_description.presence || effective_seo_description
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
#
|
|
171
|
+
# Returns the effective canonical URL for this page
|
|
172
|
+
# Falls back to the page's own URL if not explicitly set
|
|
173
|
+
#
|
|
174
|
+
# @return [String] The canonical URL to use
|
|
175
|
+
# @visibility public
|
|
176
|
+
#
|
|
177
|
+
def effective_canonical_url
|
|
178
|
+
canonical_url.presence || path
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
#
|
|
182
|
+
# Generates the robots meta tag content based on seo_index_mode
|
|
183
|
+
#
|
|
184
|
+
# @return [String] The robots meta tag content (e.g., "index, follow")
|
|
185
|
+
# @visibility public
|
|
186
|
+
#
|
|
187
|
+
def robots_meta_content
|
|
188
|
+
case seo_index_mode
|
|
189
|
+
when "visible"
|
|
190
|
+
"index, follow"
|
|
191
|
+
when "invisible"
|
|
192
|
+
"noindex, nofollow"
|
|
193
|
+
else
|
|
194
|
+
"index, follow" # Default fallback
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
59
198
|
private
|
|
60
199
|
|
|
61
200
|
def validate_unique_path_in_scope
|
|
@@ -124,6 +263,14 @@ module Panda
|
|
|
124
263
|
destination_path: new_path
|
|
125
264
|
)
|
|
126
265
|
end
|
|
266
|
+
|
|
267
|
+
def update_cached_last_updated_at
|
|
268
|
+
# Will be set to updated_at automatically during save
|
|
269
|
+
# Block content updates will call refresh_last_updated_at! separately
|
|
270
|
+
# Only update if column exists (for backwards compatibility with older schemas)
|
|
271
|
+
return unless self.class.column_names.include?("cached_last_updated_at")
|
|
272
|
+
self.cached_last_updated_at = Time.current
|
|
273
|
+
end
|
|
127
274
|
end
|
|
128
275
|
end
|
|
129
276
|
end
|
|
@@ -33,10 +33,36 @@ module Panda
|
|
|
33
33
|
enum :status, {
|
|
34
34
|
active: "active",
|
|
35
35
|
draft: "draft",
|
|
36
|
+
pending_review: "pending_review",
|
|
36
37
|
hidden: "hidden",
|
|
37
38
|
archived: "archived"
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
enum :seo_index_mode, {
|
|
42
|
+
visible: "visible",
|
|
43
|
+
invisible: "invisible"
|
|
44
|
+
}, prefix: :seo
|
|
45
|
+
|
|
46
|
+
enum :og_type, {
|
|
47
|
+
website: "website",
|
|
48
|
+
article: "article",
|
|
49
|
+
profile: "profile",
|
|
50
|
+
video: "video",
|
|
51
|
+
book: "book"
|
|
52
|
+
}, prefix: :og
|
|
53
|
+
|
|
54
|
+
# Active Storage attachment for Open Graph image
|
|
55
|
+
has_one_attached :og_image do |attachable|
|
|
56
|
+
attachable.variant :og_share, resize_to_limit: [1200, 630]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# SEO validations
|
|
60
|
+
validates :seo_title, length: {maximum: 70}, allow_blank: true
|
|
61
|
+
validates :seo_description, length: {maximum: 160}, allow_blank: true
|
|
62
|
+
validates :og_title, length: {maximum: 60}, allow_blank: true
|
|
63
|
+
validates :og_description, length: {maximum: 200}, allow_blank: true
|
|
64
|
+
validates :canonical_url, format: {with: URI::DEFAULT_PARSER.make_regexp(%w[http https])}, allow_blank: true
|
|
65
|
+
|
|
40
66
|
def to_param
|
|
41
67
|
# For date-based URLs, return just the slug portion
|
|
42
68
|
parts = CGI.unescape(slug).delete_prefix("/").split("/")
|
|
@@ -87,6 +113,78 @@ module Panda
|
|
|
87
113
|
text.truncate(length).html_safe
|
|
88
114
|
end
|
|
89
115
|
|
|
116
|
+
#
|
|
117
|
+
# Returns the effective SEO title for this post
|
|
118
|
+
# Falls back to post title if not set
|
|
119
|
+
#
|
|
120
|
+
# @return [String] The SEO title to use
|
|
121
|
+
# @visibility public
|
|
122
|
+
#
|
|
123
|
+
def effective_seo_title
|
|
124
|
+
seo_title.presence || title
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
#
|
|
128
|
+
# Returns the effective SEO description for this post
|
|
129
|
+
# Falls back to excerpt if not set
|
|
130
|
+
#
|
|
131
|
+
# @return [String, nil] The SEO description to use
|
|
132
|
+
# @visibility public
|
|
133
|
+
#
|
|
134
|
+
def effective_seo_description
|
|
135
|
+
seo_description.presence || excerpt(160, squish: true)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
#
|
|
139
|
+
# Returns the effective Open Graph title
|
|
140
|
+
# Falls back to SEO title, then post title
|
|
141
|
+
#
|
|
142
|
+
# @return [String] The OG title to use
|
|
143
|
+
# @visibility public
|
|
144
|
+
#
|
|
145
|
+
def effective_og_title
|
|
146
|
+
og_title.presence || effective_seo_title
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
#
|
|
150
|
+
# Returns the effective Open Graph description
|
|
151
|
+
# Falls back to SEO description or excerpt
|
|
152
|
+
#
|
|
153
|
+
# @return [String, nil] The OG description to use
|
|
154
|
+
# @visibility public
|
|
155
|
+
#
|
|
156
|
+
def effective_og_description
|
|
157
|
+
og_description.presence || effective_seo_description
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
#
|
|
161
|
+
# Returns the effective canonical URL for this post
|
|
162
|
+
# Falls back to the post's own URL if not explicitly set
|
|
163
|
+
#
|
|
164
|
+
# @return [String] The canonical URL to use
|
|
165
|
+
# @visibility public
|
|
166
|
+
#
|
|
167
|
+
def effective_canonical_url
|
|
168
|
+
canonical_url.presence || slug
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
#
|
|
172
|
+
# Generates the robots meta tag content based on seo_index_mode
|
|
173
|
+
#
|
|
174
|
+
# @return [String] The robots meta tag content (e.g., "index, follow")
|
|
175
|
+
# @visibility public
|
|
176
|
+
#
|
|
177
|
+
def robots_meta_content
|
|
178
|
+
case seo_index_mode
|
|
179
|
+
when "visible"
|
|
180
|
+
"index, follow"
|
|
181
|
+
when "invisible"
|
|
182
|
+
"noindex, nofollow"
|
|
183
|
+
else
|
|
184
|
+
"index, follow" # Default fallback
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
90
188
|
private
|
|
91
189
|
|
|
92
190
|
def clear_menu_cache
|
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
<html>
|
|
3
3
|
<head>
|
|
4
4
|
<title>Test Homepage</title>
|
|
5
|
-
|
|
6
|
-
<!-- Include Panda CMS assets for editor functionality when in edit mode -->
|
|
7
|
-
<%= panda_cms_complete_assets %>
|
|
8
|
-
<% end %>
|
|
5
|
+
<!-- CMS assets are automatically injected by editor_iframe_controller when in edit mode -->
|
|
9
6
|
</head>
|
|
10
7
|
<body>
|
|
11
8
|
<h1><%= @page.title %></h1>
|
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
<html>
|
|
3
3
|
<head>
|
|
4
4
|
<title>Test Page</title>
|
|
5
|
-
|
|
6
|
-
<!-- Include Panda CMS assets for editor functionality when in edit mode -->
|
|
7
|
-
<%= panda_cms_complete_assets %>
|
|
8
|
-
<% end %>
|
|
5
|
+
<!-- CMS assets are automatically injected by editor_iframe_controller when in edit mode -->
|
|
9
6
|
</head>
|
|
10
7
|
<body>
|
|
11
8
|
<h1><%= @page.title %></h1>
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<div class="" data-controller="dashboard">
|
|
2
2
|
<%= render Panda::Core::Admin::ContainerComponent.new do |container| %>
|
|
3
|
-
<% container.
|
|
4
|
-
<% heading.
|
|
3
|
+
<% container.heading(text: "Dashboard", level: 1) do |heading| %>
|
|
4
|
+
<% heading.button(action: :add, text: "Add Page", href: new_admin_cms_page_path) %>
|
|
5
5
|
<% end %>
|
|
6
6
|
<dl class="grid grid-cols-1 gap-5 mt-5 sm:grid-cols-3">
|
|
7
|
-
<%= render Panda::
|
|
8
|
-
<%= render Panda::
|
|
9
|
-
<%= render Panda::
|
|
7
|
+
<%= render Panda::Core::Admin::StatisticsComponent.new(metric: "Views Today", value: Panda::CMS::Visit.group_by_day(:visited_at, last: 1).count.values.first) %>
|
|
8
|
+
<%= render Panda::Core::Admin::StatisticsComponent.new(metric: "Views Last Week", value: Panda::CMS::Visit.group_by_week(:visited_at, last: 1).count.values.first) %>
|
|
9
|
+
<%= render Panda::Core::Admin::StatisticsComponent.new(metric: "Views Last Month", value: Panda::CMS::Visit.group_by_month(:visited_at, last: 1).count.values.first) %>
|
|
10
10
|
</dl>
|
|
11
11
|
<% end %>
|
|
12
12
|
</div>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<div>
|
|
2
|
+
<div class="block overflow-hidden w-full rounded-lg aspect-h-7 aspect-w-10">
|
|
3
|
+
<% if file.image? %>
|
|
4
|
+
<%= image_tag main_app.rails_blob_path(file, only_path: true), alt: file.filename.to_s, class: "object-cover" %>
|
|
5
|
+
<% else %>
|
|
6
|
+
<div class="flex items-center justify-center h-full bg-gray-100">
|
|
7
|
+
<div class="text-center">
|
|
8
|
+
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
|
9
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
|
10
|
+
</svg>
|
|
11
|
+
<p class="mt-1 text-xs text-gray-500 uppercase"><%= file.content_type&.split("/")&.last || "file" %></p>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
<% end %>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="flex justify-between items-start mt-4">
|
|
17
|
+
<div>
|
|
18
|
+
<h2 class="text-lg font-medium text-gray-900"><span class="sr-only">Details for </span><%= file.filename %></h2>
|
|
19
|
+
<p class="text-sm font-medium text-gray-500"><%= number_to_human_size(file.byte_size) %></p>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div>
|
|
25
|
+
<h3 class="font-medium text-gray-900">Information</h3>
|
|
26
|
+
<dl class="mt-2 border-t border-b border-gray-200 divide-y divide-gray-200">
|
|
27
|
+
<div class="flex justify-between py-3 text-sm font-medium">
|
|
28
|
+
<dt class="text-gray-500">Created</dt>
|
|
29
|
+
<dd class="text-gray-900 whitespace-nowrap"><%= file.created_at.strftime("%B %-d, %Y") %></dd>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="flex justify-between py-3 text-sm font-medium">
|
|
32
|
+
<dt class="text-gray-500">Content Type</dt>
|
|
33
|
+
<dd class="text-gray-900 whitespace-nowrap"><%= file.content_type %></dd>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="flex justify-between py-3 text-sm font-medium">
|
|
36
|
+
<dt class="text-gray-500">Checksum</dt>
|
|
37
|
+
<dd class="text-gray-900 whitespace-nowrap font-mono text-xs"><%= file.checksum %></dd>
|
|
38
|
+
</div>
|
|
39
|
+
</dl>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div class="flex gap-x-3">
|
|
43
|
+
<%= link_to "Download", main_app.rails_blob_path(file, disposition: "attachment"), class: "flex-1 py-2 px-3 text-sm font-semibold text-white bg-black rounded-md shadow-sm hover:bg-gray-800 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-panda-dark text-center" %>
|
|
44
|
+
<%= button_tag "Delete", type: "button", class: "flex-1 py-2 px-3 text-sm font-semibold text-gray-900 bg-white rounded-md ring-1 ring-inset shadow-sm hover:bg-gray-50 ring-mid", data: { confirm: "Are you sure you want to delete this file?" } %>
|
|
45
|
+
</div>
|
|
@@ -1,124 +1,17 @@
|
|
|
1
1
|
<%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
|
|
2
|
-
<% component.
|
|
2
|
+
<% component.heading(text: "Files", level: 1) %>
|
|
3
3
|
|
|
4
|
-
<% component.
|
|
5
|
-
<div class="pt-4 pb-16 space-y-6">
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
</
|
|
10
|
-
|
|
11
|
-
<div>
|
|
12
|
-
<h2 class="text-lg font-medium text-gray-900"><span class="sr-only">Details for </span>IMG_4985.HEIC</h2>
|
|
13
|
-
<p class="text-sm font-medium text-gray-500">3.9 MB</p>
|
|
14
|
-
</div>
|
|
15
|
-
<button type="button" class="flex relative justify-center items-center ml-4 w-8 h-8 text-gray-400 bg-white rounded-full hover:text-gray-500 hover:bg-gray-100 focus:ring-2 focus:outline-none focus:ring-panda-dark">
|
|
16
|
-
<span class="absolute -inset-1.5"></span>
|
|
17
|
-
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
|
|
18
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" />
|
|
19
|
-
</svg>
|
|
20
|
-
<span class="sr-only">Favorite</span>
|
|
21
|
-
</button>
|
|
22
|
-
</div>
|
|
23
|
-
</div>
|
|
24
|
-
<div>
|
|
25
|
-
<h3 class="font-medium text-gray-900">Information</h3>
|
|
26
|
-
<dl class="mt-2 border-t border-b border-gray-200 divide-y divide-gray-200">
|
|
27
|
-
<div class="flex justify-between py-3 text-sm font-medium">
|
|
28
|
-
<dt class="text-gray-500">Uploaded by</dt>
|
|
29
|
-
<dd class="text-gray-900 whitespace-nowrap">Marie Culver</dd>
|
|
30
|
-
</div>
|
|
31
|
-
<div class="flex justify-between py-3 text-sm font-medium">
|
|
32
|
-
<dt class="text-gray-500">Created</dt>
|
|
33
|
-
<dd class="text-gray-900 whitespace-nowrap">June 8, 2020</dd>
|
|
34
|
-
</div>
|
|
35
|
-
<div class="flex justify-between py-3 text-sm font-medium">
|
|
36
|
-
<dt class="text-gray-500">Last modified</dt>
|
|
37
|
-
<dd class="text-gray-900 whitespace-nowrap">June 8, 2020</dd>
|
|
38
|
-
</div>
|
|
39
|
-
<div class="flex justify-between py-3 text-sm font-medium">
|
|
40
|
-
<dt class="text-gray-500">Dimensions</dt>
|
|
41
|
-
<dd class="text-gray-900 whitespace-nowrap">4032 x 3024</dd>
|
|
42
|
-
</div>
|
|
43
|
-
<div class="flex justify-between py-3 text-sm font-medium">
|
|
44
|
-
<dt class="text-gray-500">Resolution</dt>
|
|
45
|
-
<dd class="text-gray-900 whitespace-nowrap">72 x 72</dd>
|
|
46
|
-
</div>
|
|
47
|
-
</dl>
|
|
48
|
-
</div>
|
|
49
|
-
<div>
|
|
50
|
-
<h3 class="font-medium text-gray-900">Description</h3>
|
|
51
|
-
<div class="flex justify-between items-center mt-2">
|
|
52
|
-
<p class="text-sm italic text-gray-500">Add a description to this image.</p>
|
|
53
|
-
<button type="button" class="flex relative justify-center items-center w-8 h-8 text-gray-400 bg-white rounded-full hover:text-gray-500 hover:bg-gray-100 focus:ring-2 focus:outline-none focus:ring-panda-dark">
|
|
54
|
-
<span class="absolute -inset-1.5"></span>
|
|
55
|
-
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
56
|
-
<path d="M2.695 14.763l-1.262 3.154a.5.5 0 00.65.65l3.155-1.262a4 4 0 001.343-.885L17.5 5.5a2.121 2.121 0 00-3-3L3.58 13.42a4 4 0 00-.885 1.343z" />
|
|
57
|
-
</svg>
|
|
58
|
-
<span class="sr-only">Add description</span>
|
|
59
|
-
</button>
|
|
60
|
-
</div>
|
|
61
|
-
</div>
|
|
62
|
-
<div>
|
|
63
|
-
<h3 class="font-medium text-gray-900">Shared with</h3>
|
|
64
|
-
<ul role="list" class="mt-2 border-t border-b border-gray-200 divide-y divide-gray-200">
|
|
65
|
-
<li class="flex justify-between items-center py-3">
|
|
66
|
-
<div class="flex items-center">
|
|
67
|
-
<img src="https://images.unsplash.com/photo-1502685104226-ee32379fefbe?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=3&w=1024&h=1024&q=80" alt="" class="w-8 h-8 rounded-full">
|
|
68
|
-
<p class="ml-4 text-sm font-medium text-gray-900">Aimee Douglas</p>
|
|
69
|
-
</div>
|
|
70
|
-
<button type="button" class="ml-6 text-sm font-medium bg-white rounded-md focus:ring-2 focus:ring-offset-2 focus:outline-none text-panda-dark hover:text-panda-dark focus:ring-panda-dark">Remove<span class="sr-only"> Aimee Douglas</span></button>
|
|
71
|
-
</li>
|
|
72
|
-
<li class="flex justify-between items-center py-3">
|
|
73
|
-
<div class="flex items-center">
|
|
74
|
-
<img src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-1.2.1&ixqx=oilqXxSqey&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt="" class="w-8 h-8 rounded-full">
|
|
75
|
-
<p class="ml-4 text-sm font-medium text-gray-900">Andrea McMillan</p>
|
|
76
|
-
</div>
|
|
77
|
-
<button type="button" class="ml-6 text-sm font-medium bg-white rounded-md focus:ring-2 focus:ring-offset-2 focus:outline-none text-panda-dark hover:text-panda-dark focus:ring-panda-dark">Remove<span class="sr-only"> Andrea McMillan</span></button>
|
|
78
|
-
</li>
|
|
79
|
-
|
|
80
|
-
<li class="flex justify-between items-center py-2">
|
|
81
|
-
<button type="button" class="flex items-center p-1 -ml-1 bg-white rounded-md focus:ring-2 focus:outline-none group focus:ring-panda-dark">
|
|
82
|
-
<span class="flex justify-center items-center w-8 h-8 text-gray-400 rounded-full border-2 border-gray-300 border-dashed">
|
|
83
|
-
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
84
|
-
<path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" />
|
|
85
|
-
</svg>
|
|
86
|
-
</span>
|
|
87
|
-
<span class="ml-4 text-sm font-medium text-panda-dark group-hover:text-panda-dark">Share</span>
|
|
88
|
-
</button>
|
|
89
|
-
</li>
|
|
90
|
-
</ul>
|
|
91
|
-
</div>
|
|
92
|
-
<div class="flex gap-x-3">
|
|
93
|
-
<button type="button" class="flex-1 py-2 px-3 text-sm font-semibold text-white bg-black rounded-md shadow-sm hover:bg-black focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-panda-dark">Download</button>
|
|
94
|
-
<button type="button" class="flex-1 py-2 px-3 text-sm font-semibold text-gray-900 bg-white rounded-md ring-1 ring-inset shadow-sm hover:bg-gray-50 ring-mid">Delete</button>
|
|
95
|
-
</div>
|
|
4
|
+
<% component.slideover(title: "File Details") do %>
|
|
5
|
+
<div class="pt-4 pb-16 space-y-6" data-file-gallery-target="slideoverContent">
|
|
6
|
+
<% if @selected_file %>
|
|
7
|
+
<%= render partial: "file_details", locals: { file: @selected_file } %>
|
|
8
|
+
<% else %>
|
|
9
|
+
<p class="text-sm text-gray-500">Select a file to view details</p>
|
|
10
|
+
<% end %>
|
|
96
11
|
</div>
|
|
97
12
|
<% end %>
|
|
98
13
|
|
|
99
|
-
|
|
100
|
-
<%=
|
|
101
|
-
|
|
102
|
-
<%= component.with_tab(name: "Favourited") %>
|
|
103
|
-
<% end %>
|
|
104
|
-
|
|
105
|
-
<section>
|
|
106
|
-
<h2 id="gallery-heading" class="sr-only">Recently viewed</h2>
|
|
107
|
-
<ul role="list" class="grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 sm:gap-x-6 md:grid-cols-4 lg:grid-cols-3 xl:grid-cols-4 xl:gap-x-8">
|
|
108
|
-
<li class="relative">
|
|
109
|
-
<!-- Current: "ring-2 ring-panda-dark ring-offset-2", Default: "focus-within:ring-2 focus-within:ring-panda-dark focus-within:ring-offset-2 focus-within:ring-offset-gray-100" -->
|
|
110
|
-
<div class="block overflow-hidden w-full bg-gray-100 rounded-lg ring-2 ring-offset-2 ring-panda-dark aspect-w-10 aspect-h-7 group">
|
|
111
|
-
<!-- Current: "", Default: "group-hover:opacity-75" -->
|
|
112
|
-
<img src="https://images.unsplash.com/photo-1582053433976-25c00369fc93?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=512&q=80" alt="" class="object-cover pointer-events-none">
|
|
113
|
-
<button type="button" class="absolute inset-0 focus:outline-none">
|
|
114
|
-
<span class="sr-only">View details for IMG_4985.HEIC</span>
|
|
115
|
-
</button>
|
|
116
|
-
</div>
|
|
117
|
-
<p class="block mt-2 text-sm font-medium text-gray-900 pointer-events-none truncate">IMG_4985.HEIC</p>
|
|
118
|
-
<p class="block text-sm font-medium text-gray-500 pointer-events-none">3.9 MB</p>
|
|
119
|
-
</li>
|
|
120
|
-
|
|
121
|
-
<!-- More files... -->
|
|
122
|
-
</ul>
|
|
123
|
-
</section>
|
|
14
|
+
<div data-controller="file-gallery" class="pb-24">
|
|
15
|
+
<%= Panda::Core::Admin::FileGalleryComponent.new(files: @files, selected_file: @selected_file) %>
|
|
16
|
+
</div>
|
|
124
17
|
<% end %>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
|
|
2
|
-
<% component.
|
|
2
|
+
<% component.heading(text: "Forms", level: 1, icon: "fa-solid fa-inbox") do |heading| %>
|
|
3
3
|
<% end %>
|
|
4
4
|
|
|
5
|
-
<%= render Panda::Core::Admin::TableComponent.new(term: "form", rows: forms) do |table| %>
|
|
5
|
+
<%= render Panda::Core::Admin::TableComponent.new(term: "form", rows: forms, icon: "fa-solid fa-inbox") do |table| %>
|
|
6
6
|
<% table.column("Name") { |form| block_link_to form.name, admin_cms_form_path(form) } %>
|
|
7
7
|
<% table.column("Status") { |form| render Panda::Core::Admin::TagComponent.new(status: :active) } %>
|
|
8
8
|
<% table.column("Last Submission") do |form| %>
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
|
|
2
|
-
<% component.
|
|
3
|
-
<% end %>
|
|
2
|
+
<% component.heading(text: "Add Form", level: 1) %>
|
|
4
3
|
<%= panda_cms_form_with model: page, url: admin_cms_pages_path, method: :post do |f| %>
|
|
5
4
|
<% options = nested_set_options(Panda::CMS::Page, page) { |i| "#{"-" * i.level} #{i.title} (#{i.path})" } %>
|
|
6
5
|
<div data-controller="slug">
|
|
@@ -1,35 +1,20 @@
|
|
|
1
1
|
<%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
|
|
2
|
-
<% component.
|
|
2
|
+
<% component.heading(text: form.name, level: 1) do |heading| %>
|
|
3
3
|
<% end %>
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
<th scope="col" class="py-3.5 px-3 text-sm font-semibold text-left text-gray-900">Submitted</th>
|
|
13
|
-
</tr>
|
|
14
|
-
</thead>
|
|
15
|
-
<tbody class="bg-white divide-y divide-gray-200">
|
|
16
|
-
<% submissions.each do |submission| %>
|
|
17
|
-
<tr class="relative bg-white cursor-pointer hover:bg-gray-50">
|
|
18
|
-
<% fields.each do |field| %>
|
|
19
|
-
<td class="py-5 px-3 text-sm text-gray-500">
|
|
20
|
-
<% if field[0] == "email" || field[0] == "email_address" %>
|
|
21
|
-
<a href="mailto:<%= submission.data[field[0]] %>" class="border-b border-gray-500 hover:text-gray-900"><%= submission.data[field[0]] %></a>
|
|
22
|
-
<% else %>
|
|
23
|
-
<%= simple_format(submission.data[field[0]]) %>
|
|
24
|
-
<% end %>
|
|
25
|
-
</td>
|
|
26
|
-
<% end %>
|
|
27
|
-
<td class="py-5 px-3 text-sm text-gray-500 whitespace-nowrap">
|
|
28
|
-
<div class="text-gray-500"><%= time_ago_in_words(submission.created_at) %> ago</div>
|
|
29
|
-
</td>
|
|
30
|
-
</tr>
|
|
5
|
+
<%= render Panda::Core::Admin::TableComponent.new(term: "submission", rows: submissions) do |table| %>
|
|
6
|
+
<% fields.each do |field, title| %>
|
|
7
|
+
<% table.column(title) do |submission| %>
|
|
8
|
+
<% if field == "email" || field == "email_address" %>
|
|
9
|
+
<a href="mailto:<%= submission.data[field] %>" class="border-b border-gray-500 hover:text-gray-900"><%= submission.data[field] %></a>
|
|
10
|
+
<% else %>
|
|
11
|
+
<%= simple_format(submission.data[field]) %>
|
|
31
12
|
<% end %>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
13
|
+
<% end %>
|
|
14
|
+
<% end %>
|
|
15
|
+
|
|
16
|
+
<% table.column("Submitted") do |submission| %>
|
|
17
|
+
<%= time_ago_in_words(submission.created_at) %> ago
|
|
18
|
+
<% end %>
|
|
19
|
+
<% end %>
|
|
35
20
|
<% end %>
|