railspress-engine 0.1.2 → 1.2.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/LICENSE +20 -0
- data/README.md +195 -25
- data/app/assets/javascripts/railspress/admin.js +39 -0
- data/app/assets/javascripts/railspress/markdown_mode.js +343 -0
- data/app/assets/stylesheets/application.css +0 -0
- data/app/assets/stylesheets/railspress/admin/badges.css +70 -0
- data/app/assets/stylesheets/railspress/admin/base.css +25 -0
- data/app/assets/stylesheets/railspress/admin/buttons.css +140 -0
- data/app/assets/stylesheets/railspress/admin/cards.css +52 -0
- data/app/assets/stylesheets/railspress/admin/components/exports.css +55 -0
- data/app/assets/stylesheets/railspress/admin/components/focal_point.css +801 -0
- data/app/assets/stylesheets/railspress/admin/components/imports.css +144 -0
- data/app/assets/stylesheets/railspress/admin/components/lexxy.css +156 -0
- data/app/assets/stylesheets/railspress/admin/filters.css +73 -0
- data/app/assets/stylesheets/railspress/admin/flash.css +26 -0
- data/app/assets/stylesheets/railspress/admin/forms.css +459 -0
- data/app/assets/stylesheets/railspress/admin/layout.css +256 -0
- data/app/assets/stylesheets/railspress/admin/lists.css +24 -0
- data/app/assets/stylesheets/railspress/admin/page.css +111 -0
- data/app/assets/stylesheets/railspress/admin/responsive.css +174 -0
- data/app/assets/stylesheets/railspress/admin/stats.css +43 -0
- data/app/assets/stylesheets/railspress/admin/tables.css +163 -0
- data/app/assets/stylesheets/railspress/admin/utilities.css +202 -0
- data/app/assets/stylesheets/railspress/admin/variables.css +58 -0
- data/app/assets/stylesheets/railspress/application.css +44 -13
- data/app/controllers/railspress/admin/base_controller.rb +6 -3
- data/app/controllers/railspress/admin/categories_controller.rb +1 -1
- data/app/controllers/railspress/admin/cms_transfers_controller.rb +49 -0
- data/app/controllers/railspress/admin/content_element_versions_controller.rb +12 -0
- data/app/controllers/railspress/admin/content_elements_controller.rb +143 -0
- data/app/controllers/railspress/admin/content_groups_controller.rb +69 -0
- data/app/controllers/railspress/admin/dashboard_controller.rb +6 -0
- data/app/controllers/railspress/admin/entities_controller.rb +157 -0
- data/app/controllers/railspress/admin/exports_controller.rb +55 -0
- data/app/controllers/railspress/admin/focal_points_controller.rb +100 -0
- data/app/controllers/railspress/admin/imports_controller.rb +63 -0
- data/app/controllers/railspress/admin/posts_controller.rb +58 -4
- data/app/controllers/railspress/admin/prototypes_controller.rb +30 -0
- data/app/controllers/railspress/admin/tags_controller.rb +1 -1
- data/app/controllers/railspress/application_controller.rb +1 -0
- data/app/helpers/railspress/admin_helper.rb +733 -0
- data/app/helpers/railspress/application_helper.rb +23 -0
- data/app/helpers/railspress/cms_helper.rb +319 -0
- data/app/javascript/railspress/controllers/cms_inline_editor_controller.js +147 -0
- data/app/javascript/railspress/controllers/content_element_form_controller.js +15 -0
- data/app/javascript/railspress/controllers/crop_controller.js +224 -0
- data/app/javascript/railspress/controllers/dropzone_controller.js +261 -0
- data/app/javascript/railspress/controllers/focal_point_controller.js +124 -0
- data/app/javascript/railspress/controllers/image_section_controller.js +94 -0
- data/app/javascript/railspress/controllers/index.js +37 -0
- data/app/javascript/railspress/index.js +62 -0
- data/app/jobs/railspress/export_posts_job.rb +16 -0
- data/app/jobs/railspress/import_posts_job.rb +44 -0
- data/app/models/concerns/railspress/has_focal_point.rb +242 -0
- data/app/models/concerns/railspress/soft_deletable.rb +23 -0
- data/app/models/concerns/railspress/taggable.rb +23 -0
- data/app/models/railspress/content_element.rb +103 -0
- data/app/models/railspress/content_element_version.rb +32 -0
- data/app/models/railspress/content_group.rb +39 -0
- data/app/models/railspress/export.rb +67 -0
- data/app/models/railspress/focal_point.rb +70 -0
- data/app/models/railspress/import.rb +65 -0
- data/app/models/railspress/post.rb +102 -15
- data/app/models/railspress/post_export_processor.rb +162 -0
- data/app/models/railspress/post_import_processor.rb +382 -0
- data/app/models/railspress/tag.rb +10 -3
- data/app/models/railspress/tagging.rb +11 -0
- data/app/services/railspress/content_export_service.rb +122 -0
- data/app/services/railspress/content_import_service.rb +228 -0
- data/app/views/action_text/attachables/_remote_image.html.erb +8 -0
- data/app/views/active_storage/blobs/_blob.html.erb +1 -1
- data/app/views/layouts/railspress/admin.html.erb +3 -1
- data/app/views/railspress/admin/categories/index.html.erb +11 -15
- data/app/views/railspress/admin/cms_transfers/show.html.erb +167 -0
- data/app/views/railspress/admin/content_element_versions/show.html.erb +42 -0
- data/app/views/railspress/admin/content_elements/_form.html.erb +71 -0
- data/app/views/railspress/admin/content_elements/_inline_form.html.erb +32 -0
- data/app/views/railspress/admin/content_elements/_inline_form_frame.html.erb +6 -0
- data/app/views/railspress/admin/content_elements/edit.html.erb +6 -0
- data/app/views/railspress/admin/content_elements/index.html.erb +74 -0
- data/app/views/railspress/admin/content_elements/new.html.erb +6 -0
- data/app/views/railspress/admin/content_elements/show.html.erb +124 -0
- data/app/views/railspress/admin/content_groups/_form.html.erb +9 -0
- data/app/views/railspress/admin/content_groups/edit.html.erb +6 -0
- data/app/views/railspress/admin/content_groups/index.html.erb +42 -0
- data/app/views/railspress/admin/content_groups/new.html.erb +6 -0
- data/app/views/railspress/admin/content_groups/show.html.erb +92 -0
- data/app/views/railspress/admin/dashboard/index.html.erb +36 -1
- data/app/views/railspress/admin/entities/_form.html.erb +53 -0
- data/app/views/railspress/admin/entities/edit.html.erb +4 -0
- data/app/views/railspress/admin/entities/index.html.erb +74 -0
- data/app/views/railspress/admin/entities/new.html.erb +4 -0
- data/app/views/railspress/admin/entities/show.html.erb +117 -0
- data/app/views/railspress/admin/exports/show.html.erb +62 -0
- data/app/views/railspress/admin/imports/_instructions.html.erb +56 -0
- data/app/views/railspress/admin/imports/show.html.erb +137 -0
- data/app/views/railspress/admin/posts/_form.html.erb +102 -28
- data/app/views/railspress/admin/posts/_post_row.html.erb +40 -0
- data/app/views/railspress/admin/posts/index.html.erb +47 -36
- data/app/views/railspress/admin/posts/show.html.erb +55 -19
- data/app/views/railspress/admin/prototypes/image_section.html.erb +42 -0
- data/app/views/railspress/admin/shared/_dropzone.html.erb +84 -0
- data/app/views/railspress/admin/shared/_focal_point_editor.html.erb +102 -0
- data/app/views/railspress/admin/shared/_image_section.html.erb +159 -0
- data/app/views/railspress/admin/shared/_image_section_compact.html.erb +90 -0
- data/app/views/railspress/admin/shared/_image_section_editor.html.erb +171 -0
- data/app/views/railspress/admin/shared/_image_section_v2.html.erb +205 -0
- data/app/views/railspress/admin/shared/_sidebar.html.erb +73 -5
- data/app/views/railspress/admin/tags/index.html.erb +12 -16
- data/config/brakeman.ignore +18 -0
- data/config/importmap.rb +23 -0
- data/config/routes.rb +62 -1
- data/db/migrate/20241218000004_create_railspress_post_tags.rb +1 -1
- data/db/migrate/20241218000005_create_railspress_imports.rb +21 -0
- data/db/migrate/20241218000006_create_railspress_exports.rb +20 -0
- data/db/migrate/20241218000007_create_railspress_taggings.rb +20 -0
- data/db/migrate/20241218000008_drop_railspress_post_tags.rb +14 -0
- data/db/migrate/20241218000010_add_reading_time_to_railspress_posts.rb +5 -0
- data/db/migrate/20250105000002_create_railspress_focal_points.rb +20 -0
- data/db/migrate/20260206000001_create_railspress_content_groups.rb +18 -0
- data/db/migrate/20260206000002_create_railspress_content_elements.rb +21 -0
- data/db/migrate/20260206000003_create_railspress_content_element_versions.rb +20 -0
- data/db/migrate/20260207000001_add_unique_index_to_content_elements.rb +11 -0
- data/db/migrate/20260211112812_add_image_hint_to_railspress_content_elements.rb +7 -0
- data/db/migrate/20260211154040_add_required_to_railspress_content_elements.rb +5 -0
- data/lib/generators/railspress/entity/entity_generator.rb +89 -0
- data/lib/generators/railspress/entity/templates/migration.rb.tt +13 -0
- data/lib/generators/railspress/entity/templates/model.rb.tt +21 -0
- data/lib/generators/railspress/install/install_generator.rb +51 -40
- data/lib/generators/railspress/install/templates/initializer.rb +29 -0
- data/lib/railspress/engine.rb +38 -0
- data/lib/railspress/entity.rb +239 -0
- data/lib/railspress/version.rb +1 -1
- data/lib/railspress.rb +198 -8
- data/lib/tasks/railspress_tasks.rake +49 -4
- metadata +215 -21
- data/MIT-LICENSE +0 -20
- data/app/assets/stylesheets/railspress/admin.css +0 -1207
- data/app/models/railspress/post_tag.rb +0 -8
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railspress
|
|
4
|
+
module Admin
|
|
5
|
+
class FocalPointsController < BaseController
|
|
6
|
+
before_action :set_focal_point
|
|
7
|
+
|
|
8
|
+
# PATCH /admin/focal_points/:id
|
|
9
|
+
# Handles focal point updates, image changes, and image removal
|
|
10
|
+
def update
|
|
11
|
+
image_changed = false
|
|
12
|
+
image_removed = false
|
|
13
|
+
|
|
14
|
+
# Handle image removal
|
|
15
|
+
if params[:remove_image] == "1"
|
|
16
|
+
@record.send(@attachment_name).purge
|
|
17
|
+
image_removed = true
|
|
18
|
+
# Handle image change (only if not removing)
|
|
19
|
+
elsif params[:image].present?
|
|
20
|
+
@record.send("#{@attachment_name}=", params[:image])
|
|
21
|
+
if @record.save
|
|
22
|
+
image_changed = true
|
|
23
|
+
else
|
|
24
|
+
return render_error(@record.errors.full_messages.join(", "))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# If image was changed or removed, redirect to refresh the page
|
|
29
|
+
if image_changed || image_removed
|
|
30
|
+
return redirect_to_record(notice: image_removed ? "Image removed" : "Image updated")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Otherwise, update focal point and return turbo frame
|
|
34
|
+
if @focal_point.update(focal_point_params)
|
|
35
|
+
respond_to do |format|
|
|
36
|
+
format.turbo_stream do
|
|
37
|
+
render turbo_stream: turbo_stream.replace(
|
|
38
|
+
helpers.dom_id(@record, "image_section_#{@attachment_name}"),
|
|
39
|
+
partial: "railspress/admin/shared/image_section_compact",
|
|
40
|
+
locals: {
|
|
41
|
+
record: @record,
|
|
42
|
+
attachment_name: @attachment_name.to_sym,
|
|
43
|
+
flash_message: "Focal point saved"
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
format.html do
|
|
48
|
+
redirect_to_record(notice: "Focal point saved")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
else
|
|
52
|
+
render_error(@focal_point.errors.full_messages.join(", "))
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def set_focal_point
|
|
59
|
+
@focal_point = FocalPoint.find(params[:id])
|
|
60
|
+
@record = @focal_point.record
|
|
61
|
+
@attachment_name = @focal_point.attachment_name
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def focal_point_params
|
|
65
|
+
params.require(:focal_point).permit(:focal_x, :focal_y, :overrides)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def redirect_to_record(notice: nil)
|
|
69
|
+
path = if @record.is_a?(Railspress::Post)
|
|
70
|
+
edit_admin_post_path(@record)
|
|
71
|
+
elsif @record.is_a?(Railspress::ContentElement)
|
|
72
|
+
edit_admin_content_element_path(@record)
|
|
73
|
+
else
|
|
74
|
+
entity_type = @record.class.railspress_config.route_key
|
|
75
|
+
admin_edit_entity_path(entity_type: entity_type, id: @record.id)
|
|
76
|
+
end
|
|
77
|
+
redirect_to path, notice: notice
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def render_error(message)
|
|
81
|
+
respond_to do |format|
|
|
82
|
+
format.turbo_stream do
|
|
83
|
+
render turbo_stream: turbo_stream.replace(
|
|
84
|
+
helpers.dom_id(@record, "image_section_#{@attachment_name}"),
|
|
85
|
+
partial: "railspress/admin/shared/image_section_editor",
|
|
86
|
+
locals: {
|
|
87
|
+
record: @record,
|
|
88
|
+
attachment_name: @attachment_name.to_sym,
|
|
89
|
+
error_message: message
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
format.html do
|
|
94
|
+
redirect_to_record(notice: message)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module Railspress
|
|
2
|
+
module Admin
|
|
3
|
+
class ImportsController < BaseController
|
|
4
|
+
before_action :validate_import_type, only: [ :show ]
|
|
5
|
+
|
|
6
|
+
def show
|
|
7
|
+
@import_type = params[:type]
|
|
8
|
+
@back_path = { "posts" => admin_posts_path }[@import_type]
|
|
9
|
+
@recent_imports = Import.by_type(@import_type).recent
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def create
|
|
13
|
+
uploaded_files = Array(import_params[:file]).reject(&:blank?)
|
|
14
|
+
|
|
15
|
+
if uploaded_files.empty?
|
|
16
|
+
redirect_to typed_admin_imports_path(type: import_params[:import_type]),
|
|
17
|
+
alert: "Please select at least one file to import."
|
|
18
|
+
return
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Create import record
|
|
22
|
+
import = Import.create!(
|
|
23
|
+
import_type: import_params[:import_type],
|
|
24
|
+
filename: uploaded_files.size == 1 ? uploaded_files.first.original_filename : "#{uploaded_files.size} files",
|
|
25
|
+
content_type: uploaded_files.first.content_type,
|
|
26
|
+
status: "pending"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Save uploaded files to tmp
|
|
30
|
+
file_paths = save_uploaded_files(import, uploaded_files)
|
|
31
|
+
|
|
32
|
+
# Enqueue job
|
|
33
|
+
ImportPostsJob.perform_later(import.id, file_paths)
|
|
34
|
+
|
|
35
|
+
redirect_to typed_admin_imports_path(type: import_params[:import_type]),
|
|
36
|
+
notice: "Import started. #{uploaded_files.size} file(s) queued for processing."
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def validate_import_type
|
|
42
|
+
unless Import::IMPORT_TYPES.include?(params[:type])
|
|
43
|
+
redirect_to admin_root_path, alert: "Invalid import type."
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def import_params
|
|
48
|
+
params.require(:import).permit(:import_type, file: [])
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def save_uploaded_files(import, uploaded_files)
|
|
52
|
+
upload_dir = Rails.root.join("tmp", "uploads", "import_#{import.id}")
|
|
53
|
+
FileUtils.mkdir_p(upload_dir)
|
|
54
|
+
|
|
55
|
+
uploaded_files.map do |file|
|
|
56
|
+
path = upload_dir.join(file.original_filename)
|
|
57
|
+
File.open(path, "wb") { |f| f.write(file.read) }
|
|
58
|
+
path.to_s
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
module Railspress
|
|
2
2
|
module Admin
|
|
3
3
|
class PostsController < BaseController
|
|
4
|
-
before_action :set_post, only: [:show, :edit, :update, :destroy]
|
|
5
|
-
before_action :load_categories, only: [:new, :create, :edit, :update]
|
|
4
|
+
before_action :set_post, only: [ :show, :edit, :update, :destroy, :image_editor ]
|
|
5
|
+
before_action :load_categories, only: [ :new, :create, :edit, :update ]
|
|
6
6
|
|
|
7
7
|
def index
|
|
8
|
-
@
|
|
8
|
+
@sort = params[:sort].presence || "created_at"
|
|
9
|
+
@direction = params[:direction].presence || "desc"
|
|
10
|
+
|
|
11
|
+
@posts = Post.includes(:category, :tags)
|
|
12
|
+
.search(params[:q])
|
|
13
|
+
.by_category(params[:category_id])
|
|
14
|
+
.by_status(params[:status])
|
|
15
|
+
.sorted_by(@sort, @direction)
|
|
16
|
+
|
|
17
|
+
@total_count = @posts.count
|
|
18
|
+
@page = [ params[:page].to_i, 1 ].max
|
|
19
|
+
@total_pages = (@total_count.to_f / Post::PER_PAGE).ceil
|
|
20
|
+
@posts = @posts.page(@page)
|
|
21
|
+
|
|
22
|
+
@categories = Category.ordered
|
|
9
23
|
end
|
|
10
24
|
|
|
11
25
|
def show
|
|
@@ -41,6 +55,38 @@ module Railspress
|
|
|
41
55
|
redirect_to admin_posts_path, notice: "Post deleted."
|
|
42
56
|
end
|
|
43
57
|
|
|
58
|
+
ALLOWED_ATTACHMENTS = %w[header_image].freeze
|
|
59
|
+
|
|
60
|
+
# GET /admin/posts/:id/image_editor/:attachment
|
|
61
|
+
# Returns the expanded image editor in a Turbo Frame
|
|
62
|
+
# Pass ?compact=true to get the compact view (for Cancel)
|
|
63
|
+
def image_editor
|
|
64
|
+
unless ALLOWED_ATTACHMENTS.include?(params[:attachment])
|
|
65
|
+
raise ActionController::RoutingError, "Invalid attachment"
|
|
66
|
+
end
|
|
67
|
+
@attachment_name = params[:attachment].to_sym
|
|
68
|
+
|
|
69
|
+
if params[:compact] == "true"
|
|
70
|
+
render partial: "railspress/admin/shared/image_section_compact",
|
|
71
|
+
locals: {
|
|
72
|
+
record: @post,
|
|
73
|
+
attachment_name: @attachment_name,
|
|
74
|
+
label: "Main Image"
|
|
75
|
+
}
|
|
76
|
+
else
|
|
77
|
+
# Ensure focal point is persisted before editing
|
|
78
|
+
focal_point = @post.send("#{@attachment_name}_focal_point")
|
|
79
|
+
focal_point.save! if focal_point.new_record?
|
|
80
|
+
|
|
81
|
+
render partial: "railspress/admin/shared/image_section_editor",
|
|
82
|
+
locals: {
|
|
83
|
+
record: @post,
|
|
84
|
+
attachment_name: @attachment_name,
|
|
85
|
+
contexts: Railspress.image_contexts
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
44
90
|
private
|
|
45
91
|
|
|
46
92
|
def set_post
|
|
@@ -58,12 +104,20 @@ module Railspress
|
|
|
58
104
|
:category_id,
|
|
59
105
|
:content,
|
|
60
106
|
:status,
|
|
107
|
+
:published_at,
|
|
108
|
+
:reading_time,
|
|
61
109
|
:meta_title,
|
|
62
110
|
:meta_description,
|
|
63
111
|
:tag_list
|
|
64
112
|
]
|
|
65
113
|
permitted << :author_id if authors_enabled?
|
|
66
|
-
|
|
114
|
+
if post_images_enabled?
|
|
115
|
+
permitted.push(:header_image, :remove_header_image)
|
|
116
|
+
# Focal point nested attributes
|
|
117
|
+
if Railspress.focal_points_enabled?
|
|
118
|
+
permitted.push(header_image_focal_point_attributes: [ :focal_x, :focal_y, :overrides ])
|
|
119
|
+
end
|
|
120
|
+
end
|
|
67
121
|
params.require(:post).permit(permitted)
|
|
68
122
|
end
|
|
69
123
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Railspress
|
|
2
|
+
module Admin
|
|
3
|
+
class PrototypesController < BaseController
|
|
4
|
+
# Prototype views for iterating on UI/UX
|
|
5
|
+
# Access at: /admin/prototypes/:name
|
|
6
|
+
#
|
|
7
|
+
# These views use real CSS and Stimulus controllers
|
|
8
|
+
# but work with mock data for fast iteration.
|
|
9
|
+
|
|
10
|
+
def image_section
|
|
11
|
+
# Mock data for prototyping
|
|
12
|
+
@mock_image_url = "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1200&q=80"
|
|
13
|
+
@mock_filename = "mountain-landscape.jpg"
|
|
14
|
+
@mock_filesize = "1.2 MB"
|
|
15
|
+
@mock_focal_point = { x: 0.35, y: 0.45 }
|
|
16
|
+
@contexts = Railspress.image_contexts.presence || default_contexts
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def default_contexts
|
|
22
|
+
{
|
|
23
|
+
hero: { aspect: [ 21, 9 ] },
|
|
24
|
+
card: { aspect: [ 16, 10 ] },
|
|
25
|
+
tall: { aspect: [ 4, 5 ] }
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|