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
|
@@ -1,32 +1,150 @@
|
|
|
1
1
|
<%= render Panda::Core::Admin::ContainerComponent.new(full_height: true) do |component| %>
|
|
2
|
-
<% component.heading(text: "#{page.title}", level: 1
|
|
2
|
+
<% component.heading(text: "#{page.title}", level: 1, meta: (link_to page.path, target: "_blank", class: "text-black/60 hover:text-black/80 inline-flex items-center gap-x-1" do
|
|
3
|
+
raw("#{page.path}<i class='fa-solid fa-arrow-up-right-from-square text-xs ml-1'></i>")
|
|
4
|
+
end)) do |h| %>
|
|
5
|
+
<% h.button(text: "Page Details", action: :secondary, as_button: true, size: :regular, id: "open-page-details", icon: "gear", data: { action: "click->toggle#toggle touch->toggle#toggle" }) %>
|
|
6
|
+
<% h.button(text: "Save Changes", action: :save, href: "#", size: :regular, id: "saveEditableButton", icon: "check") %>
|
|
7
|
+
<% end %>
|
|
3
8
|
<% component.with_slideover(title: "Page Details") do %>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
<% parent_seo_data = page.parent ? {
|
|
10
|
+
seoTitle: page.parent.seo_title,
|
|
11
|
+
seoDescription: page.parent.seo_description,
|
|
12
|
+
seoKeywords: page.parent.seo_keywords,
|
|
13
|
+
canonicalUrl: page.parent.canonical_url,
|
|
14
|
+
ogTitle: page.parent.og_title,
|
|
15
|
+
ogDescription: page.parent.og_description,
|
|
16
|
+
ogType: page.parent.og_type
|
|
17
|
+
}.to_json : '{}' %>
|
|
18
|
+
<% ai_generation_url = begin
|
|
19
|
+
admin_cms_pro_ai_generate_seo_path(page.id)
|
|
20
|
+
rescue NoMethodError
|
|
21
|
+
nil
|
|
22
|
+
end %>
|
|
23
|
+
<%= panda_cms_form_with model: page, url: admin_cms_page_path, method: :put, id: "page-form", data: {
|
|
24
|
+
controller: "page-form",
|
|
25
|
+
page_form_parent_seo_data_value: parent_seo_data,
|
|
26
|
+
page_form_page_id_value: page.id,
|
|
27
|
+
page_form_ai_generation_url_value: ai_generation_url
|
|
28
|
+
} do |f| %>
|
|
29
|
+
<%= f.text_field :title, { data: { page_form_target: "pageTitle" } } %>
|
|
30
|
+
<%= f.text_field :template, value: template.name, readonly: true %>
|
|
7
31
|
<%= f.select :status, options_for_select([["Active", "active"], ["Draft", "draft"], ["Hidden", "hidden"], ["Archived", "archived"]], selected: page.status) %>
|
|
8
32
|
<% if page.page_type.in?(["system", "posts"]) %>
|
|
9
|
-
<%= f.text_field :page_type, value: page.page_type.humanize, readonly: true
|
|
33
|
+
<%= f.text_field :page_type, value: page.page_type.humanize, readonly: true %>
|
|
10
34
|
<% else %>
|
|
11
|
-
<%= f.select :page_type, options_for_select([["
|
|
35
|
+
<%= f.select :page_type, options_for_select([["Standard", "standard"], ["Hidden", "hidden"], ["Code", "code"]], selected: page.page_type) %>
|
|
36
|
+
<% end %>
|
|
37
|
+
|
|
38
|
+
<%= f.section_heading "SEO Settings" %>
|
|
39
|
+
|
|
40
|
+
<% if defined?(Panda::CMS::Pro) && ai_generation_url.present? %>
|
|
41
|
+
<div class="mb-4">
|
|
42
|
+
<%= render Panda::Core::Admin::ButtonComponent.new(
|
|
43
|
+
text: "Generate With AI",
|
|
44
|
+
action: :save,
|
|
45
|
+
icon: "wand-magic-sparkles",
|
|
46
|
+
size: :regular,
|
|
47
|
+
as_button: true,
|
|
48
|
+
data: {
|
|
49
|
+
action: "click->page-form#generateSeoWithAI",
|
|
50
|
+
page_form_target: "generateButton"
|
|
51
|
+
}
|
|
52
|
+
) %>
|
|
53
|
+
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Uses AI to analyze page content and generate optimized SEO fields.</p>
|
|
54
|
+
</div>
|
|
12
55
|
<% end %>
|
|
13
|
-
|
|
56
|
+
|
|
57
|
+
<% unless page.parent_id.nil? %>
|
|
58
|
+
<%= f.check_box :inherit_seo, {
|
|
59
|
+
label: "Inherit SEO from parent page",
|
|
60
|
+
data: {
|
|
61
|
+
page_form_target: "inheritCheckbox"
|
|
62
|
+
},
|
|
63
|
+
"data-action": "change->page-form#toggleInherit"
|
|
64
|
+
} %>
|
|
65
|
+
<% end %>
|
|
66
|
+
|
|
67
|
+
<div class="space-y-4">
|
|
68
|
+
<%= f.text_field :seo_title, {
|
|
69
|
+
meta: "Max 70 characters. Leave blank to use page title.",
|
|
70
|
+
max_length: 70,
|
|
71
|
+
data: { page_form_target: "seoTitle" }
|
|
72
|
+
} %>
|
|
73
|
+
<%= f.text_area :seo_description, {
|
|
74
|
+
rows: 3,
|
|
75
|
+
meta: "Max 160 characters. Shown in search results.",
|
|
76
|
+
max_length: 160,
|
|
77
|
+
data: { page_form_target: "seoDescription" }
|
|
78
|
+
} %>
|
|
79
|
+
<%= f.text_field :seo_keywords, {
|
|
80
|
+
meta: "Comma-separated keywords for search engines.",
|
|
81
|
+
data: { page_form_target: "seoKeywords" }
|
|
82
|
+
} %>
|
|
83
|
+
<%= f.radio_button_group :seo_index_mode,
|
|
84
|
+
[["Visible to search engines", "visible"], ["Hidden from search engines", "invisible"]],
|
|
85
|
+
{ meta: "Control how search engines index this page." } %>
|
|
86
|
+
<%= f.text_field :canonical_url, {
|
|
87
|
+
meta: "Optional. Full URL to the canonical version of this page.",
|
|
88
|
+
data: { page_form_target: "canonicalUrl" }
|
|
89
|
+
} %>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<%= f.section_heading "Social Sharing" %>
|
|
93
|
+
|
|
94
|
+
<div class="space-y-4">
|
|
95
|
+
<%= f.text_field :og_title, {
|
|
96
|
+
label: "Social Media Title",
|
|
97
|
+
meta: "Max 60 characters. Leave blank to use SEO title.",
|
|
98
|
+
max_length: 60,
|
|
99
|
+
data: { page_form_target: "ogTitle" }
|
|
100
|
+
} %>
|
|
101
|
+
<%= f.text_area :og_description, {
|
|
102
|
+
label: "Social Media Description",
|
|
103
|
+
rows: 2,
|
|
104
|
+
meta: "Max 200 characters. Used on Facebook, LinkedIn, Twitter.",
|
|
105
|
+
max_length: 200,
|
|
106
|
+
data: { page_form_target: "ogDescription" }
|
|
107
|
+
} %>
|
|
108
|
+
<%= f.select :og_type,
|
|
109
|
+
options_for_select(
|
|
110
|
+
[["Website", "website"], ["Article", "article"], ["Profile", "profile"], ["Video", "video"], ["Book", "book"]],
|
|
111
|
+
selected: page.og_type
|
|
112
|
+
),
|
|
113
|
+
{ label: "Content Type" },
|
|
114
|
+
{ data: { page_form_target: "ogType" } } %>
|
|
115
|
+
<%= f.file_field :og_image, {
|
|
116
|
+
label: "Social Media Image",
|
|
117
|
+
meta: "Recommended: 1200×630px (1.91:1 ratio). Used on Facebook, LinkedIn, Twitter.",
|
|
118
|
+
accept: "image/png,image/jpeg,image/jpg,image/webp",
|
|
119
|
+
with_cropper: true,
|
|
120
|
+
aspect_ratio: 1.91,
|
|
121
|
+
min_width: 1200,
|
|
122
|
+
min_height: 630
|
|
123
|
+
} %>
|
|
124
|
+
<% if page.og_image.attached? %>
|
|
125
|
+
<div class="mt-2">
|
|
126
|
+
<%= image_tag page.og_image.variant(:og_share), class: "max-w-xs rounded border border-gray-300" %>
|
|
127
|
+
</div>
|
|
128
|
+
<% end %>
|
|
129
|
+
</div>
|
|
14
130
|
<% end %>
|
|
15
131
|
<% end %>
|
|
16
|
-
|
|
132
|
+
<% component.with_footer do %>
|
|
133
|
+
<button type="button" data-action="click->toggle#toggle touch->toggle#toggle" class="inline-flex items-center gap-x-1.5 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring inset-ring-gray-300 hover:bg-gray-50 cursor-pointer dark:bg-white/10 dark:text-gray-100 dark:shadow-none dark:inset-ring-white/5 dark:hover:bg-white/20">
|
|
134
|
+
<i class="fa-solid fa-xmark"></i> Cancel
|
|
135
|
+
</button>
|
|
136
|
+
<button type="submit" form="page-form" class="inline-flex items-center gap-x-1.5 justify-center rounded-md bg-mid px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-mid/80 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-mid cursor-pointer dark:shadow-none">
|
|
137
|
+
<i class="fa-solid fa-check"></i> Save
|
|
138
|
+
</button>
|
|
139
|
+
<% end %>
|
|
140
|
+
|
|
141
|
+
<%= content_tag :div, id: "successMessage", class: "hidden fixed top-4 right-4 z-50" do %>
|
|
17
142
|
<%= render Panda::Core::Admin::FlashMessageComponent.new(kind: :success, message: "This page was successfully updated!", temporary: false) %>
|
|
18
|
-
|
|
19
|
-
|
|
143
|
+
<% end %>
|
|
144
|
+
|
|
145
|
+
<%= content_tag :div, id: "errorMessage", class: "hidden fixed top-4 right-4 z-50" do %>
|
|
20
146
|
<%= render Panda::Core::Admin::FlashMessageComponent.new(kind: :error, message: "There was an error updating this page.", temporary: false) %>
|
|
21
|
-
|
|
22
|
-
<div class="grid grid-cols-2 mb-4 -mt-5">
|
|
23
|
-
<div>
|
|
24
|
-
<a class="inline-block mb-2 text-sm text-black/60" target="_blank" href="<%= @page.path %>"><%= @page.path %> <i class="ml-2 fa-solid fa-arrow-up-right-from-square"></i></a>
|
|
25
|
-
</div>
|
|
26
|
-
<div class="flex justify-end">
|
|
27
|
-
<%= render Panda::Core::Admin::ButtonComponent.new(text: "Save Changes", action: :save, icon: "check", link: "#", size: :regular, id: "saveEditableButton") %>
|
|
28
|
-
</div>
|
|
29
|
-
</div>
|
|
147
|
+
<% end %>
|
|
30
148
|
|
|
31
149
|
<%= content_tag :iframe, nil,
|
|
32
150
|
src: "#{page.path}?embed_id=#{page.id}",
|
|
@@ -37,6 +155,81 @@
|
|
|
37
155
|
controller: "editor-iframe",
|
|
38
156
|
editor_iframe_page_id_value: @page.id,
|
|
39
157
|
editor_iframe_admin_path_value: "#{admin_cms_dashboard_url}",
|
|
40
|
-
editor_iframe_autosave_value: false
|
|
158
|
+
editor_iframe_autosave_value: false,
|
|
159
|
+
editor_iframe_assets_value: panda_cms_injectable_assets
|
|
41
160
|
} %>
|
|
161
|
+
|
|
162
|
+
<%# Vanilla JS character counters - works independently of Stimulus %>
|
|
163
|
+
<script type="text/javascript">
|
|
164
|
+
(function() {
|
|
165
|
+
function initCharacterCounters() {
|
|
166
|
+
var fieldConfigs = [
|
|
167
|
+
{ selector: '[data-page-form-target="seoTitle"]', max: 70, label: 'SEO Title' },
|
|
168
|
+
{ selector: '[data-page-form-target="seoDescription"]', max: 160, label: 'SEO Description' },
|
|
169
|
+
{ selector: '[data-page-form-target="ogTitle"]', max: 60, label: 'Social Media Title' },
|
|
170
|
+
{ selector: '[data-page-form-target="ogDescription"]', max: 200, label: 'Social Media Description' }
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
fieldConfigs.forEach(function(config) {
|
|
174
|
+
var field = document.querySelector(config.selector);
|
|
175
|
+
if (!field) return;
|
|
176
|
+
|
|
177
|
+
createCharacterCounter(field, config.max);
|
|
178
|
+
field.addEventListener('input', function() {
|
|
179
|
+
updateCharacterCounter(field, config.max);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function createCharacterCounter(field, maxLength) {
|
|
185
|
+
var container = field.closest('.panda-core-field-container');
|
|
186
|
+
if (!container) return;
|
|
187
|
+
|
|
188
|
+
var counter = container.querySelector('.character-counter');
|
|
189
|
+
if (!counter) {
|
|
190
|
+
counter = document.createElement('div');
|
|
191
|
+
counter.className = 'character-counter text-xs mt-1 text-gray-500 dark:text-gray-400';
|
|
192
|
+
|
|
193
|
+
var errorMsg = container.querySelector('.text-red-600');
|
|
194
|
+
if (errorMsg) {
|
|
195
|
+
errorMsg.parentNode.insertBefore(counter, errorMsg);
|
|
196
|
+
} else {
|
|
197
|
+
container.appendChild(counter);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
updateCharacterCounter(field, maxLength);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function updateCharacterCounter(field, maxLength) {
|
|
205
|
+
var container = field.closest('.panda-core-field-container');
|
|
206
|
+
if (!container) return;
|
|
207
|
+
|
|
208
|
+
var counter = container.querySelector('.character-counter');
|
|
209
|
+
if (!counter) return;
|
|
210
|
+
|
|
211
|
+
var currentLength = field.value.length;
|
|
212
|
+
var remaining = maxLength - currentLength;
|
|
213
|
+
|
|
214
|
+
counter.textContent = currentLength + ' / ' + maxLength + ' characters';
|
|
215
|
+
|
|
216
|
+
counter.className = 'character-counter text-xs mt-1';
|
|
217
|
+
|
|
218
|
+
if (remaining < 0) {
|
|
219
|
+
counter.className += ' text-red-600 dark:text-red-400 font-semibold';
|
|
220
|
+
counter.textContent += ' (' + Math.abs(remaining) + ' over limit)';
|
|
221
|
+
} else if (remaining < 10) {
|
|
222
|
+
counter.className += ' text-yellow-600 dark:text-yellow-400';
|
|
223
|
+
} else {
|
|
224
|
+
counter.className += ' text-gray-500 dark:text-gray-400';
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (document.readyState === 'loading') {
|
|
229
|
+
document.addEventListener('DOMContentLoaded', initCharacterCounters);
|
|
230
|
+
} else {
|
|
231
|
+
initCharacterCounters();
|
|
232
|
+
}
|
|
233
|
+
})();
|
|
234
|
+
</script>
|
|
42
235
|
<% end %>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
|
|
2
2
|
<% component.heading(text: "Pages", level: 1) do |heading| %>
|
|
3
|
-
<% heading.button(action: :add, text: "Add Page",
|
|
3
|
+
<% heading.button(action: :add, text: "Add Page", href: new_admin_cms_page_path) %>
|
|
4
4
|
<% end %>
|
|
5
5
|
|
|
6
6
|
<% if root_page %>
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<% table.column("Name") do |page| %>
|
|
10
10
|
<div class="flex items-start" data-tree-target="row" data-page-id="<%= page.id %>" data-level="<%= page.level %>" data-parent-id="<%= page.parent_id %>">
|
|
11
11
|
<div class="flex-1 min-w-0">
|
|
12
|
-
|
|
12
|
+
<%# Add indentation for nested levels %>
|
|
13
13
|
<% indent_class = case page.level
|
|
14
14
|
when 0 then ""
|
|
15
15
|
when 1 then "ml-4"
|
|
@@ -19,10 +19,10 @@
|
|
|
19
19
|
<div class="flex items-start <%= indent_class %>">
|
|
20
20
|
<div class="flex-1 min-w-0">
|
|
21
21
|
<div class="flex items-center gap-2">
|
|
22
|
-
|
|
22
|
+
<%# Chevron/icon section %>
|
|
23
23
|
<% if page.children_count > 0 %>
|
|
24
24
|
<% if page.level > 0 %>
|
|
25
|
-
|
|
25
|
+
<%# Non-root pages with children: show chevron button only %>
|
|
26
26
|
<button type="button"
|
|
27
27
|
class="text-xs text-gray-500 hover:text-gray-700 focus:outline-none focus:ring-2 focus:ring-primary rounded px-1 w-4 flex items-center justify-center"
|
|
28
28
|
data-tree-target="toggle"
|
|
@@ -31,11 +31,11 @@
|
|
|
31
31
|
<i class="fa-solid fa-chevron-right"></i>
|
|
32
32
|
</button>
|
|
33
33
|
<% else %>
|
|
34
|
-
|
|
34
|
+
<%# Root page (Home): show folder icon %>
|
|
35
35
|
<i class="fa-solid fa-folder text-gray-500 mr-2"></i>
|
|
36
36
|
<% end %>
|
|
37
37
|
<% else %>
|
|
38
|
-
|
|
38
|
+
<%# Leaf pages: show file icon %>
|
|
39
39
|
<i class="fa-solid fa-file text-gray-400 mr-2"></i>
|
|
40
40
|
<% end %>
|
|
41
41
|
<%= link_to page.title, edit_admin_cms_page_path(page), class: "font-medium hover:text-primary #{page.children_count > 0 && page.level > 0 ? 'ml-2' : ''}" %>
|
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
<div data-controller="slug" data-slug-add-date-prefix-value="true">
|
|
5
5
|
<%= f.text_field :title,
|
|
6
6
|
data: {
|
|
7
|
-
"slug-target": "input_text"
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
"slug-target": "input_text"
|
|
8
|
+
},
|
|
9
|
+
"data-action": "focusout->slug#generatePath" %>
|
|
10
10
|
<%= f.text_field :slug,
|
|
11
11
|
data: {
|
|
12
12
|
"slug-target": "output_text"
|
|
13
13
|
} %>
|
|
14
14
|
</div>
|
|
15
|
-
<%= f.select :author_id, Panda::Core::User.
|
|
15
|
+
<%= f.select :author_id, Panda::Core::User.admins.map { |u| [u.name, u.id] } %>
|
|
16
16
|
<%= f.datetime_field :published_at %>
|
|
17
17
|
<%= f.select :status, options_for_select([["Active", "active"], ["Draft", "draft"], ["Hidden", "hidden"], ["Archived", "archived"]], selected: post.status) %>
|
|
18
18
|
|
|
@@ -22,16 +22,55 @@
|
|
|
22
22
|
<%= f.hidden_field :content,
|
|
23
23
|
data: {
|
|
24
24
|
editor_form_target: "hiddenField",
|
|
25
|
-
initial_content: editor_content_for(post, local_assigns[:preserved_content])
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
initial_content: editor_content_for(post, local_assigns[:preserved_content])
|
|
26
|
+
},
|
|
27
|
+
"data-action": "change->editor-form#handleContentChange" %>
|
|
28
28
|
<div id="<%= editor_id %>"
|
|
29
29
|
data-editor-form-target="editorContainer"
|
|
30
30
|
class="max-w-full block bg-white pt-1 mb-4 mt-2 border border-mid rounded-md min-h-[300px]">
|
|
31
31
|
</div>
|
|
32
32
|
</div>
|
|
33
33
|
|
|
34
|
-
<%= f.
|
|
34
|
+
<%= f.section_heading "SEO Settings" %>
|
|
35
|
+
|
|
36
|
+
<div class="space-y-4">
|
|
37
|
+
<%= f.text_field :seo_title, { meta: "Max 70 characters. Leave blank to use post title." } %>
|
|
38
|
+
<%= f.text_area :seo_description, { rows: 3, meta: "Max 160 characters. Leave blank to use post excerpt." } %>
|
|
39
|
+
<%= f.text_field :seo_keywords, { meta: "Comma-separated keywords for search engines." } %>
|
|
40
|
+
<%= f.radio_button_group :seo_index_mode,
|
|
41
|
+
[["Visible to search engines", "visible"], ["Hidden from search engines", "invisible"]],
|
|
42
|
+
{ meta: "Control how search engines index this post." } %>
|
|
43
|
+
<%= f.text_field :canonical_url, { meta: "Optional. Full URL to the canonical version of this post." } %>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<%= f.section_heading "Social Sharing" %>
|
|
47
|
+
|
|
48
|
+
<div class="space-y-4">
|
|
49
|
+
<%= f.text_field :og_title, { label: "Social Media Title", meta: "Max 60 characters. Leave blank to use SEO title." } %>
|
|
50
|
+
<%= f.text_area :og_description, { label: "Social Media Description", rows: 2, meta: "Max 200 characters. Used on Facebook, LinkedIn, Twitter." } %>
|
|
51
|
+
<%= f.select :og_type,
|
|
52
|
+
options_for_select(
|
|
53
|
+
[["Website", "website"], ["Article", "article"], ["Profile", "profile"], ["Video", "video"], ["Book", "book"]],
|
|
54
|
+
selected: post.og_type
|
|
55
|
+
),
|
|
56
|
+
{ label: "Content Type" } %>
|
|
57
|
+
<%= f.file_field :og_image, {
|
|
58
|
+
label: "Social Media Image",
|
|
59
|
+
meta: "Recommended: 1200×630px (1.91:1 ratio). Used on Facebook, LinkedIn, Twitter.",
|
|
60
|
+
accept: "image/png,image/jpeg,image/jpg,image/webp",
|
|
61
|
+
with_cropper: true,
|
|
62
|
+
aspect_ratio: 1.91,
|
|
63
|
+
min_width: 1200,
|
|
64
|
+
min_height: 630
|
|
65
|
+
} %>
|
|
66
|
+
<% if f.object.present? && f.object.persisted? && f.object.og_image.attached? %>
|
|
67
|
+
<div class="mt-2">
|
|
68
|
+
<%= image_tag f.object.og_image.variant(:og_share), class: "max-w-xs rounded border border-gray-300" %>
|
|
69
|
+
</div>
|
|
70
|
+
<% end %>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<%= f.submit f.object.persisted? ? "Update Post" : "Create Post",
|
|
35
74
|
class: "btn btn-primary",
|
|
36
75
|
data: {
|
|
37
76
|
disable_with: "Saving...",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
|
|
2
2
|
<% component.heading(text: post.title, level: 1) do |heading| %>
|
|
3
|
-
<% heading.button(action: :view, text: "View Post",
|
|
3
|
+
<% heading.button(action: :view, text: "View Post", href: post_path(post.admin_param)) %>
|
|
4
4
|
<% end %>
|
|
5
5
|
|
|
6
6
|
<%= render "form", post: post, url: admin_cms_post_path(post.admin_param), method: :patch %>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
|
|
2
2
|
<% component.heading(text: "Posts", level: 1, icon: "fa-solid fa-newspaper") do |heading| %>
|
|
3
|
-
<% heading.button(action: :add, text: "Add Post",
|
|
3
|
+
<% heading.button(action: :add, text: "Add Post", href: new_admin_cms_post_path) %>
|
|
4
4
|
<% end %>
|
|
5
5
|
|
|
6
6
|
<%= render Panda::Core::Admin::TableComponent.new(term: "post", rows: posts, icon: "fa-solid fa-newspaper") do |table| %>
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
</span>
|
|
13
13
|
</div>
|
|
14
14
|
<% end %>
|
|
15
|
-
<% table.column("Status") { |post| render Panda::Core::Admin::TagComponent.new(status: post.status) } %>
|
|
16
|
-
<% table.column("Published") { |post| render Panda::
|
|
17
|
-
<% table.column("Last Updated") { |post| render Panda::
|
|
15
|
+
<% table.column("Status") { |post| render Panda::Core::Admin::TagComponent.new(status: post.status.to_sym) } %>
|
|
16
|
+
<% table.column("Published") { |post| render Panda::Core::Admin::UserActivityComponent.new(at: post.published_at, user: post.author)} %>
|
|
17
|
+
<% table.column("Last Updated") { |post| render Panda::Core::Admin::UserActivityComponent.new(at: post.updated_at)} %>
|
|
18
18
|
<% end %>
|
|
19
19
|
|
|
20
20
|
<% end %>
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
<link rel="apple-touch-icon" sizes="180x180" href="/panda-
|
|
2
|
-
<link rel="icon" type="image/png" sizes="32x32" href="/panda-
|
|
3
|
-
<link rel="icon" type="image/png" sizes="16x16" href="/panda-
|
|
4
|
-
<link rel="manifest" href="/panda-
|
|
5
|
-
<link rel="mask-icon" href="/panda-
|
|
6
|
-
<link rel="shortcut icon" href="/panda-
|
|
1
|
+
<link rel="apple-touch-icon" sizes="180x180" href="/panda-core-assets/favicons/apple-touch-icon.png">
|
|
2
|
+
<link rel="icon" type="image/png" sizes="32x32" href="/panda-core-assets/favicons/favicon-32x32.png">
|
|
3
|
+
<link rel="icon" type="image/png" sizes="16x16" href="/panda-core-assets/favicons/favicon-16x16.png">
|
|
4
|
+
<link rel="manifest" href="/panda-core-assets/favicons/site.webmanifest">
|
|
5
|
+
<link rel="mask-icon" href="/panda-core-assets/favicons/safari-pinned-tab.svg" color="#5bbad5">
|
|
6
|
+
<link rel="shortcut icon" href="/panda-core-assets/favicons/favicon.ico">
|
|
7
7
|
<meta name="msapplication-TileColor" content="#b91d47">
|
|
8
|
-
<meta name="msapplication-config" content="/panda-
|
|
8
|
+
<meta name="msapplication-config" content="/panda-core-assets/favicons/browserconfig.xml">
|
|
9
9
|
<meta name="theme-color" content="#ffffff">
|
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
<html>
|
|
3
3
|
<head>
|
|
4
4
|
<title>Panda CMS 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>Test Header</h1>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ignored_warnings": [
|
|
3
|
+
{
|
|
4
|
+
"warning_type": "Dynamic Render Path",
|
|
5
|
+
"warning_code": 15,
|
|
6
|
+
"fingerprint": "cbd974174800cbaa06ad90971544cdfb087e88c17ca6ca42d5e9b7e2f62f544f",
|
|
7
|
+
"check_name": "Render",
|
|
8
|
+
"message": "Render path contains parameter value",
|
|
9
|
+
"file": "app/views/panda/cms/admin/menus/edit.html.erb",
|
|
10
|
+
"line": 5,
|
|
11
|
+
"link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
|
|
12
|
+
"code": "render(action => Panda::Core::Admin::FormErrorComponent.new(:model => Panda::CMS::Menu.find(params[:id])), { :locals => ({ :\"panda::core::admin::formerrorcomponent\" => Panda::Core::Admin::FormErrorComponent.new(:model => Panda::CMS::Menu.find(params[:id])) }) })",
|
|
13
|
+
"render_path": [
|
|
14
|
+
{
|
|
15
|
+
"type": "controller",
|
|
16
|
+
"class": "Panda::CMS::Admin::MenusController",
|
|
17
|
+
"method": "edit",
|
|
18
|
+
"line": 39,
|
|
19
|
+
"file": "app/controllers/panda/cms/admin/menus_controller.rb",
|
|
20
|
+
"rendered": {
|
|
21
|
+
"name": "panda/cms/admin/menus/edit",
|
|
22
|
+
"file": "app/views/panda/cms/admin/menus/edit.html.erb"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"location": {
|
|
27
|
+
"type": "template",
|
|
28
|
+
"template": "panda/cms/admin/menus/edit"
|
|
29
|
+
},
|
|
30
|
+
"user_input": "params[:id]",
|
|
31
|
+
"confidence": "Weak",
|
|
32
|
+
"cwe_id": [22],
|
|
33
|
+
"note": "False positive: The component class is hardcoded (Panda::Core::Admin::FormErrorComponent), not dynamically determined. The model parameter is validated through Rails' find method and only used as data to pass to the component, not to determine the rendering path."
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"updated": "2025-11-09 00:00:00 +0000",
|
|
37
|
+
"brakeman_version": "7.1.1"
|
|
38
|
+
}
|
data/config/importmap.rb
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
# Base dependencies are now in panda-core (Stimulus, Turbo, Font Awesome, etc.)
|
|
4
4
|
# This file only contains CMS-specific pins
|
|
5
|
+
# NOTE: Paths must be absolute (starting with /) because Rack::Static serves
|
|
6
|
+
# from /panda/cms/, not from asset pipeline /assets/
|
|
5
7
|
|
|
6
|
-
pin "
|
|
7
|
-
pin "@hotwired/stimulus-loading", to: "panda/cms/stimulus-loading.js", preload: true
|
|
8
|
-
pin "@editorjs/editorjs", to: "panda/cms/editor/editorjs.js" # @2.30.6
|
|
8
|
+
pin "panda/cms/application", to: "/panda/cms/application.js", preload: true
|
|
9
|
+
pin "@hotwired/stimulus-loading", to: "/panda/cms/stimulus-loading.js", preload: true
|
|
10
|
+
pin "@editorjs/editorjs", to: "/panda/cms/editor/editorjs.js" # @2.30.6
|
|
9
11
|
|
|
10
12
|
# Pin the controllers directory
|
|
11
|
-
pin "controllers", to: "panda/cms/controllers/index.js"
|
|
12
|
-
pin_all_from Panda::CMS::Engine.root.join("app/javascript/panda/cms/controllers"), under: "controllers"
|
|
13
|
-
pin_all_from Panda::CMS::Engine.root.join("app/javascript/panda/cms/editor"), under: "editor"
|
|
13
|
+
pin "panda/cms/controllers/index", to: "/panda/cms/controllers/index.js"
|
|
14
|
+
pin_all_from Panda::CMS::Engine.root.join("app/javascript/panda/cms/controllers"), under: "controllers", to: "/panda/cms/controllers"
|
data/config/locales/en.yml
CHANGED
|
@@ -29,6 +29,17 @@ en:
|
|
|
29
29
|
title: Title
|
|
30
30
|
path: URL
|
|
31
31
|
panda_cms_template_id: Template
|
|
32
|
+
page_type: Page Type
|
|
33
|
+
seo_title: SEO Title
|
|
34
|
+
seo_description: SEO Description
|
|
35
|
+
seo_keywords: SEO Keywords
|
|
36
|
+
seo_index_mode: Search Engine Visibility
|
|
37
|
+
canonical_url: Canonical URL
|
|
38
|
+
og_title: OpenGraph Title
|
|
39
|
+
og_description: OpenGraph Description
|
|
40
|
+
og_type: OpenGraph Type
|
|
41
|
+
og_image: OpenGraph Image
|
|
42
|
+
inherit_seo: Inherit Settings
|
|
32
43
|
panda/cms/post:
|
|
33
44
|
title: Title
|
|
34
45
|
slug: URL
|
|
@@ -36,6 +47,15 @@ en:
|
|
|
36
47
|
user_id: Author
|
|
37
48
|
published_at: Published At
|
|
38
49
|
post_content: Content
|
|
50
|
+
seo_title: SEO Title
|
|
51
|
+
seo_description: SEO Description
|
|
52
|
+
seo_keywords: SEO Keywords
|
|
53
|
+
seo_index_mode: Search Engine Visibility
|
|
54
|
+
canonical_url: Canonical URL
|
|
55
|
+
og_title: OpenGraph Title
|
|
56
|
+
og_description: OpenGraph Description
|
|
57
|
+
og_type: OpenGraph Type
|
|
58
|
+
og_image: OpenGraph Image
|
|
39
59
|
statuses:
|
|
40
60
|
active: Active
|
|
41
61
|
draft: Draft
|
|
@@ -48,7 +68,27 @@ en:
|
|
|
48
68
|
external_url: External URL
|
|
49
69
|
panda/cms_page_id: Page
|
|
50
70
|
panda/cms/user:
|
|
51
|
-
|
|
52
|
-
lastname: Last Name
|
|
71
|
+
name: Name
|
|
53
72
|
email: Email Address
|
|
54
73
|
current_theme: Theme
|
|
74
|
+
enums:
|
|
75
|
+
panda/cms/page:
|
|
76
|
+
seo_index_mode:
|
|
77
|
+
visible: Visible to search engines
|
|
78
|
+
invisible: Hidden from search engines
|
|
79
|
+
og_type:
|
|
80
|
+
website: Website
|
|
81
|
+
article: Article
|
|
82
|
+
profile: Profile
|
|
83
|
+
video: Video
|
|
84
|
+
book: Book
|
|
85
|
+
panda/cms/post:
|
|
86
|
+
seo_index_mode:
|
|
87
|
+
visible: Visible to search engines
|
|
88
|
+
invisible: Hidden from search engines
|
|
89
|
+
og_type:
|
|
90
|
+
website: Website
|
|
91
|
+
article: Article
|
|
92
|
+
profile: Profile
|
|
93
|
+
video: Video
|
|
94
|
+
book: Book
|
data/config/routes.rb
CHANGED
|
@@ -10,7 +10,7 @@ Panda::CMS::Engine.routes.draw do
|
|
|
10
10
|
|
|
11
11
|
namespace admin_path.delete_prefix("/").to_sym, path: "#{admin_path}/cms", as: :admin_cms, module: :admin do
|
|
12
12
|
resources :files
|
|
13
|
-
resources :forms
|
|
13
|
+
resources :forms
|
|
14
14
|
resources :menus
|
|
15
15
|
resources :pages do
|
|
16
16
|
resources :block_contents, only: %i[update]
|
|
@@ -2,18 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
class AddNestedSetsToPandaCMSPages < ActiveRecord::Migration[7.1]
|
|
4
4
|
def self.up
|
|
5
|
-
Panda::CMS::Page.where(parent_id: 0).update_all(parent_id: nil)
|
|
6
5
|
add_column :panda_cms_pages, :lft, :integer
|
|
7
6
|
add_column :panda_cms_pages, :rgt, :integer
|
|
8
|
-
|
|
9
|
-
# This is necessary to update :lft and :rgt columns
|
|
10
|
-
Panda::CMS::Page.reset_column_information
|
|
11
|
-
|
|
12
|
-
# Only rebuild if there are existing pages
|
|
13
|
-
# On fresh installs, there won't be any pages yet
|
|
14
|
-
if Panda::CMS::Page.any?
|
|
15
|
-
Panda::CMS::Page.rebuild!
|
|
16
|
-
end
|
|
17
7
|
end
|
|
18
8
|
|
|
19
9
|
def self.down
|
|
@@ -11,16 +11,10 @@ class AddNestedToPandaCMSMenuItems < ActiveRecord::Migration[7.1]
|
|
|
11
11
|
add_index :panda_cms_menu_items, :lft
|
|
12
12
|
add_index :panda_cms_menu_items, :rgt
|
|
13
13
|
|
|
14
|
-
Panda::CMS::MenuItem.reset_column_information
|
|
15
|
-
Panda::CMS::MenuItem.rebuild!
|
|
16
|
-
|
|
17
14
|
# Update pages whilst we're at it
|
|
18
15
|
add_column :panda_cms_pages, :depth, :integer
|
|
19
16
|
add_column :panda_cms_pages, :children_count, :integer, null: false, default: 0
|
|
20
17
|
add_index :panda_cms_pages, :lft
|
|
21
18
|
add_index :panda_cms_pages, :rgt
|
|
22
|
-
|
|
23
|
-
Panda::CMS::Page.reset_column_information
|
|
24
|
-
Panda::CMS::Page.rebuild!
|
|
25
19
|
end
|
|
26
20
|
end
|
|
@@ -8,7 +8,7 @@ class CreatePandaCMSVisits < ActiveRecord::Migration[7.1]
|
|
|
8
8
|
|
|
9
9
|
t.references :panda_cms_page, null: true, foreign_key: true, type: :uuid
|
|
10
10
|
t.references :panda_cms_redirect, null: true, foreign_key: true, type: :uuid
|
|
11
|
-
t.references :
|
|
11
|
+
t.references :panda_core_user, null: true, foreign_key: true, type: :uuid
|
|
12
12
|
t.timestamps
|
|
13
13
|
end
|
|
14
14
|
end
|
|
@@ -17,7 +17,7 @@ class CreatePandaCMSPosts < ActiveRecord::Migration[7.1]
|
|
|
17
17
|
t.text :content
|
|
18
18
|
t.datetime :published_at
|
|
19
19
|
t.references :post_tag, type: :uuid, null: false, foreign_key: {to_table: :panda_cms_post_tags}
|
|
20
|
-
t.references :user, type: :uuid, null: false, foreign_key: {to_table: :
|
|
20
|
+
t.references :user, type: :uuid, null: false, foreign_key: {to_table: :panda_core_users}
|
|
21
21
|
t.timestamps
|
|
22
22
|
t.index :slug, unique: true
|
|
23
23
|
end
|
|
@@ -5,7 +5,7 @@ class FixPandaCMSVisitsColumnNames < ActiveRecord::Migration[7.1]
|
|
|
5
5
|
change_table :panda_cms_visits do |t|
|
|
6
6
|
t.rename :panda_cms_page_id, :page_id
|
|
7
7
|
t.rename :panda_cms_redirect_id, :redirect_id
|
|
8
|
-
t.rename :
|
|
8
|
+
t.rename :panda_core_user_id, :user_id
|
|
9
9
|
t.string :referrer, null: true
|
|
10
10
|
t.datetime :visited_at
|
|
11
11
|
t.string :url, null: true
|