panda-cms 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/assets/builds/panda.cms.css +0 -50
- data/app/components/panda/cms/admin/table_component.html.erb +4 -1
- data/app/components/panda/cms/code_component.rb +2 -1
- data/app/components/panda/cms/rich_text_component.html.erb +86 -2
- data/app/components/panda/cms/rich_text_component.rb +131 -20
- data/app/controllers/panda/cms/admin/block_contents_controller.rb +18 -7
- data/app/controllers/panda/cms/admin/files_controller.rb +22 -12
- data/app/controllers/panda/cms/admin/posts_controller.rb +33 -11
- data/app/controllers/panda/cms/pages_controller.rb +29 -0
- data/app/controllers/panda/cms/posts_controller.rb +26 -4
- data/app/helpers/panda/cms/admin/posts_helper.rb +23 -32
- data/app/helpers/panda/cms/posts_helper.rb +32 -0
- data/app/javascript/panda/cms/controllers/dashboard_controller.js +0 -1
- data/app/javascript/panda/cms/controllers/editor_form_controller.js +134 -11
- data/app/javascript/panda/cms/controllers/editor_iframe_controller.js +395 -130
- data/app/javascript/panda/cms/controllers/slug_controller.js +33 -43
- data/app/javascript/panda/cms/editor/editor_js_config.js +202 -73
- data/app/javascript/panda/cms/editor/editor_js_initializer.js +243 -194
- data/app/javascript/panda/cms/editor/plain_text_editor.js +1 -1
- data/app/javascript/panda/cms/editor/resource_loader.js +89 -0
- data/app/javascript/panda/cms/editor/rich_text_editor.js +162 -0
- data/app/models/panda/cms/page.rb +18 -0
- data/app/models/panda/cms/post.rb +61 -3
- data/app/models/panda/cms/redirect.rb +2 -2
- data/app/views/panda/cms/admin/posts/_form.html.erb +15 -4
- data/app/views/panda/cms/admin/posts/index.html.erb +5 -3
- data/config/routes.rb +34 -6
- data/db/migrate/20250106223303_add_author_id_to_panda_cms_posts.rb +5 -0
- data/lib/panda/cms/editor_js_content.rb +14 -1
- data/lib/panda/cms/engine.rb +4 -0
- data/lib/panda-cms/version.rb +1 -1
- metadata +5 -2
| @@ -0,0 +1,162 @@ | |
| 1 | 
            +
            import EditorJS from "@editorjs/editorjs"
         | 
| 2 | 
            +
            import Paragraph from "@editorjs/paragraph"
         | 
| 3 | 
            +
            import Header from "@editorjs/header"
         | 
| 4 | 
            +
            import List from "@editorjs/list"
         | 
| 5 | 
            +
            import Quote from "@editorjs/quote"
         | 
| 6 | 
            +
            import Table from "@editorjs/table"
         | 
