panda-cms 0.7.0 → 0.7.3
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 +0 -50
- data/app/components/panda/cms/admin/table_component.html.erb +1 -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 +3 -6
- data/lib/panda-cms/version.rb +1 -1
- data/lib/panda-cms.rb +5 -11
- metadata +290 -35
@@ -1,46 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Panda
|
2
4
|
module CMS
|
3
5
|
module Admin
|
4
6
|
module PostsHelper
|
5
7
|
def editor_content_for(post, preserved_content = nil)
|
6
|
-
|
7
|
-
Rails.logger.debug "Preserved content: #{preserved_content.inspect}"
|
8
|
-
Rails.logger.debug "Post content: #{post.content.inspect}"
|
8
|
+
content = preserved_content || post.content
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
10
|
+
# Return empty structure if no content
|
11
|
+
json_content = if content.blank?
|
12
|
+
{blocks: []}.to_json
|
13
|
+
# If content is already JSON string, use it
|
14
|
+
elsif content.is_a?(String) && valid_json?(content)
|
15
|
+
content
|
16
|
+
# If it's a hash, convert to JSON
|
17
|
+
elsif content.is_a?(Hash)
|
18
|
+
content.to_json
|
19
|
+
# Default to empty structure
|
18
20
|
else
|
19
|
-
|
21
|
+
{blocks: []}.to_json
|
20
22
|
end
|
21
23
|
|
22
|
-
|
24
|
+
# Base64 encode the JSON content
|
25
|
+
Base64.strict_encode64(json_content)
|
26
|
+
end
|
23
27
|
|
24
|
-
|
25
|
-
content = if content.blank? || !content.is_a?(Hash) || !content.key?("blocks")
|
26
|
-
Rails.logger.debug "Creating new editor content structure"
|
27
|
-
{
|
28
|
-
time: Time.now.to_i * 1000,
|
29
|
-
blocks: [],
|
30
|
-
version: "2.28.2"
|
31
|
-
}
|
32
|
-
else
|
33
|
-
# Ensure we have all required fields
|
34
|
-
Rails.logger.debug "Ensuring required fields are present"
|
35
|
-
content["time"] ||= Time.now.to_i * 1000
|
36
|
-
content["version"] ||= "2.28.2"
|
37
|
-
content["blocks"] ||= []
|
38
|
-
content
|
39
|
-
end
|
28
|
+
private
|
40
29
|
|
41
|
-
|
42
|
-
|
43
|
-
|
30
|
+
def valid_json?(string)
|
31
|
+
JSON.parse(string)
|
32
|
+
true
|
33
|
+
rescue JSON::ParserError
|
34
|
+
false
|
44
35
|
end
|
45
36
|
end
|
46
37
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Panda
|
2
|
+
module CMS
|
3
|
+
module PostsHelper
|
4
|
+
def display_post_path(post)
|
5
|
+
# Unescape the path for display purposes
|
6
|
+
CGI.unescape(post.slug)
|
7
|
+
end
|
8
|
+
|
9
|
+
def posts_months_menu
|
10
|
+
Rails.cache.fetch("panda_cms_posts_months_menu", expires_in: 1.hour) do
|
11
|
+
Panda::CMS::Post
|
12
|
+
.where(status: :active)
|
13
|
+
.select(
|
14
|
+
Arel.sql("DATE_TRUNC('month', published_at) as month"),
|
15
|
+
Arel.sql("COUNT(*) as post_count")
|
16
|
+
)
|
17
|
+
.group(Arel.sql("DATE_TRUNC('month', published_at)"))
|
18
|
+
.order(Arel.sql("DATE_TRUNC('month', published_at) DESC"))
|
19
|
+
.map do |result|
|
20
|
+
date = result.month
|
21
|
+
{
|
22
|
+
year: date.strftime("%Y"),
|
23
|
+
month: date.strftime("%m"),
|
24
|
+
month_name: "#{date.strftime("%B")} #{date.year}",
|
25
|
+
post_count: result.post_count
|
26
|
+
}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -1,4 +1,6 @@
|
|
1
1
|
import { Controller } from "@hotwired/stimulus";
|
2
|
+
import { EDITOR_JS_RESOURCES, EDITOR_JS_CSS } from "panda/cms/editor/editor_js_config";
|
3
|
+
import { ResourceLoader } from "panda/cms/editor/resource_loader";
|
2
4
|
|
3
5
|
export default class extends Controller {
|
4
6
|
static targets = ["editorContainer", "hiddenField"];
|
@@ -7,7 +9,27 @@ export default class extends Controller {
|
|
7
9
|
};
|
8
10
|
|
9
11
|
connect() {
|
10
|
-
this.
|
12
|
+
this.loadEditorResources();
|
13
|
+
}
|
14
|
+
|
15
|
+
async loadEditorResources() {
|
16
|
+
try {
|
17
|
+
// First load EditorJS core
|
18
|
+
const editorCore = EDITOR_JS_RESOURCES[0];
|
19
|
+
await ResourceLoader.loadScript(document, document.head, editorCore);
|
20
|
+
|
21
|
+
// Load CSS
|
22
|
+
await ResourceLoader.embedCSS(document, document.head, EDITOR_JS_CSS);
|
23
|
+
|
24
|
+
// Then load all tools sequentially
|
25
|
+
for (const resource of EDITOR_JS_RESOURCES.slice(1)) {
|
26
|
+
await ResourceLoader.loadScript(document, document.head, resource);
|
27
|
+
}
|
28
|
+
|
29
|
+
await this.initializeEditor();
|
30
|
+
} catch (error) {
|
31
|
+
console.error("[Panda CMS] Failed to load editor resources:", error);
|
32
|
+
}
|
11
33
|
}
|
12
34
|
|
13
35
|
async initializeEditor() {
|
@@ -26,13 +48,15 @@ export default class extends Controller {
|
|
26
48
|
const { getEditorConfig } = await import(
|
27
49
|
"panda/cms/editor/editor_js_config"
|
28
50
|
);
|
29
|
-
const config = getEditorConfig(holderId, this.getInitialContent());
|
30
51
|
|
31
|
-
|
52
|
+
// Get initial content before creating config
|
53
|
+
const initialContent = this.getInitialContent();
|
54
|
+
console.debug("[Panda CMS] Using initial content:", initialContent);
|
32
55
|
|
33
|
-
|
34
|
-
...
|
56
|
+
const config = {
|
57
|
+
...getEditorConfig(holderId, initialContent),
|
35
58
|
holder: holderId,
|
59
|
+
data: initialContent,
|
36
60
|
autofocus: false,
|
37
61
|
minHeight: 1,
|
38
62
|
logLevel: "ERROR",
|
@@ -40,26 +64,125 @@ export default class extends Controller {
|
|
40
64
|
if (!this.editor) return;
|
41
65
|
this.editor.save().then((outputData) => {
|
42
66
|
outputData.source = "editorJS";
|
43
|
-
|
67
|
+
const jsonString = JSON.stringify(outputData);
|
68
|
+
// Store both base64 and regular JSON
|
69
|
+
this.editorContainerTarget.dataset.editablePreviousData = btoa(jsonString);
|
70
|
+
this.editorContainerTarget.dataset.editableContent = jsonString;
|
71
|
+
this.hiddenFieldTarget.value = jsonString;
|
44
72
|
});
|
45
73
|
},
|
46
|
-
|
74
|
+
onReady: () => {
|
75
|
+
console.debug("[Panda CMS] Editor ready with content:", initialContent);
|
76
|
+
this.editorContainerTarget.dataset.editorInitialized = "true";
|
77
|
+
holderDiv.dataset.editorInitialized = "true";
|
78
|
+
// Add a class to indicate the editor is ready
|
79
|
+
holderDiv.classList.add("editor-ready");
|
80
|
+
// Dispatch an event when editor is ready
|
81
|
+
this.editorContainerTarget.dispatchEvent(new CustomEvent("editor:ready"));
|
82
|
+
},
|
83
|
+
tools: {
|
84
|
+
paragraph: {
|
85
|
+
class: window.Paragraph,
|
86
|
+
inlineToolbar: true
|
87
|
+
},
|
88
|
+
header: {
|
89
|
+
class: window.Header,
|
90
|
+
inlineToolbar: true
|
91
|
+
},
|
92
|
+
list: {
|
93
|
+
class: window.NestedList,
|
94
|
+
inlineToolbar: true,
|
95
|
+
config: {
|
96
|
+
defaultStyle: 'unordered',
|
97
|
+
enableLineBreaks: true
|
98
|
+
}
|
99
|
+
},
|
100
|
+
quote: {
|
101
|
+
class: window.Quote,
|
102
|
+
inlineToolbar: true
|
103
|
+
},
|
104
|
+
table: {
|
105
|
+
class: window.Table,
|
106
|
+
inlineToolbar: true
|
107
|
+
}
|
108
|
+
}
|
109
|
+
};
|
110
|
+
|
111
|
+
// Ensure EditorJS is available
|
112
|
+
const EditorJS = window.EditorJS;
|
113
|
+
if (!EditorJS) {
|
114
|
+
throw new Error("EditorJS not loaded");
|
115
|
+
}
|
116
|
+
|
117
|
+
this.editor = new EditorJS(config);
|
118
|
+
|
119
|
+
// Wait for editor to be ready
|
120
|
+
await this.editor.isReady;
|
121
|
+
console.debug("[Panda CMS] Editor initialized successfully");
|
122
|
+
this.editorContainerTarget.dataset.editorInitialized = "true";
|
123
|
+
holderDiv.dataset.editorInitialized = "true";
|
124
|
+
// Add a class to indicate the editor is ready
|
125
|
+
holderDiv.classList.add("editor-ready");
|
126
|
+
// Dispatch an event when editor is ready
|
127
|
+
this.editorContainerTarget.dispatchEvent(new CustomEvent("editor:ready"));
|
128
|
+
|
47
129
|
} catch (error) {
|
48
130
|
console.error("[Panda CMS] Editor setup failed:", error);
|
131
|
+
this.editorContainerTarget.dataset.editorInitialized = "false";
|
132
|
+
if (holderDiv) {
|
133
|
+
holderDiv.dataset.editorInitialized = "false";
|
134
|
+
holderDiv.classList.remove("editor-ready");
|
135
|
+
}
|
49
136
|
}
|
50
137
|
}
|
51
138
|
|
52
139
|
getInitialContent() {
|
53
140
|
try {
|
54
|
-
const
|
55
|
-
if (
|
56
|
-
|
57
|
-
|
141
|
+
const initialContent = this.hiddenFieldTarget.getAttribute("data-initial-content");
|
142
|
+
if (initialContent && initialContent !== "{}") {
|
143
|
+
try {
|
144
|
+
// First try to decode as base64
|
145
|
+
try {
|
146
|
+
const decodedData = atob(initialContent);
|
147
|
+
const data = JSON.parse(decodedData);
|
148
|
+
if (data.blocks) return data;
|
149
|
+
} catch (e) {
|
150
|
+
// If base64 decode fails, try direct JSON parse
|
151
|
+
const data = JSON.parse(initialContent);
|
152
|
+
if (data.blocks) return data;
|
153
|
+
}
|
154
|
+
} catch (e) {
|
155
|
+
console.error("[Panda CMS] Failed to parse content:", e);
|
156
|
+
}
|
157
|
+
}
|
158
|
+
|
159
|
+
// Try to get content from the editor container's data attributes
|
160
|
+
const previousData = this.editorContainerTarget.dataset.editablePreviousData;
|
161
|
+
const editorContent = this.editorContainerTarget.dataset.editableContent;
|
162
|
+
|
163
|
+
if (previousData) {
|
164
|
+
try {
|
165
|
+
const decodedData = atob(previousData);
|
166
|
+
const data = JSON.parse(decodedData);
|
167
|
+
if (data.blocks) return data;
|
168
|
+
} catch (e) {
|
169
|
+
console.debug("[Panda CMS] Failed to parse base64 data:", e);
|
170
|
+
}
|
171
|
+
}
|
172
|
+
|
173
|
+
if (editorContent && editorContent !== "{}") {
|
174
|
+
try {
|
175
|
+
const data = JSON.parse(editorContent);
|
176
|
+
if (data.blocks) return data;
|
177
|
+
} catch (e) {
|
178
|
+
console.debug("[Panda CMS] Failed to parse editor content:", e);
|
179
|
+
}
|
58
180
|
}
|
59
181
|
} catch (e) {
|
60
182
|
console.warn("[Panda CMS] Could not parse initial content:", e);
|
61
183
|
}
|
62
184
|
|
185
|
+
// Return default content if nothing else works
|
63
186
|
return {
|
64
187
|
time: Date.now(),
|
65
188
|
blocks: [{ type: "paragraph", data: { text: "" } }],
|