panda_cms 0.5.2 → 0.5.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -5
  3. data/app/assets/builds/panda_cms.css +1 -1
  4. data/app/assets/stylesheets/panda_cms/application.tailwind.css +34 -0
  5. data/app/builders/panda_cms/form_builder.rb +54 -16
  6. data/app/components/panda_cms/admin/button_component.rb +3 -3
  7. data/app/components/panda_cms/admin/flash_message_component.rb +5 -5
  8. data/app/components/panda_cms/admin/user_activity_component.html.erb +2 -2
  9. data/app/components/panda_cms/admin/user_activity_component.rb +17 -6
  10. data/app/controllers/panda_cms/admin/pages_controller.rb +3 -3
  11. data/app/controllers/panda_cms/admin/posts_controller.rb +73 -7
  12. data/app/controllers/panda_cms/pages_controller.rb +1 -1
  13. data/app/controllers/panda_cms/posts_controller.rb +2 -1
  14. data/app/helpers/panda_cms/application_helper.rb +1 -1
  15. data/app/lib/panda_cms/demo_site_generator.rb +13 -5
  16. data/app/models/action_text/rich_text_version.rb +6 -0
  17. data/app/models/panda_cms/page.rb +7 -0
  18. data/app/models/panda_cms/post.rb +14 -1
  19. data/app/models/panda_cms/post_tag.rb +7 -0
  20. data/app/models/panda_cms/template.rb +4 -1
  21. data/app/models/panda_cms/user.rb +10 -6
  22. data/app/views/active_storage/blobs/blobs/_blob.html.erb +14 -0
  23. data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
  24. data/app/views/panda_cms/admin/pages/index.html.erb +14 -8
  25. data/app/views/panda_cms/admin/pages/new.html.erb +1 -1
  26. data/app/views/panda_cms/admin/posts/_form.html.erb +17 -0
  27. data/app/views/panda_cms/admin/posts/edit.html.erb +6 -0
  28. data/app/views/panda_cms/admin/posts/index.html.erb +2 -0
  29. data/app/views/panda_cms/admin/posts/new.html.erb +6 -0
  30. data/app/views/panda_cms/shared/_header.html.erb +1 -0
  31. data/config/importmap.rb +1 -0
  32. data/config/initializers/panda_cms/form_errors.rb +2 -2
  33. data/config/initializers/panda_cms/paper_trail.rb +7 -0
  34. data/config/initializers/panda_cms.rb +5 -3
  35. data/config/locales/en.yml +18 -1
  36. data/config/tailwind.config.js +1 -0
  37. data/db/migrate/20240904200605_create_action_text_tables.action_text.rb +24 -0
  38. data/lib/panda_cms/version.rb +1 -1
  39. metadata +39 -41
@@ -11,7 +11,7 @@ module PandaCms
11
11
  #
12
12
  # Creates initial templates and empty blocks
13
13
  #
14
- # @return [Hash] A hash containing the created templates
14
+ # @return void
15
15
  def create_templates
16
16
  # Templates
17
17
  initial_templates = [
@@ -20,10 +20,13 @@ module PandaCms
20
20
  ]
21
21
 
22
22
  initial_templates.each do |template|
23
- PandaCms::Template.find_or_create_by!(template)
23
+ key = template[:name].downcase.to_sym
24
+ @templates[key] = PandaCms::Template.find_or_create_by!(template)
24
25
  end
25
26
 
26
27
  PandaCms::Template.generate_missing_blocks
28
+
29
+ @templates
27
30
  end
28
31
 
29
32
  #
@@ -34,21 +37,26 @@ module PandaCms
34
37
  @pages[:home] = PandaCms::Page.find_or_create_by!({path: "/", title: "Home", template: @templates[:homepage]})
35
38
  @pages[:about] = PandaCms::Page.find_or_create_by!({path: "/about", title: "About", template: @templates[:page], parent: @pages[:home]})
36
39
  @pages[:terms] = PandaCms::Page.find_or_create_by!({path: "/terms-and-conditions", title: "Terms & Conditions", template: @templates[:page], parent: @pages[:home]})
40
+
41
+ @pages
37
42
  end
38
43
 
39
44
  #
40
45
  # Creates initial menus
41
46
  #
42
47
  # @return [Hash] A hash containing the created menus
43
- #
44
48
  def create_menus
45
49
  @menus = {}
46
50
  @menus[:main] = PandaCms::Menu.find_or_create_by!(name: "Main Menu")
47
51
  @menus[:footer] = PandaCms::Menu.find_or_create_by!(name: "Footer Menu")
