panda-cms 0.7.3 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -4
  3. data/app/assets/builds/panda.cms.css +2 -6
  4. data/app/assets/tailwind/application.css +178 -0
  5. data/app/assets/tailwind/tailwind.config.js +15 -0
  6. data/app/builders/panda/cms/form_builder.rb +15 -32
  7. data/app/components/panda/cms/admin/flash_message_component.html.erb +2 -2
  8. data/app/components/panda/cms/admin/user_activity_component.rb +7 -18
  9. data/app/controllers/panda/cms/admin/block_contents_controller.rb +0 -1
  10. data/app/controllers/panda/cms/admin/forms_controller.rb +0 -1
  11. data/app/controllers/panda/cms/admin/my_profile_controller.rb +43 -0
  12. data/app/controllers/panda/cms/admin/pages_controller.rb +14 -4
  13. data/app/controllers/panda/cms/admin/posts_controller.rb +7 -22
  14. data/app/controllers/panda/cms/form_submissions_controller.rb +2 -0
  15. data/app/controllers/panda/cms/pages_controller.rb +10 -8
  16. data/app/javascript/panda/cms/controllers/index.js +4 -2
  17. data/app/javascript/panda/cms/controllers/slug_controller.js +64 -31
  18. data/app/javascript/panda/cms/controllers/theme_form_controller.js +9 -0
  19. data/app/jobs/panda/cms/record_visit_job.rb +12 -14
  20. data/app/models/panda/cms/application_record.rb +1 -0
  21. data/app/models/panda/cms/block.rb +9 -17
  22. data/app/models/panda/cms/block_content.rb +5 -6
  23. data/app/models/panda/cms/page.rb +24 -11
  24. data/app/models/panda/cms/post.rb +3 -5
  25. data/app/models/panda/cms/redirect.rb +5 -0
  26. data/app/models/panda/cms/template.rb +6 -7
  27. data/app/models/panda/cms/visit.rb +1 -1
  28. data/app/models/panda/social/instagram_post.rb +15 -0
  29. data/app/services/panda/cms/html_to_editor_js_converter.rb +6 -13
  30. data/app/services/panda/social/instagram_feed_service.rb +61 -0
  31. data/app/views/panda/cms/admin/my_profile/edit.html.erb +35 -0
  32. data/app/views/panda/cms/admin/pages/index.html.erb +1 -1
  33. data/app/views/panda/cms/admin/pages/new.html.erb +3 -3
  34. data/app/views/panda/cms/admin/posts/_form.html.erb +10 -0
  35. data/app/views/panda/cms/admin/posts/edit.html.erb +3 -2
  36. data/app/views/panda/cms/admin/posts/index.html.erb +1 -1
  37. data/app/views/panda/cms/admin/settings/index.html.erb +2 -0
  38. data/app/views/panda/cms/admin/shared/_sidebar.html.erb +1 -1
  39. data/app/views/panda/cms/shared/_header.html.erb +4 -2
  40. data/app/views/panda/cms/shared/_importmap.html.erb +1 -0
  41. data/config/locales/en.yml +5 -0
  42. data/config/routes.rb +3 -0
  43. data/db/migrate/20250120235542_remove_paper_trail.rb +55 -0
  44. data/db/migrate/20250126234001_create_panda_social_instagram_posts.rb +14 -0
  45. data/db/migrate/20250504221812_add_current_theme_to_panda_cms_users.rb +5 -0
  46. data/lib/panda/cms/demo_site_generator.rb +25 -4
  47. data/lib/panda/cms/editor_js_content.rb +21 -0
  48. data/lib/panda/cms/engine.rb +7 -5
  49. data/lib/panda-cms/version.rb +1 -1
  50. data/lib/panda-cms.rb +13 -0
  51. data/lib/tasks/panda/cms/install.rake +23 -0
  52. data/lib/tasks/panda/social/instagram.rake +18 -0
  53. data/public/panda-cms-assets/editor-js/core/editorjs.min.js +83 -0
  54. data/public/panda-cms-assets/editor-js/plugins/embed.min.js +2 -0
  55. data/public/panda-cms-assets/editor-js/plugins/header.min.js +9 -0
  56. data/public/panda-cms-assets/editor-js/plugins/nested-list.min.js +2 -0
  57. data/public/panda-cms-assets/editor-js/plugins/paragraph.min.js +9 -0
  58. data/public/panda-cms-assets/editor-js/plugins/quote.min.js +2 -0
  59. data/public/panda-cms-assets/editor-js/plugins/simple-image.min.js +2 -0
  60. data/public/panda-cms-assets/editor-js/plugins/table.min.js +2 -0
  61. metadata +36 -557
  62. data/app/models/action_text/rich_text_version.rb +0 -6
  63. data/app/models/panda/cms/block_content_version.rb +0 -8
  64. data/app/models/panda/cms/page_version.rb +0 -8
  65. data/app/models/panda/cms/post_version.rb +0 -8
  66. data/app/models/panda/cms/template_version.rb +0 -8
  67. data/app/models/panda/cms/version.rb +0 -8
  68. data/config/initializers/panda/cms/paper_trail.rb +0 -7
  69. data/db/migrate/20240904200605_create_action_text_tables.action_text.rb +0 -24
  70. data/db/migrate/20241119214549_remove_action_text_from_posts.rb +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e27fb8ea71f2ea75ce8b82dc1a572e9fc6c55ee7d0e1df48b3af93228881bc0e