| 7 | 
            +
            import NestedList from "@editorjs/nested-list"
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            export default class RichTextEditor {
         | 
| 10 | 
            +
              constructor(element, iframe) {
         | 
| 11 | 
            +
                this.element = element
         | 
| 12 | 
            +
                this.iframe = iframe
         | 
| 13 | 
            +
                this.editor = null
         | 
| 14 | 
            +
                this.initialized = false
         | 
| 15 | 
            +
                this.initialize()
         | 
| 16 | 
            +
              }
         | 
| 17 | 
            +
             | 
| 18 | 
            +
              async initialize() {
         | 
| 19 | 
            +
                if (this.initialized) return
         | 
| 20 | 
            +
                console.debug("[Panda CMS] Initializing EditorJS")
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                try {
         | 
| 23 | 
            +
                  let content = this.element.dataset.editableContent || ""
         | 
| 24 | 
            +
                  let previousData = this.element.dataset.editablePreviousData || ""
         | 
| 25 | 
            +
                  console.debug("[Panda CMS] Initial content:", content)
         | 
| 26 | 
            +
                  console.debug("[Panda CMS] Previous data:", previousData)
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  let parsedContent
         | 
| 29 | 
            +
                  if (previousData) {
         | 
| 30 | 
            +
                    try {
         | 
| 31 | 
            +
                      // Try to decode base64 first
         | 
| 32 | 
            +
                      const decodedData = atob(previousData)
         | 
| 33 | 
            +
                      console.debug("[Panda CMS] Decoded base64 data:", decodedData)
         | 
| 34 | 
            +
                      parsedContent = JSON.parse(decodedData)
         | 
| 35 | 
            +
                      console.debug("[Panda CMS] Successfully parsed base64 data:", parsedContent)
         | 
| 36 | 
            +
                    } catch (e) {
         | 
| 37 | 
            +
                      console.debug("[Panda CMS] Not base64 encoded or invalid, trying direct JSON parse:", e)
         | 
| 38 | 
            +
                      try {
         | 
| 39 | 
            +
                        parsedContent = JSON.parse(previousData)
         | 
| 40 | 
            +
                        console.debug("[Panda CMS] Successfully parsed JSON data:", parsedContent)
         | 
| 41 | 
            +
                      } catch (e2) {
         | 
| 42 | 
            +
                        console.error("[Panda CMS] Failed to parse previous data:", e2)
         | 
| 43 | 
            +
                        parsedContent = this.getDefaultContent()
         | 
| 44 | 
            +
                      }
         | 
| 45 | 
            +
                    }
         | 
| 46 | 
            +
                  } else if (content) {
         | 
| 47 | 
            +
                    try {
         | 
| 48 | 
            +
                      parsedContent = JSON.parse(content)
         | 
| 49 | 
            +
                      console.debug("[Panda CMS] Successfully parsed content:", parsedContent)
         | 
| 50 | 
            +
                    } catch (e) {
         | 
| 51 | 
            +
                      console.error("[Panda CMS] Failed to parse content:", e)
         | 
| 52 | 
            +
                      parsedContent = this.getDefaultContent()
         | 
| 53 | 
            +
                    }
         | 
| 54 | 
            +
                  } else {
         | 
| 55 | 
            +
                    parsedContent = this.getDefaultContent()
         | 
| 56 | 
            +
                  }
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  // Create holder element before initialization
         | 
| 59 | 
            +
                  const holderId = `editor-${Math.random().toString(36).substr(2, 9)}`
         | 
| 60 | 
            +
                  const holderElement = document.createElement("div")
         | 
| 61 | 
            +
                  holderElement.id = holderId
         | 
| 62 | 
            +
                  holderElement.className = "editor-js-holder codex-editor"
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                  // Clear any existing content and append holder
         | 
| 65 | 
            +
                  this.element.textContent = ""
         | 
| 66 | 
            +
                  this.element.appendChild(holderElement)
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                  // Initialize EditorJS
         | 
| 69 | 
            +
                  this.editor = new EditorJS({
         | 
| 70 | 
            +
                    holder: holderId,
         | 
| 71 | 
            +
                    data: parsedContent,
         | 
| 72 | 
            +
                    placeholder: "Click to start writing...",
         | 
| 73 | 
            +
                    tools: {
         | 
| 74 | 
            +
                      paragraph: {
         | 
| 75 | 
            +
                        class: Paragraph,
         | 
| 76 | 
            +
                        inlineToolbar: true
         | 
| 77 | 
            +
                      },
         | 
| 78 | 
            +
                      header: {
         | 
| 79 | 
            +
                        class: Header,
         | 
| 80 | 
            +
                        inlineToolbar: true
         | 
| 81 | 
            +
                      },
         | 
| 82 | 
            +
                      list: {
         | 
| 83 | 
            +
                        class: NestedList,
         | 
| 84 | 
            +
                        inlineToolbar: true,
         | 
| 85 | 
            +
                        config: {
         | 
| 86 | 
            +
                          defaultStyle: 'unordered',
         | 
| 87 | 
            +
                          enableLineBreaks: true
         | 
| 88 | 
            +
                        }
         | 
| 89 | 
            +
                      },
         | 
| 90 | 
            +
                      quote: {
         | 
| 91 | 
            +
                        class: Quote,
         | 
| 92 | 
            +
                        inlineToolbar: true
         | 
| 93 | 
            +
                      },
         | 
| 94 | 
            +
                      table: {
         | 
| 95 | 
            +
                        class: Table,
         | 
| 96 | 
            +
                        inlineToolbar: true
         | 
| 97 | 
            +
                      }
         | 
| 98 | 
            +
                    },
         | 
| 99 | 
            +
                    onChange: () => {
         | 
| 100 | 
            +
                      this.save()
         | 
| 101 | 
            +
                    }
         | 
| 102 | 
            +
                  })
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                  await this.editor.isReady
         | 
| 105 | 
            +
                  this.initialized = true
         | 
| 106 | 
            +
                  console.debug("[Panda CMS] EditorJS initialized successfully")
         | 
| 107 | 
            +
                } catch (error) {
         | 
| 108 | 
            +
                  console.error("[Panda CMS] Error initializing EditorJS:", error)
         | 
| 109 | 
            +
                }
         | 
| 110 | 
            +
              }
         | 
| 111 | 
            +
             | 
| 112 | 
            +
              getDefaultContent() {
         | 
| 113 | 
            +
                return {
         | 
| 114 | 
            +
                  time: Date.now(),
         | 
| 115 | 
            +
                  blocks: [
         | 
| 116 | 
            +
                    {
         | 
| 117 | 
            +
                      type: "paragraph",
         | 
| 118 | 
            +
                      data: {
         | 
| 119 | 
            +
                        text: ""
         | 
| 120 | 
            +
                      }
         | 
| 121 | 
            +
                    }
         | 
| 122 | 
            +
                  ],
         | 
| 123 | 
            +
                  version: "2.28.2"
         | 
| 124 | 
            +
                }
         | 
| 125 | 
            +
              }
         | 
| 126 | 
            +
             | 
| 127 | 
            +
              async save() {
         | 
| 128 | 
            +
                if (!this.editor) return null
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                try {
         | 
| 131 | 
            +
                  const savedData = await this.editor.save()
         | 
| 132 | 
            +
                  const jsonString = JSON.stringify(savedData)
         | 
| 133 | 
            +
                  // Store both base64 and regular JSON
         | 
| 134 | 
            +
                  this.element.dataset.editablePreviousData = btoa(jsonString)
         | 
| 135 | 
            +
                  this.element.dataset.editableContent = jsonString
         | 
| 136 | 
            +
                  return jsonString
         | 
| 137 | 
            +
                } catch (error) {
         | 
| 138 | 
            +
                  console.error("[Panda CMS] Error saving EditorJS content:", error)
         | 
| 139 | 
            +
                  return null
         | 
| 140 | 
            +
                }
         | 
| 141 | 
            +
              }
         | 
| 142 | 
            +
             | 
| 143 | 
            +
              async clear() {
         | 
| 144 | 
            +
                if (!this.editor) return
         | 
| 145 | 
            +
             | 
| 146 | 
            +
                try {
         | 
| 147 | 
            +
                  await this.editor.clear()
         | 
| 148 | 
            +
                  this.element.dataset.editablePreviousData = ""
         | 
| 149 | 
            +
                  this.element.dataset.editableContent = ""
         | 
| 150 | 
            +
                } catch (error) {
         | 
| 151 | 
            +
                  console.error("[Panda CMS] Error clearing EditorJS content:", error)
         | 
| 152 | 
            +
                }
         | 
| 153 | 
            +
              }
         | 
| 154 | 
            +
             | 
| 155 | 
            +
              destroy() {
         | 
| 156 | 
            +
                if (this.editor) {
         | 
| 157 | 
            +
                  this.editor.destroy()
         | 
| 158 | 
            +
                  this.editor = null
         | 
| 159 | 
            +
                  this.initialized = false
         | 
| 160 | 
            +
                }
         | 
| 161 | 
            +
              }
         | 
| 162 | 
            +
            }
         | 
