easy-admin-rails 0.1.14 → 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.
Files changed (93) 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/components/easy_admin/versions/diff_modal_component.rb +5 -1
  13. data/app/controllers/easy_admin/application_controller.rb +131 -1
  14. data/app/controllers/easy_admin/batch_actions_controller.rb +27 -0
  15. data/app/controllers/easy_admin/concerns/belongs_to_editing.rb +201 -0
  16. data/app/controllers/easy_admin/concerns/inline_field_editing.rb +297 -0
  17. data/app/controllers/easy_admin/concerns/resource_authorization.rb +55 -0
  18. data/app/controllers/easy_admin/concerns/resource_filtering.rb +178 -0
  19. data/app/controllers/easy_admin/concerns/resource_loading.rb +149 -0
  20. data/app/controllers/easy_admin/concerns/resource_pagination.rb +135 -0
  21. data/app/controllers/easy_admin/dashboard_controller.rb +2 -1
  22. data/app/controllers/easy_admin/dashboards_controller.rb +6 -40
  23. data/app/controllers/easy_admin/resources_controller.rb +13 -762
  24. data/app/controllers/easy_admin/row_actions_controller.rb +25 -0
  25. data/app/helpers/easy_admin/fields_helper.rb +61 -9
  26. data/app/javascript/easy_admin/controllers/event_emitter_controller.js +2 -4
  27. data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +0 -10
  28. data/app/javascript/easy_admin/controllers/jsoneditor_controller.js +1 -4
  29. data/app/javascript/easy_admin/controllers/permission_toggle_controller.js +227 -0
  30. data/app/javascript/easy_admin/controllers/role_preview_controller.js +93 -0
  31. data/app/javascript/easy_admin/controllers/select_field_controller.js +1 -2
  32. data/app/javascript/easy_admin/controllers/settings_button_controller.js +1 -2
  33. data/app/javascript/easy_admin/controllers/settings_sidebar_controller.js +1 -4
  34. data/app/javascript/easy_admin/controllers/turbo_stream_redirect.js +0 -2
  35. data/app/javascript/easy_admin/controllers.js +5 -1
  36. data/app/models/easy_admin/admin_user.rb +6 -0
  37. data/app/policies/admin_user_policy.rb +36 -0
  38. data/app/policies/application_policy.rb +83 -0
  39. data/app/views/easy_admin/application/authorization_failure.turbo_stream.erb +8 -0
  40. data/app/views/easy_admin/dashboards/card.html.erb +5 -0
  41. data/app/views/easy_admin/dashboards/card.turbo_stream.erb +7 -0
  42. data/app/views/easy_admin/dashboards/card_error.html.erb +3 -0
  43. data/app/views/easy_admin/dashboards/card_error.turbo_stream.erb +5 -0
  44. data/app/views/easy_admin/dashboards/show.turbo_stream.erb +7 -0
  45. data/app/views/easy_admin/resources/belongs_to_edit_attached.html.erb +6 -0
  46. data/app/views/easy_admin/resources/belongs_to_edit_attached.turbo_stream.erb +8 -0
  47. data/app/views/easy_admin/resources/belongs_to_reattach.html.erb +5 -0
  48. data/app/views/easy_admin/resources/edit.html.erb +1 -1
  49. data/app/views/easy_admin/resources/edit_field.html.erb +5 -0
  50. data/app/views/easy_admin/resources/edit_field.turbo_stream.erb +7 -0
  51. data/app/views/easy_admin/resources/index.html.erb +1 -1
  52. data/app/views/easy_admin/resources/index_frame.html.erb +8 -142
  53. data/app/views/easy_admin/resources/update_belongs_to_attached.turbo_stream.erb +25 -0
  54. data/app/views/layouts/easy_admin/application.html.erb +15 -2
  55. data/config/initializers/easy_admin_permissions.rb +73 -0
  56. data/db/seeds/easy_admin_permissions.rb +121 -0
  57. data/lib/easy-admin-rails.rb +2 -0
  58. data/lib/easy_admin/permissions/component.rb +168 -0
  59. data/lib/easy_admin/permissions/configuration.rb +37 -0
  60. data/lib/easy_admin/permissions/controller.rb +164 -0
  61. data/lib/easy_admin/permissions/dsl.rb +180 -0
  62. data/lib/easy_admin/permissions/models.rb +44 -0
  63. data/lib/easy_admin/permissions/permission_denied_component.rb +121 -0
  64. data/lib/easy_admin/permissions/resource_permissions.rb +231 -0
  65. data/lib/easy_admin/permissions/role_definition.rb +45 -0
  66. data/lib/easy_admin/permissions/role_denied_component.rb +159 -0
  67. data/lib/easy_admin/permissions/role_dsl.rb +73 -0
  68. data/lib/easy_admin/permissions/user_extensions.rb +129 -0
  69. data/lib/easy_admin/permissions.rb +113 -0
  70. data/lib/easy_admin/resource/base.rb +119 -0
  71. data/lib/easy_admin/resource/configuration.rb +148 -0
  72. data/lib/easy_admin/resource/dsl.rb +117 -0
  73. data/lib/easy_admin/resource/field_registry.rb +189 -0
  74. data/lib/easy_admin/resource/form_builder.rb +123 -0
  75. data/lib/easy_admin/resource/layout_builder.rb +249 -0
  76. data/lib/easy_admin/resource/scope_manager.rb +252 -0
  77. data/lib/easy_admin/resource/show_builder.rb +359 -0
  78. data/lib/easy_admin/resource.rb +8 -835
  79. data/lib/easy_admin/resource_modules.rb +11 -0
  80. data/lib/easy_admin/version.rb +1 -1
  81. data/lib/generators/easy_admin/permissions/install_generator.rb +90 -0
  82. data/lib/generators/easy_admin/permissions/templates/initializers/permissions.rb +37 -0
  83. data/lib/generators/easy_admin/permissions/templates/migrations/create_permission_tables.rb +27 -0
  84. data/lib/generators/easy_admin/permissions/templates/migrations/update_users_for_permissions.rb +6 -0
  85. data/lib/generators/easy_admin/permissions/templates/models/permission.rb +9 -0
  86. data/lib/generators/easy_admin/permissions/templates/models/role.rb +9 -0
  87. data/lib/generators/easy_admin/permissions/templates/models/role_permission.rb +9 -0
  88. data/lib/generators/easy_admin/permissions/templates/models/user_role.rb +9 -0
  89. data/lib/generators/easy_admin/permissions/templates/policies/application_policy.rb +47 -0
  90. data/lib/generators/easy_admin/permissions/templates/policies/user_policy.rb +36 -0
  91. data/lib/generators/easy_admin/permissions/templates/seeds/permissions.rb +89 -0
  92. metadata +62 -5
  93. data/db/migrate/20250101000001_create_easy_admin_admin_users.rb +0 -45
@@ -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
@@ -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