panda-cms 0.7.4 → 0.8.0
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/README.md +37 -2
- data/Rakefile +2 -0
- data/app/components/panda/cms/admin/statistics_component.rb +1 -2
- data/app/components/panda/cms/admin/user_activity_component.html.erb +3 -1
- data/app/components/panda/cms/admin/user_activity_component.rb +3 -5
- data/app/components/panda/cms/admin/user_display_component.html.erb +1 -1
- data/app/components/panda/cms/admin/user_display_component.rb +2 -2
- data/app/components/panda/cms/code_component.rb +8 -4
- data/app/components/panda/cms/menu_component.rb +7 -6
- data/app/components/panda/cms/page_menu_component.rb +15 -17
- data/app/components/panda/cms/rich_text_component.rb +10 -11
- data/app/components/panda/cms/text_component.rb +6 -7
- data/app/controllers/panda/cms/admin/base_controller.rb +18 -0
- data/app/controllers/panda/cms/admin/block_contents_controller.rb +1 -2
- data/app/controllers/panda/cms/admin/dashboard_controller.rb +14 -9
- data/app/controllers/panda/cms/admin/files_controller.rb +1 -3
- data/app/controllers/panda/cms/admin/forms_controller.rb +3 -6
- data/app/controllers/panda/cms/admin/menus_controller.rb +2 -3
- data/app/controllers/panda/cms/admin/pages_controller.rb +9 -8
- data/app/controllers/panda/cms/admin/posts_controller.rb +9 -11
- data/app/controllers/panda/cms/admin/settings/bulk_editor_controller.rb +32 -25
- data/app/controllers/panda/cms/admin/settings_controller.rb +13 -10
- data/app/controllers/panda/cms/application_controller.rb +19 -6
- data/app/controllers/panda/cms/errors_controller.rb +5 -2
- data/app/controllers/panda/cms/form_submissions_controller.rb +2 -0
- data/app/controllers/panda/cms/pages_controller.rb +34 -31
- data/app/controllers/panda/cms/posts_controller.rb +2 -0
- data/app/helpers/panda/cms/admin/files_helper.rb +5 -1
- data/app/helpers/panda/cms/admin/pages_helper.rb +5 -1
- data/app/helpers/panda/cms/application_helper.rb +3 -3
- data/app/helpers/panda/cms/asset_helper.rb +195 -0
- data/app/helpers/panda/cms/pages_helper.rb +2 -0
- data/app/helpers/panda/cms/posts_helper.rb +2 -0
- data/app/helpers/panda/cms/theme_helper.rb +2 -0
- data/app/javascript/panda/cms/application_panda_cms.js +2 -34
- data/app/javascript/panda/cms/controllers/editor_form_controller.js +59 -6
- data/app/javascript/panda/cms/controllers/index.js +8 -24
- data/app/javascript/panda/cms/stimulus-loading.js +39 -0
- data/app/javascript/panda_cms/stimulus-loading.js +39 -0
- data/app/jobs/panda/cms/application_job.rb +2 -0
- data/app/jobs/panda/cms/record_visit_job.rb +2 -0
- data/app/mailers/panda/cms/application_mailer.rb +2 -0
- data/app/mailers/panda/cms/form_mailer.rb +3 -1
- data/app/models/panda/cms/application_record.rb +2 -0
- data/app/models/panda/cms/block.rb +4 -1
- data/app/models/panda/cms/block_content.rb +3 -1
- data/app/models/panda/cms/current.rb +5 -12
- data/app/models/panda/cms/form.rb +2 -0
- data/app/models/panda/cms/form_submission.rb +2 -0
- data/app/models/panda/cms/menu.rb +12 -9
- data/app/models/panda/cms/menu_item.rb +10 -6
- data/app/models/panda/cms/page.rb +14 -12
- data/app/models/panda/cms/post.rb +12 -8
- data/app/models/panda/cms/redirect.rb +6 -3
- data/app/models/panda/cms/template.rb +12 -7
- data/app/models/panda/cms/visit.rb +3 -1
- data/app/models/panda/social/instagram_post.rb +2 -0
- data/app/services/panda/social/instagram_feed_service.rb +3 -1
- data/app/views/layouts/different_page.html.erb +6 -0
- data/app/views/layouts/homepage.html.erb +37 -0
- data/app/views/layouts/page.html.erb +18 -0
- data/app/views/layouts/panda/cms/application.html.erb +2 -1
- data/app/views/panda/cms/admin/dashboard/show.html.erb +2 -2
- data/app/views/panda/cms/admin/files/index.html.erb +1 -1
- data/app/views/panda/cms/admin/forms/index.html.erb +4 -4
- data/app/views/panda/cms/admin/forms/new.html.erb +2 -2
- data/app/views/panda/cms/admin/forms/show.html.erb +1 -1
- data/app/views/panda/cms/admin/menus/index.html.erb +4 -4
- data/app/views/panda/cms/admin/pages/edit.html.erb +6 -6
- data/app/views/panda/cms/admin/pages/index.html.erb +5 -5
- data/app/views/panda/cms/admin/pages/new.html.erb +16 -10
- data/app/views/panda/cms/admin/posts/_form.html.erb +1 -1
- data/app/views/panda/cms/admin/posts/edit.html.erb +2 -2
- data/app/views/panda/cms/admin/posts/index.html.erb +5 -5
- data/app/views/panda/cms/admin/posts/new.html.erb +1 -1
- data/app/views/panda/cms/admin/settings/bulk_editor/new.html.erb +1 -1
- data/app/views/panda/cms/admin/settings/index.html.erb +4 -4
- data/app/views/panda/cms/admin/shared/_breadcrumbs.html.erb +3 -3
- data/app/views/panda/cms/admin/shared/_flash.html.erb +1 -1
- data/app/views/panda/cms/admin/shared/_sidebar.html.erb +8 -8
- data/app/views/panda/cms/shared/_header.html.erb +10 -2
- data/app/views/panda/cms/shared/_importmap.html.erb +1 -1
- data/app/views/shared/_footer.html.erb +3 -0
- data/app/views/shared/_header.html.erb +11 -0
- data/config/importmap.rb +2 -0
- data/config/initializers/inflections.rb +2 -0
- data/config/initializers/panda/cms/form_errors.rb +20 -21
- data/config/initializers/panda/cms/healthcheck_log_silencer.rb +2 -0
- data/config/initializers/panda/cms.rb +8 -3
- data/config/initializers/zeitwork.rb +2 -0
- data/config/puma/test.rb +3 -1
- data/config/routes.rb +11 -19
- data/db/migrate/20240205223709_create_panda_cms_pages.rb +2 -0
- data/db/migrate/20240219213327_create_panda_cms_page_versions.rb +2 -0
- data/db/migrate/20240303002805_create_panda_cms_templates.rb +4 -1
- data/db/migrate/20240303003434_create_panda_cms_template_versions.rb +2 -0
- data/db/migrate/20240303022441_create_panda_cms_blocks.rb +4 -1
- data/db/migrate/20240303024256_create_panda_cms_block_contents.rb +2 -0
- data/db/migrate/20240303024746_create_panda_cms_block_content_versions.rb +2 -0
- data/db/migrate/20240303233238_add_panda_cms_menu_table.rb +2 -0
- data/db/migrate/20240303234724_add_panda_cms_menu_item_table.rb +2 -0
- data/db/migrate/20240304134343_add_parent_id_to_panda_cms_pages.rb +2 -0
- data/db/migrate/20240315125411_add_status_to_panda_cms_pages.rb +7 -5
- data/db/migrate/20240315125421_add_nested_sets_to_panda_cms_pages.rb +2 -0
- data/db/migrate/20240316212822_add_kind_to_panda_cms_menus.rb +3 -1
- data/db/migrate/20240316221425_add_start_page_to_panda_cms_menus.rb +2 -0
- data/db/migrate/20240316230706_add_nested_to_panda_cms_menu_items.rb +2 -0
- data/db/migrate/20240317010532_create_panda_cms_users.rb +2 -0
- data/db/migrate/20240317161534_add_max_uses_to_panda_cms_template.rb +2 -0
- data/db/migrate/20240317163053_reset_counter_cache_on_panda_cms_template.rb +2 -0
- data/db/migrate/20240317214827_create_panda_cms_redirects.rb +2 -0
- data/db/migrate/20240317230622_create_panda_cms_visits.rb +2 -0
- data/db/migrate/20240324205703_create_active_storage_tables.active_storage.rb +5 -2
- data/db/migrate/20240408084718_default_panda_cms_users_admin_to_false.rb +2 -0
- data/db/migrate/20240701225422_add_service_name_to_active_storage_blobs.active_storage.rb +8 -6
- data/db/migrate/20240701225423_create_active_storage_variant_records.active_storage.rb +2 -0
- data/db/migrate/20240701225424_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb +2 -0
- data/db/migrate/20240804235210_create_panda_cms_forms.rb +2 -0
- data/db/migrate/20240805013612_create_panda_cms_form_submissions.rb +2 -0
- data/db/migrate/20240805121123_create_panda_cms_posts.rb +3 -1
- data/db/migrate/20240805123104_create_panda_cms_post_versions.rb +2 -0
- data/db/migrate/20240806112735_fix_panda_cms_visits_column_names.rb +2 -0
- data/db/migrate/20240806204412_add_completion_path_to_panda_cms_forms.rb +2 -0
- data/db/migrate/20240820081917_change_form_submissions_to_submission_count.rb +2 -0
- data/db/migrate/20240923234535_add_depth_to_panda_cms_menus.rb +6 -4
- data/db/migrate/20241031205109_add_cached_content_to_panda_cms_block_contents.rb +2 -0
- data/db/migrate/20241119214548_convert_post_content_to_editor_js.rb +2 -0
- data/db/migrate/20241120000419_remove_post_tag_references.rb +2 -0
- data/db/migrate/20241120110943_add_editor_js_to_posts.rb +2 -0
- data/db/migrate/20241120113859_add_cached_content_to_panda_cms_posts.rb +2 -0
- data/db/migrate/20241123234140_remove_post_tag_id_from_posts.rb +2 -0
- data/db/migrate/20250106223303_add_author_id_to_panda_cms_posts.rb +5 -1
- data/db/migrate/20250120235542_remove_paper_trail.rb +5 -4
- data/db/migrate/20250126234001_create_panda_social_instagram_posts.rb +4 -0
- data/db/migrate/20250809231125_migrate_users_to_panda_core.rb +111 -0
- data/db/migrate/20250811111000_make_post_user_references_nullable.rb +11 -0
- data/db/seeds.rb +2 -0
- data/lib/generators/panda/cms/install_generator.rb +2 -0
- data/lib/panda/cms/asset_loader.rb +390 -0
- data/lib/panda/cms/bulk_editor.rb +7 -3
- data/lib/panda/cms/demo_site_generator.rb +2 -0
- data/lib/panda/cms/engine.rb +57 -116
- data/lib/panda/cms/exceptions_app.rb +2 -0
- data/lib/panda/cms/railtie.rb +2 -0
- data/lib/panda/cms/slug.rb +3 -1
- data/lib/panda-cms/version.rb +3 -1
- data/lib/panda-cms.rb +54 -42
- data/lib/tasks/assets.rake +587 -0
- data/lib/tasks/panda/cms/install.rake +2 -0
- data/lib/tasks/panda/cms/migrations.rake +13 -0
- data/lib/tasks/panda/social/instagram.rake +2 -0
- data/lib/tasks/panda_cms.rake +3 -30
- data/public/panda-cms-assets/manifest.json +20 -0
- data/public/panda-cms-assets/panda-cms-0.7.4.css +26 -0
- data/public/panda-cms-assets/panda-cms-0.7.4.js +150 -0
- metadata +186 -49
- data/app/builders/panda/cms/form_builder.rb +0 -217
- data/app/components/panda/cms/admin/button_component.rb +0 -70
- data/app/components/panda/cms/admin/container_component.rb +0 -13
- data/app/components/panda/cms/admin/flash_message_component.rb +0 -47
- data/app/components/panda/cms/admin/heading_component.rb +0 -45
- data/app/components/panda/cms/admin/panel_component.rb +0 -13
- data/app/components/panda/cms/admin/table_component.rb +0 -46
- data/app/components/panda/cms/admin/tag_component.rb +0 -35
- data/app/constraints/panda/cms/admin_constraint.rb +0 -18
- data/app/controllers/panda/cms/admin/my_profile_controller.rb +0 -43
- data/app/controllers/panda/cms/admin/sessions_controller.rb +0 -94
- data/app/javascript/panda/cms/controllers/theme_form_controller.js +0 -9
- data/app/javascript/panda/cms/editor/css_extractor.js +0 -80
- data/app/javascript/panda/cms/editor/editor_js_config.js +0 -306
- data/app/javascript/panda/cms/editor/editor_js_initializer.js +0 -334
- data/app/javascript/panda/cms/editor/plain_text_editor.js +0 -110
- data/app/javascript/panda/cms/editor/resource_loader.js +0 -204
- data/app/javascript/panda/cms/editor/rich_text_editor.js +0 -162
- data/app/models/panda/cms/breadcrumb.rb +0 -12
- data/app/models/panda/cms/user.rb +0 -31
- data/app/services/panda/cms/html_to_editor_js_converter.rb +0 -193
- data/app/views/panda/cms/admin/my_profile/edit.html.erb +0 -35
- data/app/views/panda/cms/admin/sessions/new.html.erb +0 -17
- data/db/migrate/20250504221812_add_current_theme_to_panda_cms_users.rb +0 -5
- data/lib/panda/cms/editor_js/blocks/alert.rb +0 -34
- data/lib/panda/cms/editor_js/blocks/base.rb +0 -33
- data/lib/panda/cms/editor_js/blocks/header.rb +0 -15
- data/lib/panda/cms/editor_js/blocks/image.rb +0 -36
- data/lib/panda/cms/editor_js/blocks/list.rb +0 -32
- data/lib/panda/cms/editor_js/blocks/paragraph.rb +0 -15
- data/lib/panda/cms/editor_js/blocks/quote.rb +0 -41
- data/lib/panda/cms/editor_js/blocks/table.rb +0 -50
- data/lib/panda/cms/editor_js/renderer.rb +0 -124
- data/lib/panda/cms/editor_js.rb +0 -16
- data/lib/panda/cms/editor_js_content.rb +0 -55
@@ -1,162 +0,0 @@
|
|
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
|
-
}
|
@@ -1,31 +0,0 @@
|
|
1
|
-
module Panda
|
2
|
-
module CMS
|
3
|
-
class User < ApplicationRecord
|
4
|
-
validates :firstname, presence: true
|
5
|
-
validates :lastname, presence: true
|
6
|
-
validates :email, presence: true, uniqueness: true
|
7
|
-
|
8
|
-
before_save :downcase_email
|
9
|
-
|
10
|
-
scope :admin, -> { where(admin: true) }
|
11
|
-
|
12
|
-
def is_admin?
|
13
|
-
admin
|
14
|
-
end
|
15
|
-
|
16
|
-
def name
|
17
|
-
"#{firstname} #{lastname}"
|
18
|
-
end
|
19
|
-
|
20
|
-
def self.for_select_list(scope = :all, order = {firstname: :asc, lastname: :asc})
|
21
|
-
Panda::CMS::User.send(scope).order(order).map { |u| [u.name, u.id] }
|
22
|
-
end
|
23
|
-
|
24
|
-
private
|
25
|
-
|
26
|
-
def downcase_email
|
27
|
-
self.email = email.to_s.downcase
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
@@ -1,193 +0,0 @@
|
|
1
|
-
module Panda
|
2
|
-
module CMS
|
3
|
-
class HtmlToEditorJsConverter
|
4
|
-
class ConversionError < StandardError; end
|
5
|
-
|
6
|
-
def self.convert(html)
|
7
|
-
return {} if html.blank?
|
8
|
-
|
9
|
-
# If it's already in EditorJS format, return as is
|
10
|
-
return html if html.is_a?(Hash) && (html["blocks"].present? || html[:blocks].present?)
|
11
|
-
|
12
|
-
begin
|
13
|
-
# Parse the HTML content
|
14
|
-
doc = Nokogiri::HTML.fragment(html.to_s)
|
15
|
-
raise ConversionError, "Failed to parse HTML content" unless doc
|
16
|
-
|
17
|
-
blocks = []
|
18
|
-
current_text = ""
|
19
|
-
|
20
|
-
doc.children.each do |node|
|
21
|
-
case node.name
|
22
|
-
when "h1", "h2", "h3", "h4", "h5", "h6"
|
23
|
-
# Add any accumulated text as a paragraph before the header
|
24
|
-
if current_text.present?
|
25
|
-
blocks << create_paragraph_block(current_text)
|
26
|
-
current_text = ""
|
27
|
-
end
|
28
|
-
|
29
|
-
blocks << {
|
30
|
-
"type" => "header",
|
31
|
-
"data" => {
|
32
|
-
"text" => node.text.strip,
|
33
|
-
"level" => node.name[1].to_i
|
34
|
-
}
|
35
|
-
}
|
36
|
-
when "p", "div"
|
37
|
-
# Add any accumulated text first
|
38
|
-
if current_text.present?
|
39
|
-
blocks << create_paragraph_block(current_text)
|
40
|
-
current_text = ""
|
41
|
-
end
|
42
|
-
|
43
|
-
if node.name == "div"
|
44
|
-
# Process div children separately
|
45
|
-
node.children.each do |child|
|
46
|
-
case child.name
|
47
|
-
when "h1", "h2", "h3", "h4", "h5", "h6"
|
48
|
-
blocks << {
|
49
|
-
"type" => "header",
|
50
|
-
"data" => {
|
51
|
-
"text" => child.text.strip,
|
52
|
-
"level" => child.name[1].to_i
|
53
|
-
}
|
54
|
-
}
|
55
|
-
when "p"
|
56
|
-
text = process_inline_elements(child)
|
57
|
-
paragraphs = text.split(/<br\s*\/?>\s*<br\s*\/?>/).map(&:strip)
|
58
|
-
paragraphs.each do |paragraph|
|
59
|
-
blocks << create_paragraph_block(paragraph) if paragraph.present?
|
60
|
-
end
|
61
|
-
when "ul", "ol"
|
62
|
-
items = child.css("li").map { |li| process_inline_elements(li) }
|
63
|
-
next if items.empty?
|
64
|
-
|
65
|
-
blocks << {
|
66
|
-
"type" => "list",
|
67
|
-
"data" => {
|
68
|
-
"style" => (child.name == "ul") ? "unordered" : "ordered",
|
69
|
-
"items" => items
|
70
|
-
}
|
71
|
-
}
|
72
|
-
when "blockquote"
|
73
|
-
blocks << {
|
74
|
-
"type" => "quote",
|
75
|
-
"data" => {
|
76
|
-
"text" => process_inline_elements(child),
|
77
|
-
"caption" => "",
|
78
|
-
"alignment" => "left"
|
79
|
-
}
|
80
|
-
}
|
81
|
-
when "text"
|
82
|
-
text = child.text.strip
|
83
|
-
current_text += text if text.present?
|
84
|
-
end
|
85
|
-
end
|
86
|
-
else
|
87
|
-
# Handle p with nested content
|
88
|
-
text = process_inline_elements(node)
|
89
|
-
paragraphs = text.split(/<br\s*\/?>\s*<br\s*\/?>/).map(&:strip)
|
90
|
-
paragraphs.each do |paragraph|
|
91
|
-
blocks << create_paragraph_block(paragraph) if paragraph.present?
|
92
|
-
end
|
93
|
-
end
|
94
|
-
when "br"
|
95
|
-
current_text += "\n\n"
|
96
|
-
when "text"
|
97
|
-
text = node.text.strip
|
98
|
-
current_text += text if text.present?
|
99
|
-
when "ul", "ol"
|
100
|
-
# Add any accumulated text first
|
101
|
-
if current_text.present?
|
102
|
-
blocks << create_paragraph_block(current_text)
|
103
|
-
current_text = ""
|
104
|
-
end
|
105
|
-
|
106
|
-
items = node.css("li").map { |li| process_inline_elements(li) }
|
107
|
-
next if items.empty?
|
108
|
-
|
109
|
-
blocks << {
|
110
|
-
"type" => "list",
|
111
|
-
"data" => {
|
112
|
-
"style" => (node.name == "ul") ? "unordered" : "ordered",
|
113
|
-
"items" => items
|
114
|
-
}
|
115
|
-
}
|
116
|
-
when "blockquote"
|
117
|
-
# Add any accumulated text first
|
118
|
-
if current_text.present?
|
119
|
-
blocks << create_paragraph_block(current_text)
|
120
|
-
current_text = ""
|
121
|
-
end
|
122
|
-
|
123
|
-
blocks << {
|
124
|
-
"type" => "quote",
|
125
|
-
"data" => {
|
126
|
-
"text" => process_inline_elements(node),
|
127
|
-
"caption" => "",
|
128
|
-
"alignment" => "left"
|
129
|
-
}
|
130
|
-
}
|
131
|
-
end
|
132
|
-
end
|
133
|
-
|
134
|
-
# Add any remaining text
|
135
|
-
blocks << create_paragraph_block(current_text) if current_text.present?
|
136
|
-
|
137
|
-
# Return the complete EditorJS structure
|
138
|
-
{
|
139
|
-
"time" => Time.current.to_i * 1000,
|
140
|
-
"blocks" => blocks,
|
141
|
-
"version" => "2.28.2"
|
142
|
-
}
|
143
|
-
rescue => e
|
144
|
-
Rails.logger.error "HTML to EditorJS conversion failed: #{e.message}"
|
145
|
-
Rails.logger.error e.backtrace.join("\n")
|
146
|
-
raise ConversionError, "Failed to convert HTML to EditorJS format: #{e.message}"
|
147
|
-
end
|
148
|
-
end
|
149
|
-
|
150
|
-
def self.create_paragraph_block(text)
|
151
|
-
{
|
152
|
-
"type" => "paragraph",
|
153
|
-
"data" => {
|
154
|
-
"text" => text.strip
|
155
|
-
}
|
156
|
-
}
|
157
|
-
end
|
158
|
-
|
159
|
-
def self.process_inline_elements(node)
|
160
|
-
result = ""
|
161
|
-
node.children.each do |child|
|
162
|
-
case child.name
|
163
|
-
when "br"
|
164
|
-
result += "<br>"
|
165
|
-
when "text"
|
166
|
-
result += child.text
|
167
|
-
when "strong", "b"
|
168
|
-
result += "<b>#{child.text}</b>"
|
169
|
-
when "em", "i"
|
170
|
-
result += "<i>#{child.text}</i>"
|
171
|
-
when "a"
|
172
|
-
href = child["href"]
|
173
|
-
text = child.text.strip
|
174
|
-
# Handle email links specially
|
175
|
-
if href&.start_with?("mailto:")
|
176
|
-
email = href.sub("mailto:", "")
|
177
|
-
result += "<a href=\"mailto:#{email}\">#{text}</a>"
|
178
|
-
else
|
179
|
-
result += "<a href=\"#{href}\">#{text}</a>"
|
180
|
-
end
|
181
|
-
else
|
182
|
-
result += if child.text?
|
183
|
-
child.text
|
184
|
-
else
|
185
|
-
child.to_html
|
186
|
-
end
|
187
|
-
end
|
188
|
-
end
|
189
|
-
result.strip
|
190
|
-
end
|
191
|
-
end
|
192
|
-
end
|
193
|
-
end
|
@@ -1,35 +0,0 @@
|
|
1
|
-
<%= render Panda::CMS::Admin::ContainerComponent.new do |component| %>
|
2
|
-
<% component.with_heading(text: "My Profile", level: 1) %>
|
3
|
-
|
4
|
-
<%= panda_cms_form_with model: user,
|
5
|
-
url: admin_my_profile_path,
|
6
|
-
method: :patch,
|
7
|
-
data: { controller: "theme-form" } do |f| %>
|
8
|
-
<% if user.errors.any? %>
|
9
|
-
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-md">
|
10
|
-
<div class="text-sm text-red-600">
|
11
|
-
<% user.errors.full_messages.each do |message| %>
|
12
|
-
<p><%= message %></p>
|
13
|
-
<% end %>
|
14
|
-
</div>
|
15
|
-
</div>
|
16
|
-
<% end %>
|
17
|
-
|
18
|
-
<div class="space-y-4">
|
19
|
-
<%= f.text_field :firstname %>
|
20
|
-
|
21
|
-
<%= f.text_field :lastname %>
|
22
|
-
|
23
|
-
<%= f.email_field :email %>
|
24
|
-
|
25
|
-
<%= f.select :current_theme,
|
26
|
-
[["Default", "default"], ["Sky", "sky"]],
|
27
|
-
{ label: "Theme" },
|
28
|
-
data: { action: "change->theme-form#updateTheme" } %>
|
29
|
-
</div>
|
30
|
-
|
31
|
-
<%= f.submit "Update Profile",
|
32
|
-
class: "btn btn-primary mt-6",
|
33
|
-
data: { disable_with: "Saving..." } %>
|
34
|
-
<% end %>
|
35
|
-
<% end %>
|
@@ -1,17 +0,0 @@
|
|
1
|
-
<div class="flex flex-col justify-center py-12 px-6 min-h-full text-center lg:px-8">
|
2
|
-
<div class="text-center sm:mx-auto sm:w-full sm:max-w-sm">
|
3
|
-
<img src="/panda-cms-assets/panda-nav.png" class="py-2 mx-auto w-auto h-32">
|
4
|
-
<h2 class="mt-10 mb-6 text-2xl font-bold text-center text-white"><%= t("panda.cms.admin.sessions.new.title") %></h2>
|
5
|
-
</div>
|
6
|
-
<% @providers.each do |provider| %>
|
7
|
-
<div class="mt-4 text-center sm:mx-auto sm:w-full sm:max-w-sm">
|
8
|
-
<%= form_tag "#{Panda::CMS.root_path}/auth/#{provider}", method: "post", data: {turbo: false} do %>
|
9
|
-
<input type="hidden" name="redirect_uri" value="<%= admin_login_callback_url(provider: provider) %>">
|
10
|
-
<button type="submit" id="button-sign-in-<%= provider %>" class="inline-flex gap-x-2 items-center py-2.5 px-3.5 mx-auto mb-4 bg-white rounded-md border min-w-56 border-neutral-400">
|
11
|
-
<i class="fa-brands fa-<%= provider %> text-xl mr-1"></i>
|
12
|
-
<%= t("panda.cms.authentication.sign_in_with", provider: t("panda.cms.authentication.providers.#{provider}")) %>
|
13
|
-
</button>
|
14
|
-
<% end %>
|
15
|
-
</div>
|
16
|
-
<% end %>
|
17
|
-
</div>
|
@@ -1,34 +0,0 @@
|
|
1
|
-
module Panda
|
2
|
-
module CMS
|
3
|
-
module EditorJs
|
4
|
-
module Blocks
|
5
|
-
class Alert < Base
|
6
|
-
def render
|
7
|
-
message = sanitize(data["message"])
|
8
|
-
type = data["type"] || "primary"
|
9
|
-
|
10
|
-
html_safe(
|
11
|
-
"<div class=\"#{alert_classes(type)} p-4 mb-4 rounded-lg\">" \
|
12
|
-
"#{message}" \
|
13
|
-
"</div>"
|
14
|
-
)
|
15
|
-
end
|
16
|
-
|
17
|
-
private
|
18
|
-
|
19
|
-
def alert_classes(type)
|
20
|
-
case type
|
21
|
-
when "primary" then "bg-blue-100 text-blue-800"
|
22
|
-
when "secondary" then "bg-gray-100 text-gray-800"
|
23
|
-
when "success" then "bg-green-100 text-green-800"
|
24
|
-
when "danger" then "bg-red-100 text-red-800"
|
25
|
-
when "warning" then "bg-yellow-100 text-yellow-800"
|
26
|
-
when "info" then "bg-indigo-100 text-indigo-800"
|
27
|
-
else "bg-blue-100 text-blue-800"
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
@@ -1,33 +0,0 @@
|
|
1
|
-
module Panda
|
2
|
-
module CMS
|
3
|
-
module EditorJs
|
4
|
-
module Blocks
|
5
|
-
class Base
|
6
|
-
include ActionView::Helpers::SanitizeHelper
|
7
|
-
include ActionView::Helpers::TagHelper
|
8
|
-
|
9
|
-
attr_reader :data, :options
|
10
|
-
|
11
|
-
def initialize(data, options = {})
|
12
|
-
@data = data
|
13
|
-
@options = options
|
14
|
-
end
|
15
|
-
|
16
|
-
def render
|
17
|
-
""
|
18
|
-
end
|
19
|
-
|
20
|
-
protected
|
21
|
-
|
22
|
-
def html_safe(content)
|
23
|
-
content.html_safe
|
24
|
-
end
|
25
|
-
|
26
|
-
def sanitize(text)
|
27
|
-
Rails::Html::SafeListSanitizer.new.sanitize(text, tags: %w[b i u a code])
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
@@ -1,36 +0,0 @@
|
|
1
|
-
module Panda
|
2
|
-
module CMS
|
3
|
-
module EditorJs
|
4
|
-
module Blocks
|
5
|
-
class Image < Base
|
6
|
-
def render
|
7
|
-
url = data["url"]
|
8
|
-
caption = sanitize(data["caption"])
|
9
|
-
with_border = data["withBorder"]
|
10
|
-
with_background = data["withBackground"]
|
11
|
-
stretched = data["stretched"]
|
12
|
-
|
13
|
-
css_classes = ["prose"]
|
14
|
-
css_classes << "border" if with_border
|
15
|
-
css_classes << "bg-gray-100" if with_background
|
16
|
-
css_classes << "w-full" if stretched
|
17
|
-
|
18
|
-
html_safe(<<~HTML)
|
19
|
-
<figure class="#{css_classes.join(" ")}">
|
20
|
-
<img src="#{url}" alt="#{caption}" />
|
21
|
-
#{caption_element(caption)}
|
22
|
-
</figure>
|
23
|
-
HTML
|
24
|
-
end
|
25
|
-
|
26
|
-
private
|
27
|
-
|
28
|
-
def caption_element(caption)
|
29
|
-
return "" if caption.blank?
|
30
|
-
"<figcaption>#{caption}</figcaption>"
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
@@ -1,32 +0,0 @@
|
|
1
|
-
module Panda
|
2
|
-
module CMS
|
3
|
-
module EditorJs
|
4
|
-
module Blocks
|
5
|
-
class List < Base
|
6
|
-
def render
|
7
|
-
list_type = (data["style"] == "ordered") ? "ol" : "ul"
|
8
|
-
html_safe(
|
9
|
-
"<#{list_type}>" \
|
10
|
-
"#{render_items(data["items"])}" \
|
11
|
-
"</#{list_type}>"
|
12
|
-
)
|
13
|
-
end
|
14
|
-
|
15
|
-
private
|
16
|
-
|
17
|
-
def render_items(items)
|
18
|
-
items.map do |item|
|
19
|
-
content = item.is_a?(Hash) ? item["content"] : item
|
20
|
-
nested = (item.is_a?(Hash) && item["items"].present?) ? render_nested(item["items"]) : ""
|
21
|
-
"<li>#{sanitize(content)}#{nested}</li>"
|
22
|
-
end.join
|
23
|
-
end
|
24
|
-
|
25
|
-
def render_nested(items)
|
26
|
-
self.class.new({"items" => items, "style" => data["style"]}).render
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
@@ -1,41 +0,0 @@
|
|
1
|
-
module Panda
|
2
|
-
module CMS
|
3
|
-
module EditorJs
|
4
|
-
module Blocks
|
5
|
-
class Quote < Base
|
6
|
-
def render
|
7
|
-
text = data["text"]
|
8
|
-
caption = data["caption"]
|
9
|
-
alignment = data["alignment"] || "left"
|
10
|
-
|
11
|
-
# Build the HTML structure
|
12
|
-
html = "<figure class=\"text-#{alignment}\">" \
|
13
|
-
"<blockquote>#{wrap_text_in_p(text)}</blockquote>" \
|
14
|
-
"#{caption_element(caption)}" \
|
15
|
-
"</figure>"
|
16
|
-
|
17
|
-
# Return raw HTML - validation will be handled by the main renderer if enabled
|
18
|
-
html_safe(html)
|
19
|
-
end
|
20
|
-
|
21
|
-
private
|
22
|
-
|
23
|
-
def wrap_text_in_p(text)
|
24
|
-
# Only wrap in <p> if it's not already wrapped
|
25
|
-
text = sanitize(text)
|
26
|
-
if text.start_with?("<p>") && text.end_with?("</p>")
|
27
|
-
text
|
28
|
-
else
|
29
|
-
"<p>#{text}</p>"
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
def caption_element(caption)
|
34
|
-
return "" if caption.blank?
|
35
|
-
"<figcaption>#{sanitize(caption)}</figcaption>"
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|