super_admin 0.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 +7 -0
- data/README.md +216 -0
- data/Rakefile +30 -0
- data/app/assets/stylesheets/super_admin/application.css +15 -0
- data/app/assets/stylesheets/super_admin/tailwind.css +1 -0
- data/app/assets/stylesheets/super_admin/tailwind.source.css +25 -0
- data/app/controllers/super_admin/application_controller.rb +89 -0
- data/app/controllers/super_admin/associations_controller.rb +136 -0
- data/app/controllers/super_admin/audit_logs_controller.rb +39 -0
- data/app/controllers/super_admin/base_controller.rb +133 -0
- data/app/controllers/super_admin/dashboard_controller.rb +29 -0
- data/app/controllers/super_admin/exports_controller.rb +109 -0
- data/app/controllers/super_admin/resources_controller.rb +201 -0
- data/app/dashboards/super_admin/base_dashboard.rb +200 -0
- data/app/errors/super_admin/configuration_error.rb +6 -0
- data/app/helpers/super_admin/application_helper.rb +84 -0
- data/app/helpers/super_admin/exports_helper.rb +16 -0
- data/app/helpers/super_admin/resources_helper.rb +204 -0
- data/app/helpers/super_admin/route_helper.rb +7 -0
- data/app/javascript/super_admin/application.js +263 -0
- data/app/jobs/super_admin/application_job.rb +4 -0
- data/app/jobs/super_admin/generate_super_admin_csv_export_job.rb +100 -0
- data/app/mailers/super_admin/application_mailer.rb +6 -0
- data/app/models/super_admin/application_record.rb +5 -0
- data/app/models/super_admin/audit_log.rb +35 -0
- data/app/models/super_admin/csv_export.rb +67 -0
- data/app/services/super_admin/auditing.rb +74 -0
- data/app/services/super_admin/authorization.rb +113 -0
- data/app/services/super_admin/authorization_adapters/base_adapter.rb +100 -0
- data/app/services/super_admin/authorization_adapters/default_adapter.rb +77 -0
- data/app/services/super_admin/authorization_adapters/proc_adapter.rb +65 -0
- data/app/services/super_admin/authorization_adapters/pundit_adapter.rb +81 -0
- data/app/services/super_admin/csv_export_creator.rb +45 -0
- data/app/services/super_admin/dashboard_registry.rb +90 -0
- data/app/services/super_admin/dashboard_resolver.rb +100 -0
- data/app/services/super_admin/filter_builder.rb +185 -0
- data/app/services/super_admin/form_builder.rb +59 -0
- data/app/services/super_admin/form_fields/array_field.rb +35 -0
- data/app/services/super_admin/form_fields/association_field.rb +146 -0
- data/app/services/super_admin/form_fields/base_field.rb +53 -0
- data/app/services/super_admin/form_fields/boolean_field.rb +29 -0
- data/app/services/super_admin/form_fields/date_field.rb +15 -0
- data/app/services/super_admin/form_fields/date_time_field.rb +15 -0
- data/app/services/super_admin/form_fields/enum_field.rb +27 -0
- data/app/services/super_admin/form_fields/factory.rb +102 -0
- data/app/services/super_admin/form_fields/nested_field.rb +120 -0
- data/app/services/super_admin/form_fields/number_field.rb +29 -0
- data/app/services/super_admin/form_fields/text_area_field.rb +19 -0
- data/app/services/super_admin/model_inspector.rb +182 -0
- data/app/services/super_admin/queries/base_query.rb +45 -0
- data/app/services/super_admin/queries/filter_query.rb +188 -0
- data/app/services/super_admin/queries/resource_scope_query.rb +74 -0
- data/app/services/super_admin/queries/search_query.rb +146 -0
- data/app/services/super_admin/queries/sort_query.rb +41 -0
- data/app/services/super_admin/resource_configuration.rb +63 -0
- data/app/services/super_admin/resource_exporter.rb +78 -0
- data/app/services/super_admin/resource_query.rb +40 -0
- data/app/services/super_admin/resources/association_inspector.rb +112 -0
- data/app/services/super_admin/resources/collection_presenter.rb +63 -0
- data/app/services/super_admin/resources/context.rb +63 -0
- data/app/services/super_admin/resources/filter_params.rb +29 -0
- data/app/services/super_admin/resources/permitted_attributes.rb +104 -0
- data/app/services/super_admin/resources/value_normalizer.rb +121 -0
- data/app/services/super_admin/sensitive_attributes.rb +166 -0
- data/app/views/layouts/super_admin.html.erb +74 -0
- data/app/views/super_admin/audit_logs/index.html.erb +143 -0
- data/app/views/super_admin/dashboard/index.html.erb +79 -0
- data/app/views/super_admin/exports/index.html.erb +84 -0
- data/app/views/super_admin/exports/show.html.erb +57 -0
- data/app/views/super_admin/resources/_form.html.erb +42 -0
- data/app/views/super_admin/resources/destroy.turbo_stream.erb +17 -0
- data/app/views/super_admin/resources/edit.html.erb +37 -0
- data/app/views/super_admin/resources/index.html.erb +189 -0
- data/app/views/super_admin/resources/new.html.erb +31 -0
- data/app/views/super_admin/resources/show.html.erb +106 -0
- data/app/views/super_admin/shared/_breadcrumbs.html.erb +12 -0
- data/app/views/super_admin/shared/_custom_styles.html.erb +132 -0
- data/app/views/super_admin/shared/_flash.html.erb +55 -0
- data/app/views/super_admin/shared/_form_field.html.erb +35 -0
- data/app/views/super_admin/shared/_navigation.html.erb +92 -0
- data/app/views/super_admin/shared/_nested_fields.html.erb +59 -0
- data/app/views/super_admin/shared/_nested_record_fields.html.erb +45 -0
- data/config/importmap.rb +4 -0
- data/config/initializers/rack_attack.rb +134 -0
- data/config/initializers/super_admin.rb +117 -0
- data/config/locales/super_admin.en.yml +197 -0
- data/config/locales/super_admin.fr.yml +197 -0
- data/config/routes.rb +22 -0
- data/lib/generators/super_admin/dashboard_generator.rb +50 -0
- data/lib/generators/super_admin/install_generator.rb +58 -0
- data/lib/generators/super_admin/templates/20240101000001_create_super_admin_audit_logs.rb +24 -0
- data/lib/generators/super_admin/templates/20240101000002_create_super_admin_csv_exports.rb +33 -0
- data/lib/generators/super_admin/templates/super_admin.rb +58 -0
- data/lib/super_admin/dashboard_creator.rb +256 -0
- data/lib/super_admin/engine.rb +53 -0
- data/lib/super_admin/install_task.rb +96 -0
- data/lib/super_admin/version.rb +3 -0
- data/lib/super_admin.rb +7 -0
- data/lib/tasks/super_admin_tasks.rake +38 -0
- metadata +239 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SuperAdmin
|
|
4
|
+
module FormFields
|
|
5
|
+
class EnumField < BaseField
|
|
6
|
+
def render
|
|
7
|
+
form.select(attribute_name, enum_options, { include_blank: include_blank_option }, options.except(:required))
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def type
|
|
11
|
+
:enum
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def enum_options
|
|
17
|
+
SuperAdmin::ModelInspector.enum_values(model_class, attribute_name).keys.map do |key|
|
|
18
|
+
[ key.humanize, key ]
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def include_blank_option
|
|
23
|
+
column&.null ? true : nil
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SuperAdmin
|
|
4
|
+
module FormFields
|
|
5
|
+
# Responsible for selecting the correct form field class
|
|
6
|
+
# based on the attribute metadata exposed by the FormBuilder.
|
|
7
|
+
class Factory
|
|
8
|
+
ASSOCIATION_SUFFIX = "_id".freeze
|
|
9
|
+
NESTED_SUFFIX = "_attributes".freeze
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def build(builder)
|
|
13
|
+
new(builder).build
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(builder)
|
|
18
|
+
@builder = builder
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def build
|
|
22
|
+
field_klass.new(builder)
|
|
23
|
+
rescue StandardError => e
|
|
24
|
+
Rails.logger.error("SuperAdmin::FormFields::Factory build error: #{e.message}")
|
|
25
|
+
BaseField.new(builder)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
attr_reader :builder
|
|
31
|
+
|
|
32
|
+
delegate :model_class, :attribute_name, :column, to: :builder
|
|
33
|
+
|
|
34
|
+
def field_klass
|
|
35
|
+
return NestedField if nested_attribute?
|
|
36
|
+
return AssociationField if association_attribute?
|
|
37
|
+
return EnumField if enum_attribute?
|
|
38
|
+
return BooleanField if boolean_attribute?
|
|
39
|
+
return DateTimeField if datetime_attribute?
|
|
40
|
+
return DateField if date_attribute?
|
|
41
|
+
return NumberField if number_attribute?
|
|
42
|
+
return ArrayField if array_attribute?
|
|
43
|
+
return TextAreaField if text_area_attribute?
|
|
44
|
+
|
|
45
|
+
BaseField
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def nested_attribute?
|
|
49
|
+
attribute_name.to_s.end_with?(NESTED_SUFFIX)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def association_attribute?
|
|
53
|
+
association_name && model_class.reflect_on_association(association_name)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def association_name
|
|
57
|
+
return @association_name if defined?(@association_name)
|
|
58
|
+
|
|
59
|
+
name = attribute_name.to_s
|
|
60
|
+
@association_name = name.end_with?(ASSOCIATION_SUFFIX) ? name.delete_suffix(ASSOCIATION_SUFFIX).to_sym : nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def enum_attribute?
|
|
64
|
+
SuperAdmin::ModelInspector.enum_values(model_class, attribute_name).present?
|
|
65
|
+
rescue StandardError
|
|
66
|
+
false
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def boolean_attribute?
|
|
70
|
+
column_type?(:boolean)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def datetime_attribute?
|
|
74
|
+
column_type?(:datetime, :timestamp)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def date_attribute?
|
|
78
|
+
column_type?(:date)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def number_attribute?
|
|
82
|
+
column_type?(:integer, :float, :decimal, :bigint)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def text_area_attribute?
|
|
86
|
+
column_type?(:text)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def column_type?(*types)
|
|
90
|
+
return false unless column
|
|
91
|
+
|
|
92
|
+
types.include?(column.type)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def array_attribute?
|
|
96
|
+
return false unless column
|
|
97
|
+
|
|
98
|
+
column.respond_to?(:array) && column.array
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SuperAdmin
|
|
4
|
+
module FormFields
|
|
5
|
+
class NestedField < BaseField
|
|
6
|
+
MAX_DEPTH_SAFETY = 10
|
|
7
|
+
|
|
8
|
+
def render
|
|
9
|
+
return missing_association_warning unless reflection
|
|
10
|
+
return max_depth_message if max_depth_exceeded?
|
|
11
|
+
|
|
12
|
+
prepare_nested_records
|
|
13
|
+
|
|
14
|
+
view_context.render(
|
|
15
|
+
partial: "super_admin/shared/nested_fields",
|
|
16
|
+
locals: {
|
|
17
|
+
form: form,
|
|
18
|
+
parent_model_class: model_class,
|
|
19
|
+
association: reflection,
|
|
20
|
+
nested_model_class: reflection.klass,
|
|
21
|
+
nested_attributes: nested_editable_attributes,
|
|
22
|
+
nested_options: nested_options,
|
|
23
|
+
label: label,
|
|
24
|
+
current_depth: current_depth
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def type
|
|
30
|
+
:nested
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def label
|
|
34
|
+
return association_name.to_s.humanize unless reflection
|
|
35
|
+
|
|
36
|
+
count = reflection.collection? ? 2 : 1
|
|
37
|
+
reflection.klass.model_name.human(count: count)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def options
|
|
41
|
+
{}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def association_name
|
|
47
|
+
attribute_name.to_s.delete_suffix("_attributes")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def reflection
|
|
51
|
+
@reflection ||= model_class.reflect_on_association(association_name.to_sym)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def nested_options
|
|
55
|
+
return {} unless model_class.respond_to?(:nested_attributes_options)
|
|
56
|
+
|
|
57
|
+
model_class.nested_attributes_options[association_name.to_sym]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def nested_editable_attributes
|
|
61
|
+
attributes = SuperAdmin::DashboardResolver.form_attributes_for(reflection.klass)
|
|
62
|
+
attributes = SuperAdmin::ResourceConfiguration.editable_attributes(reflection.klass) if attributes.blank?
|
|
63
|
+
|
|
64
|
+
attributes = attributes.reject { |attr| attr.to_s.end_with?("_attributes") }
|
|
65
|
+
attributes = attributes.map { |attr| attr.to_sym }
|
|
66
|
+
attributes -= [ reflection.foreign_key.to_sym ] if reflection.respond_to?(:foreign_key)
|
|
67
|
+
attributes
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def prepare_nested_records
|
|
71
|
+
parent = form.object
|
|
72
|
+
return unless parent
|
|
73
|
+
return if reflection.through_reflection
|
|
74
|
+
|
|
75
|
+
if parent.respond_to?(:association)
|
|
76
|
+
association_proxy = parent.association(reflection.name)
|
|
77
|
+
association_proxy.load_target if association_proxy.respond_to?(:load_target)
|
|
78
|
+
else
|
|
79
|
+
parent.public_send(reflection.name)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def current_depth
|
|
84
|
+
depth = 0
|
|
85
|
+
current_form = form
|
|
86
|
+
|
|
87
|
+
while current_form.respond_to?(:object) && current_form.respond_to?(:object_name) && current_form.object_name.to_s.include?("[")
|
|
88
|
+
depth += 1
|
|
89
|
+
parent = current_form.instance_variable_get(:@parent_builder)
|
|
90
|
+
break unless parent
|
|
91
|
+
|
|
92
|
+
current_form = parent
|
|
93
|
+
break if depth >= MAX_DEPTH_SAFETY
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
depth
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def max_depth_exceeded?
|
|
100
|
+
current_depth >= SuperAdmin.max_nested_depth
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def max_depth_message
|
|
104
|
+
view_context.content_tag(
|
|
105
|
+
:div,
|
|
106
|
+
view_context.content_tag(:p, view_context.t("super_admin.resources.nested.max_depth_exceeded", max: SuperAdmin.max_nested_depth), class: "text-sm text-amber-600"),
|
|
107
|
+
class: "mb-4 bg-amber-50 border border-amber-200 rounded-md p-3"
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def missing_association_warning
|
|
112
|
+
view_context.content_tag(
|
|
113
|
+
:p,
|
|
114
|
+
view_context.t("super_admin.resources.nested.missing_association", name: association_name.humanize),
|
|
115
|
+
class: "text-sm text-red-600"
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SuperAdmin
|
|
4
|
+
module FormFields
|
|
5
|
+
class NumberField < BaseField
|
|
6
|
+
def render
|
|
7
|
+
form.number_field(attribute_name, options)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def type
|
|
11
|
+
:number
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def options
|
|
15
|
+
base_options.merge(step: step_value)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def step_value
|
|
21
|
+
column = builder.column
|
|
22
|
+
return "0.01" if column&.type == :decimal && column.scale && column.scale.positive?
|
|
23
|
+
return "0.01" if column&.type == :float
|
|
24
|
+
|
|
25
|
+
"1"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SuperAdmin
|
|
4
|
+
module FormFields
|
|
5
|
+
class TextAreaField < BaseField
|
|
6
|
+
def render
|
|
7
|
+
form.text_area(attribute_name, options)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def type
|
|
11
|
+
:text_area
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def options
|
|
15
|
+
base_options.merge(rows: 4)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SuperAdmin
|
|
4
|
+
# Service responsible for inspecting ActiveRecord models.
|
|
5
|
+
# Extracts information about attributes, types, and associations.
|
|
6
|
+
class ModelInspector
|
|
7
|
+
# System models to exclude from SuperAdmin interface
|
|
8
|
+
EXCLUDED_MODELS = %w[
|
|
9
|
+
ApplicationRecord
|
|
10
|
+
ActiveRecord::Base
|
|
11
|
+
ActiveRecord::SchemaMigration
|
|
12
|
+
ActiveRecord::InternalMetadata
|
|
13
|
+
ActionText::Record
|
|
14
|
+
ActionText::RichText
|
|
15
|
+
ActionText::EncryptedRichText
|
|
16
|
+
ActionMailbox::Record
|
|
17
|
+
ActionMailbox::InboundEmail
|
|
18
|
+
ActiveStorage::Record
|
|
19
|
+
ActiveStorage::Blob
|
|
20
|
+
ActiveStorage::Attachment
|
|
21
|
+
ActiveStorage::VariantRecord
|
|
22
|
+
SolidQueue::Job
|
|
23
|
+
SolidQueue::Process
|
|
24
|
+
SolidQueue::RecurringTask
|
|
25
|
+
SolidQueue::ScheduledExecution
|
|
26
|
+
SolidQueue::ReadyExecution
|
|
27
|
+
SolidQueue::ClaimedExecution
|
|
28
|
+
SolidQueue::FailedExecution
|
|
29
|
+
SolidQueue::BlockedExecution
|
|
30
|
+
SolidQueue::Semaphore
|
|
31
|
+
SolidQueue::Pause
|
|
32
|
+
SolidCable::Message
|
|
33
|
+
SolidCache::Entry
|
|
34
|
+
].freeze
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
# Returns list of all administrable models
|
|
38
|
+
# @return [Array<Class>] List of model classes
|
|
39
|
+
def all_models
|
|
40
|
+
Rails.application.eager_load! unless Rails.application.config.eager_load
|
|
41
|
+
|
|
42
|
+
SuperAdmin::DashboardRegistry.instance.resource_classes
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Returns the list of models that could be managed by SuperAdmin, regardless of dashboard presence.
|
|
46
|
+
# @return [Array<Class>]
|
|
47
|
+
def discoverable_models
|
|
48
|
+
Rails.application.eager_load! unless Rails.application.config.eager_load
|
|
49
|
+
|
|
50
|
+
ActiveRecord::Base.descendants
|
|
51
|
+
.reject { |model| excluded_model?(model) }
|
|
52
|
+
.sort_by(&:name)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Returns detailed information about a model
|
|
56
|
+
# @param model_class [Class] The model class
|
|
57
|
+
# @return [Hash] Model information
|
|
58
|
+
def inspect_model(model_class)
|
|
59
|
+
{
|
|
60
|
+
name: model_class.name,
|
|
61
|
+
table_name: model_class.table_name,
|
|
62
|
+
human_name: model_class.model_name.human,
|
|
63
|
+
attributes: inspect_attributes(model_class),
|
|
64
|
+
associations: inspect_associations(model_class),
|
|
65
|
+
validations: inspect_validations(model_class)
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Returns model attributes with their metadata
|
|
70
|
+
# @param model_class [Class] The model class
|
|
71
|
+
# @return [Hash] Attributes with type, null, default, etc.
|
|
72
|
+
def inspect_attributes(model_class)
|
|
73
|
+
model_class.columns.each_with_object({}) do |column, hash|
|
|
74
|
+
hash[column.name] = {
|
|
75
|
+
type: column.type,
|
|
76
|
+
sql_type: column.sql_type,
|
|
77
|
+
null: column.null,
|
|
78
|
+
default: column.default,
|
|
79
|
+
limit: column.limit,
|
|
80
|
+
precision: column.precision,
|
|
81
|
+
scale: column.scale
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Returns model associations
|
|
87
|
+
# @param model_class [Class] The model class
|
|
88
|
+
# @return [Hash] Associations grouped by type
|
|
89
|
+
def inspect_associations(model_class)
|
|
90
|
+
model_class.reflect_on_all_associations.each_with_object({}) do |assoc, hash|
|
|
91
|
+
hash[assoc.name] = {
|
|
92
|
+
type: assoc.macro,
|
|
93
|
+
class_name: assoc.class_name,
|
|
94
|
+
foreign_key: assoc.foreign_key,
|
|
95
|
+
primary_key: assoc.association_primary_key,
|
|
96
|
+
polymorphic: assoc.polymorphic?,
|
|
97
|
+
through: assoc.through_reflection&.name
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Returns model validations
|
|
103
|
+
# @param model_class [Class] The model class
|
|
104
|
+
# @return [Hash] Validations by attribute
|
|
105
|
+
def inspect_validations(model_class)
|
|
106
|
+
model_class.validators.each_with_object(Hash.new { |h, k| h[k] = [] }) do |validator, hash|
|
|
107
|
+
validator.attributes.each do |attribute|
|
|
108
|
+
hash[attribute] << {
|
|
109
|
+
kind: validator.kind,
|
|
110
|
+
options: validator.options.except(:class)
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Checks if an attribute is an enum
|
|
117
|
+
# @param model_class [Class] The model class
|
|
118
|
+
# @param attribute_name [String, Symbol] The attribute name
|
|
119
|
+
# @return [Boolean]
|
|
120
|
+
def enum?(model_class, attribute_name)
|
|
121
|
+
model_class.defined_enums.key?(attribute_name.to_s)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Returns possible values for an enum
|
|
125
|
+
# @param model_class [Class] The model class
|
|
126
|
+
# @param attribute_name [String, Symbol] The attribute name
|
|
127
|
+
# @return [Hash, nil] Hash of enum values or nil
|
|
128
|
+
def enum_values(model_class, attribute_name)
|
|
129
|
+
model_class.defined_enums[attribute_name.to_s]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Finds a model class by its name
|
|
133
|
+
# @param model_name [String] The model name (plural or singular)
|
|
134
|
+
# @return [Class, nil] The model class or nil
|
|
135
|
+
def find_model(model_name)
|
|
136
|
+
name = model_name.to_s
|
|
137
|
+
|
|
138
|
+
candidates = build_candidate_names(name)
|
|
139
|
+
|
|
140
|
+
candidates.each do |candidate|
|
|
141
|
+
klass = candidate.safe_constantize
|
|
142
|
+
return klass if valid_model_class?(klass)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Fallback to registered dashboard models
|
|
146
|
+
normalized_name = name.singularize.camelize
|
|
147
|
+
all_models.find { |m| m.name == normalized_name }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def build_candidate_names(name)
|
|
153
|
+
variations = [ name, name.singularize, name.pluralize ].uniq
|
|
154
|
+
|
|
155
|
+
variations.flat_map do |variation|
|
|
156
|
+
[ variation, variation.camelize, variation.singularize.camelize ]
|
|
157
|
+
end.uniq
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def valid_model_class?(klass)
|
|
161
|
+
klass.is_a?(Class) && klass < ActiveRecord::Base && !klass.abstract_class?
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Checks if a model should be excluded
|
|
165
|
+
# @param model [Class] The model class
|
|
166
|
+
# @return [Boolean]
|
|
167
|
+
def excluded_model?(model)
|
|
168
|
+
return true if model.abstract_class?
|
|
169
|
+
return true if EXCLUDED_MODELS.include?(model.name)
|
|
170
|
+
return true if model.name.start_with?("ActiveRecord::", "ActionText::", "ActionMailbox::", "ActiveStorage::", "SolidQueue::", "SolidCable::", "SolidCache::")
|
|
171
|
+
return true unless model.table_exists?
|
|
172
|
+
|
|
173
|
+
false
|
|
174
|
+
rescue StandardError => error
|
|
175
|
+
Rails.logger.warn(
|
|
176
|
+
"[SuperAdmin::ModelInspector] Failed to evaluate exclusion for #{model}: #{error.class} - #{error.message}"
|
|
177
|
+
)
|
|
178
|
+
true
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SuperAdmin
|
|
4
|
+
module Queries
|
|
5
|
+
# Abstract class for SuperAdmin query objects.
|
|
6
|
+
# Provides common interface and shared utilities.
|
|
7
|
+
class BaseQuery
|
|
8
|
+
attr_reader :scope, :model_class
|
|
9
|
+
|
|
10
|
+
# @param scope [ActiveRecord::Relation]
|
|
11
|
+
# @param model_class [Class]
|
|
12
|
+
def initialize(scope, model_class)
|
|
13
|
+
@scope = scope
|
|
14
|
+
@model_class = model_class
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Abstract method to implement in subclasses
|
|
18
|
+
# @return [ActiveRecord::Relation]
|
|
19
|
+
def call
|
|
20
|
+
raise NotImplementedError, "#{self.class}#call must be implemented"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# Returns the model's Arel table
|
|
26
|
+
# @return [Arel::Table]
|
|
27
|
+
def arel_table
|
|
28
|
+
@arel_table ||= model_class.arel_table
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Returns the model's columns
|
|
32
|
+
# @return [Array<ActiveRecord::ConnectionAdapters::Column>]
|
|
33
|
+
def columns
|
|
34
|
+
@columns ||= model_class.columns
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns columns of specified type
|
|
38
|
+
# @param types [Array<Symbol>] Column types (e.g., [:string, :text])
|
|
39
|
+
# @return [Array<ActiveRecord::ConnectionAdapters::Column>]
|
|
40
|
+
def columns_of_type(*types)
|
|
41
|
+
columns.select { |column| types.include?(column.type) }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|