4
- data.tar.gz: a58147bb5374e6bfdc70aed581c9b98048b0e3937a4037c2daae7e5b6c70ae57
3
+ metadata.gz: 07d84fd0b6bd17f5e5f253a570bae55270227bec0196037960aa3358d09903bd
4
+ data.tar.gz: c6bc6d0173f89d965a23132bc016bfd3a44e6bd0b2533960cca495fa28e645e3
5
5
  SHA512:
6
- metadata.gz: e6494d67893174959b47efe2276c1fe1ae27606cc268e62d1bf77566bcbf95c37cc935f2a62c0bdc0db24f62b42d2ca44cc36c8a9a2a328bee634d5e079374c1
7
- data.tar.gz: 2d263e8ca147f3c0bc9061f5f9c7149d4705a7b16fbc1d02a7072a71884292574677a588b9dc9dbbae30d90da6e2495f6b43f319fe674b98da2076fe97a64cc7
6
+ metadata.gz: 63b30314a40563ab4cb8f99d516cd30edc3aed7ffe8d1841d4b8760ea6c80c8b253a983ca7cadd473caba240a7b18132cda903b23ed35da845833aef4880b2d4
7
+ data.tar.gz: 820e1b606f088e8bacd7e03d997812f5a2255941e399e8fe863076a0a1196935c66c6a2aca34d50f9dd69563280cda9d795692ccba6df5af565aefd2b38439e2
data/README.md CHANGED
@@ -44,8 +44,8 @@ For initial setup, run:
44
44
 
45
45
  ```shell
46
46
  bundle install
47
- rails generate panda-cms:install
48
- rails panda-cms:install:migrations
47
+ rails generate panda_cms:install
48
+ rails panda_cms:install:migrations
49
49
  rails db:seed
50
50
  ```
51
51
 
@@ -57,7 +57,7 @@ If you don't want to use GitHub to login (or are at a URL other than http://loca
57
57
 
58
58
  This is a non-exhuastive list (there will be many more):
59
59
 
60
- * To date, this has only been tested with Rails 7.1 and 7.2.
60
+ * To date, this has only been tested with Rails 7.1, 7.2 and 8.0
61
61
  * There may be conflicts if you're not using Tailwind CSS on the frontend. Please report this.
62
62
 
63
63
  ## Contributing
