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.
- checksums.yaml +4 -4
- data/app/assets/builds/easy_admin.base.js +95 -0
- data/app/assets/builds/easy_admin.base.js.map +3 -3
- data/app/assets/builds/easy_admin.css +226 -0
- data/app/components/easy_admin/fields/form/belongs_to_component.rb +0 -1
- data/app/components/easy_admin/form_layout_component.rb +553 -0
- data/app/components/easy_admin/navbar_component.rb +19 -4
- data/app/components/easy_admin/permissions/user_role_permissions_component.rb +1 -3
- data/app/components/easy_admin/profile/change_password_modal_component.rb +75 -0
- data/app/components/easy_admin/profile/profile_tab_component.rb +92 -0
- data/app/components/easy_admin/profile/security_tab_component.rb +53 -0
- data/app/components/easy_admin/profile/settings_component.rb +103 -0
- data/app/components/easy_admin/show_layout_component.rb +694 -24
- data/app/components/easy_admin/two_factor/backup_codes_component.rb +118 -0
- data/app/components/easy_admin/two_factor/setup_component.rb +124 -0
- data/app/components/easy_admin/two_factor/status_component.rb +92 -0
- data/app/controllers/concerns/easy_admin/two_factor.rb +110 -0
- data/app/controllers/easy_admin/application_controller.rb +10 -5
- data/app/controllers/easy_admin/batch_actions_controller.rb +0 -1
- data/app/controllers/easy_admin/concerns/inline_field_editing.rb +4 -11
- data/app/controllers/easy_admin/concerns/resource_loading.rb +10 -9
- data/app/controllers/easy_admin/concerns/resource_pagination.rb +3 -0
- data/app/controllers/easy_admin/dashboards_controller.rb +0 -1
- data/app/controllers/easy_admin/profile_controller.rb +25 -0
- data/app/controllers/easy_admin/resources_controller.rb +1 -5
- data/app/controllers/easy_admin/row_actions_controller.rb +1 -4
- data/app/controllers/easy_admin/sessions_controller.rb +107 -1
- data/app/helpers/easy_admin/fields_helper.rb +8 -22
- data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +12 -0
- data/app/javascript/easy_admin/controllers/vertical_tabs_controller.js +112 -0
- data/app/javascript/easy_admin/controllers.js +3 -1
- data/app/models/easy_admin/admin_user.rb +3 -0
- data/app/views/easy_admin/profile/backup_codes_regenerated.turbo_stream.erb +12 -0
- data/app/views/easy_admin/profile/change_password.html.erb +24 -0
- data/app/views/easy_admin/profile/index.html.erb +1 -0
- data/app/views/easy_admin/profile/password_error.turbo_stream.erb +6 -0
- data/app/views/easy_admin/profile/password_invalid_current.turbo_stream.erb +6 -0
- data/app/views/easy_admin/profile/password_updated.turbo_stream.erb +9 -0
- data/app/views/easy_admin/profile/two_factor_backup_codes.html.erb +24 -0
- data/app/views/easy_admin/profile/two_factor_enabled.turbo_stream.erb +12 -0
- data/app/views/easy_admin/profile/two_factor_invalid_code.turbo_stream.erb +6 -0
- data/app/views/easy_admin/profile/two_factor_not_enabled.turbo_stream.erb +6 -0
- data/app/views/easy_admin/profile/two_factor_setup.html.erb +24 -0
- data/app/views/easy_admin/profile/two_factor_unavailable.turbo_stream.erb +6 -0
- data/app/views/easy_admin/resources/edit.html.erb +2 -2
- data/app/views/easy_admin/resources/new.html.erb +2 -2
- data/app/views/easy_admin/resources/show.html.erb +3 -1
- data/app/views/easy_admin/sessions/two_factor_verification.html.erb +48 -0
- data/app/views/easy_admin/sessions/verify_2fa_error.turbo_stream.erb +13 -0
- data/config/routes.rb +20 -1
- data/lib/easy-admin-rails.rb +1 -0
- data/lib/easy_admin/field.rb +3 -2
- data/lib/easy_admin/layouts/builders/base_layout_builder.rb +245 -0
- data/lib/easy_admin/layouts/builders/form_layout_builder.rb +208 -0
- data/lib/easy_admin/layouts/builders/index_layout_builder.rb +22 -0
- data/lib/easy_admin/layouts/builders/show_layout_builder.rb +199 -0
- data/lib/easy_admin/layouts/dsl.rb +200 -0
- data/lib/easy_admin/layouts/layout_context.rb +189 -0
- data/lib/easy_admin/layouts/nodes/base_node.rb +88 -0
- data/lib/easy_admin/layouts/nodes/divider.rb +27 -0
- data/lib/easy_admin/layouts/nodes/field_node.rb +57 -0
- data/lib/easy_admin/layouts/nodes/grid.rb +60 -0
- data/lib/easy_admin/layouts/nodes/render_node.rb +41 -0
- data/lib/easy_admin/layouts/nodes/root.rb +25 -0
- data/lib/easy_admin/layouts/nodes/section.rb +46 -0
- data/lib/easy_admin/layouts/nodes/spacer.rb +17 -0
- data/lib/easy_admin/layouts/nodes/stubs.rb +109 -0
- data/lib/easy_admin/layouts/nodes/tab.rb +40 -0
- data/lib/easy_admin/layouts/nodes/tabs.rb +40 -0
- data/lib/easy_admin/layouts.rb +28 -0
- data/lib/easy_admin/permissions/resource_permissions.rb +1 -5
- data/lib/easy_admin/resource/base.rb +2 -2
- data/lib/easy_admin/resource/dsl.rb +2 -11
- data/lib/easy_admin/resource/field_registry.rb +58 -2
- data/lib/easy_admin/resource.rb +0 -9
- data/lib/easy_admin/resource_modules.rb +21 -4
- data/lib/easy_admin/two_factor_authentication.rb +156 -0
- data/lib/easy_admin/version.rb +1 -1
- data/lib/generators/easy_admin/permissions/install_generator.rb +0 -10
- data/lib/generators/easy_admin/permissions/templates/migrations/create_permission_tables.rb +33 -3
- data/lib/generators/easy_admin/two_factor/templates/README +29 -0
- data/lib/generators/easy_admin/two_factor/templates/migration.rb +10 -0
- data/lib/generators/easy_admin/two_factor/two_factor_generator.rb +22 -0
- metadata +49 -9
- data/lib/easy_admin/resource/form_builder.rb +0 -123
- data/lib/easy_admin/resource/layout_builder.rb +0 -249
- data/lib/easy_admin/resource/show_builder.rb +0 -359
- data/lib/generators/easy_admin/permissions/templates/migrations/update_users_for_permissions.rb +0 -6
- data/lib/generators/easy_admin/rbac/rbac_generator.rb +0 -244
- data/lib/generators/easy_admin/rbac/templates/add_rbac_to_admin_users.rb +0 -23
- 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
|