panda-cms 0.7.3 → 0.7.5

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 (182) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +40 -5
  3. data/Rakefile +2 -0
  4. data/app/assets/builds/panda.cms.css +2 -6
  5. data/app/assets/tailwind/application.css +178 -0
  6. data/app/assets/tailwind/tailwind.config.js +15 -0
  7. data/app/builders/panda/cms/form_builder.rb +27 -36
  8. data/app/components/panda/cms/admin/flash_message_component.html.erb +2 -2
  9. data/app/components/panda/cms/admin/heading_component.rb +5 -4
  10. data/app/components/panda/cms/admin/panel_component.rb +2 -2
  11. data/app/components/panda/cms/admin/statistics_component.rb +1 -2
  12. data/app/components/panda/cms/admin/user_activity_component.html.erb +3 -1
  13. data/app/components/panda/cms/admin/user_activity_component.rb +8 -21
  14. data/app/components/panda/cms/code_component.rb +8 -4
  15. data/app/components/panda/cms/menu_component.rb +7 -6
  16. data/app/components/panda/cms/page_menu_component.rb +15 -17
  17. data/app/components/panda/cms/rich_text_component.rb +5 -6
  18. data/app/components/panda/cms/text_component.rb +6 -7
  19. data/app/constraints/panda/cms/admin_constraint.rb +4 -1
  20. data/app/controllers/panda/cms/admin/block_contents_controller.rb +0 -1
  21. data/app/controllers/panda/cms/admin/dashboard_controller.rb +13 -9
  22. data/app/controllers/panda/cms/admin/forms_controller.rb +0 -3
  23. data/app/controllers/panda/cms/admin/my_profile_controller.rb +44 -0
  24. data/app/controllers/panda/cms/admin/pages_controller.rb +15 -4
  25. data/app/controllers/panda/cms/admin/posts_controller.rb +6 -22
  26. data/app/controllers/panda/cms/admin/sessions_controller.rb +3 -5
  27. data/app/controllers/panda/cms/admin/settings/bulk_editor_controller.rb +32 -25
  28. data/app/controllers/panda/cms/admin/settings_controller.rb +14 -10
  29. data/app/controllers/panda/cms/application_controller.rb +7 -2
  30. data/app/controllers/panda/cms/errors_controller.rb +5 -2
  31. data/app/controllers/panda/cms/form_submissions_controller.rb +4 -0
  32. data/app/controllers/panda/cms/pages_controller.rb +40 -35
  33. data/app/controllers/panda/cms/posts_controller.rb +2 -0
  34. data/app/helpers/panda/cms/admin/files_helper.rb +5 -1
  35. data/app/helpers/panda/cms/admin/pages_helper.rb +5 -1
  36. data/app/helpers/panda/cms/asset_helper.rb +182 -0
  37. data/app/helpers/panda/cms/pages_helper.rb +2 -0
  38. data/app/helpers/panda/cms/posts_helper.rb +2 -0
  39. data/app/helpers/panda/cms/theme_helper.rb +2 -0
  40. data/app/javascript/panda/cms/controllers/editor_form_controller.js +59 -6
  41. data/app/javascript/panda/cms/controllers/index.js +5 -9
  42. data/app/javascript/panda/cms/controllers/slug_controller.js +64 -31
  43. data/app/javascript/panda/cms/controllers/theme_form_controller.js +25 -0
  44. data/app/javascript/panda/cms/stimulus-loading.js +39 -0
  45. data/app/javascript/panda_cms/stimulus-loading.js +39 -0
  46. data/app/jobs/panda/cms/application_job.rb +2 -0
  47. data/app/jobs/panda/cms/record_visit_job.rb +14 -14
  48. data/app/mailers/panda/cms/application_mailer.rb +2 -0
  49. data/app/mailers/panda/cms/form_mailer.rb +3 -1
  50. data/app/models/panda/cms/application_record.rb +3 -0
  51. data/app/models/panda/cms/block.rb +12 -17
  52. data/app/models/panda/cms/block_content.rb +7 -6
  53. data/app/models/panda/cms/breadcrumb.rb +2 -0
  54. data/app/models/panda/cms/current.rb +2 -0
  55. data/app/models/panda/cms/form.rb +2 -0
  56. data/app/models/panda/cms/form_submission.rb +2 -0
  57. data/app/models/panda/cms/menu.rb +12 -9
  58. data/app/models/panda/cms/menu_item.rb +10 -6
  59. data/app/models/panda/cms/page.rb +31 -16
  60. data/app/models/panda/cms/post.rb +12 -10
  61. data/app/models/panda/cms/redirect.rb +9 -1
  62. data/app/models/panda/cms/template.rb +17 -13
  63. data/app/models/panda/cms/user.rb +2 -0
  64. data/app/models/panda/cms/visit.rb +3 -1
  65. data/app/models/panda/social/instagram_post.rb +17 -0
  66. data/app/services/panda/cms/html_to_editor_js_converter.rb +10 -15
  67. data/app/services/panda/social/instagram_feed_service.rb +63 -0
  68. data/app/views/layouts/different_page.html.erb +6 -0
  69. data/app/views/layouts/homepage.html.erb +37 -0
  70. data/app/views/layouts/page.html.erb +18 -0
  71. data/app/views/layouts/panda/cms/application.html.erb +1 -0
  72. data/app/views/panda/cms/admin/my_profile/edit.html.erb +35 -0
  73. data/app/views/panda/cms/admin/pages/index.html.erb +1 -1
  74. data/app/views/panda/cms/admin/pages/new.html.erb +14 -8
  75. data/app/views/panda/cms/admin/posts/_form.html.erb +10 -0
  76. data/app/views/panda/cms/admin/posts/edit.html.erb +3 -2
  77. data/app/views/panda/cms/admin/posts/index.html.erb +1 -1
  78. data/app/views/panda/cms/admin/settings/index.html.erb +3 -1
  79. data/app/views/panda/cms/admin/shared/_sidebar.html.erb +1 -1
  80. data/app/views/panda/cms/shared/_header.html.erb +14 -4
  81. data/app/views/panda/cms/shared/_importmap.html.erb +2 -1
  82. data/app/views/shared/_footer.html.erb +3 -0
  83. data/app/views/shared/_header.html.erb +11 -0
  84. data/config/importmap.rb +2 -0
  85. data/config/initializers/inflections.rb +2 -0
  86. data/config/initializers/panda/cms/form_errors.rb +20 -21
  87. data/config/initializers/panda/cms/healthcheck_log_silencer.rb +2 -0
  88. data/config/initializers/panda/cms.rb +2 -0
  89. data/config/initializers/zeitwork.rb +2 -0
  90. data/config/locales/en.yml +5 -0
  91. data/config/puma/test.rb +3 -1
  92. data/config/routes.rb +11 -8
  93. data/db/migrate/20240205223709_create_panda_cms_pages.rb +2 -0
  94. data/db/migrate/20240219213327_create_panda_cms_page_versions.rb +2 -0
  95. data/db/migrate/20240303002805_create_panda_cms_templates.rb +4 -1
  96. data/db/migrate/20240303003434_create_panda_cms_template_versions.rb +2 -0
  97. data/db/migrate/20240303022441_create_panda_cms_blocks.rb +4 -1
  98. data/db/migrate/20240303024256_create_panda_cms_block_contents.rb +2 -0
  99. data/db/migrate/20240303024746_create_panda_cms_block_content_versions.rb +2 -0
  100. data/db/migrate/20240303233238_add_panda_cms_menu_table.rb +2 -0
  101. data/db/migrate/20240303234724_add_panda_cms_menu_item_table.rb +2 -0
  102. data/db/migrate/20240304134343_add_parent_id_to_panda_cms_pages.rb +2 -0
  103. data/db/migrate/20240315125411_add_status_to_panda_cms_pages.rb +7 -5
  104. data/db/migrate/20240315125421_add_nested_sets_to_panda_cms_pages.rb +2 -0
  105. data/db/migrate/20240316212822_add_kind_to_panda_cms_menus.rb +3 -1
  106. data/db/migrate/20240316221425_add_start_page_to_panda_cms_menus.rb +2 -0
  107. data/db/migrate/20240316230706_add_nested_to_panda_cms_menu_items.rb +2 -0
  108. data/db/migrate/20240317010532_create_panda_cms_users.rb +2 -0
  109. data/db/migrate/20240317161534_add_max_uses_to_panda_cms_template.rb +2 -0
  110. data/db/migrate/20240317163053_reset_counter_cache_on_panda_cms_template.rb +2 -0
  111. data/db/migrate/20240317214827_create_panda_cms_redirects.rb +2 -0
  112. data/db/migrate/20240317230622_create_panda_cms_visits.rb +2 -0
  113. data/db/migrate/20240324205703_create_active_storage_tables.active_storage.rb +5 -2
  114. data/db/migrate/20240408084718_default_panda_cms_users_admin_to_false.rb +2 -0
  115. data/db/migrate/20240701225422_add_service_name_to_active_storage_blobs.active_storage.rb +8 -6
  116. data/db/migrate/20240701225423_create_active_storage_variant_records.active_storage.rb +2 -0
  117. data/db/migrate/20240701225424_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb +2 -0
  118. data/db/migrate/20240804235210_create_panda_cms_forms.rb +2 -0
  119. data/db/migrate/20240805013612_create_panda_cms_form_submissions.rb +2 -0
  120. data/db/migrate/20240805121123_create_panda_cms_posts.rb +3 -1
  121. data/db/migrate/20240805123104_create_panda_cms_post_versions.rb +2 -0
  122. data/db/migrate/20240806112735_fix_panda_cms_visits_column_names.rb +2 -0
  123. data/db/migrate/20240806204412_add_completion_path_to_panda_cms_forms.rb +2 -0
  124. data/db/migrate/20240820081917_change_form_submissions_to_submission_count.rb +2 -0
  125. data/db/migrate/20240923234535_add_depth_to_panda_cms_menus.rb +6 -4
  126. data/db/migrate/20241031205109_add_cached_content_to_panda_cms_block_contents.rb +2 -0
  127. data/db/migrate/20241119214548_convert_post_content_to_editor_js.rb +2 -0
  128. data/db/migrate/20241120000419_remove_post_tag_references.rb +2 -0
  129. data/db/migrate/20241120110943_add_editor_js_to_posts.rb +2 -0
  130. data/db/migrate/20241120113859_add_cached_content_to_panda_cms_posts.rb +2 -0
  131. data/db/migrate/20241123234140_remove_post_tag_id_from_posts.rb +2 -0
  132. data/db/migrate/20250106223303_add_author_id_to_panda_cms_posts.rb +2 -0
  133. data/db/migrate/20250120235542_remove_paper_trail.rb +56 -0
  134. data/db/migrate/20250126234001_create_panda_social_instagram_posts.rb +16 -0
  135. data/db/migrate/20250504221812_add_current_theme_to_panda_cms_users.rb +7 -0
  136. data/db/seeds.rb +2 -0
  137. data/lib/generators/panda/cms/install_generator.rb +2 -0
  138. data/lib/panda/cms/asset_loader.rb +390 -0
  139. data/lib/panda/cms/bulk_editor.rb +7 -3
  140. data/lib/panda/cms/demo_site_generator.rb +27 -4
  141. data/lib/panda/cms/editor_js/blocks/alert.rb +2 -0
  142. data/lib/panda/cms/editor_js/blocks/base.rb +2 -0
  143. data/lib/panda/cms/editor_js/blocks/header.rb +2 -0
  144. data/lib/panda/cms/editor_js/blocks/image.rb +3 -0
  145. data/lib/panda/cms/editor_js/blocks/list.rb +2 -0
  146. data/lib/panda/cms/editor_js/blocks/paragraph.rb +3 -0
  147. data/lib/panda/cms/editor_js/blocks/quote.rb +3 -0
  148. data/lib/panda/cms/editor_js/blocks/table.rb +3 -1
  149. data/lib/panda/cms/editor_js/renderer.rb +3 -0
  150. data/lib/panda/cms/editor_js.rb +2 -0
  151. data/lib/panda/cms/editor_js_content.rb +50 -23
  152. data/lib/panda/cms/engine.rb +36 -37
  153. data/lib/panda/cms/exceptions_app.rb +2 -0
  154. data/lib/panda/cms/railtie.rb +2 -0
  155. data/lib/panda/cms/slug.rb +3 -1
  156. data/lib/panda-cms/version.rb +3 -1
  157. data/lib/panda-cms.rb +17 -2
  158. data/lib/tasks/assets.rake +547 -0
  159. data/lib/tasks/panda/cms/install.rake +25 -0
  160. data/lib/tasks/panda/social/instagram.rake +20 -0
  161. data/lib/tasks/panda_cms.rake +3 -30
  162. data/public/panda-cms-assets/editor-js/core/editorjs.min.js +83 -0
  163. data/public/panda-cms-assets/editor-js/plugins/embed.min.js +2 -0
  164. data/public/panda-cms-assets/editor-js/plugins/header.min.js +9 -0
  165. data/public/panda-cms-assets/editor-js/plugins/nested-list.min.js +2 -0
  166. data/public/panda-cms-assets/editor-js/plugins/paragraph.min.js +9 -0
  167. data/public/panda-cms-assets/editor-js/plugins/quote.min.js +2 -0
  168. data/public/panda-cms-assets/editor-js/plugins/simple-image.min.js +2 -0
  169. data/public/panda-cms-assets/editor-js/plugins/table.min.js +2 -0
  170. data/public/panda-cms-assets/manifest.json +20 -0
  171. data/public/panda-cms-assets/panda-cms-0.7.4.css +26 -0
  172. data/public/panda-cms-assets/panda-cms-0.7.4.js +150 -0
  173. metadata +71 -438
  174. data/app/models/action_text/rich_text_version.rb +0 -6
  175. data/app/models/panda/cms/block_content_version.rb +0 -8
  176. data/app/models/panda/cms/page_version.rb +0 -8
  177. data/app/models/panda/cms/post_version.rb +0 -8
  178. data/app/models/panda/cms/template_version.rb +0 -8
  179. data/app/models/panda/cms/version.rb +0 -8
  180. data/config/initializers/panda/cms/paper_trail.rb +0 -7
  181. data/db/migrate/20240904200605_create_action_text_tables.action_text.rb +0 -24
  182. 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: a11a8249369ac564953d025d4887020ced356b6357a5d8c823580c27e39394cb
