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.
- checksums.yaml +4 -4
- data/README.md +6 -5
- data/app/assets/builds/panda_cms.css +1 -1
- data/app/assets/stylesheets/panda_cms/application.tailwind.css +34 -0
- data/app/builders/panda_cms/form_builder.rb +54 -16
- data/app/components/panda_cms/admin/button_component.rb +3 -3
- data/app/components/panda_cms/admin/flash_message_component.rb +5 -5
- data/app/components/panda_cms/admin/user_activity_component.html.erb +2 -2
- data/app/components/panda_cms/admin/user_activity_component.rb +17 -6
- data/app/controllers/panda_cms/admin/pages_controller.rb +3 -3
- data/app/controllers/panda_cms/admin/posts_controller.rb +73 -7
- data/app/controllers/panda_cms/pages_controller.rb +1 -1
- data/app/controllers/panda_cms/posts_controller.rb +2 -1
- data/app/helpers/panda_cms/application_helper.rb +1 -1
- data/app/lib/panda_cms/demo_site_generator.rb +13 -5
- data/app/models/action_text/rich_text_version.rb +6 -0
- data/app/models/panda_cms/page.rb +7 -0
- data/app/models/panda_cms/post.rb +14 -1
- data/app/models/panda_cms/post_tag.rb +7 -0
- data/app/models/panda_cms/template.rb +4 -1
- data/app/models/panda_cms/user.rb +10 -6
- data/app/views/active_storage/blobs/blobs/_blob.html.erb +14 -0
- data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
- data/app/views/panda_cms/admin/pages/index.html.erb +14 -8
- data/app/views/panda_cms/admin/pages/new.html.erb +1 -1
- data/app/views/panda_cms/admin/posts/_form.html.erb +17 -0
- data/app/views/panda_cms/admin/posts/edit.html.erb +6 -0
- data/app/views/panda_cms/admin/posts/index.html.erb +2 -0
- data/app/views/panda_cms/admin/posts/new.html.erb +6 -0
- data/app/views/panda_cms/shared/_header.html.erb +1 -0
- data/config/importmap.rb +1 -0
- data/config/initializers/panda_cms/form_errors.rb +2 -2
- data/config/initializers/panda_cms/paper_trail.rb +7 -0
- data/config/initializers/panda_cms.rb +5 -3
- data/config/locales/en.yml +18 -1
- data/config/tailwind.config.js +1 -0
- data/db/migrate/20240904200605_create_action_text_tables.action_text.rb +24 -0
- data/lib/panda_cms/version.rb +1 -1
- metadata +39 -41
@@ -11,7 +11,7 @@ module PandaCms
|
|
11
11
|
#
|
12
12
|
# Creates initial templates and empty blocks
|
13
13
|
#
|
14
|
-
# @return
|
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
|
-
|
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
|
-
|
51
|
-
|
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
|
@@ -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] +
|
35
|
+
"/" + PandaCms.posts[:prefix] + slug.to_s
|
23
36
|
end
|
24
37
|
end
|
25
38
|
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
|
-
|
9
|
-
|
10
|
-
|
7
|
+
def is_admin?
|
8
|
+
admin
|
9
|
+
end
|
10
|
+
|
11
|
+
def name
|
12
|
+
"#{firstname} #{lastname}"
|
13
|
+
end
|
11
14
|
|
12
|
-
|
13
|
-
|
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>
|
@@ -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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
<%=
|
10
|
-
|
11
|
-
|
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
|
-
|
14
|
-
|
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 %>
|
@@ -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
|
|
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-
|
5
|
-
message_class = "block w-full text-
|
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|
|
@@ -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:
|
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
|
}
|
data/config/locales/en.yml
CHANGED
@@ -3,8 +3,21 @@ en:
|
|
3
3
|
attributes:
|
4
4
|
panda_cms/page:
|
5
5
|
title: Title
|
6
|
-
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.
|
data/config/tailwind.config.js
CHANGED
@@ -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
|
data/lib/panda_cms/version.rb
CHANGED