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,58 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* RailsPress Admin - CSS Variables
|
|
3
|
+
* Design tokens for the admin interface
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
:root {
|
|
7
|
+
/* Typography */
|
|
8
|
+
--rp-font-display: 'Source Serif 4', Georgia, serif;
|
|
9
|
+
--rp-font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
10
|
+
--rp-font-mono: 'SF Mono', Consolas, monospace;
|
|
11
|
+
|
|
12
|
+
/* Colors - Warm Neutrals */
|
|
13
|
+
--rp-sidebar-bg: #1a1d21;
|
|
14
|
+
--rp-sidebar-hover: #2a2e33;
|
|
15
|
+
--rp-sidebar-text: #a8adb3;
|
|
16
|
+
--rp-sidebar-text-active: #ffffff;
|
|
17
|
+
|
|
18
|
+
--rp-bg: #f8f7f5;
|
|
19
|
+
--rp-bg-elevated: #ffffff;
|
|
20
|
+
--rp-border: #e5e2dc;
|
|
21
|
+
--rp-border-light: #f0ede8;
|
|
22
|
+
|
|
23
|
+
--rp-text: #2c2a27;
|
|
24
|
+
--rp-text-muted: #6b6860;
|
|
25
|
+
--rp-text-light: #9a958c;
|
|
26
|
+
|
|
27
|
+
/* Accents */
|
|
28
|
+
--rp-primary: #3d5a80;
|
|
29
|
+
--rp-primary-hover: #2c4a6e;
|
|
30
|
+
--rp-primary-light: #e8eef4;
|
|
31
|
+
|
|
32
|
+
--rp-success: #4a7c59;
|
|
33
|
+
--rp-success-light: #e8f2eb;
|
|
34
|
+
|
|
35
|
+
--rp-danger: #a63d40;
|
|
36
|
+
--rp-danger-light: #f9ebeb;
|
|
37
|
+
|
|
38
|
+
--rp-info-light: #f0f7fb;
|
|
39
|
+
|
|
40
|
+
/* Spacing */
|
|
41
|
+
--rp-space-xs: 0.25rem;
|
|
42
|
+
--rp-space-sm: 0.5rem;
|
|
43
|
+
--rp-space-md: 1rem;
|
|
44
|
+
--rp-space-lg: 1.5rem;
|
|
45
|
+
--rp-space-xl: 2rem;
|
|
46
|
+
--rp-space-2xl: 3rem;
|
|
47
|
+
|
|
48
|
+
/* Sizing */
|
|
49
|
+
--rp-sidebar-width: 240px;
|
|
50
|
+
--rp-sidebar-width-collapsed: 64px;
|
|
51
|
+
--rp-header-height: 56px;
|
|
52
|
+
--rp-radius: 6px;
|
|
53
|
+
|
|
54
|
+
/* Shadows - Enhanced for depth */
|
|
55
|
+
--rp-shadow-sm: 0 1px 3px rgba(44, 42, 39, 0.06), 0 1px 2px rgba(44, 42, 39, 0.04);
|
|
56
|
+
--rp-shadow-md: 0 4px 16px rgba(44, 42, 39, 0.1), 0 2px 4px rgba(44, 42, 39, 0.06);
|
|
57
|
+
--rp-shadow-lg: 0 8px 24px rgba(44, 42, 39, 0.12), 0 4px 8px rgba(44, 42, 39, 0.08);
|
|
58
|
+
}
|
|
@@ -1,15 +1,46 @@
|
|
|
1
1
|
/*
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
2
|
+
* RailsPress Admin Styles
|
|
3
|
+
* Manifest file - loads all admin stylesheets in order
|
|
4
|
+
*
|
|
5
|
+
*= require ./admin/variables
|
|
6
|
+
*= require ./admin/base
|
|
7
|
+
*= require ./admin/layout
|
|
8
|
+
*= require ./admin/page
|
|
9
|
+
*= require ./admin/cards
|
|
10
|
+
*= require ./admin/stats
|
|
11
|
+
*= require ./admin/tables
|
|
12
|
+
*= require ./admin/filters
|
|
13
|
+
*= require ./admin/forms
|
|
14
|
+
*= require ./admin/buttons
|
|
15
|
+
*= require ./admin/badges
|
|
16
|
+
*= require ./admin/lists
|
|
17
|
+
*= require ./admin/flash
|
|
18
|
+
*= require ./admin/utilities
|
|
19
|
+
*= require ./admin/responsive
|
|
20
|
+
*= require ./admin/components/lexxy
|
|
21
|
+
*= require ./admin/components/imports
|
|
22
|
+
*= require ./admin/components/exports
|
|
23
|
+
*/
|
|
24
|
+
/*
|
|
25
|
+
* RailsPress Admin Styles
|
|
15
26
|
*/
|
|
27
|
+
|
|
28
|
+
@import url("admin/variables.css");
|
|
29
|
+
@import url("admin/base.css");
|
|
30
|
+
@import url("admin/layout.css");
|
|
31
|
+
@import url("admin/page.css");
|
|
32
|
+
@import url("admin/cards.css");
|
|
33
|
+
@import url("admin/stats.css");
|
|
34
|
+
@import url("admin/tables.css");
|
|
35
|
+
@import url("admin/filters.css");
|
|
36
|
+
@import url("admin/forms.css");
|
|
37
|
+
@import url("admin/buttons.css");
|
|
38
|
+
@import url("admin/badges.css");
|
|
39
|
+
@import url("admin/lists.css");
|
|
40
|
+
@import url("admin/flash.css");
|
|
41
|
+
@import url("admin/utilities.css");
|
|
42
|
+
@import url("admin/responsive.css");
|
|
43
|
+
@import url("admin/components/lexxy.css");
|
|
44
|
+
@import url("admin/components/imports.css");
|
|
45
|
+
@import url("admin/components/exports.css");
|
|
46
|
+
@import url("admin/components/focal_point.css");
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
module Railspress
|
|
2
2
|
module Admin
|
|
3
3
|
class BaseController < ActionController::Base
|
|
4
|
+
protect_from_forgery with: :exception
|
|
5
|
+
|
|
4
6
|
layout "railspress/admin"
|
|
7
|
+
helper Railspress::AdminHelper
|
|
5
8
|
|
|
6
|
-
helper_method :current_author, :available_authors, :authors_enabled?, :
|
|
9
|
+
helper_method :current_author, :available_authors, :authors_enabled?, :post_images_enabled?
|
|
7
10
|
|
|
8
11
|
# Authentication hook - to be configured later
|
|
9
12
|
# before_action :authenticate_admin!
|
|
@@ -18,8 +21,8 @@ module Railspress
|
|
|
18
21
|
Railspress.authors_enabled?
|
|
19
22
|
end
|
|
20
23
|
|
|
21
|
-
def
|
|
22
|
-
Railspress.
|
|
24
|
+
def post_images_enabled?
|
|
25
|
+
Railspress.post_images_enabled?
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
def current_author
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railspress
|
|
4
|
+
module Admin
|
|
5
|
+
class CmsTransfersController < BaseController
|
|
6
|
+
def show
|
|
7
|
+
load_content_summary
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def export
|
|
11
|
+
result = ContentExportService.new.call
|
|
12
|
+
|
|
13
|
+
send_data result.zip_data,
|
|
14
|
+
filename: result.filename,
|
|
15
|
+
type: "application/zip",
|
|
16
|
+
disposition: "attachment"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def import
|
|
20
|
+
if params[:file].blank?
|
|
21
|
+
redirect_to admin_cms_transfer_path, alert: "Please select a ZIP file to import."
|
|
22
|
+
return
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
@result = ContentImportService.new(params[:file]).call
|
|
26
|
+
load_content_summary
|
|
27
|
+
|
|
28
|
+
if @result.errors.any?
|
|
29
|
+
flash.now[:alert] = "Import completed with #{@result.errors.size} error(s)."
|
|
30
|
+
else
|
|
31
|
+
flash.now[:notice] = "Import successful! #{@result.total_processed} items processed."
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
render :show
|
|
35
|
+
rescue ArgumentError => e
|
|
36
|
+
redirect_to admin_cms_transfer_path, alert: e.message
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def load_content_summary
|
|
42
|
+
@groups = ContentGroup.active.includes(:content_elements).order(:name)
|
|
43
|
+
@group_count = @groups.size
|
|
44
|
+
@element_count = ContentElement.active.count
|
|
45
|
+
@image_count = ContentElement.active.image.count
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railspress
|
|
4
|
+
module Admin
|
|
5
|
+
class ContentElementVersionsController < BaseController
|
|
6
|
+
def show
|
|
7
|
+
@version = ContentElementVersion.find(params[:id])
|
|
8
|
+
@content_element = @version.content_element
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railspress
|
|
4
|
+
module Admin
|
|
5
|
+
class ContentElementsController < BaseController
|
|
6
|
+
before_action :set_content_element, only: [ :show, :edit, :update, :destroy, :inline, :image_editor ]
|
|
7
|
+
|
|
8
|
+
def index
|
|
9
|
+
scope = ContentElement.active
|
|
10
|
+
scope = scope.where(content_group_id: params[:content_group_id]) if params[:content_group_id].present?
|
|
11
|
+
@content_elements = scope.includes(:content_group)
|
|
12
|
+
.order(created_at: :desc)
|
|
13
|
+
@content_groups = ContentGroup.active.order(:name)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def show
|
|
17
|
+
@versions = @content_element.versions.limit(10)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def new
|
|
21
|
+
@content_element = ContentElement.new
|
|
22
|
+
@content_groups = ContentGroup.active.order(:name)
|
|
23
|
+
@content_element.content_group_id = params[:content_group_id] if params[:content_group_id].present?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def create
|
|
27
|
+
@content_element = ContentElement.new(content_element_params)
|
|
28
|
+
@content_element.author_id = current_author&.id if authors_enabled?
|
|
29
|
+
|
|
30
|
+
if @content_element.save
|
|
31
|
+
redirect_to admin_content_element_path(@content_element), notice: "Content element '#{@content_element.name}' created."
|
|
32
|
+
else
|
|
33
|
+
@content_groups = ContentGroup.active.order(:name)
|
|
34
|
+
render :new, status: :unprocessable_entity
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def edit
|
|
39
|
+
@content_groups = ContentGroup.active.order(:name)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def update
|
|
43
|
+
if @content_element.update(content_element_params)
|
|
44
|
+
Railspress::CmsHelper.clear_cache if defined?(Railspress::CmsHelper)
|
|
45
|
+
|
|
46
|
+
if request.headers["Turbo-Frame"].present?
|
|
47
|
+
form_frame_id = params[:form_frame_id].presence || "cms_inline_editor_form_#{@content_element.id}"
|
|
48
|
+
display_frame_id = params[:display_frame_id].presence
|
|
49
|
+
|
|
50
|
+
streams = []
|
|
51
|
+
streams << turbo_stream.replace(
|
|
52
|
+
form_frame_id,
|
|
53
|
+
partial: "railspress/admin/content_elements/inline_form_frame",
|
|
54
|
+
locals: { content_element: @content_element, form_frame_id: form_frame_id, display_frame_id: display_frame_id }
|
|
55
|
+
)
|
|
56
|
+
if display_frame_id
|
|
57
|
+
streams << turbo_stream.replace(
|
|
58
|
+
display_frame_id,
|
|
59
|
+
helpers.cms_element_display_frame(@content_element, display_frame_id)
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
render turbo_stream: streams
|
|
63
|
+
else
|
|
64
|
+
redirect_to admin_content_element_path(@content_element), notice: "Content element '#{@content_element.name}' updated."
|
|
65
|
+
end
|
|
66
|
+
else
|
|
67
|
+
@content_groups = ContentGroup.active.order(:name)
|
|
68
|
+
|
|
69
|
+
if request.headers["Turbo-Frame"].present?
|
|
70
|
+
form_frame_id = params[:form_frame_id].presence || "cms_inline_editor_form_#{@content_element.id}"
|
|
71
|
+
display_frame_id = params[:display_frame_id].presence
|
|
72
|
+
|
|
73
|
+
render turbo_stream: turbo_stream.replace(
|
|
74
|
+
form_frame_id,
|
|
75
|
+
partial: "railspress/admin/content_elements/inline_form_frame",
|
|
76
|
+
locals: { content_element: @content_element, form_frame_id: form_frame_id, display_frame_id: display_frame_id }
|
|
77
|
+
), status: :unprocessable_entity
|
|
78
|
+
else
|
|
79
|
+
render :edit, status: :unprocessable_entity
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def destroy
|
|
85
|
+
if @content_element.soft_delete
|
|
86
|
+
redirect_to admin_content_elements_path,
|
|
87
|
+
notice: "Content element '#{@content_element.name}' deleted."
|
|
88
|
+
else
|
|
89
|
+
redirect_to admin_content_elements_path,
|
|
90
|
+
alert: "Cannot delete '#{@content_element.name}' — it is a required element. To delete it, first unmark it as required."
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def inline
|
|
95
|
+
if request.headers["Turbo-Frame"].present?
|
|
96
|
+
render partial: "railspress/admin/content_elements/inline_form_frame",
|
|
97
|
+
locals: {
|
|
98
|
+
content_element: @content_element,
|
|
99
|
+
form_frame_id: params[:form_frame_id],
|
|
100
|
+
display_frame_id: params[:display_frame_id]
|
|
101
|
+
}
|
|
102
|
+
else
|
|
103
|
+
redirect_to edit_admin_content_element_path(@content_element)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def image_editor
|
|
108
|
+
if params[:compact] == "true"
|
|
109
|
+
render partial: "railspress/admin/shared/image_section_compact",
|
|
110
|
+
locals: {
|
|
111
|
+
record: @content_element,
|
|
112
|
+
attachment_name: :image,
|
|
113
|
+
label: "Element Image",
|
|
114
|
+
editor_url: image_editor_admin_content_element_path(@content_element)
|
|
115
|
+
}
|
|
116
|
+
else
|
|
117
|
+
focal_point = @content_element.image_focal_point
|
|
118
|
+
focal_point.save! if focal_point.new_record?
|
|
119
|
+
|
|
120
|
+
render partial: "railspress/admin/shared/image_section_editor",
|
|
121
|
+
locals: {
|
|
122
|
+
record: @content_element,
|
|
123
|
+
attachment_name: :image,
|
|
124
|
+
contexts: Railspress.image_contexts
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
def set_content_element
|
|
132
|
+
@content_element = ContentElement.active.find(params[:id])
|
|
133
|
+
rescue ActiveRecord::RecordNotFound
|
|
134
|
+
redirect_to admin_content_elements_path, alert: "Content element not found."
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def content_element_params
|
|
138
|
+
params.require(:content_element).permit(:name, :content_group_id, :content_type, :position, :text_content, :image, :required, :image_hint,
|
|
139
|
+
image_focal_point_attributes: [ :id, :focal_x, :focal_y ])
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railspress
|
|
4
|
+
module Admin
|
|
5
|
+
class ContentGroupsController < BaseController
|
|
6
|
+
before_action :set_content_group, only: [ :show, :edit, :update, :destroy ]
|
|
7
|
+
|
|
8
|
+
def index
|
|
9
|
+
@content_groups = ContentGroup.active
|
|
10
|
+
.includes(:content_elements)
|
|
11
|
+
.order(created_at: :desc)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def show
|
|
15
|
+
@content_elements = @content_group.content_elements
|
|
16
|
+
.active
|
|
17
|
+
.ordered
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def new
|
|
21
|
+
@content_group = ContentGroup.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def create
|
|
25
|
+
@content_group = ContentGroup.new(content_group_params)
|
|
26
|
+
@content_group.author_id = current_author&.id if authors_enabled?
|
|
27
|
+
|
|
28
|
+
if @content_group.save
|
|
29
|
+
redirect_to admin_content_group_path(@content_group), notice: "Content group '#{@content_group.name}' created."
|
|
30
|
+
else
|
|
31
|
+
render :new, status: :unprocessable_entity
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def edit
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def update
|
|
39
|
+
if @content_group.update(content_group_params)
|
|
40
|
+
redirect_to admin_content_group_path(@content_group), notice: "Content group '#{@content_group.name}' updated."
|
|
41
|
+
else
|
|
42
|
+
render :edit, status: :unprocessable_entity
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def destroy
|
|
47
|
+
if @content_group.soft_delete
|
|
48
|
+
redirect_to admin_content_groups_path,
|
|
49
|
+
notice: "Content group '#{@content_group.name}' deleted."
|
|
50
|
+
else
|
|
51
|
+
redirect_to admin_content_group_path(@content_group),
|
|
52
|
+
alert: "Cannot delete '#{@content_group.name}' — it contains required content elements."
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def set_content_group
|
|
59
|
+
@content_group = ContentGroup.active.find(params[:id])
|
|
60
|
+
rescue ActiveRecord::RecordNotFound
|
|
61
|
+
redirect_to admin_content_groups_path, alert: "Content group not found."
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def content_group_params
|
|
65
|
+
params.require(:content_group).permit(:name, :description)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -6,6 +6,12 @@ module Railspress
|
|
|
6
6
|
@categories_count = Category.count
|
|
7
7
|
@tags_count = Tag.count
|
|
8
8
|
@recent_posts = Post.ordered.limit(5)
|
|
9
|
+
|
|
10
|
+
if Railspress.cms_enabled?
|
|
11
|
+
@content_groups_count = ContentGroup.active.count
|
|
12
|
+
@content_elements_count = ContentElement.active.count
|
|
13
|
+
@recent_content_elements = ContentElement.active.includes(:content_group).order(updated_at: :desc).limit(5)
|
|
14
|
+
end
|
|
9
15
|
end
|
|
10
16
|
end
|
|
11
17
|
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railspress
|
|
4
|
+
module Admin
|
|
5
|
+
class EntitiesController < BaseController
|
|
6
|
+
before_action :set_entity_config
|
|
7
|
+
before_action :set_record, only: [ :show, :edit, :update, :destroy, :image_editor ]
|
|
8
|
+
|
|
9
|
+
def index
|
|
10
|
+
@records = entity_class.order(created_at: :desc)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def show
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def new
|
|
17
|
+
@record = entity_class.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def create
|
|
21
|
+
@record = entity_class.new(entity_params)
|
|
22
|
+
|
|
23
|
+
if @record.save
|
|
24
|
+
redirect_to entity_index_path, notice: "#{entity_config.singular_label} created."
|
|
25
|
+
else
|
|
26
|
+
render :new, status: :unprocessable_entity
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def edit
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def update
|
|
34
|
+
purge_removed_attachments
|
|
35
|
+
if @record.update(entity_params)
|
|
36
|
+
redirect_to entity_index_path, notice: "#{entity_config.singular_label} updated."
|
|
37
|
+
else
|
|
38
|
+
render :edit, status: :unprocessable_entity
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def destroy
|
|
43
|
+
@record.destroy
|
|
44
|
+
redirect_to entity_index_path, notice: "#{entity_config.singular_label} deleted."
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# GET /admin/entities/:entity_type/:id/image_editor/:attachment
|
|
48
|
+
# Returns the expanded image editor in a Turbo Frame
|
|
49
|
+
# Pass ?compact=true to get the compact view (for Cancel)
|
|
50
|
+
def image_editor
|
|
51
|
+
allowed = entity_config.fields
|
|
52
|
+
.select { |_, f| [ :attachment, :attachments, :focal_point_image ].include?(f[:type]) }
|
|
53
|
+
.keys.map(&:to_s)
|
|
54
|
+
unless allowed.include?(params[:attachment])
|
|
55
|
+
raise ActionController::RoutingError, "Invalid attachment"
|
|
56
|
+
end
|
|
57
|
+
@attachment_name = params[:attachment].to_sym
|
|
58
|
+
|
|
59
|
+
if params[:compact] == "true"
|
|
60
|
+
render partial: "railspress/admin/shared/image_section_compact",
|
|
61
|
+
locals: {
|
|
62
|
+
record: @record,
|
|
63
|
+
attachment_name: @attachment_name,
|
|
64
|
+
label: entity_config.singular_label
|
|
65
|
+
}
|
|
66
|
+
else
|
|
67
|
+
# Ensure focal point is persisted before editing
|
|
68
|
+
focal_point = @record.send("#{@attachment_name}_focal_point")
|
|
69
|
+
focal_point.save! if focal_point.new_record?
|
|
70
|
+
|
|
71
|
+
render partial: "railspress/admin/shared/image_section_editor",
|
|
72
|
+
locals: {
|
|
73
|
+
record: @record,
|
|
74
|
+
attachment_name: @attachment_name,
|
|
75
|
+
contexts: Railspress.image_contexts
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def set_entity_config
|
|
83
|
+
@entity_config = Railspress.entity_for(params[:entity_type])
|
|
84
|
+
raise ActionController::RoutingError, "Entity not found: #{params[:entity_type]}" unless @entity_config
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def entity_config
|
|
88
|
+
@entity_config
|
|
89
|
+
end
|
|
90
|
+
helper_method :entity_config
|
|
91
|
+
|
|
92
|
+
def entity_class
|
|
93
|
+
entity_config.model_class
|
|
94
|
+
end
|
|
95
|
+
helper_method :entity_class
|
|
96
|
+
|
|
97
|
+
def set_record
|
|
98
|
+
@record = entity_class.find(params[:id])
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def entity_params
|
|
102
|
+
permitted = []
|
|
103
|
+
entity_config.fields.each do |name, field|
|
|
104
|
+
case field[:type]
|
|
105
|
+
when :attachments
|
|
106
|
+
permitted << { name => [] }
|
|
107
|
+
when :list, :lines
|
|
108
|
+
# Permit virtual attribute for HTML form input
|
|
109
|
+
permitted << "#{name}_list"
|
|
110
|
+
# Also permit direct array for API/agent access
|
|
111
|
+
permitted << { name => [] }
|
|
112
|
+
else
|
|
113
|
+
permitted << name
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
params.require(entity_config.param_key).permit(*permitted)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def purge_removed_attachments
|
|
120
|
+
entity_config.fields.each do |name, field|
|
|
121
|
+
next unless [ :attachment, :attachments ].include?(field[:type])
|
|
122
|
+
|
|
123
|
+
remove_key = "remove_#{name}"
|
|
124
|
+
remove_ids = params.dig(entity_config.param_key, remove_key)
|
|
125
|
+
next if remove_ids.blank?
|
|
126
|
+
|
|
127
|
+
if field[:type] == :attachments
|
|
128
|
+
@record.public_send(name).where(id: remove_ids).each(&:purge)
|
|
129
|
+
else
|
|
130
|
+
@record.public_send(name).purge if remove_ids == "1"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Route helpers for views
|
|
136
|
+
def entity_index_path
|
|
137
|
+
railspress.admin_entity_index_path(entity_type: entity_config.route_key)
|
|
138
|
+
end
|
|
139
|
+
helper_method :entity_index_path
|
|
140
|
+
|
|
141
|
+
def entity_show_path(record)
|
|
142
|
+
railspress.admin_entity_path(entity_type: entity_config.route_key, id: record.id)
|
|
143
|
+
end
|
|
144
|
+
helper_method :entity_show_path
|
|
145
|
+
|
|
146
|
+
def entity_new_path
|
|
147
|
+
railspress.admin_new_entity_path(entity_type: entity_config.route_key)
|
|
148
|
+
end
|
|
149
|
+
helper_method :entity_new_path
|
|
150
|
+
|
|
151
|
+
def entity_edit_path(record)
|
|
152
|
+
railspress.admin_edit_entity_path(entity_type: entity_config.route_key, id: record.id)
|
|
153
|
+
end
|
|
154
|
+
helper_method :entity_edit_path
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module Railspress
|
|
2
|
+
module Admin
|
|
3
|
+
class ExportsController < BaseController
|
|
4
|
+
before_action :validate_export_type, only: [ :show ]
|
|
5
|
+
before_action :set_export, only: [ :download ]
|
|
6
|
+
|
|
7
|
+
def show
|
|
8
|
+
@export_type = params[:type]
|
|
9
|
+
@back_path = { "posts" => admin_posts_path }[@export_type]
|
|
10
|
+
@recent_exports = Export.by_type(@export_type).recent
|
|
11
|
+
@post_count = Post.count
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create
|
|
15
|
+
export = Export.create!(
|
|
16
|
+
export_type: export_params[:export_type],
|
|
17
|
+
status: "pending"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
ExportPostsJob.perform_later(export.id)
|
|
21
|
+
|
|
22
|
+
redirect_to typed_admin_exports_path(type: export_params[:export_type]),
|
|
23
|
+
notice: "Export started. You'll be able to download the file once processing completes."
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def download
|
|
27
|
+
if @export.file.attached?
|
|
28
|
+
send_data @export.file.download,
|
|
29
|
+
filename: @export.filename,
|
|
30
|
+
type: "application/zip",
|
|
31
|
+
disposition: "attachment"
|
|
32
|
+
else
|
|
33
|
+
redirect_to typed_admin_exports_path(type: @export.export_type),
|
|
34
|
+
alert: "Export file not available."
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def validate_export_type
|
|
41
|
+
unless Export::EXPORT_TYPES.include?(params[:type])
|
|
42
|
+
redirect_to admin_root_path, alert: "Invalid export type."
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def set_export
|
|
47
|
+
@export = Export.find(params[:id])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def export_params
|
|
51
|
+
params.require(:export).permit(:export_type)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|