panda_cms 0.5.3 → 0.5.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/assets/builds/panda_cms.css +1 -1
- data/app/assets/stylesheets/panda_cms/application.tailwind.css +39 -3
- 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 +3 -3
- data/app/components/panda_cms/admin/user_activity_component.rb +18 -6
- data/app/components/panda_cms/admin/user_display_component.html.erb +10 -4
- data/app/components/panda_cms/rich_text_component.html.erb +1 -3
- data/app/components/panda_cms/rich_text_component.rb +2 -0
- data/app/controllers/panda_cms/admin/pages_controller.rb +2 -2
- 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 +3 -2
- data/app/helpers/panda_cms/application_helper.rb +1 -1
- 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 +23 -2
- data/app/models/panda_cms/post_tag.rb +7 -0
- data/app/models/panda_cms/template.rb +1 -0
- 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/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
- data/public/panda-cms-assets/javascripts/embed/editable.js +87 -37
- data/public/panda-cms-assets/javascripts/embed/rich_text.css +1294 -0
- metadata +40 -42
- data/config/tailwind.embed.config.js +0 -20
@@ -11,15 +11,36 @@ 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
|
-
content.gsub(/<[^>]*>/, "").truncate(length)
|
31
|
+
content.gsub(/<[^>]*>/, "").truncate(length).html_safe
|
19
32
|
end
|
20
33
|
|
21
34
|
def path
|
22
|
-
PandaCms.posts[:prefix] +
|
35
|
+
"/" + PandaCms.posts[:prefix] + slug.to_s
|
36
|
+
end
|
37
|
+
|
38
|
+
def formatted_slug
|
39
|
+
if params[:slug][0] != "/"
|
40
|
+
"/#{params[:slug]}"
|
41
|
+
else
|
42
|
+
params[:slug]
|
43
|
+
end
|
23
44
|
end
|
24
45
|
end
|
25
46
|
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]
|
@@ -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|
|
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
@@ -30,7 +30,6 @@ class EditableController {
|
|
30
30
|
|
31
31
|
this.embedPlainTextEditors();
|
32
32
|
|
33
|
-
this.styleRichTextEditor();
|
34
33
|
this.embedRichTextEditor();
|
35
34
|
}
|
36
35
|
|
@@ -119,23 +118,87 @@ class EditableController {
|
|
119
118
|
}
|
120
119
|
|
121
120
|
embedRichTextEditor() {
|
122
|
-
if (this.body.getElementsByClassName("
|
121
|
+
if (this.body.getElementsByClassName("content-rich-text").length == 0) {
|
123
122
|
this.setFrameVisible();
|
124
123
|
return;
|
125
124
|
}
|
126
125
|
|
126
|
+
console.debug("[Panda CMS] Loading Quill rich text editor...");
|
127
|
+
|
127
128
|
this.addStylesheet(
|
128
129
|
this.frameDocument,
|
129
130
|
this.head,
|
130
|
-
"
|
131
|
+
"/panda-cms-assets/javascripts/embed/rich_text.css?ver=1.0.0"
|
132
|
+
);
|
133
|
+
|
134
|
+
var style = this.frameDocument.createElement("style");
|
135
|
+
// TODO: Base these on "default" set styles
|
136
|
+
style.innerHTML = `
|
137
|
+
.ql-snow .ql-editor h2 {
|
138
|
+
font-size: 1.5em
|
139
|
+
}
|
140
|
+
|
141
|
+
.ql-snow .ql-editor h3 {
|
142
|
+
font-size: 1.17em
|
143
|
+
}
|
144
|
+
|
145
|
+
.ql-snow .ql-editor h4 {
|
146
|
+
font-size: 1em
|
147
|
+
}
|
148
|
+
|
149
|
+
.ql-snow .ql-editor h5 {
|
150
|
+
font-size: .83em
|
151
|
+
}
|
152
|
+
|
153
|
+
.ql-snow .ql-editor h6 {
|
154
|
+
font-size: .67em
|
155
|
+
}
|
156
|
+
|
157
|
+
.ql-snow .ql-editor a {
|
158
|
+
text-decoration: underline
|
159
|
+
}
|
160
|
+
|
161
|
+
.ql-snow .ql-editor blockquote {
|
162
|
+
border-left: 4px solid #ccc;
|
163
|
+
margin-bottom: 5px;
|
164
|
+
margin-top: 5px;
|
165
|
+
padding-left: 16px
|
166
|
+
}
|
167
|
+
|
168
|
+
.ql-snow .ql-editor code,
|
169
|
+
.ql-snow .ql-editor .ql-code-block-container {
|
170
|
+
background-color: #f0f0f0;
|
171
|
+
border-radius: 3px
|
172
|
+
}
|
173
|
+
|
174
|
+
.ql-snow .ql-editor .ql-code-block-container {
|
175
|
+
margin-bottom: 5px;
|
176
|
+
margin-top: 5px;
|
177
|
+
padding: 5px 10px
|
178
|
+
}
|
179
|
+
|
180
|
+
.ql-snow .ql-editor code {
|
181
|
+
font-size: 85%;
|
182
|
+
padding: 2px 4px
|
183
|
+
}
|
184
|
+
|
185
|
+
.ql-snow .ql-editor .ql-code-block-container {
|
186
|
+
background-color: #23241f;
|
187
|
+
color: #f8f8f2;
|
188
|
+
overflow: visible
|
189
|
+
}
|
190
|
+
|
191
|
+
.ql-snow .ql-editor img {
|
192
|
+
max-width: 100%
|
193
|
+
}
|
194
|
+
`;
|
195
|
+
this.head.append(style);
|
196
|
+
|
197
|
+
this.loadScript(
|
198
|
+
this.frameDocument,
|
199
|
+
this.head,
|
200
|
+
"https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.js"
|
131
201
|
)
|
132
|
-
.then(() => {
|
133
|
-
return this.loadScript(
|
134
|
-
this.frameDocument,
|
135
|
-
this.head,
|
136
|
-
"https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.js"
|
137
|
-
);
|
138
|
-
})
|
139
202
|
.then(() => {
|
140
203
|
return this.loadScript(
|
141
204
|
this.frameDocument,
|
@@ -150,14 +213,16 @@ class EditableController {
|
|
150
213
|
"https://unpkg.com/quill-magic-url@3.0.0/dist/index.js"
|
151
214
|
);
|
152
215
|
})
|
216
|
+
// .then(() => {
|
217
|
+
// return this.loadScript(
|
218
|
+
// this.frameDocument,
|
219
|
+
// this.head,
|
220
|
+
// "https://cdn.jsdelivr.net/npm/quill-markdown-shortcuts@latest/dist/markdownShortcuts.js"
|
221
|
+
// );
|
222
|
+
// })
|
153
223
|
.then(() => {
|
154
|
-
|
155
|
-
|
156
|
-
this.head,
|
157
|
-
"https://cdn.jsdelivr.net/npm/quill-markdown-shortcuts@latest/dist/markdownShortcuts.js"
|
158
|
-
);
|
159
|
-
})
|
160
|
-
.then(() => {
|
224
|
+
this.styleRichTextEditor();
|
225
|
+
|
161
226
|
console.debug(
|
162
227
|
"[Panda CMS] Dispatching event: pandaCmsRichTextEditorLoaded"
|
163
228
|
);
|
@@ -203,28 +268,17 @@ class EditableController {
|
|
203
268
|
}
|
204
269
|
|
205
270
|
var style = this.frameDocument.createElement("style");
|
206
|
-
style.innerHTML = `.ql-container
|
207
|
-
|
208
|
-
margin: inherit !important;
|
209
|
-
padding: inherit !important;
|
210
|
-
font-size: inherit !important;
|
211
|
-
line-height: inherit !important;
|
212
|
-
font-family: inherit !important;
|
271
|
+
style.innerHTML = `.ql-editor:hover, .ql-container:hover {
|
272
|
+
cursor: pointer !important;
|
213
273
|
}
|
214
274
|
|
215
|
-
.ql-
|
216
|
-
|
217
|
-
border-radius: 10px;
|
218
|
-
}
|
219
|
-
|
220
|
-
.ql-editor:hover, .ql-container:hover {
|
221
|
-
cursor: pointer !important;
|
275
|
+
.ql-container.ql-snow {
|
276
|
+
margin-top: 0 important;
|
222
277
|
}
|
223
278
|
|
224
279
|
.ql-editor:hover, .ql-editor:focus, .ql-editor:active {
|
225
280
|
background-color: #e1effa;
|
226
281
|
cursor: pointer !important;
|
227
|
-
font-size: inherit !important;
|
228
282
|
-webkit-transition: background-color 500ms linear;
|
229
283
|
-ms-transition: background-color 500ms linear;
|
230
284
|
transition: background-color 500ms linear;
|
@@ -235,14 +289,10 @@ class EditableController {
|
|
235
289
|
-webkit-transition: background-color 1000ms linear;
|
236
290
|
-ms-transition: background-color 1000ms linear;
|
237
291
|
transition: background-color 1000ms linear;
|
238
|
-
}
|
239
|
-
|
240
|
-
.ql-tooltip {
|
241
|
-
z-index: 999;
|
242
|
-
min-width: 90vw;
|
243
292
|
}`;
|
244
293
|
|
245
294
|
this.head.append(style);
|
295
|
+
console.log("Appended styles to head");
|
246
296
|
}
|
247
297
|
|
248
298
|
bindSaveHandler(blockContentId, content) {
|