easy-admin-rails 0.2.6 → 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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/easy_admin.base.js +7 -0
  3. data/app/assets/builds/easy_admin.base.js.map +2 -2
  4. data/app/assets/builds/easy_admin.css +207 -35
  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/permissions/user_role_permissions_component.rb +1 -3
  8. data/app/components/easy_admin/show_layout_component.rb +694 -24
  9. data/app/controllers/easy_admin/application_controller.rb +0 -5
  10. data/app/controllers/easy_admin/batch_actions_controller.rb +0 -1
  11. data/app/controllers/easy_admin/concerns/inline_field_editing.rb +4 -11
  12. data/app/controllers/easy_admin/concerns/resource_loading.rb +10 -9
  13. data/app/controllers/easy_admin/concerns/resource_pagination.rb +3 -0
  14. data/app/controllers/easy_admin/dashboards_controller.rb +0 -1
  15. data/app/controllers/easy_admin/resources_controller.rb +1 -5
  16. data/app/controllers/easy_admin/row_actions_controller.rb +1 -4
  17. data/app/helpers/easy_admin/fields_helper.rb +8 -22
  18. data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +12 -0
  19. data/app/views/easy_admin/resources/edit.html.erb +2 -2
  20. data/app/views/easy_admin/resources/new.html.erb +2 -2
  21. data/app/views/easy_admin/resources/show.html.erb +3 -1
  22. data/lib/easy_admin/field.rb +3 -2
  23. data/lib/easy_admin/layouts/builders/base_layout_builder.rb +245 -0
  24. data/lib/easy_admin/layouts/builders/form_layout_builder.rb +208 -0
  25. data/lib/easy_admin/layouts/builders/index_layout_builder.rb +22 -0
  26. data/lib/easy_admin/layouts/builders/show_layout_builder.rb +199 -0
  27. data/lib/easy_admin/layouts/dsl.rb +200 -0
  28. data/lib/easy_admin/layouts/layout_context.rb +189 -0
  29. data/lib/easy_admin/layouts/nodes/base_node.rb +88 -0
  30. data/lib/easy_admin/layouts/nodes/divider.rb +27 -0
  31. data/lib/easy_admin/layouts/nodes/field_node.rb +57 -0
  32. data/lib/easy_admin/layouts/nodes/grid.rb +60 -0
  33. data/lib/easy_admin/layouts/nodes/render_node.rb +41 -0
  34. data/lib/easy_admin/layouts/nodes/root.rb +25 -0
  35. data/lib/easy_admin/layouts/nodes/section.rb +46 -0
  36. data/lib/easy_admin/layouts/nodes/spacer.rb +17 -0
  37. data/lib/easy_admin/layouts/nodes/stubs.rb +109 -0
  38. data/lib/easy_admin/layouts/nodes/tab.rb +40 -0
  39. data/lib/easy_admin/layouts/nodes/tabs.rb +40 -0
  40. data/lib/easy_admin/layouts.rb +28 -0
  41. data/lib/easy_admin/permissions/resource_permissions.rb +1 -5
  42. data/lib/easy_admin/resource/base.rb +2 -2
  43. data/lib/easy_admin/resource/dsl.rb +2 -11
  44. data/lib/easy_admin/resource/field_registry.rb +58 -2
  45. data/lib/easy_admin/resource.rb +0 -9
  46. data/lib/easy_admin/resource_modules.rb +21 -4
  47. data/lib/easy_admin/version.rb +1 -1
  48. data/lib/generators/easy_admin/permissions/install_generator.rb +0 -10
  49. data/lib/generators/easy_admin/permissions/templates/migrations/create_permission_tables.rb +33 -3
  50. metadata +21 -9
  51. data/lib/easy_admin/resource/form_builder.rb +0 -123
  52. data/lib/easy_admin/resource/layout_builder.rb +0 -249
  53. data/lib/easy_admin/resource/show_builder.rb +0 -359
  54. data/lib/generators/easy_admin/permissions/templates/migrations/update_users_for_permissions.rb +0 -6
  55. data/lib/generators/easy_admin/rbac/rbac_generator.rb +0 -244
  56. data/lib/generators/easy_admin/rbac/templates/add_rbac_to_admin_users.rb +0 -23
  57. data/lib/generators/easy_admin/rbac/templates/super_admin.rb +0 -34