| @@ -67,6 +67,7 @@ module Panda | |
| 67 67 | 
             
                    generate_content_blocks
         | 
| 68 68 | 
             
                    update_existing_menu_items
         | 
| 69 69 | 
             
                    update_auto_menus
         | 
| 70 | 
            +
                    create_redirect_if_path_changed
         | 
| 70 71 | 
             
                  end
         | 
| 71 72 |  | 
| 72 73 | 
             
                  def generate_content_blocks
         | 
| @@ -91,6 +92,23 @@ module Panda | |
| 91 92 | 
             
                  def update_existing_menu_items
         | 
| 92 93 | 
             
                    menu_items.where.not(text: title).update_all(text: title)
         | 
| 93 94 | 
             
                  end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                  def create_redirect_if_path_changed
         | 
| 97 | 
            +
                    return unless saved_change_to_path?
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                    old_path = saved_changes["path"].first
         | 
| 100 | 
            +
                    new_path = saved_changes["path"].last
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                    # Create a redirect from the old path to the new path
         | 
| 103 | 
            +
                    Panda::CMS::Redirect.create!(
         | 
| 104 | 
            +
                      origin_panda_cms_page_id: id,
         | 
| 105 | 
            +
                      destination_panda_cms_page_id: id,
         | 
| 106 | 
            +
                      status_code: 301,
         | 
| 107 | 
            +
                      visits: 0,
         | 
| 108 | 
            +
                      origin_path: old_path,
         | 
| 109 | 
            +
                      destination_path: new_path
         | 
| 110 | 
            +
                    )
         | 