48
52
 
49
53
  # Automatically create main menu from homepage
50
- @menus[:main].update(kind: :auto, start_page: @pages[:home])
51
- @menus[:main].generate_auto_menu_items
54
+ unless @pages[:home].nil?
55
+ @menus[:main].update(kind: :auto, start_page: @pages[:home])
56
+ @menus[:main].generate_auto_menu_items
57
+ end
58
+
59
+ @menus
52
60
  end
53
61
  end
54
62
  end
@@ -0,0 +1,6 @@
1
+ module ActionText
2
+ class RichTextVersion < ::PandaCms::Version
3
+ self.table_name = :action_text_rich_text_versions
4
+ self.sequence_name = :action_text_rich_text_versions_id_seq
5
+ end
6
+ end
@@ -30,6 +30,13 @@ module PandaCms
30
30
 
31
31
  scope :ordered, -> { order(:lft) }
32
32
 
33
+ enum :status, {
34
+ active: "active",
35
+ draft: "draft",
36
+ hidden: "hidden",
37
+ archived: "archived"
38
+ }
39
+
33
40
  private
34
41
 
35
42
  #
@@ -11,15 +11,28 @@ module PandaCms
11
11
  belongs_to :user, class_name: "PandaCms::User"
12
12
 
13
13
  validates :title, presence: true
14
+ validates :slug, presence: true, uniqueness: true, format: {with: /\A[a-z0-9-]+\z/}
14
15
 
15
16
  scope :ordered, -> { order(published_at: :desc) }
17
+ scope :with_user, -> { includes(:user) }
18
+
19
+ has_rich_text :post_content
20
+
21
+ belongs_to :tag, class_name: "PandaCms::PostTag", foreign_key: :post_tag_id
22
+
23
+ enum :status, {
24
+ active: "active",
25
+ draft: "draft",
26
+ hidden: "hidden",
27
+ archived: "archived"
28
+ }
16
29
 
17
30
  def excerpt(length = 100)
18
31
  content.gsub(/<[^>]*>/, "").truncate(length)
19
32
  end
20
33
 
21
34
  def path
22
- PandaCms.posts[:prefix] + "/" + slug.to_s
35
+ "/" + PandaCms.posts[:prefix] + slug.to_s
23
36
  end
24
37
  end
25
38
  end
@@ -0,0 +1,7 @@
1
+ module PandaCms
2
+ class PostTag < ApplicationRecord
3
+ self.table_name = "panda_cms_post_tags"
4
+
5
+ validates :tag, presence: true
6
+ end
7
+ end
@@ -26,6 +26,7 @@ module PandaCms
26
26
  # Scopes
27
27
  scope :ordered, -> { order(:sort_order) }
28
28
  scope :available, -> { where("max_uses IS NULL OR (pages_count < max_uses)") }
29
+ scope :most_used, -> { order(pages_count: :desc).first }
29
30
 
30
31
  # Generate missing blocks for all templates
31
32
  # @return [void]
@@ -39,7 +40,6 @@ module PandaCms
39
40
  # PandaCms::RichTextComponent.new(key: :value)
40
41
  # PandaCms::RichTextComponent.new key: :value, key: value
41
42
  line.match(/PandaCms::([a-zA-Z]+)Component\.new[ \(]+([^\)]+)[\)]*/) do |match|
42
- puts "- #{match[0]}"
43
43
  # Extract the hash values
44
44
  template_path = file.gsub("app/views/", "").gsub(".html.erb", "")
45
45
  template_name = template_path.gsub("layouts/", "").titleize
@@ -49,6 +49,9 @@ module PandaCms
49
49
  template.name = template_name
50
50
  end
51
51
 
52
+ next if match[1] == "PageMenu" # Skip PageMenu blocks
53
+ next if match[1] == "Menu" # Skip Menu blocks
54
+
52
55
  # Previously used match[1].underscore but this supports more complex database
53
56
  # operations, and is more secure as it'll force the usage of a class
54
57
  block_kind = "PandaCms::#{match[1]}Component".constantize::KIND
@@ -3,13 +3,17 @@ module PandaCms
3
3
  validates :firstname, presence: true
4
4
  validates :lastname, presence: true
5
5
  validates :email, presence: true, uniqueness: {case_sensitive: true}
6
- end
7
6
 
8
- def is_admin?
9
- admin
10
- end
7
+ def is_admin?
8
+ admin
9
+ end
10
+
11
+ def name
12
+ "#{firstname} #{lastname}"
13
+ end
11
14
 