@@ -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
@@ -0,0 +1,46 @@
1
+ module EasyAdmin
2
+ module Layouts
3
+ module Nodes
4
+ # Section node for grouping content
5
+ class Section < BaseNode
6
+ def initialize(title = nil, attributes = {})
7
+ super(attributes)
8
+ @attributes[:title] = title
9
+ @attributes[:description] ||= nil
10
+ @attributes[:collapsible] ||= false
11
+ @attributes[:collapsed] ||= false
12
+ @attributes[:icon] ||= nil
13
+ @attributes[:css_class] ||= nil
14
+ end
15
+
16
+ def title
17
+ @attributes[:title]
18
+ end
19
+
20
+ def description
21
+ @attributes[:description]
22
+ end
23
+
24
+ def collapsible?
25
+ @attributes[:collapsible]
26
+ end
27
+
28
+ def collapsed?
29
+ @attributes[:collapsed]
30
+ end
31
+
32
+ def icon
33
+ @attributes[:icon]
34
+ end
35
+
36
+ def css_class
37
+ @attributes[:css_class]
38
+ end
39
+
40
+ def section_id
41
+ "section-#{(title || 'untitled').parameterize}"
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,17 @@
1
+ module EasyAdmin
2
+ module Layouts
3
+ module Nodes
4
+ # Spacer node for adding vertical space
5
+ class Spacer < BaseNode
6
+ def initialize(attributes = {})
7
+ super
8
+ @attributes[:size] ||= 4 # Tailwind spacing size
9
+ end
10
+
11
+ def size
12
+ @attributes[:size]
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,109 @@
1
+ # Stub node classes - to be fully implemented
2
+ module EasyAdmin
3
+ module Layouts
4
+ module Nodes
5
+ # Show page nodes
6
+ class DescriptionList < BaseNode; end
7
+ class Card < BaseNode
8
+ def initialize(title: nil, **attributes)
9
+ super(attributes)
10
+ @attributes[:title] = title
11
+ end
12
+ end
13
+ class MetricCard < BaseNode
14
+ def initialize(title: nil, label: nil, value: nil, **attributes)
15
+ super(attributes)
16
+ @attributes[:title] = title || label
17
+ @attributes[:value] = value
18
+ end
19
+ end
20
+ class ActionBar < BaseNode; end
21
+ class Panel < BaseNode
22
+ def initialize(title, **attributes)
23
+ super(attributes)
24
+ @attributes[:title] = title
25
+ end
26
+ end
27
+ class Badge < BaseNode
28
+ def initialize(text, **attributes)
29
+ super(attributes)
30
+ @attributes[:text] = text
31
+ end
32
+ end
33
+ class RelatedResources < BaseNode
34
+ def initialize(resource_name, **attributes)
35
+ super(attributes)
36
+ @attributes[:resource_name] = resource_name
37
+ end
38
+ end
39
+ class Action < BaseNode
40
+ def initialize(type, **attributes)
41
+ super(attributes)
42
+ @attributes[:type] = type
43
+ end
44
+ end
45
+ class Dropdown < BaseNode; end
46
+
47
+ # Form page nodes
48
+ class Fieldset < BaseNode
49
+ def initialize(legend = nil, **attributes)
50
+ super(attributes)
51
+ @attributes[:legend] = legend
52
+ end
53
+ end
54
+ class FormActions < BaseNode; end
55
+ class InlineFields < BaseNode; end
56
+ class ConditionalFields < BaseNode; end
57
+ class NestedFields < BaseNode
58
+ def initialize(association, **attributes)
59
+ super(attributes)
60
+ @attributes[:association] = association
61
+ end
62
+ end
63
+ class HelpText < BaseNode
64
+ def initialize(text, **attributes)
65
+ super(attributes)
66
+ @attributes[:text] = text
67
+ end
68
+ end
69
+ class ErrorsSummary < BaseNode; end
70
+ class FormAction < BaseNode
71
+ def initialize(type, **attributes)
72
+ super(attributes)
73
+ @attributes[:type] = type
74
+ end
75
+ end
76
+
77
+ # Additional nodes needed by DSL
78
+ class Content < BaseNode
79
+ def initialize(attributes = {}, block: nil)
80
+ @block = block
81
+ super(attributes)
82
+ end
83
+
84
+ attr_reader :block
85
+ end
86
+
87
+ class Row < BaseNode
88
+ def initialize(attributes = {})
89
+ super(attributes)
90
+ end
91
+ end
92
+
93
+ class Column < BaseNode
94
+ def initialize(attributes = {})
95
+ super(attributes)
96
+ end
97
+ end
98
+
99
+ class Heading < BaseNode
100
+ def initialize(text, attributes = {})
101
+ @text = text
102
+ super(attributes)
103
+ end
104
+
105
+ attr_reader :text
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,40 @@
1
+ module EasyAdmin
2
+ module Layouts
3
+ module Nodes
4
+ # Individual tab within a tabs container
5
+ class Tab < BaseNode
6
+ def initialize(name, attributes = {})
7
+ super(attributes)
8
+ @attributes[:name] = name.to_s
9
+ @attributes[:label] ||= name.to_s.humanize
10
+ @attributes[:icon] ||= nil
11
+ @attributes[:badge] ||= nil
12
+ end
13
+
14
+ def name
15
+ @attributes[:name]
16
+ end
17
+
18
+ def label
19
+ @attributes[:label]
20
+ end
21
+
22
+ def icon
23
+ @attributes[:icon]
24
+ end
25
+
26
+ def badge
27
+ @attributes[:badge]
28
+ end
29
+
30
+ def tab_id
31
+ "tab-#{name.parameterize}"
32
+ end
33
+
34
+ def panel_id
35
+ "panel-#{name.parameterize}"
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end