easy-admin-rails 0.2.5 → 0.2.7

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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/easy_admin.base.js +95 -0
  3. data/app/assets/builds/easy_admin.base.js.map +3 -3
  4. data/app/assets/builds/easy_admin.css +226 -0
  5. data/app/components/easy_admin/fields/form/belongs_to_component.rb +0 -1
  6. data/app/components/easy_admin/form_layout_component.rb +553 -0
  7. data/app/components/easy_admin/navbar_component.rb +19 -4
  8. data/app/components/easy_admin/permissions/user_role_permissions_component.rb +1 -3
  9. data/app/components/easy_admin/profile/change_password_modal_component.rb +75 -0
  10. data/app/components/easy_admin/profile/profile_tab_component.rb +92 -0
  11. data/app/components/easy_admin/profile/security_tab_component.rb +53 -0
  12. data/app/components/easy_admin/profile/settings_component.rb +103 -0
  13. data/app/components/easy_admin/show_layout_component.rb +694 -24
  14. data/app/components/easy_admin/two_factor/backup_codes_component.rb +118 -0
  15. data/app/components/easy_admin/two_factor/setup_component.rb +124 -0
  16. data/app/components/easy_admin/two_factor/status_component.rb +92 -0
  17. data/app/controllers/concerns/easy_admin/two_factor.rb +110 -0
  18. data/app/controllers/easy_admin/application_controller.rb +10 -5
  19. data/app/controllers/easy_admin/batch_actions_controller.rb +0 -1
  20. data/app/controllers/easy_admin/concerns/inline_field_editing.rb +4 -11
  21. data/app/controllers/easy_admin/concerns/resource_loading.rb +10 -9
  22. data/app/controllers/easy_admin/concerns/resource_pagination.rb +3 -0
  23. data/app/controllers/easy_admin/dashboards_controller.rb +0 -1
  24. data/app/controllers/easy_admin/profile_controller.rb +25 -0
  25. data/app/controllers/easy_admin/resources_controller.rb +1 -5
  26. data/app/controllers/easy_admin/row_actions_controller.rb +1 -4
  27. data/app/controllers/easy_admin/sessions_controller.rb +107 -1
  28. data/app/helpers/easy_admin/fields_helper.rb +8 -22
  29. data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +12 -0
  30. data/app/javascript/easy_admin/controllers/vertical_tabs_controller.js +112 -0
  31. data/app/javascript/easy_admin/controllers.js +3 -1
  32. data/app/models/easy_admin/admin_user.rb +3 -0
  33. data/app/views/easy_admin/profile/backup_codes_regenerated.turbo_stream.erb +12 -0
  34. data/app/views/easy_admin/profile/change_password.html.erb +24 -0
  35. data/app/views/easy_admin/profile/index.html.erb +1 -0
  36. data/app/views/easy_admin/profile/password_error.turbo_stream.erb +6 -0
  37. data/app/views/easy_admin/profile/password_invalid_current.turbo_stream.erb +6 -0
  38. data/app/views/easy_admin/profile/password_updated.turbo_stream.erb +9 -0
  39. data/app/views/easy_admin/profile/two_factor_backup_codes.html.erb +24 -0
  40. data/app/views/easy_admin/profile/two_factor_enabled.turbo_stream.erb +12 -0
  41. data/app/views/easy_admin/profile/two_factor_invalid_code.turbo_stream.erb +6 -0
  42. data/app/views/easy_admin/profile/two_factor_not_enabled.turbo_stream.erb +6 -0
  43. data/app/views/easy_admin/profile/two_factor_setup.html.erb +24 -0
  44. data/app/views/easy_admin/profile/two_factor_unavailable.turbo_stream.erb +6 -0
  45. data/app/views/easy_admin/resources/edit.html.erb +2 -2
  46. data/app/views/easy_admin/resources/new.html.erb +2 -2
  47. data/app/views/easy_admin/resources/show.html.erb +3 -1
  48. data/app/views/easy_admin/sessions/two_factor_verification.html.erb +48 -0
  49. data/app/views/easy_admin/sessions/verify_2fa_error.turbo_stream.erb +13 -0
  50. data/config/routes.rb +20 -1
  51. data/lib/easy-admin-rails.rb +1 -0
  52. data/lib/easy_admin/field.rb +3 -2
  53. data/lib/easy_admin/layouts/builders/base_layout_builder.rb +245 -0
  54. data/lib/easy_admin/layouts/builders/form_layout_builder.rb +208 -0
  55. data/lib/easy_admin/layouts/builders/index_layout_builder.rb +22 -0
  56. data/lib/easy_admin/layouts/builders/show_layout_builder.rb +199 -0
  57. data/lib/easy_admin/layouts/dsl.rb +200 -0
  58. data/lib/easy_admin/layouts/layout_context.rb +189 -0
  59. data/lib/easy_admin/layouts/nodes/base_node.rb +88 -0
  60. data/lib/easy_admin/layouts/nodes/divider.rb +27 -0
  61. data/lib/easy_admin/layouts/nodes/field_node.rb +57 -0
  62. data/lib/easy_admin/layouts/nodes/grid.rb +60 -0
  63. data/lib/easy_admin/layouts/nodes/render_node.rb +41 -0
  64. data/lib/easy_admin/layouts/nodes/root.rb +25 -0
  65. data/lib/easy_admin/layouts/nodes/section.rb +46 -0
  66. data/lib/easy_admin/layouts/nodes/spacer.rb +17 -0
  67. data/lib/easy_admin/layouts/nodes/stubs.rb +109 -0
  68. data/lib/easy_admin/layouts/nodes/tab.rb +40 -0
  69. data/lib/easy_admin/layouts/nodes/tabs.rb +40 -0
  70. data/lib/easy_admin/layouts.rb +28 -0
  71. data/lib/easy_admin/permissions/resource_permissions.rb +1 -5
  72. data/lib/easy_admin/resource/base.rb +2 -2
  73. data/lib/easy_admin/resource/dsl.rb +2 -11
  74. data/lib/easy_admin/resource/field_registry.rb +58 -2
  75. data/lib/easy_admin/resource.rb +0 -9
  76. data/lib/easy_admin/resource_modules.rb +21 -4
  77. data/lib/easy_admin/two_factor_authentication.rb +156 -0
  78. data/lib/easy_admin/version.rb +1 -1
  79. data/lib/generators/easy_admin/permissions/install_generator.rb +0 -10
  80. data/lib/generators/easy_admin/permissions/templates/migrations/create_permission_tables.rb +33 -3
  81. data/lib/generators/easy_admin/two_factor/templates/README +29 -0
  82. data/lib/generators/easy_admin/two_factor/templates/migration.rb +10 -0
  83. data/lib/generators/easy_admin/two_factor/two_factor_generator.rb +22 -0
  84. metadata +49 -9
  85. data/lib/easy_admin/resource/form_builder.rb +0 -123
  86. data/lib/easy_admin/resource/layout_builder.rb +0 -249
  87. data/lib/easy_admin/resource/show_builder.rb +0 -359
  88. data/lib/generators/easy_admin/permissions/templates/migrations/update_users_for_permissions.rb +0 -6
  89. data/lib/generators/easy_admin/rbac/rbac_generator.rb +0 -244
  90. data/lib/generators/easy_admin/rbac/templates/add_rbac_to_admin_users.rb +0 -23
  91. data/lib/generators/easy_admin/rbac/templates/super_admin.rb +0 -34
