easy-admin-rails 0.1.15 → 0.2.1
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/app/assets/builds/easy_admin.base.js +254 -18
- data/app/assets/builds/easy_admin.base.js.map +4 -4
- data/app/assets/builds/easy_admin.css +112 -18
- data/app/components/easy_admin/base_component.rb +1 -0
- data/app/components/easy_admin/form_tabs_component.rb +5 -2
- data/app/components/easy_admin/navbar_component.rb +5 -1
- data/app/components/easy_admin/permissions/user_role_assignment_component.rb +254 -0
- data/app/components/easy_admin/permissions/user_role_permissions_component.rb +186 -0
- data/app/components/easy_admin/resources/index_component.rb +1 -4
- data/app/components/easy_admin/sidebar_component.rb +67 -2
- data/app/controllers/easy_admin/application_controller.rb +131 -1
- data/app/controllers/easy_admin/batch_actions_controller.rb +27 -0
- data/app/controllers/easy_admin/concerns/belongs_to_editing.rb +201 -0
- data/app/controllers/easy_admin/concerns/inline_field_editing.rb +297 -0
- data/app/controllers/easy_admin/concerns/resource_authorization.rb +55 -0
- data/app/controllers/easy_admin/concerns/resource_filtering.rb +178 -0
- data/app/controllers/easy_admin/concerns/resource_loading.rb +149 -0
- data/app/controllers/easy_admin/concerns/resource_pagination.rb +135 -0
- data/app/controllers/easy_admin/dashboard_controller.rb +2 -1
- data/app/controllers/easy_admin/dashboards_controller.rb +6 -40
- data/app/controllers/easy_admin/resources_controller.rb +13 -762
- data/app/controllers/easy_admin/row_actions_controller.rb +25 -0
- data/app/helpers/easy_admin/fields_helper.rb +61 -9
- data/app/javascript/easy_admin/controllers/event_emitter_controller.js +2 -4
- data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +0 -10
- data/app/javascript/easy_admin/controllers/jsoneditor_controller.js +1 -4
- data/app/javascript/easy_admin/controllers/permission_toggle_controller.js +227 -0
- data/app/javascript/easy_admin/controllers/role_preview_controller.js +93 -0
- data/app/javascript/easy_admin/controllers/select_field_controller.js +1 -2
- data/app/javascript/easy_admin/controllers/settings_button_controller.js +1 -2
- data/app/javascript/easy_admin/controllers/settings_sidebar_controller.js +1 -4
- data/app/javascript/easy_admin/controllers/turbo_stream_redirect.js +0 -2
- data/app/javascript/easy_admin/controllers.js +5 -1
- data/app/models/easy_admin/admin_user.rb +6 -0
- data/app/policies/admin_user_policy.rb +36 -0
- data/app/policies/application_policy.rb +83 -0
- data/app/views/easy_admin/application/authorization_failure.turbo_stream.erb +8 -0
- data/app/views/easy_admin/dashboards/card.html.erb +5 -0
- data/app/views/easy_admin/dashboards/card.turbo_stream.erb +7 -0
- data/app/views/easy_admin/dashboards/card_error.html.erb +3 -0
- data/app/views/easy_admin/dashboards/card_error.turbo_stream.erb +5 -0
- data/app/views/easy_admin/dashboards/show.turbo_stream.erb +7 -0
- data/app/views/easy_admin/resources/belongs_to_edit_attached.html.erb +6 -0
- data/app/views/easy_admin/resources/belongs_to_edit_attached.turbo_stream.erb +8 -0
- data/app/views/easy_admin/resources/belongs_to_reattach.html.erb +5 -0
- data/app/views/easy_admin/resources/edit.html.erb +1 -1
- data/app/views/easy_admin/resources/edit_field.html.erb +5 -0
- data/app/views/easy_admin/resources/edit_field.turbo_stream.erb +7 -0
- data/app/views/easy_admin/resources/index.html.erb +1 -1
- data/app/views/easy_admin/resources/index_frame.html.erb +8 -142
- data/app/views/easy_admin/resources/update_belongs_to_attached.turbo_stream.erb +25 -0
- data/app/views/layouts/easy_admin/application.html.erb +15 -2
- data/config/initializers/easy_admin_permissions.rb +73 -0
- data/db/seeds/easy_admin_permissions.rb +121 -0
- data/lib/easy-admin-rails.rb +2 -0
- data/lib/easy_admin/permissions/component.rb +168 -0
- data/lib/easy_admin/permissions/configuration.rb +37 -0
- data/lib/easy_admin/permissions/controller.rb +164 -0
- data/lib/easy_admin/permissions/dsl.rb +160 -0
- data/lib/easy_admin/permissions/models.rb +44 -0
- data/lib/easy_admin/permissions/permission_denied_component.rb +121 -0
- data/lib/easy_admin/permissions/resource_permissions.rb +231 -0
- data/lib/easy_admin/permissions/role_definition.rb +45 -0
- data/lib/easy_admin/permissions/role_denied_component.rb +159 -0
- data/lib/easy_admin/permissions/role_dsl.rb +73 -0
- data/lib/easy_admin/permissions/user_extensions.rb +129 -0
- data/lib/easy_admin/permissions.rb +113 -0
- data/lib/easy_admin/resource/base.rb +119 -0
- data/lib/easy_admin/resource/configuration.rb +148 -0
- data/lib/easy_admin/resource/dsl.rb +117 -0
- data/lib/easy_admin/resource/field_registry.rb +189 -0
- data/lib/easy_admin/resource/form_builder.rb +123 -0
- data/lib/easy_admin/resource/layout_builder.rb +249 -0
- data/lib/easy_admin/resource/scope_manager.rb +252 -0
- data/lib/easy_admin/resource/show_builder.rb +359 -0
- data/lib/easy_admin/resource.rb +8 -835
- data/lib/easy_admin/resource_modules.rb +11 -0
- data/lib/easy_admin/version.rb +1 -1
- data/lib/generators/easy_admin/permissions/install_generator.rb +90 -0
- data/lib/generators/easy_admin/permissions/templates/initializers/permissions.rb +37 -0
- data/lib/generators/easy_admin/permissions/templates/migrations/create_permission_tables.rb +27 -0
- data/lib/generators/easy_admin/permissions/templates/migrations/update_users_for_permissions.rb +6 -0
- data/lib/generators/easy_admin/permissions/templates/models/permission.rb +9 -0
- data/lib/generators/easy_admin/permissions/templates/models/role.rb +9 -0
- data/lib/generators/easy_admin/permissions/templates/models/role_permission.rb +9 -0
- data/lib/generators/easy_admin/permissions/templates/models/user_role.rb +9 -0
- data/lib/generators/easy_admin/permissions/templates/policies/application_policy.rb +47 -0
- data/lib/generators/easy_admin/permissions/templates/policies/user_policy.rb +36 -0
- data/lib/generators/easy_admin/permissions/templates/seeds/permissions.rb +89 -0
- metadata +62 -5
- data/db/migrate/20250101000001_create_easy_admin_admin_users.rb +0 -45
@@ -0,0 +1,123 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module ResourceModules
|
3
|
+
# FormBuilder class for form DSL
|
4
|
+
# Handles all form tab and field definitions within forms
|
5
|
+
class FormBuilder
|
6
|
+
def initialize(resource_class)
|
7
|
+
@resource_class = resource_class
|
8
|
+
@current_tab = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def tab(name = "General", **options, &block)
|
12
|
+
tab_config = {
|
13
|
+
name: name,
|
14
|
+
label: options[:label] || name,
|
15
|
+
icon: options[:icon],
|
16
|
+
fields: []
|
17
|
+
}
|
18
|
+
|
19
|
+
@current_tab = tab_config
|
20
|
+
@resource_class.add_form_tab(tab_config)
|
21
|
+
|
22
|
+
instance_eval(&block) if block_given?
|
23
|
+
@current_tab = nil
|
24
|
+
end
|
25
|
+
|
26
|
+
# Field definition methods within tabs - delegate to resource
|
27
|
+
def field(name, type = :string, **options)
|
28
|
+
field_config = @resource_class.build_field_config(name, type, **options)
|
29
|
+
|
30
|
+
if @current_tab
|
31
|
+
@current_tab[:fields] << field_config
|
32
|
+
end
|
33
|
+
|
34
|
+
# Also add to main fields_config for compatibility
|
35
|
+
@resource_class.add_field_to_registry(field_config)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Convenience methods for form fields
|
39
|
+
def text_field(name, **options)
|
40
|
+
field(name, :string, **options)
|
41
|
+
end
|
42
|
+
|
43
|
+
def textarea_field(name, **options)
|
44
|
+
field(name, :text, **options)
|
45
|
+
end
|
46
|
+
|
47
|
+
def number_field(name, **options)
|
48
|
+
field(name, :number, **options)
|
49
|
+
end
|
50
|
+
|
51
|
+
def email_field(name, **options)
|
52
|
+
field(name, :email, **options)
|
53
|
+
end
|
54
|
+
|
55
|
+
def date_field(name, **options)
|
56
|
+
field(name, :date, **options)
|
57
|
+
end
|
58
|
+
|
59
|
+
def datetime_field(name, **options)
|
60
|
+
field(name, :datetime, **options)
|
61
|
+
end
|
62
|
+
|
63
|
+
def boolean_field(name, **options)
|
64
|
+
field(name, :boolean, **options)
|
65
|
+
end
|
66
|
+
|
67
|
+
def select_field(name, options:, **other_options)
|
68
|
+
field(name, :select, options: options, **other_options)
|
69
|
+
end
|
70
|
+
|
71
|
+
def belongs_to_field(name, **options)
|
72
|
+
field(name, :belongs_to, **options)
|
73
|
+
end
|
74
|
+
|
75
|
+
def has_many_field(name, **options)
|
76
|
+
field(name, :has_many, **options)
|
77
|
+
end
|
78
|
+
|
79
|
+
def file_field(name, **options)
|
80
|
+
field(name, :file, **options)
|
81
|
+
end
|
82
|
+
|
83
|
+
def json_field(name, **options)
|
84
|
+
field(name, :json, **options)
|
85
|
+
end
|
86
|
+
|
87
|
+
def password_field(name = :password, **options)
|
88
|
+
field(name, :password, **options)
|
89
|
+
field("#{name}_confirmation".to_sym, :password,
|
90
|
+
label: options[:confirmation_label] || "Confirm #{name.to_s.humanize}",
|
91
|
+
**options.except(:confirmation_label))
|
92
|
+
end
|
93
|
+
|
94
|
+
# Support for rendering custom components in forms
|
95
|
+
def render(component_class_or_instance, **options)
|
96
|
+
content_config = {
|
97
|
+
type: :custom_component,
|
98
|
+
component_class: component_class_or_instance.class,
|
99
|
+
component_options: options,
|
100
|
+
component_instance: component_class_or_instance
|
101
|
+
}
|
102
|
+
|
103
|
+
if @current_tab
|
104
|
+
@current_tab[:fields] << content_config
|
105
|
+
end
|
106
|
+
|
107
|
+
component_class_or_instance
|
108
|
+
end
|
109
|
+
|
110
|
+
# Support for custom content blocks in forms
|
111
|
+
def content(&block)
|
112
|
+
content_config = {
|
113
|
+
type: :custom_content,
|
114
|
+
block: block
|
115
|
+
}
|
116
|
+
|
117
|
+
if @current_tab
|
118
|
+
@current_tab[:fields] << content_config
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,249 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module ResourceModules
|
3
|
+
# LayoutBuilder module for handling form and show layout configuration
|
4
|
+
# Manages form tabs and show page layout elements
|
5
|
+
module LayoutBuilder
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
class_attribute :form_tabs_config, :show_layout_config
|
10
|
+
|
11
|
+
def self.initialize_layout_builder
|
12
|
+
self.form_tabs_config = []
|
13
|
+
self.show_layout_config = []
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class_methods do
|
18
|
+
# Form tabs management
|
19
|
+
def add_form_tab(tab_config)
|
20
|
+
self.form_tabs_config = (self.form_tabs_config || []) + [tab_config]
|
21
|
+
end
|
22
|
+
|
23
|
+
def form_tabs
|
24
|
+
form_tabs_config
|
25
|
+
end
|
26
|
+
|
27
|
+
def has_form_tabs?
|
28
|
+
form_tabs_config.any?
|
29
|
+
end
|
30
|
+
|
31
|
+
def clear_form_tabs
|
32
|
+
self.form_tabs_config = []
|
33
|
+
end
|
34
|
+
|
35
|
+
# Show layout management
|
36
|
+
def add_show_layout_element(element_config)
|
37
|
+
self.show_layout_config = (self.show_layout_config || []) + [element_config]
|
38
|
+
end
|
39
|
+
|
40
|
+
def show_layout
|
41
|
+
show_layout_config
|
42
|
+
end
|
43
|
+
|
44
|
+
def has_show_layout?
|
45
|
+
show_layout_config.any?
|
46
|
+
end
|
47
|
+
|
48
|
+
def clear_show_layout
|
49
|
+
self.show_layout_config = []
|
50
|
+
end
|
51
|
+
|
52
|
+
# Form tab utilities
|
53
|
+
def find_form_tab(tab_name)
|
54
|
+
form_tabs_config.find { |tab| tab[:name].to_s == tab_name.to_s }
|
55
|
+
end
|
56
|
+
|
57
|
+
def form_tab_names
|
58
|
+
form_tabs_config.map { |tab| tab[:name] }
|
59
|
+
end
|
60
|
+
|
61
|
+
def form_tab_fields(tab_name)
|
62
|
+
tab = find_form_tab(tab_name)
|
63
|
+
tab ? tab[:fields] : []
|
64
|
+
end
|
65
|
+
|
66
|
+
def total_form_tabs_count
|
67
|
+
form_tabs_config.count
|
68
|
+
end
|
69
|
+
|
70
|
+
# Show layout utilities
|
71
|
+
def show_layout_rows
|
72
|
+
show_layout_config.select { |element| element[:type] == :row }
|
73
|
+
end
|
74
|
+
|
75
|
+
def show_layout_elements_count
|
76
|
+
count = 0
|
77
|
+
show_layout_config.each do |element|
|
78
|
+
count += count_nested_elements(element)
|
79
|
+
end
|
80
|
+
count
|
81
|
+
end
|
82
|
+
|
83
|
+
def find_show_layout_element(element_type)
|
84
|
+
find_element_in_layout(show_layout_config, element_type)
|
85
|
+
end
|
86
|
+
|
87
|
+
def has_show_layout_tabs?
|
88
|
+
find_show_layout_element(:tabs).present?
|
89
|
+
end
|
90
|
+
|
91
|
+
def has_show_layout_cards?
|
92
|
+
find_show_layout_element(:card).present?
|
93
|
+
end
|
94
|
+
|
95
|
+
def has_metric_cards?
|
96
|
+
find_show_layout_element(:metric_card).present?
|
97
|
+
end
|
98
|
+
|
99
|
+
def has_chart_cards?
|
100
|
+
find_show_layout_element(:chart_card).present?
|
101
|
+
end
|
102
|
+
|
103
|
+
# Layout validation
|
104
|
+
def validate_form_structure
|
105
|
+
errors = []
|
106
|
+
|
107
|
+
if has_form_tabs?
|
108
|
+
form_tabs_config.each_with_index do |tab, index|
|
109
|
+
unless tab[:name].present?
|
110
|
+
errors << "Form tab at index #{index} is missing a name"
|
111
|
+
end
|
112
|
+
|
113
|
+
unless tab[:fields].is_a?(Array)
|
114
|
+
errors << "Form tab '#{tab[:name]}' fields must be an array"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
errors
|
120
|
+
end
|
121
|
+
|
122
|
+
def validate_show_layout_structure
|
123
|
+
errors = []
|
124
|
+
|
125
|
+
show_layout_config.each_with_index do |element, index|
|
126
|
+
case element[:type]
|
127
|
+
when :row
|
128
|
+
unless element[:columns].is_a?(Array)
|
129
|
+
errors << "Row at index #{index} must have columns array"
|
130
|
+
end
|
131
|
+
|
132
|
+
if element[:columns].empty?
|
133
|
+
errors << "Row at index #{index} cannot have empty columns"
|
134
|
+
end
|
135
|
+
when :column
|
136
|
+
unless element[:elements].is_a?(Array)
|
137
|
+
errors << "Column at index #{index} must have elements array"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
errors
|
143
|
+
end
|
144
|
+
|
145
|
+
# Layout rebuild methods
|
146
|
+
def rebuild_form_from_fields
|
147
|
+
return if fields_config.empty?
|
148
|
+
|
149
|
+
# Create a default tab with all non-readonly fields
|
150
|
+
default_tab = {
|
151
|
+
name: "General",
|
152
|
+
label: "General",
|
153
|
+
icon: nil,
|
154
|
+
fields: fields_config.reject { |field| field[:readonly] }
|
155
|
+
}
|
156
|
+
|
157
|
+
self.form_tabs_config = [default_tab]
|
158
|
+
end
|
159
|
+
|
160
|
+
def rebuild_show_layout_from_fields
|
161
|
+
return if fields_config.empty?
|
162
|
+
|
163
|
+
# Create a simple single-column layout with all fields
|
164
|
+
row_config = {
|
165
|
+
type: :row,
|
166
|
+
columns_count: 1,
|
167
|
+
spacing: "medium",
|
168
|
+
columns: [{
|
169
|
+
type: :column,
|
170
|
+
size: 12,
|
171
|
+
elements: [{
|
172
|
+
type: :card,
|
173
|
+
title: "Details",
|
174
|
+
color: "light",
|
175
|
+
padding: "medium",
|
176
|
+
shadow: "small",
|
177
|
+
elements: fields_config.map { |field|
|
178
|
+
{
|
179
|
+
type: :field,
|
180
|
+
name: field[:name],
|
181
|
+
label: field[:label],
|
182
|
+
field_type: :default,
|
183
|
+
format: field[:format],
|
184
|
+
css_classes: nil,
|
185
|
+
show_label: true
|
186
|
+
}
|
187
|
+
}
|
188
|
+
}]
|
189
|
+
}]
|
190
|
+
}
|
191
|
+
|
192
|
+
self.show_layout_config = [row_config]
|
193
|
+
end
|
194
|
+
|
195
|
+
private
|
196
|
+
|
197
|
+
def count_nested_elements(element)
|
198
|
+
count = 1
|
199
|
+
|
200
|
+
case element[:type]
|
201
|
+
when :row
|
202
|
+
element[:columns]&.each { |column| count += count_nested_elements(column) }
|
203
|
+
when :column
|
204
|
+
element[:elements]&.each { |child| count += count_nested_elements(child) }
|
205
|
+
when :card
|
206
|
+
element[:elements]&.each { |child| count += count_nested_elements(child) }
|
207
|
+
when :tabs
|
208
|
+
element[:tabs]&.each { |tab| count += count_nested_elements(tab) }
|
209
|
+
when :tab
|
210
|
+
element[:elements]&.each { |child| count += count_nested_elements(child) }
|
211
|
+
end
|
212
|
+
|
213
|
+
count
|
214
|
+
end
|
215
|
+
|
216
|
+
def find_element_in_layout(layout, element_type)
|
217
|
+
layout.each do |element|
|
218
|
+
return element if element[:type] == element_type
|
219
|
+
|
220
|
+
case element[:type]
|
221
|
+
when :row
|
222
|
+
element[:columns]&.each do |column|
|
223
|
+
found = find_element_in_layout([column], element_type)
|
224
|
+
return found if found
|
225
|
+
end
|
226
|
+
when :column, :card
|
227
|
+
if element[:elements]
|
228
|
+
found = find_element_in_layout(element[:elements], element_type)
|
229
|
+
return found if found
|
230
|
+
end
|
231
|
+
when :tabs
|
232
|
+
element[:tabs]&.each do |tab|
|
233
|
+
found = find_element_in_layout([tab], element_type)
|
234
|
+
return found if found
|
235
|
+
end
|
236
|
+
when :tab
|
237
|
+
if element[:elements]
|
238
|
+
found = find_element_in_layout(element[:elements], element_type)
|
239
|
+
return found if found
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
nil
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
@@ -0,0 +1,252 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module ResourceModules
|
3
|
+
# ScopeManager module for handling resource scopes and filtering
|
4
|
+
# Manages scope definitions, default scopes, and scope-related queries
|
5
|
+
module ScopeManager
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
class_attribute :scopes_config
|
10
|
+
|
11
|
+
def self.initialize_scope_manager
|
12
|
+
self.scopes_config = []
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class_methods do
|
17
|
+
# Core scope definition method
|
18
|
+
def scope(name, **options)
|
19
|
+
scope_config = {
|
20
|
+
name: name,
|
21
|
+
label: options[:label] || name.to_s.humanize,
|
22
|
+
scope_method: options[:scope] || name,
|
23
|
+
default: options.fetch(:default, false),
|
24
|
+
icon: options[:icon],
|
25
|
+
color: options[:color] || 'blue',
|
26
|
+
count: options.fetch(:show_count, true),
|
27
|
+
condition: options[:if] # Conditional scope visibility
|
28
|
+
}
|
29
|
+
|
30
|
+
add_scope(scope_config)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Add scope to configuration
|
34
|
+
def add_scope(scope_config)
|
35
|
+
self.scopes_config = (self.scopes_config || []) + [scope_config]
|
36
|
+
end
|
37
|
+
|
38
|
+
# Scope query methods
|
39
|
+
def scopes
|
40
|
+
scopes_config
|
41
|
+
end
|
42
|
+
|
43
|
+
def has_scopes?
|
44
|
+
scopes_config.any?
|
45
|
+
end
|
46
|
+
|
47
|
+
def scope_names
|
48
|
+
scopes_config.map { |scope| scope[:name] }
|
49
|
+
end
|
50
|
+
|
51
|
+
def find_scope(name)
|
52
|
+
scopes_config.find { |scope| scope[:name].to_s == name.to_s }
|
53
|
+
end
|
54
|
+
|
55
|
+
def scope_exists?(name)
|
56
|
+
find_scope(name).present?
|
57
|
+
end
|
58
|
+
|
59
|
+
# Default scope management
|
60
|
+
def default_scope
|
61
|
+
scopes_config.find { |scope| scope[:default] } || scopes_config.first
|
62
|
+
end
|
63
|
+
|
64
|
+
def set_default_scope(name)
|
65
|
+
# Remove existing default
|
66
|
+
scopes_config.each { |scope| scope[:default] = false }
|
67
|
+
|
68
|
+
# Set new default
|
69
|
+
scope = find_scope(name)
|
70
|
+
scope[:default] = true if scope
|
71
|
+
end
|
72
|
+
|
73
|
+
def has_default_scope?
|
74
|
+
default_scope.present?
|
75
|
+
end
|
76
|
+
|
77
|
+
# Conditional scopes
|
78
|
+
def visible_scopes(context = {})
|
79
|
+
scopes_config.select do |scope_config|
|
80
|
+
condition = scope_config[:condition]
|
81
|
+
condition.nil? || (condition.respond_to?(:call) ? context.instance_eval(&condition) : condition)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def conditional_scopes
|
86
|
+
scopes_config.select { |scope| scope[:condition].present? }
|
87
|
+
end
|
88
|
+
|
89
|
+
# Scope application methods
|
90
|
+
def apply_scope_to_records(records, scope_name)
|
91
|
+
return records unless scope_exists?(scope_name)
|
92
|
+
|
93
|
+
scope_config = find_scope(scope_name)
|
94
|
+
return records if scope_config[:name] == :all
|
95
|
+
|
96
|
+
scope_method = scope_config[:scope_method]
|
97
|
+
if records.respond_to?(scope_method)
|
98
|
+
records.public_send(scope_method)
|
99
|
+
else
|
100
|
+
records
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def apply_default_scope_to_records(records)
|
105
|
+
return records unless has_default_scope?
|
106
|
+
|
107
|
+
default = default_scope
|
108
|
+
return records if default[:name] == :all
|
109
|
+
|
110
|
+
apply_scope_to_records(records, default[:name])
|
111
|
+
end
|
112
|
+
|
113
|
+
# Scope counts calculation
|
114
|
+
def calculate_scope_counts(base_records = nil)
|
115
|
+
base_records ||= model_class.all
|
116
|
+
counts = {}
|
117
|
+
|
118
|
+
scopes_config.each do |scope_config|
|
119
|
+
scope_name = scope_config[:name]
|
120
|
+
next unless scope_config[:count] # Skip if count is disabled
|
121
|
+
|
122
|
+
if scope_name == :all
|
123
|
+
counts[scope_name] = base_records.count
|
124
|
+
else
|
125
|
+
scope_method = scope_config[:scope_method]
|
126
|
+
if model_class.respond_to?(scope_method)
|
127
|
+
counts[scope_name] = model_class.public_send(scope_method).count
|
128
|
+
else
|
129
|
+
counts[scope_name] = 0
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
counts
|
135
|
+
end
|
136
|
+
|
137
|
+
def scope_count(scope_name, base_records = nil)
|
138
|
+
return 0 unless scope_exists?(scope_name)
|
139
|
+
|
140
|
+
scope_config = find_scope(scope_name)
|
141
|
+
return 0 unless scope_config[:count]
|
142
|
+
|
143
|
+
if scope_name == :all
|
144
|
+
base_records ||= model_class.all
|
145
|
+
base_records.count
|
146
|
+
else
|
147
|
+
scope_method = scope_config[:scope_method]
|
148
|
+
if model_class.respond_to?(scope_method)
|
149
|
+
model_class.public_send(scope_method).count
|
150
|
+
else
|
151
|
+
0
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Scope utilities
|
157
|
+
def scope_label(scope_name)
|
158
|
+
scope_config = find_scope(scope_name)
|
159
|
+
scope_config ? scope_config[:label] : scope_name.to_s.humanize
|
160
|
+
end
|
161
|
+
|
162
|
+
def scope_icon(scope_name)
|
163
|
+
scope_config = find_scope(scope_name)
|
164
|
+
scope_config&.dig(:icon)
|
165
|
+
end
|
166
|
+
|
167
|
+
def scope_color(scope_name)
|
168
|
+
scope_config = find_scope(scope_name)
|
169
|
+
scope_config&.dig(:color) || 'blue'
|
170
|
+
end
|
171
|
+
|
172
|
+
def scope_method_name(scope_name)
|
173
|
+
scope_config = find_scope(scope_name)
|
174
|
+
scope_config ? scope_config[:scope_method] : scope_name
|
175
|
+
end
|
176
|
+
|
177
|
+
# Scope validation
|
178
|
+
def validate_scopes
|
179
|
+
errors = []
|
180
|
+
|
181
|
+
scopes_config.each do |scope_config|
|
182
|
+
scope_name = scope_config[:name]
|
183
|
+
scope_method = scope_config[:scope_method]
|
184
|
+
|
185
|
+
# Check if scope method exists on model (skip :all)
|
186
|
+
if scope_name != :all && !model_class.respond_to?(scope_method)
|
187
|
+
errors << "Scope method '#{scope_method}' does not exist on #{model_class.name}"
|
188
|
+
end
|
189
|
+
|
190
|
+
# Check for duplicate scope names
|
191
|
+
duplicate_count = scopes_config.count { |s| s[:name] == scope_name }
|
192
|
+
if duplicate_count > 1
|
193
|
+
errors << "Duplicate scope name '#{scope_name}' found"
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
errors
|
198
|
+
end
|
199
|
+
|
200
|
+
def valid_scopes?
|
201
|
+
validate_scopes.empty?
|
202
|
+
end
|
203
|
+
|
204
|
+
# Scope management
|
205
|
+
def clear_scopes
|
206
|
+
self.scopes_config = []
|
207
|
+
end
|
208
|
+
|
209
|
+
def remove_scope(scope_name)
|
210
|
+
self.scopes_config = scopes_config.reject { |scope| scope[:name] == scope_name }
|
211
|
+
end
|
212
|
+
|
213
|
+
def total_scopes_count
|
214
|
+
scopes_config.count
|
215
|
+
end
|
216
|
+
|
217
|
+
def enabled_scope_counts_count
|
218
|
+
scopes_config.count { |scope| scope[:count] }
|
219
|
+
end
|
220
|
+
|
221
|
+
# Predefined scope helpers
|
222
|
+
def add_all_scope(label: "All", default: true)
|
223
|
+
scope(:all, label: label, scope: :all, default: default)
|
224
|
+
end
|
225
|
+
|
226
|
+
def add_recent_scope(days: 7, label: "Recent")
|
227
|
+
return unless model_class.column_names.include?('created_at')
|
228
|
+
|
229
|
+
scope(:recent,
|
230
|
+
label: label,
|
231
|
+
scope: -> { where('created_at >= ?', days.days.ago) })
|
232
|
+
end
|
233
|
+
|
234
|
+
def add_published_scope(column: :published, label: "Published")
|
235
|
+
return unless model_class.column_names.include?(column.to_s)
|
236
|
+
|
237
|
+
scope(:published,
|
238
|
+
label: label,
|
239
|
+
scope: -> { where(column => true) })
|
240
|
+
end
|
241
|
+
|
242
|
+
def add_unpublished_scope(column: :published, label: "Unpublished")
|
243
|
+
return unless model_class.column_names.include?(column.to_s)
|
244
|
+
|
245
|
+
scope(:unpublished,
|
246
|
+
label: label,
|
247
|
+
scope: -> { where(column => [false, nil]) })
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|