12
- def name
13
- "#{firstname} #{lastname}"
15
+ def self.for_select_list(scope = :all, order = {firstname: :asc, lastname: :asc})
16
+ PandaCms::User.send(scope).order(order).map { |u| [u.name, u.id] }
17
+ end
14
18
  end
15
19
  end
@@ -0,0 +1,14 @@
1
+ <figure class="attachment attachment--<%= blob.representable? ? "preview" : "file" %> attachment--<%= blob.filename.extension %>">
2
+ <% if blob.representable? %>
3
+ <%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
4
+ <% end %>
5
+
6
+ <figcaption class="attachment__caption">
7
+ <% if caption = blob.try(:caption) %>
8
+ <%= caption %>
9
+ <% else %>
10
+ <span class="attachment__name"><%= blob.filename %></span>
11
+ <span class="attachment__size"><%= number_to_human_size blob.byte_size %></span>
12
+ <% end %>
13
+ </figcaption>
14
+ </figure>
@@ -0,0 +1,3 @@
1
+ <div class="trix-content">
2
+ <%= yield -%>
3
+ </div>
@@ -3,14 +3,20 @@
3
3
  <% heading.with_button(action: :add, text: "Add Page", link: new_admin_page_path) %>
4
4
  <% end %>
5
5
 
6
- <%= render PandaCms::Admin::TableComponent.new(rows: root_page.self_and_descendants) do |table| %>
7
- <% table.column("Name") do |page| %>
8
- <div class="<%= table_indent(page) %>">
9
- <%= link_to page.title, edit_admin_page_path(page), class: "block h-full w-full" %>
10
- <span class="block text-xs text-black/60"><%= page.path %></span>
11
- </div>
6
+ <% if root_page %>
7
+ <%= render PandaCms::Admin::TableComponent.new(rows: root_page.self_and_descendants) do |table| %>
8
+ <% table.column("Name") do |page| %>
9
+ <div class="<%= table_indent(page) %>">
10
+ <%= link_to page.title, edit_admin_page_path(page), class: "block h-full w-full" %>
11
+ <span class="block text-xs text-black/60"><%= page.path %></span>
12
+ </div>
13
+ <% end %>
14
+ <% table.column("Status") { |page| render PandaCms::Admin::TagComponent.new(status: page.status) } %>
15
+ <% table.column("Last Updated") { |page| render PandaCms::Admin::UserActivityComponent.new(whodunnit_to: page)} %>
12
16
  <% end %>
13
- <% table.column("Status") { |page| render PandaCms::Admin::TagComponent.new(status: page.status) } %>
14
- <% table.column("Last Updated") { |page| render PandaCms::Admin::UserActivityComponent.new(whodunnit_to: page)} %>
17
+ <% else %>
18
+ <div class="p-6 bg-error/10 text-error rounded-lg">
19
+ <p class="text-base">No homepage (at <code>/</code>) found. Please create a homepage to start building your site.</p>
20
+ </div>
15
21
  <% end %>
16
22
  <% end %>
@@ -8,7 +8,7 @@
8
8
  <input type="hidden" value="<%= PandaCms::Current.root %>" data-text-field-update-target="existing_root">
9
9
  <%= f.select :parent_id, options, {}, { "data-text-field-update-target": "input_select", "data-action": "change->text-field-update#setPrePath" } %>
10
10
  <%= f.text_field :title, { data: { "text-field-update-target": "input_text", action: "focusout->text-field-update#generatePath" } } %>
11
- <%= f.text_field :path, { data: { prefix: PandaCms::Current.root, "text-field-update-target": "output_text" } } %>
11
+ <%= f.text_field :path, { meta: t(".path.meta"), data: { prefix: PandaCms::Current.root, "text-field-update-target": "output_text" } } %>
12
12
  <%= f.collection_select :panda_cms_template_id, PandaCms::Template.available, :id, :name %>
13
13
  <%= f.button %>
14
14
  </div>