@@ -0,0 +1,200 @@
1
+ module EasyAdmin
2
+ module Layouts
3
+ # DSL module to be included in Resource classes
4
+ module DSL
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ class_attribute :show_layout_definition, default: nil
9
+ class_attribute :form_layout_definition, default: nil
10
+ class_attribute :index_layout_definition, default: nil
11
+ end
12
+
13
+ class_methods do
14
+ # Define show page layout
15
+ def show(component_or_block = nil, &block)
16
+ if component_or_block.is_a?(Class)
17
+ # Direct component mode
18
+ self.show_layout_definition = [:component, component_or_block]
19
+ elsif block_given?
20
+ # DSL mode - build AST
21
+ builder = Builders::ShowLayoutBuilder.new(resource_class: self)
22
+ builder.instance_exec(&block)
23
+ self.show_layout_definition = [:ast, builder.build]
24
+ else
25
+ # Return current definition
26
+ self.show_layout_definition
27
+ end
28
+ end
29
+
30
+ # Define form page layout
31
+ def form(component_or_block = nil, &block)
32
+ if component_or_block.is_a?(Class)
33
+ # Direct component mode
34
+ self.form_layout_definition = [:component, component_or_block]
35
+ elsif block_given?
36
+ # DSL mode - build AST
37
+ builder = Builders::FormLayoutBuilder.new(resource_class: self)
38
+ builder.instance_exec(&block)
39
+ self.form_layout_definition = [:ast, builder.build]
40
+ else
41
+ # Return current definition
42
+ self.form_layout_definition
43
+ end
44
+ end
45
+
46
+ # Define index page layout
47
+ def index(component_or_block = nil, &block)
48
+ if component_or_block.is_a?(Class)
49
+ # Direct component mode
50
+ self.index_layout_definition = [:component, component_or_block]
51
+ elsif block_given?
52
+ # DSL mode - build AST
53
+ builder = Builders::IndexLayoutBuilder.new(resource_class: self)
54
+ builder.instance_exec(&block)
55
+ self.index_layout_definition = [:ast, builder.build]
56
+ else
57
+ # Return current definition
58
+ self.index_layout_definition
59
+ end
60
+ end
61
+
62
+ # Check if custom show layout is defined
63
+ def has_custom_show_layout?
64
+ !show_layout_definition.nil?
65
+ end
66
+
67
+ # Check if custom form layout is defined
68
+ def has_custom_form_layout?
69
+ !form_layout_definition.nil?
70
+ end
71
+
72
+ # Check if custom index layout is defined
73
+ def has_custom_index_layout?
74
+ !index_layout_definition.nil?
75
+ end
76
+
77
+ # Check if form layout contains tabs
78
+ def has_form_tabs?
79
+ return false unless has_custom_form_layout?
80
+
81
+ # Build the form layout to check if it contains tabs
82
+ begin
83
+ builder = Builders::FormLayoutBuilder.new(resource_class: self)
84
+ builder.instance_exec(&form_layout_content)
85
+ root_node = builder.build
86
+
87
+ # Check if the AST contains any tabs nodes
88
+ contains_tabs_node?(root_node)
89
+ rescue => e
90
+ false
91
+ end
92
+ end
93
+
94
+ # Get show layout type
95
+ def show_layout_type
96
+ return nil unless show_layout_definition
97
+ show_layout_definition[0]
98
+ end
99
+
100
+ # Get form layout type
101
+ def form_layout_type
102
+ return nil unless form_layout_definition
103
+ form_layout_definition[0]
104
+ end
105
+
106
+ # Get show layout content
107
+ def show_layout_content
108
+ return nil unless show_layout_definition
109
+ show_layout_definition[1]
110
+ end
111
+
112
+ # Get form layout content
113
+ def form_layout_content
114
+ return nil unless form_layout_definition
115
+ form_layout_definition[1]
116
+ end
117
+
118
+ # Get index layout type
119
+ def index_layout_type
120
+ return nil unless index_layout_definition
121
+ index_layout_definition[0]
122
+ end
123
+
124
+ # Get index layout content
125
+ def index_layout_content
126
+ return nil unless index_layout_definition
127
+ index_layout_definition[1]
128
+ end
129
+
130
+ # Reset layouts
131
+ def reset_layouts!
132
+ self.show_layout_definition = nil
133
+ self.form_layout_definition = nil
134
+ self.index_layout_definition = nil
135
+ end
136
+
137
+ # Generate default show layout from fields
138
+ def generate_default_show_layout
139
+ builder = Builders::ShowLayoutBuilder.new(resource_class: self)
140
+
141
+ # Group fields by category or just show all
142
+ if fields_config.any?
143
+ builder.card(title: resource_title) do
144
+ builder.grid(columns: 2) do
145
+ fields_config.each do |field_config|
146
+ next if field_config[:show] == false
147
+ builder.field(field_config[:name], **field_config.except(:name))
148
+ end
149
+ end
150
+ end
151
+ end
152
+
153
+ [:ast, builder.build]
154
+ end
155
+
156
+ # Generate default form layout from fields
157
+ def generate_default_form_layout
158
+ builder = Builders::FormLayoutBuilder.new(resource_class: self)
159
+
160
+ if fields_config.any?
161
+ # Group editable fields
162
+ editable_fields = fields_config.reject { |f| f[:readonly] || f[:form] == false }
163
+
164
+ if editable_fields.any?
165
+ builder.section("Details") do
166
+ editable_fields.each do |field_config|
167
+ builder.field(field_config[:name], **field_config.except(:name))
168
+ end
169
+ end
170
+ end
171
+
172
+ builder.form_actions do
173
+ builder.submit
174
+ builder.cancel
175
+ end
176
+ end
177
+
178
+ [:ast, builder.build]
179
+ end
180
+
181
+ private
182
+
183
+ # Recursively check if AST contains tabs nodes
184
+ def contains_tabs_node?(node)
185
+ return false unless node
186
+
187
+ # Check if current node is a tabs node
188
+ return true if node.is_a?(EasyAdmin::Layouts::Nodes::Tabs)
189
+
190
+ # Check children recursively
191
+ if node.respond_to?(:children) && node.children
192
+ node.children.any? { |child| contains_tabs_node?(child) }
193
+ else
194
+ false
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,189 @@
1
+ module EasyAdmin
2
+ module Layouts
3
+ # Context object for safe data passing through layout rendering
4
+ class LayoutContext
5
+ attr_reader :record, :resource_class, :current_user, :view_context, :request_context
6
+ attr_accessor :form_builder
7
+
8
+ # Alias for form_builder to match expected API
9
+ def form
10
+ @form_builder
11
+ end
12
+
13
+ def initialize(record: nil, resource_class: nil, current_user: nil, view_context: nil, form_builder: nil, request_context: {})
14
+ @record = record
15
+ @resource_class = resource_class
16
+ @current_user = current_user
17
+ @view_context = view_context
18
+ @form_builder = form_builder
19
+ @request_context = request_context
20
+ @data = {} # Custom data storage
21
+ end
22
+
23
+ # Store custom data
24
+ def set(key, value)
25
+ @data[key.to_sym] = value
26
+ end
27
+
28
+ # Retrieve custom data
29
+ def get(key)
30
+ @data[key.to_sym]
31
+ end
32
+
33
+ # Check if custom data exists
34
+ def has?(key)
35
+ @data.key?(key.to_sym)
36
+ end
37
+
38
+ # Merge additional context
39
+ def merge(additional_context)
40
+ additional_context.each do |key, value|
41
+ set(key, value)
42
+ end
43
+ self
44
+ end
45
+
46
+ # Create a child context with additional data
47
+ def with(additional_context = {})
48
+ child = self.class.new(
49
+ record: @record,
50
+ resource_class: @resource_class,
51
+ current_user: @current_user,
52
+ view_context: @view_context,
53
+ form_builder: @form_builder,
54
+ request_context: @request_context
55
+ )
56
+
57
+ # Copy parent data
58
+ @data.each { |k, v| child.set(k, v) }
59
+
60
+ # Add new data
61
+ additional_context.each { |k, v| child.set(k, v) }
62
+
63
+ child
64
+ end
65
+
66
+ # Safe evaluation context for conditions
67
+ def instance_exec(&block)
68
+ CleanRoom.new(self).instance_exec(&block)
69
+ end
70
+
71
+ # Check if we're in a form context
72
+ def form_context?
73
+ !@form_builder.nil?
74
+ end
75
+
76
+ # Check if we're in a show context
77
+ def show_context?
78
+ @form_builder.nil? && @record.present?
79
+ end
80
+
81
+ # Get current action from request context
82
+ def current_action
83
+ @request_context[:action]
84
+ end
85
+
86
+ # Check if current user can perform action
87
+ def can?(action, subject = nil)
88
+ return false unless @current_user
89
+
90
+ # If ActionPolicy is available, use it
91
+ if defined?(ActionPolicy) && @view_context
92
+ @view_context.allowed_to?(action, subject || @record)
93
+ else
94
+ # Fallback to basic checks
95
+ true
96
+ end
97
+ rescue
98
+ false
99
+ end
100
+
101
+ # Access helpers from view context
102
+ def helpers
103
+ @view_context
104
+ end
105
+
106
+ # Method missing to delegate to view_context helpers
107
+ def method_missing(method, *args, &block)
108
+ if @view_context && @view_context.respond_to?(method)
109
+ @view_context.send(method, *args, &block)
110
+ else
111
+ super
112
+ end
113
+ end
114
+
115
+ def respond_to_missing?(method, include_private = false)
116
+ (@view_context && @view_context.respond_to?(method)) || super
117
+ end
118
+
119
+ # Clean room for safe evaluation
120
+ class CleanRoom < BasicObject
121
+ def initialize(context)
122
+ @context = context
123
+ end
124
+
125
+ def record
126
+ @context.record
127
+ end
128
+
129
+ def current_user
130
+ @context.current_user
131
+ end
132
+
133
+ def resource_class
134
+ @context.resource_class
135
+ end
136
+
137
+ def form_builder
138
+ @context.form_builder
139
+ end
140
+
141
+ def form
142
+ @context.form
143
+ end
144
+
145
+ def can?(action, subject = nil)
146
+ @context.can?(action, subject)
147
+ end
148
+
149
+ def form_context?
150
+ @context.form_context?
151
+ end
152
+
153
+ def show_context?
154
+ @context.show_context?
155
+ end
156
+
157
+ def get(key)
158
+ @context.get(key)
159
+ end
160
+
161
+ def has?(key)
162
+ @context.has?(key)
163
+ end
164
+
165
+ # Allow render method for components
166
+ def render(component)
167
+ component
168
+ end
169
+
170
+ # Allow safe navigation and HTML helpers
171
+ def method_missing(method, *args, &block)
172
+ if [:record, :current_user, :resource_class].include?(method)
173
+ @context.public_send(method)
174
+ elsif @context.view_context && @context.view_context.respond_to?(method)
175
+ # Allow HTML helper methods from view context
176
+ @context.view_context.send(method, *args, &block)
177
+ else
178
+ ::Kernel.raise ::NoMethodError, "undefined method `#{method}' in layout context"
179
+ end
180
+ end
181
+
182
+ def respond_to_missing?(method, include_private = false)
183
+ [:record, :current_user, :resource_class, :form_builder, :form, :can?, :form_context?, :show_context?, :get, :has?, :render].include?(method) ||
184
+ (@context.view_context && @context.view_context.respond_to?(method))
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,88 @@
1
+ module EasyAdmin
2
+ module Layouts
3
+ module Nodes
4
+ # Base class for all layout AST nodes
5
+ class BaseNode
6
+ attr_reader :attributes, :children, :visible_if, :metadata
7
+
8
+ def initialize(attributes = {}, &block)
9
+ @attributes = attributes.dup
10
+ @visible_if = @attributes.delete(:visible_if)
11
+ @metadata = @attributes.delete(:metadata) || {}
12
+ @children = []
13
+ end
14
+
15
+ # Check if node should be visible based on context
16
+ def visible?(context)
17
+ return true unless visible_if
18
+
19
+ case visible_if
20
+ when Proc
21
+ # Evaluate in safe context
22
+ context.instance_exec(&visible_if)
23
+ when Symbol
24
+ # Call method on record
25
+ context.record.public_send(visible_if) if context.record.respond_to?(visible_if)
26
+ when String
27
+ # Evaluate as method chain (e.g., "record.published?")
28
+ eval_method_chain(context, visible_if)
29
+ else
30
+ # Direct boolean value
31
+ !!visible_if
32
+ end
33
+ rescue => e
34
+ true # Default to visible on error
35
+ end
36
+
37
+ # Accept visitor for traversal
38
+ def accept(visitor, context)
39
+ visitor.visit(self, context) if visible?(context)
40
+ end
41
+
42
+ # Add child node
43
+ def add_child(node)
44
+ @children << node if node.is_a?(BaseNode)
45
+ end
46
+
47
+ # Get attribute value
48
+ def [](key)
49
+ @attributes[key]
50
+ end
51
+
52
+ # Set attribute value
53
+ def []=(key, value)
54
+ @attributes[key] = value
55
+ end
56
+
57
+ # Check if node has children
58
+ def children?
59
+ @children.any?
60
+ end
61
+
62
+ # Node type for identification
63
+ def node_type
64
+ self.class.name.demodulize.underscore.to_sym
65
+ end
66
+
67
+ private
68
+
69
+ def eval_method_chain(context, chain)
70
+ parts = chain.split('.')
71
+ object = context
72
+
73
+ parts.each do |part|
74
+ if part.end_with?('?') || part.end_with?('!')
75
+ method = part.to_sym
76
+ else
77
+ method = part.to_sym
78
+ end
79
+
80
+ object = object.public_send(method) if object.respond_to?(method)
81
+ end
82
+
83
+ object
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,27 @@
1
+ module EasyAdmin
2
+ module Layouts
3
+ module Nodes
4
+ # Divider node for visual separation
5
+ class Divider < BaseNode
6
+ def initialize(attributes = {})
7
+ super
8
+ @attributes[:style] ||= :solid # solid, dashed, dotted
9
+ @attributes[:margin] ||= 4 # Tailwind margin size
10
+ @attributes[:color] ||= 'gray-200'
11
+ end
12
+
13
+ def style
14
+ @attributes[:style]
15
+ end
16
+
17
+ def margin
18
+ @attributes[:margin]
19
+ end
20
+
21
+ def color
22
+ @attributes[:color]
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,57 @@
1
+ module EasyAdmin
2
+ module Layouts
3
+ module Nodes
4
+ # Node representing a field to be rendered
5
+ class FieldNode < BaseNode
6
+ def initialize(name, attributes = {})
7
+ super(attributes)
8
+ @field_name = name.to_sym
9
+ @attributes[:as] ||= nil # Field type override
10
+ @attributes[:label] ||= nil # Custom label
11
+ @attributes[:readonly] ||= false
12
+ @attributes[:required] ||= false
13
+ @attributes[:hint] ||= nil
14
+ @attributes[:placeholder] ||= nil
15
+ @attributes[:wrapper_class] ||= nil
16
+ end
17
+
18
+ def field_name
19
+ @field_name
20
+ end
21
+
22
+ def field_type
23
+ @attributes[:as]
24
+ end
25
+
26
+ def label
27
+ @attributes[:label]
28
+ end
29
+
30
+ def readonly?
31
+ @attributes[:readonly]
32
+ end
33
+
34
+ def required?
35
+ @attributes[:required]
36
+ end
37
+
38
+ def hint
39
+ @attributes[:hint]
40
+ end
41
+
42
+ def placeholder
43
+ @attributes[:placeholder]
44
+ end
45
+
46
+ def wrapper_class
47
+ @attributes[:wrapper_class]
48
+ end
49
+
50
+ # Get all field options for rendering
51
+ def field_options
52
+ @attributes.except(:visible_if, :metadata)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,60 @@
1
+ module EasyAdmin
2
+ module Layouts
3
+ module Nodes
4
+ # Grid layout node for responsive column layouts
5
+ class Grid < BaseNode
6
+ def initialize(attributes = {})
7
+ super
8
+ @attributes[:columns] ||= 2
9
+ @attributes[:gap] ||= 4 # Tailwind gap size
10
+ @attributes[:responsive] ||= true
11
+ @attributes[:css_class] ||= nil
12
+ end
13
+
14
+ def columns
15
+ @attributes[:columns]
16
+ end
17
+
18
+ def gap
19
+ @attributes[:gap]
20
+ end
21
+
22
+ def responsive?
23
+ @attributes[:responsive]
24
+ end
25
+
26
+ def css_class
27
+ @attributes[:css_class]
28
+ end
29
+
30
+ # Generate Tailwind grid classes
31
+ def grid_classes
32
+ classes = []
33
+
34
+ if responsive?
35
+ # Responsive grid classes
36
+ case columns
37
+ when 1
38
+ classes << "grid-cols-1"
39
+ when 2
40
+ classes << "grid-cols-1 md:grid-cols-2"
41
+ when 3
42
+ classes << "grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
43
+ when 4
44
+ classes << "grid-cols-1 md:grid-cols-2 lg:grid-cols-4"
45
+ else
46
+ classes << "grid-cols-#{columns}"
47
+ end
48
+ else
49
+ classes << "grid-cols-#{columns}"
50
+ end
51
+
52
+ classes << "gap-#{gap}"
53
+ classes << css_class if css_class
54
+
55
+ classes.join(' ')
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,41 @@
1
+ module EasyAdmin
2
+ module Layouts
3
+ module Nodes
4
+ # Node for rendering custom components
5
+ class RenderNode < BaseNode
6
+ def initialize(component_class, props = {})
7
+ attributes = props.dup
8
+ super(attributes)
9
+ @component_class = component_class
10
+ @props = attributes
11
+ end
12
+
13
+ def component_class
14
+ @component_class
15
+ end
16
+
17
+ def props
18
+ @props
19
+ end
20
+
21
+ # Instantiate the component with props and context
22
+ def build_component(context)
23
+ # Merge context into props if component accepts it
24
+ component_props = @props.dup
25
+
26
+ # Add standard context props if not already present
27
+ component_props[:record] ||= context.record if context.respond_to?(:record)
28
+ component_props[:resource_class] ||= context.resource_class if context.respond_to?(:resource_class)
29
+ component_props[:current_user] ||= context.current_user if context.respond_to?(:current_user)
30
+
31
+ # Handle form builder for form contexts
32
+ if context.respond_to?(:form_builder) && context.form_builder
33
+ component_props[:form] ||= context.form_builder
34
+ end
35
+
36
+ @component_class.new(**component_props)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,25 @@
1
+ module EasyAdmin
2
+ module Layouts
3
+ module Nodes
4
+ # Root node of the layout AST
5
+ class Root < BaseNode
6
+ def initialize(attributes = {})
7
+ super
8
+ @attributes[:layout_type] = attributes[:layout_type] || :show
9
+ end
10
+
11
+ def layout_type
12
+ @attributes[:layout_type]
13
+ end
14
+
15
+ def form_layout?
16
+ layout_type == :form
17
+ end
18
+
19
+ def show_layout?
20
+ layout_type == :show
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end