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.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/easy_admin.base.js +254 -18
  3. data/app/assets/builds/easy_admin.base.js.map +4 -4
  4. data/app/assets/builds/easy_admin.css +112 -18
  5. data/app/components/easy_admin/base_component.rb +1 -0
  6. data/app/components/easy_admin/form_tabs_component.rb +5 -2
  7. data/app/components/easy_admin/navbar_component.rb +5 -1
  8. data/app/components/easy_admin/permissions/user_role_assignment_component.rb +254 -0
  9. data/app/components/easy_admin/permissions/user_role_permissions_component.rb +186 -0
  10. data/app/components/easy_admin/resources/index_component.rb +1 -4
  11. data/app/components/easy_admin/sidebar_component.rb +67 -2
  12. data/app/controllers/easy_admin/application_controller.rb +131 -1
  13. data/app/controllers/easy_admin/batch_actions_controller.rb +27 -0
  14. data/app/controllers/easy_admin/concerns/belongs_to_editing.rb +201 -0
  15. data/app/controllers/easy_admin/concerns/inline_field_editing.rb +297 -0
  16. data/app/controllers/easy_admin/concerns/resource_authorization.rb +55 -0
  17. data/app/controllers/easy_admin/concerns/resource_filtering.rb +178 -0
  18. data/app/controllers/easy_admin/concerns/resource_loading.rb +149 -0
  19. data/app/controllers/easy_admin/concerns/resource_pagination.rb +135 -0
  20. data/app/controllers/easy_admin/dashboard_controller.rb +2 -1
  21. data/app/controllers/easy_admin/dashboards_controller.rb +6 -40
  22. data/app/controllers/easy_admin/resources_controller.rb +13 -762
  23. data/app/controllers/easy_admin/row_actions_controller.rb +25 -0
  24. data/app/helpers/easy_admin/fields_helper.rb +61 -9
  25. data/app/javascript/easy_admin/controllers/event_emitter_controller.js +2 -4
  26. data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +0 -10
  27. data/app/javascript/easy_admin/controllers/jsoneditor_controller.js +1 -4
  28. data/app/javascript/easy_admin/controllers/permission_toggle_controller.js +227 -0
  29. data/app/javascript/easy_admin/controllers/role_preview_controller.js +93 -0
  30. data/app/javascript/easy_admin/controllers/select_field_controller.js +1 -2
  31. data/app/javascript/easy_admin/controllers/settings_button_controller.js +1 -2
  32. data/app/javascript/easy_admin/controllers/settings_sidebar_controller.js +1 -4
  33. data/app/javascript/easy_admin/controllers/turbo_stream_redirect.js +0 -2
  34. data/app/javascript/easy_admin/controllers.js +5 -1
  35. data/app/models/easy_admin/admin_user.rb +6 -0
  36. data/app/policies/admin_user_policy.rb +36 -0
  37. data/app/policies/application_policy.rb +83 -0
  38. data/app/views/easy_admin/application/authorization_failure.turbo_stream.erb +8 -0
  39. data/app/views/easy_admin/dashboards/card.html.erb +5 -0
  40. data/app/views/easy_admin/dashboards/card.turbo_stream.erb +7 -0
  41. data/app/views/easy_admin/dashboards/card_error.html.erb +3 -0
  42. data/app/views/easy_admin/dashboards/card_error.turbo_stream.erb +5 -0
  43. data/app/views/easy_admin/dashboards/show.turbo_stream.erb +7 -0
  44. data/app/views/easy_admin/resources/belongs_to_edit_attached.html.erb +6 -0
  45. data/app/views/easy_admin/resources/belongs_to_edit_attached.turbo_stream.erb +8 -0
  46. data/app/views/easy_admin/resources/belongs_to_reattach.html.erb +5 -0
  47. data/app/views/easy_admin/resources/edit.html.erb +1 -1
  48. data/app/views/easy_admin/resources/edit_field.html.erb +5 -0
  49. data/app/views/easy_admin/resources/edit_field.turbo_stream.erb +7 -0
  50. data/app/views/easy_admin/resources/index.html.erb +1 -1
  51. data/app/views/easy_admin/resources/index_frame.html.erb +8 -142
  52. data/app/views/easy_admin/resources/update_belongs_to_attached.turbo_stream.erb +25 -0
  53. data/app/views/layouts/easy_admin/application.html.erb +15 -2
  54. data/config/initializers/easy_admin_permissions.rb +73 -0
  55. data/db/seeds/easy_admin_permissions.rb +121 -0
  56. data/lib/easy-admin-rails.rb +2 -0
  57. data/lib/easy_admin/permissions/component.rb +168 -0
  58. data/lib/easy_admin/permissions/configuration.rb +37 -0
  59. data/lib/easy_admin/permissions/controller.rb +164 -0
  60. data/lib/easy_admin/permissions/dsl.rb +160 -0
  61. data/lib/easy_admin/permissions/models.rb +44 -0
  62. data/lib/easy_admin/permissions/permission_denied_component.rb +121 -0
  63. data/lib/easy_admin/permissions/resource_permissions.rb +231 -0
  64. data/lib/easy_admin/permissions/role_definition.rb +45 -0
  65. data/lib/easy_admin/permissions/role_denied_component.rb +159 -0
  66. data/lib/easy_admin/permissions/role_dsl.rb +73 -0
  67. data/lib/easy_admin/permissions/user_extensions.rb +129 -0
  68. data/lib/easy_admin/permissions.rb +113 -0
  69. data/lib/easy_admin/resource/base.rb +119 -0
  70. data/lib/easy_admin/resource/configuration.rb +148 -0
  71. data/lib/easy_admin/resource/dsl.rb +117 -0
  72. data/lib/easy_admin/resource/field_registry.rb +189 -0
  73. data/lib/easy_admin/resource/form_builder.rb +123 -0
  74. data/lib/easy_admin/resource/layout_builder.rb +249 -0
  75. data/lib/easy_admin/resource/scope_manager.rb +252 -0
  76. data/lib/easy_admin/resource/show_builder.rb +359 -0
  77. data/lib/easy_admin/resource.rb +8 -835
  78. data/lib/easy_admin/resource_modules.rb +11 -0
  79. data/lib/easy_admin/version.rb +1 -1
  80. data/lib/generators/easy_admin/permissions/install_generator.rb +90 -0
  81. data/lib/generators/easy_admin/permissions/templates/initializers/permissions.rb +37 -0
  82. data/lib/generators/easy_admin/permissions/templates/migrations/create_permission_tables.rb +27 -0
  83. data/lib/generators/easy_admin/permissions/templates/migrations/update_users_for_permissions.rb +6 -0
  84. data/lib/generators/easy_admin/permissions/templates/models/permission.rb +9 -0
  85. data/lib/generators/easy_admin/permissions/templates/models/role.rb +9 -0
  86. data/lib/generators/easy_admin/permissions/templates/models/role_permission.rb +9 -0
  87. data/lib/generators/easy_admin/permissions/templates/models/user_role.rb +9 -0
  88. data/lib/generators/easy_admin/permissions/templates/policies/application_policy.rb +47 -0
  89. data/lib/generators/easy_admin/permissions/templates/policies/user_policy.rb +36 -0
  90. data/lib/generators/easy_admin/permissions/templates/seeds/permissions.rb +89 -0
  91. metadata +62 -5
  92. data/db/migrate/20250101000001_create_easy_admin_admin_users.rb +0 -45