@@ -0,0 +1,17 @@
1
+ <%= panda_cms_form_with model: post, url: url do |f| %>
2
+ <div data-controller="text-field-update">
3
+ <input type="hidden" value="<%= PandaCms::Current.root %>" data-text-field-update-target="existing_root">
4
+ <%= f.text_field :title, { required: true, data: { "text-field-update-target": "input_text", action: "focusout->text-field-update#generatePath" } } %>
5
+ <%= f.text_field :slug, { required: true, data: { prefix: PandaCms::Current.root + "/#{PandaCms.posts[:prefix]}", "text-field-update-target": "output_text" } } %>
6
+ <%= f.select :user_id, PandaCms::User.for_select_list %>
7
+ <%= f.datetime_field :published_at, { required: true } %>
8
+ <%= f.select :status, PandaCms::Post.statuses.keys.map { |status| [status.humanize, status] } %>
9
+ <%= f.rich_text_area :post_content, { meta: "Your content here will not auto-save! 😬 Use Ctrl + ⇧ + V (Win) or ⌘ + ⇧ + V (macOS) to paste without formatting." } %>
10
+ <%= f.button %>
11
+ </div>
12
+ <% end %>
13
+
14
+ <% content_for :head do %>
15
+ <link rel="stylesheet" type="text/css" href="https://unpkg.com/trix@2.0.8/dist/trix.css">
16
+ <script type="text/javascript" src="https://unpkg.com/trix@2.0.8/dist/trix.umd.min.js"></script>
17
+ <% end %>
@@ -0,0 +1,6 @@
1
+ <%= render PandaCms::Admin::ContainerComponent.new do |component| %>
2
+ <% component.with_heading(text: "Edit Post", level: 1) do |heading| %>
3
+ <% end %>
4
+
5
+ <%= render "form", post: post, url: url %>
6
+ <% end %>
@@ -1,5 +1,6 @@
1
1
  <%= render PandaCms::Admin::ContainerComponent.new do |component| %>
2
2
  <% component.with_heading(text: "Posts", level: 1) do |heading| %>
3
+ <% heading.with_button(action: :add, text: "Add Post", link: new_admin_post_path) %>
3
4
  <% end %>
4
5
 
5
6
  <%= render PandaCms::Admin::TableComponent.new(rows: posts) do |table| %>
@@ -10,6 +11,7 @@
10
11
  </div>
11
12
  <% end %>
12
13
  <% table.column("Status") { |post| render PandaCms::Admin::TagComponent.new(status: post.status) } %>
14
+ <% table.column("Published") { |post| render PandaCms::Admin::UserActivityComponent.new(at: post.published_at, user: post.user)} %>
13
15
  <% table.column("Last Updated") { |post| render PandaCms::Admin::UserActivityComponent.new(whodunnit_to: post)} %>
14
16
  <% end %>
15
17
 
@@ -0,0 +1,6 @@
1
+ <%= render PandaCms::Admin::ContainerComponent.new do |component| %>
2
+ <% component.with_heading(text: "Add Post", level: 1) do |heading| %>
3
+ <% end %>
4
+
5
+ <%= render "form", post: post, url: url %>
6
+ <% end %>
@@ -9,6 +9,7 @@
9
9
  <%= stylesheet_link_tag "panda_cms", "data-turbo-track": "reload" %>
10
10
  <%= javascript_importmap_tags "panda_cms/base" %>
11
11
  <%= render "panda_cms/shared/favicons" %>
12
+ <%= yield :head %>
12
13
  </head>
13
14
 
14
15
  <body class="overflow-hidden h-full">
data/config/importmap.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  pin "@hotwired/stimulus", to: "https://ga.jspm.io/npm:stimulus@3.2.2/dist/stimulus.js"
2
2
  pin "@hotwired/stimulus-loading", to: "/panda-cms-assets/javascripts/vendor/stimulus-loading.js" # 3.2.2
3
+ pin "@rails/activestorage", to: "https://ga.jspm.io/npm:@rails/activestorage@7.2.0/app/assets/javascripts/activestorage.esm.js"
3
4
 
4
5
  pin "panda_cms/vendor/stimulus-components-rails-nested-form", to: "/panda-cms-assets/javascripts/vendor/stimulus-components-rails-nested-form.js", preload: false
5
6
  pin "panda_cms/vendor/tailwindcss-stimulus-components", to: "/panda-cms-assets/javascripts/vendor/tailwindcss-stimulus-components.js", preload: false
@@ -1,8 +1,8 @@
1
1
  ActionView::Base.field_error_proc = proc do |html_tag, instance|
2
2
  html = ""
3
3
  form_fields = %w[input select textarea trix-editor label].join(", ")
4
- error_class = "text-red-600 dark:text-red-500 bg-red-50 border-red-500 border-1 box-shadow-red-500 focus:ring-red-500 focus:border-red-500 dark:bg-red-900 dark:border-red-500 dark:focus:ring-red-500 dark:focus:border-red-500"
5
- message_class = "block w-full text-sm font-bold p-0 m-0 mt-1 text-red-700"
4
+ error_class = "text-error bg-error border-error border-1 box-shadow-error focus:ring-error focus:border-error "
5
+ message_class = "block w-full text-base p-0 m-0 mt-1 text-error font-semibold"
6
6
  autofocused = false