4
+ data.tar.gz: 04d6f8dab1ae48f77414ea10b515739f36a54c0f37b79e82f7d9282305af03a1
5
5
  SHA512:
6
- metadata.gz: e6494d67893174959b47efe2276c1fe1ae27606cc268e62d1bf77566bcbf95c37cc935f2a62c0bdc0db24f62b42d2ca44cc36c8a9a2a328bee634d5e079374c1
7
- data.tar.gz: 2d263e8ca147f3c0bc9061f5f9c7149d4705a7b16fbc1d02a7072a71884292574677a588b9dc9dbbae30d90da6e2495f6b43f319fe674b98da2076fe97a64cc7
6
+ metadata.gz: eb71f7f01ba585e142738e5d28510c5a0508b69843aab7c5b60fbf5cbaa7bb89419a916df923d0282cd2311a61614d836002635e67b9d6dfadd4bd6dba4e3f81
7
+ data.tar.gz: a795545501c2633b3e911b93ccd5c302adbb7d9c9f5a0ab6c363196ae4e0d18089a2b99cc3ada4b66e44902c8a5828212251b331e1b8b4d4762dfafef74dfe0f
data/README.md CHANGED
@@ -12,7 +12,7 @@ Better websites, on Rails.
12
12
  🐼 is grown from our work at [Otaina](https://www.otaina.co.uk), a small group of freelancers. We needed something that could handle websites large and small – but where we could expand it too. We sent our first websites live in March 2024.
13
13
 
14
14
  ![Gem Version](https://img.shields.io/gem/v/panda-cms) ![Build Status](https://img.shields.io/github/actions/workflow/status/tastybamboo/panda-cms/ci.yml)
15
- ![GitHub Last Commit](https://img.shields.io/github/last-commit/tasty-bamboo/panda-cms) [![Ruby Code Style](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/standardrb/standard)
15
+ ![GitHub Last Commit](https://img.shields.io/github/last-commit/tastybamboo/panda-cms) [![Ruby Code Style](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/standardrb/standard)
16
16
 
17
17
  ## Usage
18
18
 
@@ -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
@@ -66,8 +66,43 @@ We welcome contributions.
66
66
 
67
67
  See our [Contributing Guidelines](https://docs.pandacms.io/developers/contributing/)
68
68
 
69
+ ### Testing
70
+
71
+ Panda CMS uses RSpec for testing.
72
+
73
+ #### Using Fixtures
74
+
75
+ We encourage using fixtures for tests instead of factories for consistent test data:
76
+
77
+ 1. Create fixture files in `spec/fixtures` named after the model's table (e.g., `panda_cms_pages.yml`)
78
+ 2. Define records with unique names and their attributes
79
+ 3. Use helper methods to create test templates with mocked file validation
80
+
81
+ Example fixture format:
82
+
83
+ ```yaml
84
+ # spec/fixtures/panda_cms_pages.yml
85
+ home_page:
86
+ title: "Home"
87
+ path: "/"
88
+ panda_cms_template_id: <%= ActiveRecord::FixtureSet.identify(:page_template) %>
89
+ status: "active"
90
+ created_at: <%= Time.current %>
91
+ updated_at: <%= Time.current %>
92
+ ```
93
+
94
+ Example test using fixtures:
95
+
96
+ ```ruby
97
+ # Access fixture using table name and record name
98
+ page = panda_cms_pages(:home_page)
99
+ expect(page.title).to eq("Home")
100
+ ```
101
+
102
+ When testing models with file validations or complex callbacks, use the helper methods in `spec/models/panda/cms/page_spec.rb` as a reference.
103
+
69
104
  ## License
70
105
 
71
106
  The gem is available as open source under the terms of the [BSD-3-Clause License](https://opensource.org/licenses/bsd-3-clause).
72
107
 
73
- Copyright © 2024, Panda Software Limited.
108
+ Copyright © 2024 - 2025, Otaina Limited.
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/setup"
2
4
 
3
5
  APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
@@ -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,4 +1,4 @@
1
- require "ostruct"
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Panda
4
4
  module CMS
@@ -6,7 +6,7 @@ module Panda
6
6
  include ActionView::Helpers::TagHelper
7
7
  include ActionView::Helpers::FormTagHelper
8
8
 
9
- def label(attribute, text = nil, options = {}, &block)
9
+ def label(attribute, text = nil, options = {})
10
10
  super(attribute, text, options.reverse_merge(class: label_styles))
11
11
  end
12
12
 
@@ -15,50 +15,53 @@ module Panda
15
15
  content_tag :div, class: container_styles do
16
16
  label(attribute) + meta_text(options) +
17
17
  content_tag(:div, class: "flex flex-grow") do
18
- 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
- super(attribute, options.reverse_merge(class: input_styles_prefix + " input-prefix rounded-l-none border-l-none"))
20
- end
18
+ content_tag(:span,
19
+ class: "inline-flex items-center px-3 text-base border border-r-none rounded-s-md whitespace-nowrap break-keep") do
20
+ options.dig(:data, :prefix)
21
+ end +
22
+ super(attribute, options.reverse_merge(class: "#{input_styles_prefix} input-prefix rounded-l-none border-l-none"))
23
+ end + error_message(attribute)
21
24
  end
22
25
  else
23
26
  content_tag :div, class: container_styles do
24
- label(attribute) + meta_text(options) + super(attribute, options.reverse_merge(class: input_styles))
27
+ label(attribute) + meta_text(options) + super(attribute, options.reverse_merge(class: input_styles)) + error_message(attribute)
25
28
  end
26
29
  end
27
30
  end
28
31
 
29
32
  def email_field(method, options = {})
30
33
  content_tag :div, class: container_styles do
31
- label(method) + meta_text(options) + super(method, options.reverse_merge(class: input_styles))
34
+ label(method) + meta_text(options) + super(method, options.reverse_merge(class: input_styles)) + error_message(method)
32
35
  end
33
36
  end
34
37
 
35
38
  def datetime_field(method, options = {})
36
39
  content_tag :div, class: container_styles do
37
- label(method) + meta_text(options) + super(method, options.reverse_merge(class: input_styles))
40
+ label(method) + meta_text(options) + super(method, options.reverse_merge(class: input_styles)) + error_message(method)
38
41
  end
39
42
  end
40
43
 
41
44
  def text_area(method, options = {})
42
45
  content_tag :div, class: container_styles do
43
- label(method) + meta_text(options) + super(method, options.reverse_merge(class: input_styles))
46
+ label(method) + meta_text(options) + super(method, options.reverse_merge(class: input_styles)) + error_message(method)
44
47
  end
45
48
  end
46
49
 
47
50
  def password_field(attribute, options = {})
48
51
  content_tag :div, class: container_styles do
49
- label(attribute) + meta_text(options) + super(attribute, options.reverse_merge(class: input_styles))
52
+ label(attribute) + meta_text(options) + super(attribute, options.reverse_merge(class: input_styles)) + error_message(attribute)
50
53
  end
51
54
  end
52
55
 
53
- def select(method, choices = nil, options = {}, html_options = {}, &block)
56
+ def select(method, choices = nil, options = {}, html_options = {})
54
57
  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
58
+ label(method) + meta_text(options) + super(method, choices, options, html_options.reverse_merge(class: select_styles)) + select_svg + error_message(method)
56
59
  end
57
60
  end
58
61
 
59
62
  def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
60
63
  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))
64
+ label(method) + meta_text(options) + super(method, collection, value_method, text_method, options, html_options.reverse_merge(class: input_styles)) + error_message(method)
62
65
  end
63
66
  end
64
67
 
@@ -147,30 +150,9 @@ module Panda
147
150
  end
148
151
  end
149
152
 
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
153
  def meta_text(options)
173
154
  return unless options[:meta]
155
+
174
156
  @template.content_tag(:p, options[:meta], class: "block text-black/60 text-sm mb-2")
175
157
  end
176
158
 
@@ -197,7 +179,8 @@ module Panda
197
179
  end
198
180
 
199
181
  def select_svg
200
- @template.content_tag(:svg, class: "pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-gray-400", aria_hidden: true) do
182
+ @template.content_tag(:svg,
183
+ class: "pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-gray-400", aria_hidden: true) do
201
184
  @template.content_tag(:path, d: "M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z")
202
185
  end
203
186
  end
@@ -229,6 +212,14 @@ module Panda
229
212
  def field_wrapper_styles
230
213
  "mt-1"
231
214
  end
215
+
216
+ def error_message(attribute)
217
+ return unless object.respond_to?(:errors) && object.errors[attribute]&.any?
218
+
219
+ content_tag(:p, class: "mt-2 text-sm text-red-600") do
220
+ object.errors[attribute].join(", ")
221
+ end
222
+ end
232
223
  end
233
224
  end
234
225
  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">
@@ -29,13 +29,14 @@ module Panda
29
29
  output = output.html_safe
30
30
  base_heading_styles = "flex pt-1 text-black mb-5 -mt-1"
31
31
 
32
- if level == 1
32
+ case level
33
+ when 1
33
34
  content_tag(:h1, output, class: [base_heading_styles, "text-2xl font-medium", @additional_styles])
34
- elsif level == 2
35
+ when 2
35
36
  content_tag(:h2, output, class: [base_heading_styles, "text-xl font-medium", @additional_styles])
36
- elsif level == 3
37
+ when 3
37
38
  content_tag(:h3, output, class: [base_heading_styles, "text-xl", "font-light", @additional_styles])
38
- elsif level == :panel
39
+ when :panel
39
40
  content_tag(:h3, output, class: ["text-base font-medium p-4 text-white"])
40
41
  end
41
42
  end
@@ -4,9 +4,9 @@ module Panda
4
4
  module CMS
5
5
  module Admin
6
6
  class PanelComponent < ViewComponent::Base
7
- renders_one :heading, ->(text:, icon: "", level: :panel, additional_styles: "") do
7
+ renders_one :heading, lambda { |text:, icon: "", level: :panel, additional_styles: ""|
8
8
  Panda::CMS::Admin::HeadingComponent.new(text: text, icon: icon, level: level, additional_styles: additional_styles)
9
- end
9
+ }
10
10
  end
11
11
  end
12
12
  end
@@ -4,8 +4,7 @@ module Panda
4
4
  module CMS
5
5
  module Admin
6
6
  class StatisticsComponent < ViewComponent::Base
7
- attr_reader :metric
8
- attr_reader :value
7
+ attr_reader :metric, :value
9
8
 
10
9
  def initialize(metric:, value:)
11
10
  @metric = metric
@@ -1,5 +1,7 @@
1
- <% if user.is_a?(Panda::CMS::User) %>
1
+ <% if user.is_a?(Panda::CMS::User) && time %>
2
2
  <%= render Panda::CMS::Admin::UserDisplayComponent.new(user: user, metadata: "#{time_ago_in_words(time)} ago") %>
3
+ <% elsif user.is_a?(Panda::CMS::User) %>
4
+ <%= render Panda::CMS::Admin::UserDisplayComponent.new(user: user, metadata: "Not published") %>
3
5
  <% elsif time %>
4
6
  <div class="text-black/60"><%= time_ago_in_words(time) %> ago</div>
5
7
  <% end %>
@@ -4,28 +4,15 @@ module Panda
4
4
  module CMS
5
5
  module Admin
6
6
  class UserActivityComponent < ViewComponent::Base
7
- attr_accessor :model
8
- attr_accessor :time
9
- attr_accessor :user
7
+ attr_accessor :model, :time, :user
10
8
 
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
9
+ # @param model [ActiveRecord::Base] Model instance to which the user activity is related
10
+ # @param at [ActiveSupport::TimeWithZone] Time of the activity
11
+ # @param user [Panda::CMS::User] User who performed the activity
12
+ def initialize(model: nil, at: nil, user: nil)
13
+ @model = model
14
+ @user = user if user.is_a?(::Panda::CMS::User)
15
+ @time = at if at.is_a?(::ActiveSupport::TimeWithZone)
29
16
  end
30
17
  end
31
18
  end
@@ -17,15 +17,19 @@ module Panda
17
17
  @options[:id] ||= "code-#{key.to_s.dasherize}"
18
18
  @editable = editable
19
19
 
20
- raise BlockError.new("Key 'code' is not allowed for CodeComponent") if key == :code
20
+ raise BlockError, "Key 'code' is not allowed for CodeComponent" if key == :code
21
21
  end
22
22
 
23
23
  def call
24
24
  # TODO: For the non-editable version, grab this from a cache or similar?
25
- block = Panda::CMS::Block.find_by(kind: KIND, key: @key, panda_cms_template_id: Current.page.panda_cms_template_id)
25
+ block = Panda::CMS::Block.find_by(kind: KIND, key: @key,
26
+ panda_cms_template_id: Current.page.panda_cms_template_id)
26
27
 
27
28
  if block.nil?
28
- raise Panda::CMS::MissingBlockError.new("Block with key #{@key} not found for page #{Current.page.title}") unless Rails.env.production?
29
+ unless Rails.env.production?
30
+ raise Panda::CMS::MissingBlockError, "Block with key #{@key} not found for page #{Current.page.title}"
31
+ end
32
+
29
33
  return false
30
34
  end
31
35
 
@@ -58,7 +62,7 @@ module Panda
58
62
 
59
63
  def is_embedded?
60
64
  # TODO: Check security on this - embed_id should match something?
61
- request.params.dig(:embed_id).present?
65
+ request.params[:embed_id].present?
62
66
  end
63
67
  end
64
68
  end
@@ -23,24 +23,25 @@ module Panda
23
23
 
24
24
  @menu_items = @menu_items.order(:lft).map do |menu_item|
25
25
  if is_active?(menu_item)
26
- menu_item.define_singleton_method(:css_classes) { styles[:default] + " " + styles[:active] }
26
+ menu_item.define_singleton_method(:css_classes) { "#{styles[:default]} #{styles[:active]}" }
27
27
  else
28
- menu_item.define_singleton_method(:css_classes) { styles[:default] + " " + styles[:inactive] }
28
+ menu_item.define_singleton_method(:css_classes) { "#{styles[:default]} #{styles[:inactive]}" }
29
29
  end
30
30
 
31
31
  menu_item
32
32
  end
33
33
 
34
34
  # TODO: Surely don't need this but Current.page isn't working in the component
35
- if @render_page_menu
36
- @current_page = Panda::CMS::Page.find_by(path: @current_path)
37
- @page_menu_styles = page_menu_styles
38
- end
35
+ return unless @render_page_menu
36
+
37
+ @current_page = Panda::CMS::Page.find_by(path: @current_path)
38
+ @page_menu_styles = page_menu_styles
39
39
  end
40
40
 
41
41
  def is_active?(menu_item)
42
42
  return true if @current_path == "/" && active_link?(menu_item.page.path, match: :exact)
43
43
  return true if menu_item.page.path != "/" && active_link?(menu_item.page.path, match: :starts_with)
44
+
44
45
  false
45
46
  end
46
47
 
@@ -3,31 +3,29 @@
3
3
  module Panda
4
4
  module CMS
5
5
  class PageMenuComponent < ViewComponent::Base
6
- attr_accessor :page
7
- attr_accessor :menu_item
8
- attr_accessor :styles
6
+ attr_accessor :page, :menu_item, :styles
9
7
 
10
8
  def initialize(page:, start_depth:, styles: {}, show_heading: true)
11
9
  @page = page
12
10
 
13
- unless @page.nil?
14
- start_page = if @page.depth == start_depth
15
- @page
16
- else
17
- @page.ancestors.find { |anc| anc.depth == start_depth }
18
- end
11
+ return if @page.nil?
19
12
 
20
- menu = start_page&.page_menu
21
- return if menu.nil?
13
+ start_page = if @page.depth == start_depth
14
+ @page
15
+ else
16
+ @page.ancestors.find { |anc| anc.depth == start_depth }
17
+ end
22
18
 
23
- @menu_item = menu.menu_items.order(:lft)&.first
19
+ menu = start_page&.page_menu
20
+ return if menu.nil?
24
21
 
25
- @show_heading = show_heading
22
+ @menu_item = menu.menu_items.order(:lft)&.first
26
23
 
27
- # Set some default styles for sanity
28
- @styles = styles
29
- @styles[:indent_with] ||= "pl-2"
30
- end
24
+ @show_heading = show_heading
25
+
26
+ # Set some default styles for sanity
27
+ @styles = styles
28
+ @styles[:indent_with] ||= "pl-2"
31
29
  end
32
30
 
33
31
  def render?