| 111 | 
            +
                  end
         | 
| 94 112 | 
             
                end
         | 
| 95 113 | 
             
              end
         | 
| 96 114 | 
             
            end
         | 
| @@ -5,6 +5,9 @@ module Panda | |
| 5 5 | 
             
                class Post < ApplicationRecord
         | 
| 6 6 | 
             
                  include ::Panda::CMS::EditorJsContent
         | 
| 7 7 |  | 
| 8 | 
            +
                  after_commit :clear_menu_cache
         | 
| 9 | 
            +
                  before_validation :format_slug
         | 
| 10 | 
            +
             | 
| 8 11 | 
             
                  self.table_name = "panda_cms_posts"
         | 
| 9 12 |  | 
| 10 13 | 
             
                  has_paper_trail versions: {
         | 
| @@ -12,18 +15,20 @@ module Panda | |
| 12 15 | 
             
                  }
         | 
| 13 16 |  | 
| 14 17 | 
             
                  belongs_to :user, class_name: "Panda::CMS::User"
         | 
| 18 | 
            +
                  belongs_to :author, class_name: "Panda::CMS::User"
         | 
| 15 19 |  | 
| 16 20 | 
             
                  validates :title, presence: true
         | 
| 17 21 | 
             
                  validates :slug,
         | 
| 18 22 | 
             
                    presence: true,
         | 
| 19 23 | 
             
                    uniqueness: true,
         | 
| 20 24 | 
             
                    format: {
         | 
| 21 | 
            -
                      with: /\ | 
| 22 | 
            -
                      message: "must  | 
| 25 | 
            +
                      with: %r{\A(/\d{4}/\d{2}/[a-z0-9-]+|/[a-z0-9-]+)\z},
         | 
| 26 | 
            +
                      message: "must be in format /YYYY/MM/slug or /slug with only lowercase letters, numbers, and hyphens"
         | 
| 23 27 | 
             
                    }
         | 
| 24 28 |  | 
| 25 29 | 
             
                  scope :ordered, -> { order(published_at: :desc) }
         | 
| 26 30 | 
             
                  scope :with_user, -> { includes(:user) }
         | 
| 31 | 
            +
                  scope :with_author, -> { includes(:author) }
         | 
| 27 32 |  | 
| 28 33 | 
             
                  enum :status, {
         | 
| 29 34 | 
             
                    active: "active",
         | 
| @@ -33,7 +38,31 @@ module Panda | |
| 33 38 | 
             
                  }
         | 
| 34 39 |  | 
| 35 40 | 
             
                  def to_param
         | 
| 36 | 
            -
                    slug | 
| 41 | 
            +
                    # For date-based URLs, return just the slug portion
         | 
| 42 | 
            +
                    parts = CGI.unescape(slug).delete_prefix("/").split("/")
         | 
| 43 | 
            +
                    if parts.length == 3 # year/month/slug format
         | 
| 44 | 
            +
                      parts.last
         | 
| 45 | 
            +
                    else
         | 
| 46 | 
            +
                      parts.first
         | 
| 47 | 
            +
                    end
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  def year
         | 
| 51 | 
            +
                    return nil unless slug.match?(%r{\A/\d{4}/})
         | 
| 52 | 
            +
                    slug.split("/")[1]
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  def month
         | 
| 56 | 
            +
                    return nil unless slug.match?(%r{\A/\d{4}/\d{2}/})
         | 
| 57 | 
            +
                    slug.split("/")[2]
         | 
| 58 | 
            +
                  end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                  def route_params
         | 
| 61 | 
            +
                    if year && month
         | 