@@ -0,0 +1,113 @@
1
+ # EasyAdmin Permission Engine
2
+ # A lightweight permission management system that works with Action Policy
3
+
4
+ module EasyAdmin
5
+ module Permissions
6
+ extend ActiveSupport::Autoload
7
+
8
+ autoload :Engine
9
+ autoload :Models
10
+ autoload :DSL
11
+ autoload :Controller
12
+ autoload :Component
13
+ autoload :Cache
14
+ autoload :Configuration
15
+ autoload :UserExtensions
16
+ autoload :ResourcePermissions
17
+ autoload :RoleDefinition
18
+ autoload :RoleDSL
19
+
20
+ class << self
21
+ attr_writer :configuration, :role_definitions
22
+
23
+ def configuration
24
+ @configuration ||= Configuration.new
25
+ end
26
+
27
+ def configure
28
+ yield(configuration)
29
+ end
30
+
31
+ # Check if permissions are enabled
32
+ def enabled?
33
+ configuration.enabled
34
+ end
35
+
36
+ # Restore the original define method for the DSL
37
+ def define(&block)
38
+ DSL.evaluate(&block).tap do |data|
39
+ DSL.seed_database(data)
40
+ end
41
+ end
42
+
43
+ # DSL for defining roles
44
+ def define_roles(&block)
45
+ @role_definitions = RoleDSL.new
46
+ @role_definitions.instance_eval(&block)
47
+ @role_definitions
48
+ end
49
+
50
+ # Get defined roles
51
+ def role_definitions
52
+ @role_definitions ||= RoleDSL.new
53
+ end
54
+
55
+ # Get all available roles
56
+ def available_roles
57
+ role_definitions.all_roles
58
+ end
59
+
60
+ # Get role by slug
61
+ def get_role(slug)
62
+ role_definitions.get_role(slug)
63
+ end
64
+
65
+ # Core permission check - used by Action Policy policies
66
+ def authorized?(user, permission_name, context: nil)
67
+ return true unless enabled?
68
+ return true if user.id == 1
69
+
70
+ user&.has_permission?(permission_name, context: context) || false
71
+ end
72
+
73
+ # Check if user has role
74
+ def has_role?(user, role_name, context: nil)
75
+ return false unless user
76
+ user.has_role?(role_name, context: context)
77
+ end
78
+
79
+ # Get user permissions based on role
80
+ def user_permissions_for_role(role_slug)
81
+ role = get_role(role_slug)
82
+ return {} unless role
83
+ role.permissions
84
+ end
85
+
86
+ # Get available EasyAdmin resources
87
+ def available_resources
88
+ ResourcePermissions.available_resources
89
+ end
90
+
91
+ # Get actions for a specific resource
92
+ def actions_for_resource(resource_name)
93
+ ResourcePermissions.actions_for_resource(resource_name)
94
+ end
95
+
96
+ # Generate all possible permissions for available resources
97
+ def generate_all_permissions
98
+ permissions = {}
99
+ available_resources.each do |resource|
100
+ actions_for_resource(resource).each do |action|
101
+ permission_key = "#{resource}:#{action}"
102
+ permissions[permission_key] = {
103
+ resource: resource,
104
+ action: action,
105
+ name: permission_key
106
+ }
107
+ end
108
+ end
109
+ permissions
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,119 @@
1
+ module EasyAdmin
2
+ module ResourceModules
3
+ # Base module that provides the core resource functionality
4
+ # This module serves as the orchestrator for all resource modules
5
+ module Base
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ include EasyAdmin::Versioning
10
+ include EasyAdmin::ResourceModules::Configuration
11
+ include EasyAdmin::ResourceModules::DSL
12
+ include EasyAdmin::ResourceModules::FieldRegistry
13
+ include EasyAdmin::ResourceModules::LayoutBuilder
14
+ include EasyAdmin::ResourceModules::ScopeManager
15
+
16
+ # Class attributes for core functionality
17
+ class_attribute :resource_name, :custom_title
18
+
19
+ # Initialize configurations when resource is inherited
20
+ def self.inherited(subclass)
21
+ super
22
+ subclass.setup_resource_defaults
23
+ EasyAdmin::ResourceRegistry.register(subclass)
24
+ end
25
+
26
+ # Set up default configurations for new resource classes
27
+ def self.setup_resource_defaults
28
+ self.resource_name = name.demodulize.gsub(/Resource$/, '')
29
+ initialize_configurations
30
+ initialize_field_registry
31
+ initialize_layout_builder
32
+ initialize_scope_manager
33
+ end
34
+ end
35
+
36
+ class_methods do
37
+ # Get the associated model class
38
+ def model_class
39
+ model_class_name.constantize
40
+ rescue NameError
41
+ raise "Model class #{model_class_name} not found. Make sure the model exists."
42
+ end
43
+
44
+ # Get resource title/name
45
+ def title
46
+ return custom_title.pluralize if custom_title.present?
47
+ resource_name.underscore.humanize.pluralize
48
+ end
49
+
50
+ def singular_title
51
+ return custom_title.singularize if custom_title.present?
52
+ resource_name.underscore.humanize
53
+ end
54
+
55
+ # Route helpers
56
+ def route_key
57
+ resource_name.underscore.pluralize
58
+ end
59
+
60
+ def param_key
61
+ resource_name.underscore
62
+ end
63
+
64
+ # DSL for setting custom title
65
+ def resource_title(title_name)
66
+ self.custom_title = title_name.to_s
67
+ end
68
+
69
+ # Query methods - delegated to field registry
70
+ def all_records(sort_field: nil, sort_direction: 'asc')
71
+ records = model_class.all
72
+
73
+ if sort_field.present? && sortable_fields.any? { |f| f[:name].to_s == sort_field.to_s }
74
+ records = records.order("#{sort_field} #{sort_direction}")
75
+ end
76
+
77
+ records
78
+ end
79
+
80
+ def find_record(id)
81
+ model_class.find(id)
82
+ end
83
+
84
+ def search_records(query, sort_field: nil, sort_direction: 'asc', records: nil)
85
+ records ||= model_class.all
86
+ searchable_fields = fields_config.select { |field| field[:searchable] }
87
+
88
+ if searchable_fields.any? && query.present?
89
+ conditions = searchable_fields.map do |field|
90
+ "#{field[:name]} ILIKE ?"
91
+ end.join(' OR ')
92
+
93
+ values = Array.new(searchable_fields.length, "%#{query}%")
94
+ records = records.where(conditions, *values)
95
+ end
96
+
97
+ if sort_field.present? && sortable_fields.any? { |f| f[:name].to_s == sort_field.to_s }
98
+ records = records.order("#{sort_field} #{sort_direction}")
99
+ end
100
+
101
+ records
102
+ end
103
+
104
+ # Versioning DSL - Simple API for enabling version viewing
105
+ def versioning(enabled = true, **options)
106
+ if enabled
107
+ enable_versioning(options)
108
+ else
109
+ self.versioning_enabled = false
110
+ end
111
+ end
112
+
113
+ def has_versioning?
114
+ model_has_versions?
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,148 @@
1
+ module EasyAdmin
2
+ module ResourceModules
3
+ # Configuration module for handling resource settings
4
+ # Manages pagination, includes, batch actions, and row actions
5
+ module Configuration
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ # Core configuration class attributes
10
+ class_attribute :model_class_name, :pagination_config, :includes_config,
11
+ :batch_actions_config, :batch_actions_enabled, :row_actions_config
12
+
13
+ # Initialize default configurations
14
+ def self.initialize_configurations
15
+ self.model_class_name = resource_name
16
+ self.pagination_config = {
17
+ type: :standard,
18
+ per_page: 20,
19
+ max_per_page: 100
20
+ }
21
+ self.includes_config = {
22
+ index: [],
23
+ show: [],
24
+ form: []
25
+ }
26
+ self.batch_actions_config = []
27
+ self.batch_actions_enabled = false
28
+ self.row_actions_config = []
29
+ end
30
+ end
31
+
32
+ class_methods do
33
+ # Pagination configuration
34
+ def pagination(type: :standard, per_page: 20, **options)
35
+ self.pagination_config = {
36
+ type: type,
37
+ per_page: per_page,
38
+ max_per_page: options[:max_per_page] || 100,
39
+ infinite_scroll: options[:infinite_scroll] || false,
40
+ countless: options[:countless] || false
41
+ }
42
+ end
43
+
44
+ # Configure eager loading for associations
45
+ def includes(associations, actions: [:index, :show, :form])
46
+ actions = Array(actions)
47
+
48
+ actions.each do |action|
49
+ if self.includes_config.key?(action)
50
+ self.includes_config = self.includes_config.merge(
51
+ action => Array(associations)
52
+ )
53
+ end
54
+ end
55
+ end
56
+
57
+ # Convenience methods for pagination configuration
58
+ def per_page(count)
59
+ pagination(per_page: count)
60
+ end
61
+
62
+ def infinite_scroll(**options)
63
+ pagination(type: :infinite_scroll, infinite_scroll: true, **options)
64
+ end
65
+
66
+ def countless_pagination(**options)
67
+ pagination(countless: true, **options)
68
+ end
69
+
70
+ # Pagination helper methods
71
+ def items_per_page
72
+ pagination_config[:per_page]
73
+ end
74
+
75
+ def max_items_per_page
76
+ pagination_config[:max_per_page]
77
+ end
78
+
79
+ def pagination_type
80
+ pagination_config[:type]
81
+ end
82
+
83
+ def infinite_scroll_enabled?
84
+ pagination_config[:infinite_scroll]
85
+ end
86
+
87
+ def countless_enabled?
88
+ pagination_config[:countless]
89
+ end
90
+
91
+ # Includes configuration helpers
92
+ def index_includes
93
+ includes_config[:index] || []
94
+ end
95
+
96
+ def show_includes
97
+ includes_config[:show] || []
98
+ end
99
+
100
+ def form_includes
101
+ includes_config[:form] || []
102
+ end
103
+
104
+ # Batch Actions configuration
105
+ def add_batch_action(action_class, options = {})
106
+ self.batch_actions_config = (self.batch_actions_config || []) + [{
107
+ class: action_class,
108
+ condition: options[:if]
109
+ }]
110
+ end
111
+
112
+ def batch_actions
113
+ batch_actions_config || []
114
+ end
115
+
116
+ def has_batch_actions?
117
+ batch_actions_enabled && batch_actions_config.any?
118
+ end
119
+
120
+ # Row Actions configuration
121
+ def add_row_action(action_class, options = {})
122
+ self.row_actions_config = (self.row_actions_config || []) + [{
123
+ class: action_class,
124
+ condition: options[:if]
125
+ }]
126
+ end
127
+
128
+ def row_actions
129
+ row_actions_config || []
130
+ end
131
+
132
+ def has_row_actions?
133
+ row_actions_config.present?
134
+ end
135
+
136
+ # Available actions based on context conditions
137
+ def available_row_actions(context = {})
138
+ return [] unless has_row_actions?
139
+
140
+ row_actions_config.select do |config|
141
+ condition = config[:condition]
142
+ condition.nil? || (condition.respond_to?(:call) ? context.instance_eval(&condition) : condition)
143
+ end.map { |config| config[:class] }
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,117 @@
1
+ module EasyAdmin
2
+ module ResourceModules
3
+ # DSL module for defining fields and convenience methods
4
+ # Handles all field definition methods and form building
5
+ module DSL
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ # DSL for setting custom model name
10
+ def model(model_name)
11
+ self.model_class_name = model_name.to_s
12
+ end
13
+
14
+ # Core field definition method
15
+ def field(name, type = :string, **options)
16
+ register_field(name, type, **options)
17
+ end
18
+
19
+ # Convenience methods for common field types
20
+ def id_field(**options)
21
+ field(:id, :number, readonly: true, **options)
22
+ end
23
+
24
+ def text_field(name, **options)
25
+ field(name, :string, **options)
26
+ end
27
+
28
+ def textarea_field(name, **options)
29
+ field(name, :text, **options)
30
+ end
31
+
32
+ def number_field(name, **options)
33
+ field(name, :number, **options)
34
+ end
35
+
36
+ def email_field(name, **options)
37
+ field(name, :email, **options)
38
+ end
39
+
40
+ def date_field(name, **options)
41
+ field(name, :date, **options)
42
+ end
43
+
44
+ def datetime_field(name, **options)
45
+ field(name, :datetime, **options)
46
+ end
47
+
48
+ def boolean_field(name, **options)
49
+ field(name, :boolean, **options)
50
+ end
51
+
52
+ def select_field(name, options:, **other_options)
53
+ field(name, :select, options: options, **other_options)
54
+ end
55
+
56
+ def belongs_to_field(name, **options)
57
+ field(name, :belongs_to, **options)
58
+ end
59
+
60
+ def has_many_field(name, **options)
61
+ field(name, :has_many, **options)
62
+ end
63
+
64
+ def file_field(name, **options)
65
+ field(name, :file, **options)
66
+ end
67
+
68
+ def json_field(name, **options)
69
+ field(name, :json, **options)
70
+ end
71
+
72
+ # Password field with confirmation - new convenience method
73
+ def password_field(name = :password, **options)
74
+ field(name, :password, **options)
75
+ field("#{name}_confirmation".to_sym, :password,
76
+ label: options[:confirmation_label] || "Confirm #{name.to_s.humanize}",
77
+ **options.except(:confirmation_label))
78
+ end
79
+
80
+ # Form building DSL
81
+ def form(&block)
82
+ form_builder = EasyAdmin::ResourceModules::FormBuilder.new(self)
83
+ form_builder.instance_eval(&block)
84
+ end
85
+
86
+ # Show layout building DSL
87
+ def show(&block)
88
+ show_builder = EasyAdmin::ResourceModules::ShowBuilder.new(self)
89
+ show_builder.instance_eval(&block)
90
+ end
91
+
92
+ # Batch Actions DSL
93
+ def enable_batch_actions
94
+ self.batch_actions_enabled = true
95
+ end
96
+
97
+ def batch_action(action_class, options = {})
98
+ add_batch_action(action_class, options)
99
+ end
100
+
101
+ def available_batch_actions(context = {})
102
+ return [] unless batch_actions_enabled
103
+
104
+ batch_actions_config.select do |config|
105
+ condition = config[:condition]
106
+ condition.nil? || (condition.respond_to?(:call) ? context.instance_eval(&condition) : condition)
107
+ end.map { |config| config[:class] }
108
+ end
109
+
110
+ # Row Actions DSL (single record actions)
111
+ def row_action(action_class, options = {})
112
+ add_row_action(action_class, options)
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,189 @@
1
+ module EasyAdmin
2
+ module ResourceModules
3
+ # FieldRegistry module for centralized field management
4
+ # Handles field registration, categorization, and retrieval
5
+ module FieldRegistry
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ class_attribute :fields_config
10
+
11
+ def self.initialize_field_registry
12
+ self.fields_config = []
13
+ end
14
+ end
15
+
16
+ class_methods do
17
+ # Core field registration method
18
+ def register_field(name, type = :string, **options)
19
+ field_config = build_field_config(name, type, **options)
20
+ add_field_to_registry(field_config)
21
+ end
22
+
23
+ # Build field configuration hash
24
+ def build_field_config(name, type, **options)
25
+ {
26
+ name: name,
27
+ type: type,
28
+ label: options[:label] || name.to_s.humanize,
29
+ sortable: options.fetch(:sortable, true),
30
+ searchable: options.fetch(:searchable, false),
31
+ filterable: options.fetch(:filterable, false),
32
+ required: options.fetch(:required, false),
33
+ readonly: options.fetch(:readonly, false),
34
+ format: options[:format],
35
+ options: options[:options], # for select fields
36
+ multiple: options[:multiple], # for select fields
37
+ placeholder: options[:placeholder], # for select fields
38
+ suggest: options[:suggest], # for dynamic option loading
39
+ display_method: options[:display_method], # for association fields
40
+ help_text: options[:help_text],
41
+ editable: options[:editable],
42
+ association: options[:association],
43
+ foreign_key: options[:foreign_key] # for belongs_to fields
44
+ }
45
+ end
46
+
47
+ # Add field to the registry
48
+ def add_field_to_registry(field_config)
49
+ # Prevent duplicate fields by name
50
+ current_fields = self.fields_config || []
51
+ unless current_fields.any? { |f| f[:name] == field_config[:name] }
52
+ self.fields_config = current_fields + [field_config]
53
+ end
54
+ end
55
+
56
+ # Field categorization methods
57
+ def index_fields
58
+ fields_config.select { |field| ![:text, :has_many, :file].include?(field[:type]) }
59
+ end
60
+
61
+ def sortable_fields
62
+ fields_config.select { |field| field[:sortable] }
63
+ end
64
+
65
+ def filterable_fields
66
+ fields_config.select { |field| field[:filterable] }
67
+ end
68
+
69
+ def searchable_fields
70
+ fields_config.select { |field| field[:searchable] }
71
+ end
72
+
73
+ def show_fields
74
+ fields_config
75
+ end
76
+
77
+ def form_fields
78
+ if has_form_tabs?
79
+ # If tabs are configured, return fields from all tabs
80
+ form_tabs_config.flat_map { |tab| tab[:fields] }.reject { |field| field[:readonly] }
81
+ else
82
+ # Default behavior: return all non-readonly fields
83
+ fields_config.reject { |field| field[:readonly] }
84
+ end
85
+ end
86
+
87
+ # Field lookup methods
88
+ def find_field_config(field_name)
89
+ fields_config.find { |field| field[:name].to_s == field_name.to_s }
90
+ end
91
+
92
+ def field_exists?(field_name)
93
+ find_field_config(field_name).present?
94
+ end
95
+
96
+ def field_type(field_name)
97
+ field_config = find_field_config(field_name)
98
+ field_config&.dig(:type)
99
+ end
100
+
101
+ def field_editable?(field_name)
102
+ field_config = find_field_config(field_name)
103
+ return false unless field_config
104
+ !field_config[:readonly] && field_config.fetch(:editable, true)
105
+ end
106
+
107
+ # Field validation methods
108
+ def required_fields
109
+ fields_config.select { |field| field[:required] }
110
+ end
111
+
112
+ def readonly_fields
113
+ fields_config.select { |field| field[:readonly] }
114
+ end
115
+
116
+ # Field grouping by type
117
+ def fields_by_type(type)
118
+ fields_config.select { |field| field[:type] == type }
119
+ end
120
+
121
+ def association_fields
122
+ fields_config.select { |field| [:belongs_to, :has_many].include?(field[:type]) }
123
+ end
124
+
125
+ def belongs_to_fields
126
+ fields_by_type(:belongs_to)
127
+ end
128
+
129
+ def has_many_fields
130
+ fields_by_type(:has_many)
131
+ end
132
+
133
+ def select_fields
134
+ fields_by_type(:select)
135
+ end
136
+
137
+ def json_fields
138
+ fields_by_type(:json)
139
+ end
140
+
141
+ # Field options helpers
142
+ def field_options(field_name)
143
+ field_config = find_field_config(field_name)
144
+ field_config&.dig(:options) || []
145
+ end
146
+
147
+ def field_suggest_config(field_name)
148
+ field_config = find_field_config(field_name)
149
+ field_config&.dig(:suggest) || {}
150
+ end
151
+
152
+ def field_display_method(field_name)
153
+ field_config = find_field_config(field_name)
154
+ field_config&.dig(:display_method) || :name
155
+ end
156
+
157
+ # Advanced field queries
158
+ def fields_with_suggest
159
+ fields_config.select { |field| field[:suggest].present? }
160
+ end
161
+
162
+ def multiple_select_fields
163
+ fields_config.select { |field| field[:type] == :select && field[:multiple] }
164
+ end
165
+
166
+ def file_fields
167
+ fields_by_type(:file)
168
+ end
169
+
170
+ def password_fields
171
+ fields_by_type(:password)
172
+ end
173
+
174
+ # Field count helpers
175
+ def total_fields_count
176
+ fields_config.count
177
+ end
178
+
179
+ def editable_fields_count
180
+ fields_config.count { |field| field_editable?(field[:name]) }
181
+ end
182
+
183
+ def association_fields_count
184
+ association_fields.count
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end