ruby_cms 0.1.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 +7 -0
- data/.cursor/dhh.mdc +698 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/README.md +235 -0
- data/Rakefile +30 -0
- data/app/components/ruby_cms/admin/admin_page/admin_table_content.rb +32 -0
- data/app/components/ruby_cms/admin/admin_page.rb +345 -0
- data/app/components/ruby_cms/admin/base_component.rb +78 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table.rb +149 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_actions.rb +127 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_body.rb +15 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_cell.rb +41 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_head.rb +33 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_delete_modal.rb +174 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header.rb +59 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header_bar.rb +159 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_pagination.rb +192 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_row.rb +97 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_actions.rb +137 -0
- data/app/controllers/concerns/ruby_cms/admin_pagination.rb +120 -0
- data/app/controllers/concerns/ruby_cms/admin_turbo_table.rb +68 -0
- data/app/controllers/concerns/ruby_cms/page_tracking.rb +52 -0
- data/app/controllers/concerns/ruby_cms/visitor_error_capture.rb +39 -0
- data/app/controllers/ruby_cms/admin/analytics_controller.rb +191 -0
- data/app/controllers/ruby_cms/admin/base_controller.rb +105 -0
- data/app/controllers/ruby_cms/admin/content_blocks_controller.rb +390 -0
- data/app/controllers/ruby_cms/admin/dashboard_controller.rb +50 -0
- data/app/controllers/ruby_cms/admin/locale_controller.rb +20 -0
- data/app/controllers/ruby_cms/admin/permissions_controller.rb +66 -0
- data/app/controllers/ruby_cms/admin/settings_controller.rb +223 -0
- data/app/controllers/ruby_cms/admin/user_permissions_controller.rb +59 -0
- data/app/controllers/ruby_cms/admin/users_controller.rb +107 -0
- data/app/controllers/ruby_cms/admin/visitor_errors_controller.rb +89 -0
- data/app/controllers/ruby_cms/admin/visual_editor_controller.rb +322 -0
- data/app/controllers/ruby_cms/errors_controller.rb +35 -0
- data/app/helpers/ruby_cms/admin/admin_page_helper.rb +21 -0
- data/app/helpers/ruby_cms/admin/bulk_action_table_helper.rb +159 -0
- data/app/helpers/ruby_cms/application_helper.rb +41 -0
- data/app/helpers/ruby_cms/bulk_action_table_helper.rb +151 -0
- data/app/helpers/ruby_cms/content_blocks_helper.rb +375 -0
- data/app/helpers/ruby_cms/settings_helper.rb +160 -0
- data/app/javascript/controllers/ruby_cms/auto_save_preference_controller.js +73 -0
- data/app/javascript/controllers/ruby_cms/bulk_action_table_controller.js +553 -0
- data/app/javascript/controllers/ruby_cms/clickable_row_controller.js +28 -0
- data/app/javascript/controllers/ruby_cms/flash_messages_controller.js +29 -0
- data/app/javascript/controllers/ruby_cms/index.js +104 -0
- data/app/javascript/controllers/ruby_cms/locale_tabs_controller.js +34 -0
- data/app/javascript/controllers/ruby_cms/mobile_menu_controller.js +55 -0
- data/app/javascript/controllers/ruby_cms/nav_order_sortable_controller.js +192 -0
- data/app/javascript/controllers/ruby_cms/page_preview_controller.js +135 -0
- data/app/javascript/controllers/ruby_cms/toggle_controller.js +39 -0
- data/app/javascript/controllers/ruby_cms/visual_editor_controller.js +321 -0
- data/app/models/concerns/content_block/publishable.rb +54 -0
- data/app/models/concerns/content_block/searchable.rb +22 -0
- data/app/models/content_block.rb +155 -0
- data/app/models/ruby_cms/content_block.rb +8 -0
- data/app/models/ruby_cms/permission.rb +28 -0
- data/app/models/ruby_cms/permittable.rb +39 -0
- data/app/models/ruby_cms/preference.rb +111 -0
- data/app/models/ruby_cms/user_permission.rb +12 -0
- data/app/models/ruby_cms/visitor_error.rb +109 -0
- data/app/services/ruby_cms/analytics/report.rb +362 -0
- data/app/services/ruby_cms/security_tracker.rb +92 -0
- data/app/views/layouts/ruby_cms/_admin_flash_messages.html.erb +37 -0
- data/app/views/layouts/ruby_cms/_admin_sidebar.html.erb +121 -0
- data/app/views/layouts/ruby_cms/admin.html.erb +81 -0
- data/app/views/layouts/ruby_cms/minimal.html.erb +181 -0
- data/app/views/ruby_cms/admin/analytics/index.html.erb +160 -0
- data/app/views/ruby_cms/admin/analytics/page_details.html.erb +84 -0
- data/app/views/ruby_cms/admin/analytics/partials/_back_button.html.erb +3 -0
- data/app/views/ruby_cms/admin/analytics/partials/_browser_device.html.erb +40 -0
- data/app/views/ruby_cms/admin/analytics/partials/_daily_activity_chart.html.erb +58 -0
- data/app/views/ruby_cms/admin/analytics/partials/_hourly_activity_chart.html.erb +51 -0
- data/app/views/ruby_cms/admin/analytics/partials/_recent_activity.html.erb +31 -0
- data/app/views/ruby_cms/admin/analytics/partials/_security_alert.html.erb +4 -0
- data/app/views/ruby_cms/admin/analytics/partials/_top_referrers.html.erb +21 -0
- data/app/views/ruby_cms/admin/analytics/visitor_details.html.erb +125 -0
- data/app/views/ruby_cms/admin/content_blocks/_form.html.erb +161 -0
- data/app/views/ruby_cms/admin/content_blocks/_row.html.erb +25 -0
- data/app/views/ruby_cms/admin/content_blocks/edit.html.erb +17 -0
- data/app/views/ruby_cms/admin/content_blocks/index.html.erb +66 -0
- data/app/views/ruby_cms/admin/content_blocks/new.html.erb +5 -0
- data/app/views/ruby_cms/admin/content_blocks/show.html.erb +110 -0
- data/app/views/ruby_cms/admin/dashboard/index.html.erb +198 -0
- data/app/views/ruby_cms/admin/permissions/_row.html.erb +11 -0
- data/app/views/ruby_cms/admin/permissions/index.html.erb +62 -0
- data/app/views/ruby_cms/admin/settings/index.html.erb +220 -0
- data/app/views/ruby_cms/admin/shared/_bulk_action_table_index.html.erb +56 -0
- data/app/views/ruby_cms/admin/user_permissions/index.html.erb +55 -0
- data/app/views/ruby_cms/admin/users/_row.html.erb +14 -0
- data/app/views/ruby_cms/admin/users/index.html.erb +70 -0
- data/app/views/ruby_cms/admin/visitor_errors/_row.html.erb +35 -0
- data/app/views/ruby_cms/admin/visitor_errors/index.html.erb +57 -0
- data/app/views/ruby_cms/admin/visitor_errors/show.html.erb +147 -0
- data/app/views/ruby_cms/admin/visual_editor/index.html.erb +144 -0
- data/app/views/ruby_cms/errors/not_found.html.erb +92 -0
- data/config/database.yml +6 -0
- data/config/importmap.rb +36 -0
- data/config/locales/en.yml +101 -0
- data/config/routes.rb +65 -0
- data/db/migrate/20260125000001_create_ruby_cms_permissions.rb +14 -0
- data/db/migrate/20260125000002_create_ruby_cms_user_permissions.rb +14 -0
- data/db/migrate/20260125000003_create_ruby_cms_content_blocks.rb +19 -0
- data/db/migrate/20260125000010_add_indexes_to_ruby_cms_tables.rb +9 -0
- data/db/migrate/20260127000001_add_locale_to_ruby_cms_content_blocks.rb +34 -0
- data/db/migrate/20260129000001_create_ruby_cms_visitor_errors.rb +24 -0
- data/db/migrate/20260130000001_add_referer_and_query_to_ruby_cms_visitor_errors.rb +8 -0
- data/db/migrate/20260130000002_create_ruby_cms_preferences.rb +16 -0
- data/db/migrate/20260130000003_add_category_to_ruby_cms_preferences.rb +8 -0
- data/db/migrate/20260211000001_add_ruby_cms_analytics_fields_to_ahoy_events.rb +19 -0
- data/db/migrate/20260212000001_use_unprefixed_cms_tables.rb +146 -0
- data/exe/ruby_cms +25 -0
- data/lib/generators/ruby_cms/install_generator.rb +1062 -0
- data/lib/generators/ruby_cms/templates/admin.html.erb +82 -0
- data/lib/generators/ruby_cms/templates/ruby_cms.rb +86 -0
- data/lib/ruby_cms/app_integration.rb +82 -0
- data/lib/ruby_cms/cli.rb +169 -0
- data/lib/ruby_cms/content_blocks_grouping.rb +41 -0
- data/lib/ruby_cms/content_blocks_sync.rb +329 -0
- data/lib/ruby_cms/css_compiler.rb +35 -0
- data/lib/ruby_cms/engine.rb +498 -0
- data/lib/ruby_cms/settings.rb +145 -0
- data/lib/ruby_cms/settings_registry.rb +289 -0
- data/lib/ruby_cms/version.rb +5 -0
- data/lib/ruby_cms.rb +195 -0
- data/lib/tasks/ruby_cms.rake +27 -0
- data/log/test.log +17875 -0
- data/sig/ruby_cms.rbs +4 -0
- metadata +223 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyCms
|
|
4
|
+
module Admin
|
|
5
|
+
# Base component class for admin components
|
|
6
|
+
# Inherits from RubyUI::Base if available, otherwise Phlex::HTML
|
|
7
|
+
class BaseComponent < (defined?(RubyUI) && RubyUI.const_defined?(:Base) ? RubyUI::Base : Phlex::HTML)
|
|
8
|
+
# Include Rails helpers if Phlex::HTML is used directly
|
|
9
|
+
if defined?(Phlex::Rails::Helpers)
|
|
10
|
+
include Phlex::Rails::Helpers::FormAuthenticityToken if defined?(Phlex::Rails::Helpers::FormAuthenticityToken)
|
|
11
|
+
include Phlex::Rails::Helpers::TurboFrameTag if defined?(Phlex::Rails::Helpers::TurboFrameTag)
|
|
12
|
+
include Phlex::Rails::Helpers::Sanitize if defined?(Phlex::Rails::Helpers::Sanitize)
|
|
13
|
+
include Phlex::Rails::Helpers::LinkTo if defined?(Phlex::Rails::Helpers::LinkTo)
|
|
14
|
+
include Phlex::Rails::Helpers::ButtonTo if defined?(Phlex::Rails::Helpers::ButtonTo)
|
|
15
|
+
include Phlex::Rails::Helpers::FormWith if defined?(Phlex::Rails::Helpers::FormWith)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Helper method to build CSS classes from hash or array
|
|
19
|
+
def build_classes(*classes)
|
|
20
|
+
classes.flatten.compact.join(" ")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Helper method to merge data attributes
|
|
24
|
+
def merge_data_attributes(base_data, additional_data)
|
|
25
|
+
base_data.merge(additional_data || {})
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Helper method to conditionally add attributes
|
|
29
|
+
def conditional_attributes(condition, attributes)
|
|
30
|
+
condition ? attributes : {}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Access Rails helpers (works in Phlex and RubyUI contexts)
|
|
34
|
+
def helpers
|
|
35
|
+
@helpers ||=
|
|
36
|
+
if respond_to?(:view_context)
|
|
37
|
+
view_context
|
|
38
|
+
elsif defined?(Phlex::Rails::ViewContext)
|
|
39
|
+
Phlex::Rails::ViewContext.current
|
|
40
|
+
elsif defined?(Phlex::HTML) && respond_to?(:call)
|
|
41
|
+
Thread.current[:phlex_view_context] || raise("View context not available")
|
|
42
|
+
else
|
|
43
|
+
raise("View context not available. Ensure component is rendered from a Rails view.")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Get form authenticity token
|
|
48
|
+
def form_authenticity_token
|
|
49
|
+
return helpers.form_authenticity_token if token_from_helpers?
|
|
50
|
+
return super if respond_to?(:form_authenticity_token, true)
|
|
51
|
+
return token_from_controller if token_from_controller?
|
|
52
|
+
|
|
53
|
+
""
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def token_from_helpers?
|
|
57
|
+
respond_to?(:helpers) && helpers.respond_to?(:form_authenticity_token)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def token_from_controller?
|
|
61
|
+
defined?(ActionController::Base) && respond_to?(:controller)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def token_from_controller
|
|
65
|
+
controller&.form_authenticity_token || ""
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Access controller if available
|
|
69
|
+
def controller
|
|
70
|
+
@controller ||= if respond_to?(:helpers) && helpers.respond_to?(:controller)
|
|
71
|
+
helpers.controller
|
|
72
|
+
elsif defined?(ActionController::Base)
|
|
73
|
+
Thread.current[:phlex_controller]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyCms
|
|
4
|
+
module Admin
|
|
5
|
+
module BulkActionTable
|
|
6
|
+
# Root component for bulk action table
|
|
7
|
+
# Wraps entire table with Turbo Frame support, pagination, and bulk actions bar
|
|
8
|
+
#
|
|
9
|
+
# @param turbo_frame [String, nil] Turbo Frame ID (e.g., "admin_table_content")
|
|
10
|
+
# @param pagination [Hash, nil] Pagination hash with current_page, total_pages, etc.
|
|
11
|
+
# @param pagination_path [Proc, nil] Lambda that generates pagination URLs
|
|
12
|
+
# @param bulk_actions_url [String, nil] URL for bulk delete action
|
|
13
|
+
# @param bulk_actions_buttons [Array<Hash>] Array of custom bulk action button configs
|
|
14
|
+
# @param item_name [String] Singular name for items (e.g., "error", "schedule")
|
|
15
|
+
# @param controller_name [String] Stimulus controller identifier
|
|
16
|
+
# (default: "ruby-cms--bulk-action-table")
|
|
17
|
+
class BulkActionTable < BaseComponent
|
|
18
|
+
def initialize(**options)
|
|
19
|
+
super()
|
|
20
|
+
@turbo_frame = options[:turbo_frame]
|
|
21
|
+
@pagination = options[:pagination]
|
|
22
|
+
@pagination_path = options[:pagination_path]
|
|
23
|
+
@bulk_actions_url = options[:bulk_actions_url]
|
|
24
|
+
@bulk_actions_buttons = options[:bulk_actions_buttons] || []
|
|
25
|
+
@item_name = options.fetch(:item_name, "item")
|
|
26
|
+
@controller_name = options.fetch(:controller_name, "ruby-cms--bulk-action-table")
|
|
27
|
+
@csrf_token = options[:csrf_token]
|
|
28
|
+
@header = options[:header]
|
|
29
|
+
@header_title = options[:header_title]
|
|
30
|
+
@header_filter = options[:header_filter]
|
|
31
|
+
@header_action_icons = options[:header_action_icons] || []
|
|
32
|
+
@header_search_url = options[:header_search_url] || "#"
|
|
33
|
+
@header_search_param = options[:header_search_param] || "q"
|
|
34
|
+
@user_attrs = extract_user_attrs(options)
|
|
35
|
+
@has_bulk_actions = @bulk_actions_url.present? || @bulk_actions_buttons.any?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def extract_user_attrs(options)
|
|
39
|
+
excluded_keys = %i[
|
|
40
|
+
turbo_frame pagination pagination_path bulk_actions_url
|
|
41
|
+
bulk_actions_buttons item_name controller_name csrf_token header
|
|
42
|
+
header_title header_filter header_action_icons header_search_url header_search_param
|
|
43
|
+
]
|
|
44
|
+
options.except(*excluded_keys)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def view_template(&block)
|
|
48
|
+
if @turbo_frame
|
|
49
|
+
# Use turbo_frame_tag for proper Turbo Frame navigation (pagination, search)
|
|
50
|
+
turbo_frame_tag(@turbo_frame, **turbo_frame_options) do
|
|
51
|
+
render_table_content(&block)
|
|
52
|
+
end
|
|
53
|
+
else
|
|
54
|
+
render_table_content(&block)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def render_table_content(&block)
|
|
59
|
+
div(
|
|
60
|
+
class: build_classes(
|
|
61
|
+
"rounded-lg border border-gray-200/80 bg-white shadow-sm overflow-hidden " \
|
|
62
|
+
"flex flex-col",
|
|
63
|
+
@user_attrs[:class]
|
|
64
|
+
),
|
|
65
|
+
**table_data_attributes.except(:class)
|
|
66
|
+
) do
|
|
67
|
+
render_header
|
|
68
|
+
render_table_wrapper(&block)
|
|
69
|
+
div(class: "border-t border-gray-200/80 bg-white") do
|
|
70
|
+
render_bulk_actions if @has_bulk_actions
|
|
71
|
+
render_pagination if @pagination && @pagination_path
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def render_header
|
|
77
|
+
if @header_title.present? || @header_action_icons.any? || @header_search_url.present?
|
|
78
|
+
render BulkActionTableHeaderBar.new(
|
|
79
|
+
title: @header_title,
|
|
80
|
+
header_filter: @header_filter,
|
|
81
|
+
action_icons: @header_action_icons,
|
|
82
|
+
search_url: @header_search_url,
|
|
83
|
+
search_param: @header_search_param,
|
|
84
|
+
turbo_frame: @turbo_frame
|
|
85
|
+
)
|
|
86
|
+
elsif @header
|
|
87
|
+
div(class: "px-6 py-4 border-b border-gray-200/80 bg-white") do
|
|
88
|
+
if @header.respond_to?(:call)
|
|
89
|
+
raw(@header.call) # rubocop:disable Rails/OutputSafety -- legacy capture support
|
|
90
|
+
elsif @header.kind_of?(String)
|
|
91
|
+
raw(sanitize(@header)) # rubocop:disable Rails/OutputSafety -- legacy capture support
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def render_table_wrapper(&)
|
|
98
|
+
div(class: "w-full overflow-x-auto") do
|
|
99
|
+
table(class: "min-w-full text-sm") do
|
|
100
|
+
yield if block_given?
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def render_bulk_actions
|
|
106
|
+
render BulkActions.new(
|
|
107
|
+
controller_name: @controller_name,
|
|
108
|
+
item_name: @item_name,
|
|
109
|
+
bulk_actions_url: @bulk_actions_url,
|
|
110
|
+
bulk_action_buttons: @bulk_actions_buttons
|
|
111
|
+
)
|
|
112
|
+
render BulkActionTableDeleteModal.new(controller_name: @controller_name)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def render_pagination
|
|
116
|
+
render BulkActionTablePagination.new(
|
|
117
|
+
pagination: @pagination,
|
|
118
|
+
pagination_path: @pagination_path,
|
|
119
|
+
turbo_frame: @turbo_frame
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def table_data_attributes
|
|
126
|
+
csrf_token = @csrf_token || form_authenticity_token
|
|
127
|
+
attrs = {
|
|
128
|
+
data: {
|
|
129
|
+
controller: @controller_name,
|
|
130
|
+
"#{@controller_name}-csrf-token-value": csrf_token,
|
|
131
|
+
"#{@controller_name}-item-name-value": @item_name
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
attrs[:data]["#{@controller_name}-bulk-action-url-value"] = @bulk_actions_url if @bulk_actions_url.present?
|
|
136
|
+
|
|
137
|
+
attrs.merge(@user_attrs)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def turbo_frame_options
|
|
141
|
+
{
|
|
142
|
+
class: "flex-1 flex flex-col min-h-0",
|
|
143
|
+
data: { turbo_action: "advance" }
|
|
144
|
+
}
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyCms
|
|
4
|
+
module Admin
|
|
5
|
+
module BulkActionTable
|
|
6
|
+
# Row action buttons component
|
|
7
|
+
# Renders Edit and Delete buttons for each table row
|
|
8
|
+
#
|
|
9
|
+
# @param edit_path [String, nil] URL to edit page
|
|
10
|
+
# @param delete_path [String, nil] URL to delete action
|
|
11
|
+
# @param delete_confirm [String] Confirmation message for delete
|
|
12
|
+
# @param require_confirm [Boolean] Whether to require confirmation (default: true)
|
|
13
|
+
# @param turbo_frame [String, nil] Turbo Frame ID for updates
|
|
14
|
+
# @param controller_name [String] Stimulus controller identifier
|
|
15
|
+
class BulkActionTableActions < BaseComponent
|
|
16
|
+
def initialize(
|
|
17
|
+
delete_path:,
|
|
18
|
+
item_id:,
|
|
19
|
+
edit_path: nil,
|
|
20
|
+
delete_confirm: "Are you sure you want to delete this item?",
|
|
21
|
+
require_confirm: true,
|
|
22
|
+
turbo_frame: nil,
|
|
23
|
+
controller_name: "ruby-cms--bulk-action-table"
|
|
24
|
+
)
|
|
25
|
+
super
|
|
26
|
+
@edit_path = edit_path
|
|
27
|
+
@delete_path = delete_path
|
|
28
|
+
@item_id = item_id
|
|
29
|
+
@delete_confirm = delete_confirm
|
|
30
|
+
@require_confirm = require_confirm
|
|
31
|
+
@turbo_frame = turbo_frame
|
|
32
|
+
@controller_name = controller_name
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def view_template
|
|
36
|
+
div(class: "flex items-center justify-end gap-1") do
|
|
37
|
+
render_edit_button if @edit_path
|
|
38
|
+
|
|
39
|
+
render_delete_button if @delete_path
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def render_edit_button
|
|
46
|
+
link_options = {
|
|
47
|
+
href: @edit_path,
|
|
48
|
+
class: "inline-flex h-8 w-8 items-center justify-center rounded-md text-gray-500 " \
|
|
49
|
+
"hover:bg-gray-100 hover:text-gray-900 transition-colors"
|
|
50
|
+
}
|
|
51
|
+
link_options[:data] = { turbo_frame: @turbo_frame } if @turbo_frame
|
|
52
|
+
|
|
53
|
+
a(**link_options) do
|
|
54
|
+
svg(
|
|
55
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
56
|
+
viewBox: "0 0 20 20",
|
|
57
|
+
fill: "currentColor",
|
|
58
|
+
class: "h-4 w-4"
|
|
59
|
+
) do |s|
|
|
60
|
+
s.path(
|
|
61
|
+
d: edit_icon_path_d
|
|
62
|
+
)
|
|
63
|
+
s.path(
|
|
64
|
+
d: edit_icon_path_d2
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def render_delete_button
|
|
71
|
+
item_id = @item_id || extract_item_id_from_path
|
|
72
|
+
button(
|
|
73
|
+
type: "button",
|
|
74
|
+
class: "inline-flex h-8 w-8 items-center justify-center rounded-md text-rose-600 " \
|
|
75
|
+
"hover:bg-rose-50 hover:text-rose-700 transition-colors",
|
|
76
|
+
data: {
|
|
77
|
+
action: "click->#{@controller_name}#showIndividualDeleteDialog",
|
|
78
|
+
"#{@controller_name}-item-id-param": item_id,
|
|
79
|
+
delete_path: @delete_path,
|
|
80
|
+
require_confirm: @require_confirm
|
|
81
|
+
}
|
|
82
|
+
) do
|
|
83
|
+
svg(
|
|
84
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
85
|
+
viewBox: "0 0 20 20",
|
|
86
|
+
fill: "currentColor",
|
|
87
|
+
class: "h-4 w-4"
|
|
88
|
+
) do |s|
|
|
89
|
+
s.path(
|
|
90
|
+
fill_rule: "evenodd",
|
|
91
|
+
d: delete_icon_path_d,
|
|
92
|
+
clip_rule: "evenodd"
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def edit_icon_path_d
|
|
99
|
+
"M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3" \
|
|
100
|
+
"l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def edit_icon_path_d2
|
|
104
|
+
"M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0010 3H4.75A2.75 2.75 0 002 5.75" \
|
|
105
|
+
"v9.5A2.75 2.75 0 004.75 18h9.5A2.75 2.75 0 0017 15.25V10a.75.75 0 00-1.5 0" \
|
|
106
|
+
"v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5z"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def delete_icon_path_d
|
|
110
|
+
"M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10" \
|
|
111
|
+
".23 1.482l.149-.022.841 10.518A2.75 2.75 0 007.596 19h4.807a2.75 2.75 0 00" \
|
|
112
|
+
"2.742-2.53l.841-10.52.149.023a.75.75 0 00.23-1.482A41.03 41.03 0 0014 4.193" \
|
|
113
|
+
"V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69" \
|
|
114
|
+
"-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 " \
|
|
115
|
+
"10 4zM8.58 7.72a.75.75 0 00-1.5.06l.3 7.5a.75.75 0 101.5-.06l-.3-7.5zm4.34" \
|
|
116
|
+
".06a.75.75 0 10-1.5-.06l-.3 7.5a.75.75 0 101.5.06l.3-7.5z"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def extract_item_id_from_path
|
|
120
|
+
# Try to extract item ID from delete_path (e.g., "/admin/users/123" -> "123")
|
|
121
|
+
# This is a fallback - ideally item_id should be passed explicitly
|
|
122
|
+
@delete_path.to_s.split("/").last
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyCms
|
|
4
|
+
module Admin
|
|
5
|
+
module BulkActionTable
|
|
6
|
+
# Table body component
|
|
7
|
+
# Simple wrapper for <tbody> content
|
|
8
|
+
class BulkActionTableBody < BaseComponent
|
|
9
|
+
def view_template(&)
|
|
10
|
+
tbody(class: "divide-y divide-gray-100", &)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyCms
|
|
4
|
+
module Admin
|
|
5
|
+
module BulkActionTable
|
|
6
|
+
# Checkbox cell component
|
|
7
|
+
# Renders checkbox in table row for bulk selection
|
|
8
|
+
#
|
|
9
|
+
# @param item_id [Integer, String] Item ID for checkbox value
|
|
10
|
+
# @param controller_name [String] Stimulus controller identifier
|
|
11
|
+
class BulkActionTableCheckboxCell < BaseComponent
|
|
12
|
+
def initialize(
|
|
13
|
+
item_id:,
|
|
14
|
+
controller_name: "ruby-cms--bulk-action-table"
|
|
15
|
+
)
|
|
16
|
+
super
|
|
17
|
+
@item_id = item_id
|
|
18
|
+
@controller_name = controller_name
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def view_template
|
|
22
|
+
td(class: "w-12 px-6 py-3",
|
|
23
|
+
data: { action: "click->#{@controller_name}#stopPropagation" }) do
|
|
24
|
+
input(
|
|
25
|
+
type: "checkbox",
|
|
26
|
+
role: "checkbox",
|
|
27
|
+
value: @item_id,
|
|
28
|
+
class: "h-4 w-4 rounded border-gray-300 text-teal-600 focus:ring-teal-200",
|
|
29
|
+
data: {
|
|
30
|
+
"#{@controller_name}-target": "itemCheckbox",
|
|
31
|
+
item_id: @item_id,
|
|
32
|
+
action: "change->#{@controller_name}#updateSelection"
|
|
33
|
+
},
|
|
34
|
+
aria_label: "Select row"
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyCms
|
|
4
|
+
module Admin
|
|
5
|
+
module BulkActionTable
|
|
6
|
+
# Checkbox header cell component
|
|
7
|
+
# Renders select-all checkbox in table header
|
|
8
|
+
#
|
|
9
|
+
# @param controller_name [String] Stimulus controller identifier
|
|
10
|
+
class BulkActionTableCheckboxHead < BaseComponent
|
|
11
|
+
def initialize(controller_name: "ruby-cms--bulk-action-table")
|
|
12
|
+
super()
|
|
13
|
+
@controller_name = controller_name
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def view_template
|
|
17
|
+
th(class: "w-12 px-6 py-3") do
|
|
18
|
+
input(
|
|
19
|
+
type: "checkbox",
|
|
20
|
+
role: "checkbox",
|
|
21
|
+
class: "h-4 w-4 rounded border-gray-300 text-teal-600 focus:ring-teal-200",
|
|
22
|
+
data: {
|
|
23
|
+
"#{@controller_name}-target": "selectAllCheckbox",
|
|
24
|
+
action: "change->#{@controller_name}#toggleSelectAll"
|
|
25
|
+
},
|
|
26
|
+
aria_label: "Select all"
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyCms
|
|
4
|
+
module Admin
|
|
5
|
+
module BulkActionTable
|
|
6
|
+
# Delete confirmation modal component
|
|
7
|
+
# Shows confirmation dialog for bulk delete actions
|
|
8
|
+
#
|
|
9
|
+
# @param controller_name [String] Stimulus controller identifier
|
|
10
|
+
class BulkActionTableDeleteModal < BaseComponent
|
|
11
|
+
def initialize(controller_name: "ruby-cms--bulk-action-table")
|
|
12
|
+
super
|
|
13
|
+
@controller_name = controller_name
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def view_template
|
|
17
|
+
div(**dialog_overlay_attributes) do
|
|
18
|
+
render_backdrop
|
|
19
|
+
render_dialog_container
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def dialog_overlay_attributes
|
|
26
|
+
{
|
|
27
|
+
class: "fixed inset-0 z-50 hidden",
|
|
28
|
+
data: {
|
|
29
|
+
"#{@controller_name}-target": "dialogOverlay",
|
|
30
|
+
action: "keydown->#{@controller_name}#handleKeydown"
|
|
31
|
+
},
|
|
32
|
+
role: "dialog",
|
|
33
|
+
aria_modal: "true",
|
|
34
|
+
aria_labelledby: "dialog-title"
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def render_backdrop
|
|
39
|
+
div(
|
|
40
|
+
class: "absolute inset-0 bg-black/50",
|
|
41
|
+
data: {
|
|
42
|
+
action: "click->#{@controller_name}#closeDialog"
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def render_dialog_container
|
|
48
|
+
div(class: "relative flex min-h-full items-center justify-center p-4") do
|
|
49
|
+
div(**dialog_content_attributes) do
|
|
50
|
+
render_header_with_close
|
|
51
|
+
render_message
|
|
52
|
+
render_footer
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def render_header_with_close
|
|
58
|
+
div(class: "flex items-start justify-between gap-4") do
|
|
59
|
+
render_header
|
|
60
|
+
render_close_button
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def dialog_content_attributes
|
|
65
|
+
{
|
|
66
|
+
class: "w-full max-w-md rounded-lg border border-gray-200 bg-white p-6 shadow-lg",
|
|
67
|
+
data: {
|
|
68
|
+
"#{@controller_name}-target": "dialogContent"
|
|
69
|
+
},
|
|
70
|
+
tabindex: "-1"
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def render_close_button
|
|
75
|
+
button(
|
|
76
|
+
type: "button",
|
|
77
|
+
class: "inline-flex h-8 w-8 items-center justify-center rounded-md text-gray-500 " \
|
|
78
|
+
"hover:bg-gray-100 hover:text-gray-900 transition-colors",
|
|
79
|
+
data: {
|
|
80
|
+
action: "click->#{@controller_name}#closeDialog"
|
|
81
|
+
},
|
|
82
|
+
aria_label: "Close"
|
|
83
|
+
) do
|
|
84
|
+
render_close_icon
|
|
85
|
+
span(class: "sr-only") { "Close" }
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def render_close_icon
|
|
90
|
+
svg(
|
|
91
|
+
width: "15",
|
|
92
|
+
height: "15",
|
|
93
|
+
viewBox: "0 0 15 15",
|
|
94
|
+
fill: "none",
|
|
95
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
96
|
+
class: "h-4 w-4"
|
|
97
|
+
) do |s|
|
|
98
|
+
s.path(
|
|
99
|
+
d: close_icon_path_d,
|
|
100
|
+
fill: "currentColor",
|
|
101
|
+
fill_rule: "evenodd",
|
|
102
|
+
clip_rule: "evenodd"
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def close_icon_path_d
|
|
108
|
+
"M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 " \
|
|
109
|
+
"2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 " \
|
|
110
|
+
"3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 " \
|
|
111
|
+
"3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 " \
|
|
112
|
+
"10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 " \
|
|
113
|
+
"12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 " \
|
|
114
|
+
"11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 " \
|
|
115
|
+
"11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 " \
|
|
116
|
+
"4.03157Z"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def render_header
|
|
120
|
+
h3(
|
|
121
|
+
id: "dialog-title",
|
|
122
|
+
class: "text-base font-semibold text-gray-900",
|
|
123
|
+
data: {
|
|
124
|
+
"#{@controller_name}-target": "dialogTitle"
|
|
125
|
+
}
|
|
126
|
+
) { "Delete Selected Items" }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def render_message
|
|
130
|
+
div(
|
|
131
|
+
class: "mt-3 text-sm text-gray-600 space-y-1",
|
|
132
|
+
data: {
|
|
133
|
+
"#{@controller_name}-target": "dialogMessage"
|
|
134
|
+
}
|
|
135
|
+
) do
|
|
136
|
+
p { "Are you sure you want to delete the selected items?" }
|
|
137
|
+
p { "This action cannot be undone." }
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def render_footer
|
|
142
|
+
div(class: "mt-6 flex items-center justify-end gap-2") do
|
|
143
|
+
render_cancel_button
|
|
144
|
+
render_confirm_button
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def render_cancel_button
|
|
149
|
+
button(
|
|
150
|
+
type: "button",
|
|
151
|
+
class: "inline-flex h-9 items-center justify-center rounded-md border " \
|
|
152
|
+
"border-gray-200 bg-white px-4 text-sm font-medium text-gray-900 " \
|
|
153
|
+
"shadow-sm hover:bg-gray-50 transition-colors",
|
|
154
|
+
data: {
|
|
155
|
+
action: "click->#{@controller_name}#closeDialog"
|
|
156
|
+
}
|
|
157
|
+
) { "Cancel" }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def render_confirm_button
|
|
161
|
+
button(
|
|
162
|
+
type: "button",
|
|
163
|
+
class: "inline-flex h-9 items-center justify-center rounded-md bg-rose-600 px-4 " \
|
|
164
|
+
"text-sm font-medium text-white shadow-sm hover:bg-rose-700 transition-colors",
|
|
165
|
+
data: {
|
|
166
|
+
"#{@controller_name}-target": "dialogConfirmButton",
|
|
167
|
+
action: "click->#{@controller_name}#confirmAction"
|
|
168
|
+
}
|
|
169
|
+
) { "Delete Selected" }
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyCms
|
|
4
|
+
module Admin
|
|
5
|
+
module BulkActionTable
|
|
6
|
+
# Table header component
|
|
7
|
+
# Renders <thead> with column headers and optional select-all checkbox
|
|
8
|
+
#
|
|
9
|
+
# @param headers [Array<String>] Array of header text
|
|
10
|
+
# @param bulk_actions_enabled [Boolean] Whether to show select-all checkbox
|
|
11
|
+
# @param controller_name [String] Stimulus controller identifier
|
|
12
|
+
class BulkActionTableHeader < BaseComponent
|
|
13
|
+
def initialize(
|
|
14
|
+
headers: [],
|
|
15
|
+
bulk_actions_enabled: true,
|
|
16
|
+
controller_name: "ruby-cms--bulk-action-table"
|
|
17
|
+
)
|
|
18
|
+
super
|
|
19
|
+
@headers = headers
|
|
20
|
+
@bulk_actions_enabled = bulk_actions_enabled
|
|
21
|
+
@controller_name = controller_name
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def view_template
|
|
25
|
+
thead(class: "bg-gray-50") do
|
|
26
|
+
tr do
|
|
27
|
+
render_bulk_checkbox_header
|
|
28
|
+
render_table_headers
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def render_bulk_checkbox_header
|
|
36
|
+
return unless @bulk_actions_enabled
|
|
37
|
+
|
|
38
|
+
render BulkActionTableCheckboxHead.new(controller_name: @controller_name)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def render_table_headers
|
|
42
|
+
Array(@headers).each do |header|
|
|
43
|
+
if header.kind_of?(Hash)
|
|
44
|
+
th(class: build_classes(th_base_classes, header[:class])) do
|
|
45
|
+
header[:text] || header[:label]
|
|
46
|
+
end
|
|
47
|
+
else
|
|
48
|
+
th(class: th_base_classes) { header }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def th_base_classes
|
|
54
|
+
"px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|