| 62 | 
            +
                      {year: year, month: month, slug: to_param}
         | 
| 63 | 
            +
                    else
         | 
| 64 | 
            +
                      {slug: to_param}
         | 
| 65 | 
            +
                    end
         | 
| 37 66 | 
             
                  end
         | 
| 38 67 |  | 
| 39 68 | 
             
                  def admin_param
         | 
| @@ -55,6 +84,35 @@ module Panda | |
| 55 84 | 
             
                    text = text.squish if squish
         | 
| 56 85 | 
             
                    text.truncate(length).html_safe
         | 
| 57 86 | 
             
                  end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                  private
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                  def clear_menu_cache
         | 
| 91 | 
            +
                    Rails.cache.delete("panda_cms_news_months")
         | 
| 92 | 
            +
                  end
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                  def format_slug
         | 
| 95 | 
            +
                    return if slug.blank?
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                    # Remove any leading/trailing slashes and decode
         | 
| 98 | 
            +
                    self.slug = CGI.unescape(slug.strip.gsub(%r{^/+|/+$}, ""))
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                    # Handle the case where we already have a properly formatted slug
         | 
| 101 | 
            +
                    if slug.match?(%r{\A\d{4}/\d{2}/[^/]+\z})
         | 
| 102 | 
            +
                      return self.slug = "/#{slug}"
         | 
| 103 | 
            +
                    end
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                    # Handle the case where we have a date-prefixed slug (from JS)
         | 
| 106 | 
            +
                    if (match = slug.match(%r{\A(\d{4})-(\d{2})-(.+)\z}))
         | 
| 107 | 
            +
                      year, month, base_slug = match[1], match[2], match[3]
         | 
| 108 | 
            +
                      return self.slug = "/#{year}/#{month}/#{base_slug}"
         | 
| 109 | 
            +
                    end
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                    # For new slugs without any date structure
         | 
| 112 | 
            +
                    base_slug = slug.downcase.gsub(/[^a-z0-9-]+/, "-").gsub(/^-+|-+$/, "")
         | 
| 113 | 
            +
                    date_prefix = published_at.present? ? published_at.strftime("%Y/%m") : Time.current.strftime("%Y/%m")
         | 
| 114 | 
            +
                    self.slug = "/#{date_prefix}/#{base_slug}"
         | 
| 115 | 
            +
                  end
         | 
| 58 116 | 
             
                end
         | 
| 59 117 | 
             
              end
         | 
| 60 118 | 
             
            end
         | 
| @@ -1,8 +1,8 @@ | |
| 1 1 | 
             
            module Panda
         | 
| 2 2 | 
             
              module CMS
         | 
| 3 3 | 
             
                class Redirect < ApplicationRecord
         | 
| 4 | 
            -
                  belongs_to :origin_page, class_name: "Panda::CMS::Page", foreign_key: :origin_panda_cms_page_id
         | 
| 5 | 
            -
                  belongs_to :destination_page, class_name: "Panda::CMS::Page", foreign_key: :destination_panda_cms_page_id
         | 
| 4 | 
            +
                  belongs_to :origin_page, class_name: "Panda::CMS::Page", foreign_key: :origin_panda_cms_page_id, optional: true
         | 
| 5 | 
            +
                  belongs_to :destination_page, class_name: "Panda::CMS::Page", foreign_key: :destination_panda_cms_page_id, optional: true
         | 
| 6 6 |  | 
| 7 7 | 
             
                  validates :status_code, presence: true
         | 
| 8 8 | 
             
                  validates :visits, presence: true
         | 
| @@ -1,7 +1,18 @@ | |
| 1 1 | 
             
            <%= panda_cms_form_with model: post, url: url, method: post.persisted? ? :put : :post do |f| %>
         | 
| 2 | 
            -
               | 
| 3 | 
            -
             | 
| 4 | 
            -
             | 
| 2 | 
            +
              <div data-controller="slug" data-slug-add-date-prefix-value="true">
         | 
| 3 | 
            +
                <%= f.text_field :title,
         | 
| 4 | 
            +
                    class: "block w-full rounded-md border-0 p-2 text-gray-900 ring-1 ring-inset ring-mid placeholder:text-gray-300 focus:ring-1 focus:ring-inset focus:ring-dark sm:leading-6 hover:pointer",
         | 