7
7
 
8
8
  Nokogiri::HTML::DocumentFragment.parse(html_tag).css(form_fields).each do |element|
@@ -0,0 +1,7 @@
1
+ ActiveSupport.on_load(:action_text_rich_text) do
2
+ ActionText::RichText.class_eval do
3
+ has_paper_trail versions: {
4
+ class_name: "ActionText::RichTextVersion"
5
+ }
6
+ end
7
+ end
@@ -1,5 +1,3 @@
1
- # Base URL
2
- PandaCms.url = "http://localhost:3000"
3
1
  # The main title of your website
4
2
  # The default is "Demo Site"
5
3
  PandaCms.title = "Demo Site"
@@ -41,7 +39,11 @@ PandaCms.authentication = {
41
39
  github: {
42
40
  enabled: true,
43
41
  create_account_on_first_login: true,
44
- create_admin_account_on_first_login: false,
42
+ create_admin_account_on_first_login: true,
43
+ # client_id: Rails.application.credentials.dig(:github, :client_id),
44
+ # client_secret: Rails.application.credentials.dig(:github, :client_secret),
45
+ client_id: "Ov23li9k0LpMXtq8FShb", # Will only work on localhost
46
+ client_secret: "07233b63472b7f287ac11854e627670ddc096a22", # Will only work on localhost
45
47
  redirect_uri: Rails.application.credentials.dig(:github, :redirect_uri)
46
48
  }
47
49
  }
@@ -3,8 +3,21 @@ en:
3
3
  attributes:
4
4
  panda_cms/page:
5
5
  title: Title
6
- path: Path
6
+ path: URL
7
7
  panda_cms_template_id: Template
8
+ panda_cms/post:
9
+ title: Title
10
+ slug: URL
11
+ content: Content
12
+ panda_cms_template_id: Template
13
+ user_id: Author
14
+ published_at: Published At
15
+ post_content: Content
16
+ statuses:
17
+ active: Active
18
+ draft: Draft
19
+ archived: Archived
20
+ hidden: Hidden
8
21
  panda_cms/menu:
9
22
  name: Menu Name
10
23
  panda_cms/menu_item:
@@ -19,6 +32,10 @@ en:
19
32
  google: Google
20
33
  microsoft: Microsoft 365
21
34
  admin:
35
+ pages:
36
+ new:
37
+ path:
38
+ meta: "This will be the URL of the page. It should be unique and not contain spaces or special characters. If you're unsure, it'll be auto-generated for you. 🐼"
22
39
  sessions:
23
40
  create:
24
41
  error: There was an error logging you in. Please check your login details and try again, or contact support.
@@ -4,6 +4,7 @@ module.exports = {
4
4
  files: [
5
5
  "../public/*.html",
6
6
  "../app/views/**/*.html.erb",
7
+ "../app/builders/panda_cms/**/*.rb",
7
8
  "../app/components/panda_cms/**/*.html.erb",
8
9
  "../app/components/panda_cms/**/*.rb",
9
10
  "../app/helpers/panda_cms/**/*.rb",
@@ -0,0 +1,24 @@
1
+ # This migration comes from action_text (originally 20180528164100)
2
+ class CreateActionTextTables < ActiveRecord::Migration[7.2]
3
+ def change
4
+ create_table :action_text_rich_texts, id: :uuid do |t|
5
+ t.string :name, null: false
6
+ t.text :body, limit: 16.megabytes - 1
7
+ t.references :record, null: false, polymorphic: true, index: false, type: :uuid
8
+ t.timestamps
9
+ t.index [:record_type, :record_id, :name], name: "index_action_text_rich_texts_uniqueness", unique: true
10
+ end
11
+
12
+ create_table :action_text_rich_text_versions, id: :uuid do |t|
13
+ t.string :item_type, null: false
14
+ t.string :item_id, null: false
15
+ t.string :event, null: false
16
+ t.string :whodunnit
17
+ t.jsonb :object
18
+ t.jsonb :object_changes
19
+ t.datetime :created_at
20
+ end
21
+
22
+ add_index :action_text_rich_text_versions, %i[item_type item_id]
23
+ end
24
+ end
@@ -1,3 +1,3 @@
1
1
  module PandaCms
2
- VERSION = "0.5.2"
2
+ VERSION = "0.5.4"
3
3
  end