@@ -70,4 +70,4 @@ See our [Contributing Guidelines](https://docs.pandacms.io/developers/contributi
70
70
 
71
71
  The gem is available as open source under the terms of the [BSD-3-Clause License](https://opensource.org/licenses/bsd-3-clause).
72
72
 
73
- Copyright © 2024, Panda Software Limited.
73
+ Copyright © 2024 - 2025, Panda Software Limited.
@@ -1182,10 +1182,6 @@ a.block-link:after {
1182
1182
  pointer-events: none;
1183
1183
  }
1184
1184
 
1185
- .pointer-events-auto {
1186
- pointer-events: auto;
1187
- }
1188
-
1189
1185
  .visible {
1190
1186
  visibility: visible;
1191
1187
  }
@@ -1250,8 +1246,8 @@ a.block-link:after {
1250
1246
  z-index: 10;
1251
1247
  }
1252
1248
 
1253
- .z-50 {
1254
- z-index: 50;
1249
+ .z-\[9999\] {
1250
+ z-index: 9999;
1255
1251
  }
1256
1252
 
1257
1253
  .col-span-3 {
@@ -0,0 +1,178 @@
1
+ @import "tailwindcss";
2
+
3
+ @config "tailwind.config.js";
4
+
5
+ @theme {
6
+ --color-white: var(--color-white);
7
+ --color-black: var(--color-black);
8
+ --color-light: var(--color-light);
9
+ --color-mid: var(--color-mid);
10
+ --color-dark: var(--color-dark);
11
+ --color-highlight: var(--color-highlight);
12
+ --color-active: var(--color-active);
13
+ --color-inactive: var(--color-active);
14
+ --color-warning: var(--color-warning);
15
+ --color-error: var(--color-error);
16
+ }
17
+
18
+ @plugin "tailwindcss/typography";
19
+ @plugin "tailwindcss/forms";
20
+
21
+ @layer base {
22
+ html[data-theme="default"] {
23
+ --color-white: 249 249 249; /* #F9F9F9 */
24
+ --color-black: 26 22 29; /* #1A161D */
25
+
26
+ --color-light: 238 206 230; /* #EECEE6 */
27
+ --color-mid: 141 94 183; /* #8D5EB7 */
28
+ --color-dark: 33 29 73; /* #211D49 */
29
+
30
+ --color-highlight: 208 64 20; /* #D04014 */
31
+
32
+ --color-active: 0 135 85; /* #008755 */
33
+ --color-warning: 250 207 142; /* #FACF8E */
34
+ --color-inactive: 216 247 245; /* #d6e4f7 */
35
+ --color-error: 245 129 129; /* #F58181 */
36
+ }
37
+
38
+ html[data-theme="sky"] {
39
+ --color-white: 249 249 249; /* #F9F9F9 */
40
+ --color-black: 26 22 29; /* #1A161D */
41
+ --color-light: 204 238 242; /* #CCEEF2 */
42
+ --color-mid: 42 102 159; /* #2A669F */
43
+ --color-dark: 20 32 74; /* #14204A */
44
+ --color-highlight: 208 64 20; /* #D04014 */
45
+
46
+ --color-active: 69 154 89; /* #459A59 - darker green with better contrast */
47
+ --color-warning: 244 190 102; /* #F4BE66 */
48
+ --color-inactive: 216 247 245; /* #d6e4f7 */
49
+ --color-error: 208 64 20; /* #D04014 */
50
+ }
51
+
52
+ a.block-link:after {
53
+ position: absolute;
54
+ content: "";
55
+ inset: 0;
56
+ }
57
+ }
58
+
59
+ /* Default editor styles */
60
+ @layer components {
61
+ .codex-editor__redactor .ce-block .ce-block__content {
62
+ @apply text-base font-normal font-sans text-dark leading-[1.6] space-y-[1.6rem];
63
+
64
+ h1.ce-header {
65
+ @apply text-3xl md:text-4xl font-semibold font-sans leading-[1.2];
66
+ }
67
+
68
+ h2.ce-header {
69
+ @apply text-2xl font-medium font-sans leading-[1.3] mb-4 mt-8;
70
+ }
71
+
72
+ h3.ce-header {
73
+ @apply text-xl font-normal font-sans leading-[1.3] mb-4 mt-6;
74
+ }
75
+
76
+ p,
77
+ li {
78
+ @apply leading-[1.6] tracking-wide max-w-[85ch];
79
+
80
+ a {
81
+ @apply text-[#1A9597] underline underline-offset-2 hover:text-[#158486] focus:outline-2 focus:outline-offset-2 focus:outline-[#1A9597];
82
+ }
83
+
84
+ strong,
85
+ b {
86
+ @apply font-semibold;
87
+ }
88
+ }
89
+
90
+ p {
91
+ @apply mb-4;
92
+ }
93
+
94
+ .cdx-quote {
95
+ @apply bg-[#eef0f3] border-l-inactive border-l-8 p-6 mb-4;
96
+
97
+ .cdx-quote__caption {
98
+ @apply block ml-6 mt-2 text-sm text-dark;
99
+ }
100
+
101
+ .cdx-quote__text {
102
+ quotes: "\201C" "\201D" "\2018" "\2019";
103
+ @apply pl-6;
104
+
105
+ &:before {
106
+ @apply -ml-8 mr-2 text-dark text-6xl leading-4 align-text-bottom font-serif;
107
+ content: open-quote;
108
+ }
109
+
110
+ p {
111
+ @apply inline italic text-lg;
112
+ }
113
+ }
114
+ }
115
+
116
+ .cdx-list {
117
+ @apply mb-4 pl-6;
118
+
119
+ &--ordered {
120
+ @apply list-decimal;
121
+ }
122
+
123
+ &--unordered {
124
+ @apply list-disc;
125
+ }
126
+
127
+ .cdx-list {
128
+ @apply mt-2 mb-0;
129
+ }
130
+
131
+ .cdx-list__item {
132
+ @apply mb-2 pl-2;
133
+ }
134
+ }
135
+
136
+ .cdx-nested-list {
137
+ @apply mb-4 pl-6;
138
+
139
+ &--ordered {
140
+ @apply list-decimal;
141
+ }
142
+
143
+ &--unordered {
144
+ @apply list-disc;
145
+ }
146
+
147
+ .cdx-nested-list {
148
+ @apply mt-2 mb-0;
149
+ }
150
+
151
+ .cdx-nested-list__item {
152
+ @apply mb-2 pl-2;
153
+ }
154
+ }
155
+
156
+ .cdx-table {
157
+ @apply w-full border-collapse border-2 border-dark my-6;
158
+
159
+ &__head {
160
+ @apply font-semibold border-dark border-r-2 p-3 bg-light;
161
+ }
162
+
163
+ &__row {
164
+ @apply border-dark border-b-2;
165
+ }
166
+
167
+ &__cell {
168
+ @apply border-dark border-r-2 p-3;
169
+ }
170
+ }
171
+
172
+ .cdx-embed {
173
+ iframe {
174
+ @apply w-full border-none;
175
+ }
176
+ }
177
+ }
178
+ }
@@ -0,0 +1,15 @@
1
+ module.exports = {
2
+ content: {
3
+ relative: true,
4
+ files: [
5
+ "../../public/*.html",
6
+ "../../app/views/**/*.html.erb",
7
+ "../../app/builders/panda/cms/**/*.rb",
8
+ "../../app/components/panda/cms/**/*.html.erb",
9
+ "../../app/components/panda/cms/**/*.rb",
10
+ "../../app/helpers/panda/cms/**/*.rb",
11
+ "../../app/javascript/panda/cms/**/*.js",
12
+ "../../vendor/javascript/**/*.js",
13
+ ],
14
+ },
15
+ };
@@ -1,5 +1,3 @@
1
- require "ostruct"
2
-
3
1
  module Panda
4
2
  module CMS
5
3
  class FormBuilder < ActionView::Helpers::FormBuilder
@@ -17,48 +15,48 @@ module Panda
17
15
  content_tag(:div, class: "flex flex-grow") do
18
16
  content_tag(:span, class: "inline-flex items-center px-3 text-base border border-r-none rounded-s-md whitespace-nowrap break-keep") { options.dig(:data, :prefix) } +
19
17
  super(attribute, options.reverse_merge(class: input_styles_prefix + " input-prefix rounded-l-none border-l-none"))
20
- end
18
+ end + error_message(attribute)
21
19
  end
22
20
  else
23
21
  content_tag :div, class: container_styles do
24
- label(attribute) + meta_text(options) + super(attribute, options.reverse_merge(class: input_styles))
22
+ label(attribute) + meta_text(options) + super(attribute, options.reverse_merge(class: input_styles)) + error_message(attribute)
25
23
  end
26
24
  end
27
25
  end
28
26
 
29
27
  def email_field(method, options = {})
30
28
  content_tag :div, class: container_styles do
31
- label(method) + meta_text(options) + super(method, options.reverse_merge(class: input_styles))
29
+ label(method) + meta_text(options) + super(method, options.reverse_merge(class: input_styles)) + error_message(method)
32
30
  end
33
31
  end
34
32
 
35
33
  def datetime_field(method, options = {})
36
34
  content_tag :div, class: container_styles do
37
- label(method) + meta_text(options) + super(method, options.reverse_merge(class: input_styles))
35
+ label(method) + meta_text(options) + super(method, options.reverse_merge(class: input_styles)) + error_message(method)
38
36
  end
39
37
  end
40
38
 
41
39
  def text_area(method, options = {})
42
40
  content_tag :div, class: container_styles do
43
- label(method) + meta_text(options) + super(method, options.reverse_merge(class: input_styles))
41
+ label(method) + meta_text(options) + super(method, options.reverse_merge(class: input_styles)) + error_message(method)
44
42
  end
45
43
  end
46
44
 
47
45
  def password_field(attribute, options = {})
48
46
  content_tag :div, class: container_styles do
49
- label(attribute) + meta_text(options) + super(attribute, options.reverse_merge(class: input_styles))
47
+ label(attribute) + meta_text(options) + super(attribute, options.reverse_merge(class: input_styles)) + error_message(attribute)
50
48
  end
51
49
  end
52
50
 
53
51
  def select(method, choices = nil, options = {}, html_options = {}, &block)
54
52
  content_tag :div, class: container_styles do
55
- label(method) + meta_text(options) + super(method, choices, options, html_options.reverse_merge(class: select_styles)) + select_svg
53
+ label(method) + meta_text(options) + super(method, choices, options, html_options.reverse_merge(class: select_styles)) + select_svg + error_message(method)
56
54
  end
57
55
  end
58
56
 
59
57
  def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
60
58
  content_tag :div, class: container_styles do
61
- label(method) + meta_text(options) + super(method, collection, value_method, text_method, options, html_options.reverse_merge(class: input_styles))
59
+ label(method) + meta_text(options) + super(method, collection, value_method, text_method, options, html_options.reverse_merge(class: input_styles)) + error_message(method)
62
60
  end
63
61
  end
64
62
 
@@ -147,28 +145,6 @@ module Panda
147
145
  end
148
146
  end
149
147
 
150
- def rich_text_area(method, options = {})
151
- content_tag :div, class: container_styles do
152
- label(method) + meta_text(options) + super(method, options.reverse_merge(class: textarea_styles))
153
- end
154
- end
155
-
156
- def rich_text_field(method, options = {})
157
- wrap_field(method, options) do
158
- if defined?(ActionText)
159
- # For test environment
160
- if Rails.env.test?
161
- # Just render a textarea for testing
162
- text_area(method, options.reverse_merge(class: textarea_styles))
163
- else
164
- rich_text_area(method, options.reverse_merge(class: textarea_styles))
165
- end
166
- else
167
- text_area(method, options.reverse_merge(class: textarea_styles))
168
- end
169
- end
170
- end
171
-
172
148
  def meta_text(options)
173
149
  return unless options[:meta]
174
150
  @template.content_tag(:p, options[:meta], class: "block text-black/60 text-sm mb-2")
@@ -229,6 +205,13 @@ module Panda
229
205
  def field_wrapper_styles
230
206
  "mt-1"
231
207
  end
208
+
209
+ def error_message(attribute)
210
+ return unless object.respond_to?(:errors) && object.errors[attribute]&.any?
211
+ content_tag(:p, class: "mt-2 text-sm text-red-600") do
212
+ object.errors[attribute].join(", ")
213
+ end
214
+ end
232
215
  end
233
216
  end
234
217
  end
@@ -1,4 +1,4 @@
1
- <div class="fixed top-2 right-2 z-50 p-2 space-y-4 w-full max-w-sm pointer-events-none sm:items-end"
1
+ <div class="fixed top-2 right-2 z-[9999] p-2 space-y-4 w-full max-w-sm sm:items-end"
2
2
  data-controller="alert"
3
3
  <% if @temporary %> data-alert-dismiss-after-value="3000"<% end %>
4
4
  data-transition-enter="ease-in-out duration-500"
@@ -7,7 +7,7 @@
7
7
  data-transition-leave="ease-in-out duration-500"
8
8
  data-transition-leave-from="translate-x-0 opacity-100"
9
9
  data-transition-leave-to="translate-x-full opacity-0">
10
- <div class="overflow-hidden w-full max-w-sm bg-white rounded-lg ring-1 ring-black ring-opacity-5 shadow-lg pointer-events-auto">
10
+ <div class="overflow-hidden w-full max-w-sm bg-white rounded-lg ring-1 ring-black ring-opacity-5 shadow-lg">
11
11
  <div class="p-4">
12
12
  <div class="flex items-start">
13
13
  <div class="flex-shrink-0">
@@ -8,24 +8,13 @@ module Panda
8
8
  attr_accessor :time
9
9
  attr_accessor :user
10
10
 
11
- # @param whodunnit_to [ActiveRecord::Base] Model instance to which the user activity is related
12
- def initialize(whodunnit_to: nil, at: nil, user: nil)
13
- if whodunnit_to
14
- @model = whodunnit_to
15
- whodunnit_id = @model.versions&.last&.whodunnit
16
- if whodunnit_id
17
- @user = User.find(whodunnit_id)
18
- @time = @model.updated_at
19
- end
20
- elsif user.is_a?(::Panda::CMS::User) && at.is_a?(::ActiveSupport::TimeWithZone)
21
- @user = user
22
- @time = at
23
- end
24
-
25
- if !@time
26
- @user = nil
27
- @time = nil
28
- end
11
+ # @param model [ActiveRecord::Base] Model instance to which the user activity is related
12
+ # @param at [ActiveSupport::TimeWithZone] Time of the activity
13
+ # @param user [Panda::CMS::User] User who performed the activity
14
+ def initialize(model: nil, at: nil, user: nil)
15
+ @model = model
16
+ @user = user if user.is_a?(::Panda::CMS::User)
17
+ @time = at if at.is_a?(::ActiveSupport::TimeWithZone)
29
18
  end
30
19
  end
31
20
  end
@@ -6,7 +6,6 @@ module Panda
6
6
  class BlockContentsController < ApplicationController
7
7
  before_action :set_page, only: %i[update]
8
8
  before_action :set_block_content, only: %i[update]
9
- before_action :set_paper_trail_whodunnit, only: %i[update]
10
9
  before_action :authenticate_admin_user!
11
10
 
12
11
  # @type PATCH/PUT
@@ -5,7 +5,6 @@ module Panda
5
5
  module Admin
6
6
  class FormsController < ApplicationController
7
7
  before_action :set_initial_breadcrumb, only: %i[index show]
8
- # before_action :set_paper_trail_whodunnit, only: %i[create update]
9
8
  before_action :authenticate_admin_user!
10
9
 
11
10
  # Lists all forms
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module CMS
5
+ module Admin
6
+ class MyProfileController < ApplicationController
7
+ before_action :set_initial_breadcrumb, only: %i[edit update]
8
+ before_action :authenticate_admin_user!
9
+
10
+ # Shows the edit form for the current user's profile
11
+ # @type GET
12
+ # @return void
13
+ def edit
14
+ render :edit, locals: {user: current_user}
15
+ end
16
+
17
+ # Updates the current user's profile
18
+ # @type PATCH/PUT
19
+ # @return void
20
+ def update
21
+ if current_user.update(user_params)
22
+ redirect_to edit_admin_my_profile_path, flash: {success: "Your profile has been updated successfully."}
23
+ else
24
+ render :edit, locals: {user: current_user}, status: :unprocessable_entity
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def set_initial_breadcrumb
31
+ add_breadcrumb "My Profile", edit_admin_my_profile_path
32
+ end
33
+
34
+ # Only allow a list of trusted parameters through
35
+ # @type private
36
+ # @return ActionController::StrongParameters
37
+ def user_params
38
+ params.require(:user).permit(:firstname, :lastname, :email, :current_theme)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -5,7 +5,6 @@ module Panda
5
5
  module Admin
6
6
  class PagesController < ApplicationController
7
7
  before_action :set_initial_breadcrumb, only: %i[index edit new create update]
8
- before_action :set_paper_trail_whodunnit, only: %i[create update]
9
8
  before_action :authenticate_admin_user!
10
9
 
11
10
  # Lists all pages which can be managed by the administrator
@@ -34,11 +33,19 @@ module Panda
34
33
  # POST /admin/pages
35
34
  def create
36
35
  page = Panda::CMS::Page.new(page_params)
36
+
37
+ # Normalize empty path to nil so presence validation triggers
38
+ page.path = nil if page.path.blank?
39
+
40
+ # Set the full path before validation if we have a parent
41
+ if page.parent && page.parent.path != "/" && page.path.present?
42
+ page.path = page.parent.path + page.path
43
+ end
44
+
37
45
  if page.save
38
- page.update(path: page.parent.path + page.path) unless page.parent.path == "/"
39
46
  redirect_to edit_admin_page_path(page), notice: "The page was successfully created."
40
47
  else
41
- flash[:error] = "There was an error creating the page."
48
+ flash.now[:error] = page.errors.full_messages.to_sentence
42
49
  locals = setup_new_page_form(page: page)
43
50
  render :new, locals: locals, status: :unprocessable_entity
44
51
  end
@@ -76,7 +83,10 @@ module Panda
76
83
 
77
84
  def setup_new_page_form(page:)
78
85
  add_breadcrumb "Add Page", new_admin_page_path
79
- {page: page}
86
+ {
87
+ page: page,
88
+ available_templates: Panda::CMS::Template.available
89
+ }
80
90
  end
81
91
 
82
92
  # Only allow a list of trusted parameters through.
@@ -6,8 +6,7 @@ module Panda
6
6
  module CMS
7
7
  module Admin
8
8
  class PostsController < ApplicationController
9
- before_action :set_initial_breadcrumb, only: %i[index new edit create update]
10
- before_action :set_paper_trail_whodunnit, only: %i[create update]
9
+ before_action :set_initial_breadcrumb, only: %i[index edit new create update]
11
10
  before_action :authenticate_admin_user!
12
11
 
13
12
  # Get all posts
@@ -29,20 +28,7 @@ module Panda
29
28
  # @type GET
30
29
  def edit
31
30
  add_breadcrumb post.title, edit_admin_post_path(post.admin_param)
32
-
33
- # Get the latest version's content or fall back to post's content
34
- preserved_content = if post.versions.exists?
35
- reified_post = post.versions.last.reify
36
- reified_post&.content || post.content
37
- else
38
- post.content
39
- end
40
-
41
- render :edit, locals: {
42
- post: post,
43
- url: admin_post_path(post.admin_param),
44
- preserved_content: preserved_content
45
- }
31
+ render :edit, locals: {post: post}
46
32
  end
47
33
 
48
34
  # POST /admin/posts
@@ -53,7 +39,8 @@ module Panda
53
39
 
54
40
  if @post.save
55
41
  Rails.logger.debug "Post saved successfully"
56
- redirect_to edit_admin_post_path(@post.admin_param), success: "The post was successfully created!"
42
+ flash[:success] = "The post was successfully created!"
43
+ redirect_to edit_admin_post_path(@post.admin_param), status: :see_other
57
44
  else
58
45
  Rails.logger.debug "Post save failed: #{@post.errors.full_messages.inspect}"
59
46
  flash.now[:error] = @post.errors.full_messages.join(", ")
@@ -75,17 +62,15 @@ module Panda
75
62
  if post.update(update_params)
76
63
  Rails.logger.debug "Post updated successfully"
77
64
  add_breadcrumb post.title, edit_admin_post_path(post.admin_param)
78
- redirect_to edit_admin_post_path(post.admin_param),
79
- status: :see_other,
80
- flash: {success: "The post was successfully updated!"}
65
+ flash[:success] = "The post was successfully updated"
66
+ redirect_to edit_admin_post_path(post.admin_param), status: :see_other
81
67
  else
82
68
  Rails.logger.debug "Post update failed: #{post.errors.full_messages.inspect}"
83
69
  Rails.logger.debug "Preserving content: #{post_params[:content].inspect}"
84
- flash[:error] = post.errors.full_messages.join(", ")
85
70
  add_breadcrumb post.title.presence || "Edit Post", edit_admin_post_path(post.admin_param)
71
+ flash.now[:error] = post.errors.full_messages.join(", ")
86
72
  render :edit, locals: {
87
73
  post: post,
88
- url: admin_post_path(post.admin_param),
89
74
  preserved_content: post_params[:content]
90
75
  }, status: :unprocessable_entity
91
76
  end
@@ -1,6 +1,8 @@
1
1
  module Panda
2
2
  module CMS
3
3
  class FormSubmissionsController < ApplicationController
4
+ invisible_captcha only: [:create]
5
+
4
6
  def create
5
7
  vars = params.except(:authenticity_token, :controller, :action, :id)
6
8
 
@@ -69,21 +69,23 @@ module Panda
69
69
  # Ignore visits from bots (TODO: make this configurable)
70
70
  return true if /bot/i.match?(request.user_agent)
71
71
  # Ignore visits from Honeybadger
72
- return true if request.headers.to_h.key? "Honeybadger-Token"
73
-
72
+ return true if request.headers.to_h.key?("Honeybadger-Token") || request.user_agent == "Honeybadger Uptime Check"
73
+ # Ignore visits where we're asking for PHP files
74
+ return true if request.path.ends_with?(".php")
75
+ # Otherwise, record the visit
74
76
  false
75
77
  end
76
78
 
77
79
  def record_visit
78
80
  RecordVisitJob.perform_later(
79
- url: request.url,
81
+ path: request.path,
82
+ user_id: Current.user&.id,
83
+ redirect_id: @redirect&.id,
84
+ page_id: Current.page&.id,
80
85
  user_agent: request.user_agent,
81
- referrer: request.referrer,
82
86
  ip_address: request.remote_ip,
83
- page_id: Panda::CMS::Current.page&.id,
84
- current_user_id: current_user&.id,
85
- params: params.to_unsafe_h.except(:controller, :action, :path),
86
- visited_at: Time.zone.now
87
+ referer: request.referer, # TODO: Fix the naming of this column
88
+ params: request.parameters
87
89
  )
88
90
  end
89
91
 
@@ -7,8 +7,8 @@ const pandaCmsApplication = PandaCMSApplication.start()
7
7
  console.debug("[Panda CMS] Application started...")
8
8
 
9
9
  // Configure Stimulus development experience
10
- pandaCmsApplication.debug = false
11
- window.pandaCmsStimulus = pandaCmsApplication
10
+ const railsEnv = document.body?.dataset?.environment || "production";
11
+ pandaCmsApplication.debug = railsEnv === "development";
12
12
 
13
13
  console.debug("[Panda CMS] window.pandaCmsStimulus available...")
14
14
 
@@ -28,6 +28,8 @@ import EditorIframeController from "panda/cms/controllers/editor_iframe_controll
28
28
  pandaCmsApplication.register("editor-iframe", EditorIframeController)
29
29
 
30
30
  console.debug("[Panda CMS] Registering components...")
31
+ import ThemeFormController from "panda/cms/controllers/theme_form_controller";
32
+ pandaCmsApplication.register("theme-form", ThemeFormController);
31
33
 
32
34
  // Import and register all TailwindCSS Components or just the ones you need
33
35
  import { Alert, Autosave, ColorPreview, Dropdown, Modal, Tabs, Popover, Toggle, Slideover } from "tailwindcss-stimulus-components"