| 5 | 
            +
                    data: {
         | 
| 6 | 
            +
                      "slug-target": "input_text",
         | 
| 7 | 
            +
                      action: "focusout->slug#generatePath"
         | 
| 8 | 
            +
                    } %>
         | 
| 9 | 
            +
                <%= f.text_field :slug,
         | 
| 10 | 
            +
                    class: "block w-full rounded-md border-0 p-2 text-gray-900 ring-1 ring-inset ring-mid placeholder:text-gray-300 focus:ring-1 focus:ring-inset focus:ring-dark sm:leading-6 hover:pointer",
         | 
| 11 | 
            +
                    data: {
         | 
| 12 | 
            +
                      "slug-target": "output_text"
         | 
| 13 | 
            +
                    } %>
         | 
| 14 | 
            +
              </div>
         | 
| 15 | 
            +
              <%= f.select :author_id, Panda::CMS::User.admin.map { |u| [u.name, u.id] }, {}, class: "block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-mid focus:ring-1 focus:ring-inset focus:ring-dark sm:leading-6 hover:pointer" %>
         | 
| 5 16 | 
             
              <%= f.datetime_field :published_at, class: "block w-full rounded-md border-0 p-2 text-gray-900 ring-1 ring-inset ring-mid placeholder:text-gray-300 focus:ring-1 focus:ring-inset focus:ring-dark sm:leading-6 hover:pointer" %>
         | 
| 6 17 | 
             
              <%= f.select :status, options_for_select([["Active", "active"], ["Draft", "draft"], ["Hidden", "hidden"], ["Archived", "archived"]], selected: post.status), {}, class: "block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-mid focus:ring-1 focus:ring-inset focus:ring-dark sm:leading-6 hover:pointer" %>
         | 
| 7 18 |  | 
| @@ -16,7 +27,7 @@ | |
| 16 27 | 
             
                    } %>
         | 
| 17 28 | 
             
                <div id="<%= editor_id %>"
         | 
| 18 29 | 
             
                     data-editor-form-target="editorContainer"
         | 
| 19 | 
            -
                     class="max-w-full block  | 
| 30 | 
            +
                     class="max-w-full block bg-white pt-1 mb-4 mt-2 border border-mid rounded-md min-h-[300px]">
         | 
| 20 31 | 
             
                </div>
         | 
| 21 32 | 
             
              </div>
         | 
| 22 33 |  | 
| @@ -5,13 +5,15 @@ | |
| 5 5 |  | 
| 6 6 | 
             
              <%= render Panda::CMS::Admin::TableComponent.new(term: "post", rows: posts) do |table| %>
         | 
| 7 7 | 
             
                <% table.column("Title") do |post| %>
         | 
| 8 | 
            -
                  <div | 
| 8 | 
            +
                  <div>
         | 
| 9 9 | 
             
                    <%= link_to post.title, edit_admin_post_path(post.admin_param), class: "block h-full w-full" %>
         | 
| 10 | 
            -
                    <span class="block text-xs text-black/60" | 
| 10 | 
            +
                    <span class="block text-xs text-black/60">
         | 
| 11 | 
            +
                      <%= CGI.unescape("#{Panda::CMS.config.posts[:prefix]}#{post.slug}") %>
         | 
| 12 | 
            +
                    </span>
         | 
| 11 13 | 
             
                  </div>
         | 
| 12 14 | 
             
                <% end %>
         | 
| 13 15 | 
             
                <% table.column("Status") { |post| render Panda::CMS::Admin::TagComponent.new(status: post.status) } %>
         | 
| 14 | 
            -
                <% table.column("Published") { |post| render Panda::CMS::Admin::UserActivityComponent.new(at: post.published_at, user: post. | 
| 16 | 
            +
                <% table.column("Published") { |post| render Panda::CMS::Admin::UserActivityComponent.new(at: post.published_at, user: post.author)} %>
         | 
| 15 17 | 
             
                <% table.column("Last Updated") { |post| render Panda::CMS::Admin::UserActivityComponent.new(whodunnit_to: post)} %>
         | 
| 16 18 | 
             
              <% end %>
         | 
| 17 19 |  | 
    
        data/config/routes.rb
    CHANGED
    
    | @@ -35,14 +35,42 @@ Panda::CMS::Engine.routes.draw do | |
| 35 35 | 
             
              # OmniAuth additionally adds a GET route for "#{Panda::CMS.route_namespace}/auth/:provider" but doesn't name it
         | 
| 36 36 | 
             
              delete Panda::CMS.route_namespace, to: "admin/sessions#destroy", as: :admin_logout
         | 
| 37 37 |  | 
| 38 | 
            +
              ### APPENDED ROUTES ###
         | 
| 39 | 
            +
             | 
| 40 | 
            +
              # TODO: Allow multiple types of post in future
         | 
| 38 41 | 
             
              if Panda::CMS.config.posts[:enabled]
         | 
| 39 | 
            -
                 | 
| 40 | 
            -
                # TODO: This now requires a page to be created, make it explicit (with a rendering posts helper?)
         | 
| 41 | 
            -
                # get Panda::CMS.posts[:prefix], to: "posts#index", as: :posts
         | 
| 42 | 
            -
                get "#{Panda::CMS.config.posts[:prefix]}/:slug", to: "posts#show", as: :post
         | 
| 43 | 
            -
              end
         | 
| 42 | 
            +
                get Panda::CMS.config.posts[:prefix], to: "posts#index", as: :posts
         | 
| 44 43 |  | 
| 45 | 
            -
             | 
| 44 | 
            +
                # Route for date-based URLs that won't encode slashes
         | 
| 45 | 
            +
                get "#{Panda::CMS.config.posts[:prefix]}/:year/:month/:slug",
         | 
| 46 | 
            +
                  to: "posts#show",
         | 
| 47 | 
            +
                  as: :post_with_date,
         | 
| 48 | 
            +
                  constraints: {
         | 
| 49 | 
            +
                    year: /\d{4}/,
         | 
| 50 | 
            +
                    month: /\d{2}/,
         | 
| 51 | 
            +
                    slug: /[^\/]+/,
         | 
| 52 | 
            +
                    format: /html|json|xml/
         | 
| 53 | 
            +
                  }
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                # Route for non-date URLs
         | 
| 56 | 
            +
                get "#{Panda::CMS.config.posts[:prefix]}/:slug",
         | 
| 57 | 
            +
                  to: "posts#show",
         | 
| 58 | 
            +
                  as: :post,
         | 
| 59 | 
            +
                  constraints: {
         | 
| 60 | 
            +
                    slug: /[^\/]+/,
         | 
| 61 | 
            +
                    format: /html|json|xml/
         | 
| 62 | 
            +
                  }
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                # Route for month archive
         | 
| 65 | 
            +
                get "#{Panda::CMS.config.posts[:prefix]}/:year/:month",
         | 
| 66 | 
            +
                  to: "posts#by_month",
         | 
| 67 | 
            +
                  as: :posts_by_month,
         | 
| 68 | 
            +
                  constraints: {
         | 
| 69 | 
            +
                    year: /\d{4}/,
         | 
| 70 | 
            +
                    month: /\d{2}/,
         | 
| 71 | 
            +
                    format: /html|json|xml/
         | 
| 72 | 
            +
                  }
         | 
| 73 | 
            +
              end
         | 
| 46 74 |  | 
| 47 75 | 
             
              # See lib/panda/cms/engine.rb
         | 
| 48 76 | 
             
            end
         | 
| @@ -12,10 +12,23 @@ module Panda::CMS::EditorJsContent | |
| 12 12 |  | 
| 13 13 | 
             
              def generate_cached_content
         | 
| 14 14 | 
             
                if content.is_a?(String)
         | 
| 15 | 
            -
                   | 
| 15 | 
            +
                  begin
         | 
| 16 | 
            +
                    parsed_content = JSON.parse(content)
         | 
| 17 | 
            +
                    self.cached_content = if parsed_content.is_a?(Hash) && parsed_content["blocks"].present?
         | 
| 18 | 
            +
                      Panda::CMS::EditorJs::Renderer.new(parsed_content).render
         | 
| 19 | 
            +
                    else
         | 
| 20 | 
            +
                      content
         | 
| 21 | 
            +
                    end
         | 
| 22 | 
            +
                  rescue JSON::ParserError
         | 
| 23 | 
            +
                    # If it's not JSON, treat it as plain text
         | 
| 24 | 
            +
                    self.cached_content = content
         | 
| 25 | 
            +
                  end
         | 
| 16 26 | 
             
                elsif content.is_a?(Hash) && content["blocks"].present?
         | 
| 17 27 | 
             
                  # Process EditorJS content
         | 
| 18 28 | 
             
                  self.cached_content = Panda::CMS::EditorJs::Renderer.new(content).render
         | 
| 29 | 
            +
                else
         | 
| 30 | 
            +
                  # For any other case, store as is
         | 
| 31 | 
            +
                  self.cached_content = content.to_s
         | 
| 19 32 | 
             
                end
         | 
| 20 33 | 
             
              end
         | 
| 21 34 | 
             
            end
         | 
    
        data/lib/panda/cms/engine.rb
    CHANGED
    
    | @@ -3,8 +3,12 @@ require "turbo-rails" | |
| 3 3 | 
             
            require "stimulus-rails"
         | 
| 4 4 | 
             
            require "view_component"
         | 
| 5 5 |  | 
| 6 | 
            +
            require "panda/core"
         | 
| 6 7 | 
             
            require "panda/cms/railtie"
         | 
| 7 8 |  | 
| 9 | 
            +
            require "omniauth"
         | 
| 10 | 
            +
            require "omniauth/rails_csrf_protection"
         | 
| 11 | 
            +
             | 
| 8 12 | 
             
            module Panda
         | 
| 9 13 | 
             
              module CMS
         | 
| 10 14 | 
             
                class Engine < ::Rails::Engine
         | 
    
        data/lib/panda-cms/version.rb
    CHANGED
    
    
    
        metadata
    CHANGED
    
    | @@ -1,13 +1,13 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: panda-cms
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0.7. | 
| 4 | 
            +
              version: 0.7.2
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Panda Software Limited
         | 
| 8 8 | 
             
            bindir: bin
         | 
| 9 9 | 
             
            cert_chain: []
         | 
| 10 | 
            -
            date: 2025-01- | 
| 10 | 
            +
            date: 2025-01-19 00:00:00.000000000 Z
         | 
| 11 11 | 
             
            dependencies:
         | 
| 12 12 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 13 13 | 
             
              name: panda-core
         | 
| @@ -452,6 +452,7 @@ files: | |
| 452 452 | 
             
            - app/helpers/panda/cms/admin/posts_helper.rb
         | 
| 453 453 | 
             
            - app/helpers/panda/cms/application_helper.rb
         | 
| 454 454 | 
             
            - app/helpers/panda/cms/pages_helper.rb
         | 
| 455 | 
            +
            - app/helpers/panda/cms/posts_helper.rb
         | 
| 455 456 | 
             
            - app/helpers/panda/cms/theme_helper.rb
         | 
| 456 457 | 
             
            - app/javascript/panda/cms/@editorjs--editorjs.js
         | 
| 457 458 | 
             
            - app/javascript/panda/cms/@hotwired--stimulus.js
         | 
| @@ -468,6 +469,7 @@ files: | |
| 468 469 | 
             
            - app/javascript/panda/cms/editor/editor_js_initializer.js
         | 
| 469 470 | 
             
            - app/javascript/panda/cms/editor/plain_text_editor.js
         | 
| 470 471 | 
             
            - app/javascript/panda/cms/editor/resource_loader.js
         | 
| 472 | 
            +
            - app/javascript/panda/cms/editor/rich_text_editor.js
         | 
| 471 473 | 
             
            - app/javascript/panda/cms/tailwindcss-stimulus-components.js
         | 
| 472 474 | 
             
            - app/jobs/panda/cms/application_job.rb
         | 
| 473 475 | 
             
            - app/jobs/panda/cms/record_visit_job.rb
         | 
| @@ -581,6 +583,7 @@ files: | |
| 581 583 | 
             
            - db/migrate/20241120110943_add_editor_js_to_posts.rb
         | 
| 582 584 | 
             
            - db/migrate/20241120113859_add_cached_content_to_panda_cms_posts.rb
         | 
| 583 585 | 
             
            - db/migrate/20241123234140_remove_post_tag_id_from_posts.rb
         | 
| 586 | 
            +
            - db/migrate/20250106223303_add_author_id_to_panda_cms_posts.rb
         | 
| 584 587 | 
             
            - db/migrate/migrate
         | 
| 585 588 | 
             
            - db/seeds.rb
         | 
| 586 589 | 
             
            - lib/generators/panda/cms/install_generator.rb
         |