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
@@ -103,11 +103,6 @@ module EasyAdmin
103
103
  private
104
104
 
105
105
  def handle_authorization_failure(exception)
106
- Rails.logger.warn "🚫 Authorization failed: #{exception.message}"
107
- Rails.logger.warn "🚫 User: #{current_admin_user&.email} (Role: #{current_admin_user&.role&.name || 'None'})"
108
- Rails.logger.warn "🚫 Action: #{action_name} on #{params[:resource_name]}"
109
-
110
- # Extract meaningful error details
111
106
  action_name = extract_action_from_exception(exception)
112
107
  resource_name = extract_resource_from_exception(exception)
113
108
 
@@ -85,7 +85,6 @@ module EasyAdmin
85
85
  end
86
86
 
87
87
  rescue NameError => e
88
- Rails.logger.error e.backtrace
89
88
  respond_to do |format|
90
89
  format.turbo_stream do
91
90
  render turbo_stream: turbo_stream.update("notifications",
@@ -56,9 +56,7 @@ module EasyAdmin
56
56
  if !field_config
57
57
  field_config = @resource_class.fields_config.find { |f| f[:association].to_s == params[:field] }
58
58
  end
59
-
60
- Rails.logger.debug "Suggest field_config for #{params[:field]}: #{field_config}"
61
-
59
+
62
60
  unless field_config && field_config[:suggest]
63
61
  render json: { error: "Field not found or suggest not configured" }, status: :not_found
64
62
  return
@@ -237,10 +235,7 @@ module EasyAdmin
237
235
  display_method = field_config[:display_method] || :name
238
236
  suggest_config = field_config[:suggest] || {}
239
237
  search_fields = suggest_config[:search_fields] || [display_method]
240
-
241
- Rails.logger.debug "HasMany suggest: association=#{association_name}, display_method=#{display_method}, search_fields=#{search_fields}"
242
-
243
- # Build search query
238
+
244
239
  records = association_class.all
245
240
 
246
241
  if search_term.present?
@@ -261,7 +256,6 @@ module EasyAdmin
261
256
  results = records.limit(limit).map do |record|
262
257
  label = if record.respond_to?(display_method)
263
258
  raw_label = record.public_send(display_method)
264
- Rails.logger.debug "HasMany record #{record.id}: raw_label=#{raw_label.class}:#{raw_label}"
265
259
  raw_label.to_s
266
260
  elsif record.respond_to?(:name)
267
261
  record.name.to_s
@@ -272,8 +266,7 @@ module EasyAdmin
272
266
  end
273
267
  [label, record.id]
274
268
  end
275
-
276
- Rails.logger.debug "HasMany suggest final results: #{results}"
269
+
277
270
  results
278
271
  end
279
272
 
@@ -294,4 +287,4 @@ module EasyAdmin
294
287
  end
295
288
  end
296
289
  end
297
- end
290
+ end
@@ -57,14 +57,12 @@ module EasyAdmin
57
57
  # Check both form fields and all fields (including hidden ones)
58
58
  all_fields = resource_class.form_fields + resource_class.fields_config
59
59
  unique_fields = all_fields.uniq { |f| f[:name] }
60
-
61
- Rails.logger.debug "🔍 [ResourceLoading] All fields for #{resource_class}:"
60
+
62
61
  unique_fields.each_with_index do |field_config, index|
63
- Rails.logger.debug "🔍 [ResourceLoading] Field #{index}: #{field_config[:name]} (type: #{field_config[:type]}, readonly: #{field_config[:readonly]})"
64
62
  next if field_config[:readonly]
65
63
 
66
64
  field_name = field_config[:name]
67
-
65
+
68
66
  # Handle different field types that might need special parameter handling
69
67
  case field_config[:type]
70
68
  when :has_many
@@ -87,8 +85,12 @@ module EasyAdmin
87
85
  permitted_attrs << field_name
88
86
  end
89
87
  when :json
90
- # For JSON fields, permit nested parameters as a hash
91
- permitted_attrs << { field_name => {} }
88
+ # For JSON fields, permit as string (JSONEditor submits as JSON string)
89
+ if field_name == :permissions_cache
90
+ permitted_attrs << { field_name => {} }
91
+ else
92
+ permitted_attrs << field_name
93
+ end
92
94
  else
93
95
  # For regular fields, check if it's the permissions_cache field which needs hash permission
94
96
  if field_name == :permissions_cache
@@ -98,8 +100,7 @@ module EasyAdmin
98
100
  end
99
101
  end
100
102
  end
101
-
102
- Rails.logger.debug "🔍 [ResourceLoading] Final permitted attributes: #{permitted_attrs.inspect}"
103
+
103
104
  permitted_attrs
104
105
  end
105
106
 
@@ -146,4 +147,4 @@ module EasyAdmin
146
147
  end
147
148
  end
148
149
  end
149
- end
150
+ end
@@ -114,6 +114,9 @@ module EasyAdmin
114
114
 
115
115
  # Check if pagination should reset due to filtering
116
116
  def should_reset_pagination?
117
+ # Don't reset pagination for turbo_stream requests (infinite scroll)
118
+ return false if request.format.turbo_stream?
119
+
117
120
  has_active_filters? && !first_page?
118
121
  end
119
122
 
@@ -29,7 +29,6 @@ module EasyAdmin
29
29
  format.json { render json: @card_data }
30
30
  end
31
31
  rescue => e
32
- Rails.logger.error "Dashboard card error: #{e.message}"
33
32
  @error_message = e.message
34
33
 
35
34
  respond_to do |format|
@@ -78,11 +78,7 @@ module EasyAdmin
78
78
  end
79
79
 
80
80
  def update
81
- Rails.logger.debug "Update called with params: #{record_params}"
82
- Rails.logger.debug "Record before update: #{@record.inspect}"
83
-
84
81
  if @record.update(record_params)
85
- Rails.logger.debug "Record after update: #{@record.reload.inspect}"
86
82
  respond_to do |format|
87
83
  format.html {
88
84
  redirect_to easy_admin.resource_path(@resource_class.route_key, @record),
@@ -183,4 +179,4 @@ module EasyAdmin
183
179
  end
184
180
  end
185
181
  end
186
- end
182
+ end
@@ -72,9 +72,6 @@ module EasyAdmin
72
72
  end
73
73
  end
74
74
  rescue => e
75
- Rails.logger.error "Row action execution failed: #{e.message}"
76
- Rails.logger.error e.backtrace.join("\n")
77
-
78
75
  respond_to do |format|
79
76
  format.turbo_stream do
80
77
  render turbo_stream: turbo_stream.update(
@@ -238,4 +235,4 @@ module EasyAdmin
238
235
  end
239
236
  end
240
237
  end
241
- end
238
+ end
@@ -17,43 +17,26 @@ module EasyAdmin
17
17
  return ""
18
18
  when :custom_content
19
19
  if field[:block]
20
- Rails.logger.debug "🔍 [FieldsHelper] Processing custom_content block"
21
- Rails.logger.debug "🔍 [FieldsHelper] Form present: #{form.present?}"
22
- Rails.logger.debug "🔍 [FieldsHelper] Record present: #{record.present?}"
23
-
24
- # Create a context object with access to form, record, and helpers
25
20
  context = OpenStruct.new(form: form, record: record, helpers: self)
26
-
27
- # Call the block directly to get the result (don't use capture which converts to string)
21
+
28
22
  result = field[:block].call(context)
29
-
30
- Rails.logger.debug "🔍 [FieldsHelper] Block result class: #{result.class}"
31
- Rails.logger.debug "🔍 [FieldsHelper] Result responds to call: #{result.respond_to?(:call)}"
32
- Rails.logger.debug "🔍 [FieldsHelper] Result responds to view_template: #{result.respond_to?(:view_template)}"
33
-
34
- # Handle different types of results
35
23
  if result.respond_to?(:call) && result.respond_to?(:view_template)
36
- Rails.logger.debug "🔍 [FieldsHelper] Rendering Phlex component"
37
- # It's a Phlex component - use Rails render helper
38
24
  render(result)
39
25
  elsif result.is_a?(String)
40
- Rails.logger.debug "🔍 [FieldsHelper] Rendering string result"
41
26
  result.html_safe
42
27
  elsif result.respond_to?(:to_s)
43
- Rails.logger.debug "🔍 [FieldsHelper] Converting to string and rendering"
44
28
  result.to_s.html_safe
45
29
  else
46
- Rails.logger.debug "🔍 [FieldsHelper] No valid result, returning empty string"
47
30
  ""
48
31
  end
49
32
  else
50
- Rails.logger.debug "🔍 [FieldsHelper] No block provided for custom_content"
51
33
  ""
52
34
  end
53
35
  else
54
36
  # Regular field rendering
37
+ field_type = field[:type] || field[:field_type] || :text
55
38
  component = EasyAdmin::Field.render(
56
- field[:type],
39
+ field_type,
57
40
  action: action,
58
41
  field: field,
59
42
  value: value,
@@ -61,13 +44,16 @@ module EasyAdmin
61
44
  form: form
62
45
  )
63
46
 
64
- component.call.html_safe
47
+ result = component.call
48
+
49
+ result.html_safe
65
50
  end
66
51
  end
67
52
 
68
53
  def field_component(field, action:, value: nil, record: nil, form: nil)
54
+ field_type = field[:type] || field[:field_type] || :text
69
55
  EasyAdmin::Field.render(
70
- field[:type],
56
+ field_type,
71
57
  action: action,
72
58
  field: field,
73
59
  value: value,
@@ -19,14 +19,26 @@ export default class extends Controller {
19
19
  // Called when the element is replaced by turbo stream
20
20
  urlValueChanged() {
21
21
  if (this.observer && this.sentinel) {
22
+ // Reset loading state when URL changes (e.g., when filters are applied)
23
+ this.isLoading = false
24
+ this.hideLoading()
25
+
22
26
  // Re-setup observer with new URL
23
27
  this.setupIntersectionObserver()
28
+
29
+ // Re-initialize UI state
30
+ this.initializeUI()
24
31
  }
25
32
  }
26
33
 
27
34
  hasMoreValueChanged() {
28
35
  if (!this.hasMoreValue) {
29
36
  this.showEndMessage()
37
+ } else {
38
+ // Hide end message if we have more pages (e.g., after filters change)
39
+ if (this.hasEndTarget) {
40
+ this.endTarget.style.display = 'none'
41
+ }
30
42
  }
31
43
  }
32
44
 
@@ -116,8 +116,8 @@
116
116
  <% end %>
117
117
 
118
118
  <!-- Form Fields -->
119
- <% if @resource_class.has_form_tabs? %>
120
- <%= EasyAdmin::FormTabsComponent.new(resource_class: @resource_class, form: form, record: @record).call.html_safe %>
119
+ <% if @resource_class.has_custom_form_layout? %>
120
+ <%== EasyAdmin::FormLayoutComponent.new(resource_class: @resource_class, form: form, record: @record).call %>
121
121
  <% else %>
122
122
  <!-- Default Single Card Layout -->
123
123
  <div class="bg-white shadow-sm rounded-lg border border-gray-200">
@@ -65,8 +65,8 @@
65
65
  <% end %>
66
66
 
67
67
  <!-- Form Fields -->
68
- <% if @resource_class.has_form_tabs? %>
69
- <%= EasyAdmin::FormTabsComponent.new(resource_class: @resource_class, form: form).call.html_safe %>
68
+ <% if @resource_class.has_custom_form_layout? %>
69
+ <%== EasyAdmin::FormLayoutComponent.new(resource_class: @resource_class, form: form).call %>
70
70
  <% else %>
71
71
  <!-- Default Single Card Layout -->
72
72
  <div class="bg-white shadow-sm rounded-lg border border-gray-200">
@@ -27,5 +27,7 @@
27
27
 
28
28
  <!-- Show Layout Content -->
29
29
  <div class="show-content">
30
- <%== EasyAdmin::ShowLayoutComponent.new(resource_class: @resource_class, record: @record).call %>
30
+ <div class="container-fluid">
31
+ <%== EasyAdmin::ShowLayoutComponent.new(resource_class: @resource_class, record: @record).call %>
32
+ </div>
31
33
  </div>
@@ -92,9 +92,9 @@ module EasyAdmin
92
92
 
93
93
  begin
94
94
  action_component_name.constantize
95
- rescue NameError
95
+ rescue NameError => e
96
96
  # Fallback to base text component for the action
97
- case action
97
+ fallback_class = case action
98
98
  when :index
99
99
  EasyAdmin::Fields::Index::TextComponent
100
100
  when :show
@@ -104,6 +104,7 @@ module EasyAdmin
104
104
  else
105
105
  EasyAdmin::Fields::Index::TextComponent
106
106
  end
107
+ fallback_class
107
108
  end
108
109
  end
109
110
  end
@@ -0,0 +1,245 @@
1
+ module EasyAdmin
2
+ module Layouts
3
+ module Builders
4
+ # Base builder for constructing layout AST
5
+ class BaseLayoutBuilder
6
+ attr_reader :root_node, :resource_class
7
+ attr_accessor :current_container_stack
8
+
9
+ def initialize(layout_type = :show, resource_class: nil)
10
+ @root_node = Nodes::Root.new(layout_type: layout_type)
11
+ @current_container_stack = [@root_node]
12
+ @resource_class = resource_class
13
+ end
14
+
15
+ # Get current container for adding nodes
16
+ def current_container
17
+ @current_container_stack.last
18
+ end
19
+
20
+ # Add node to current container
21
+ def add_node(node)
22
+ current_container.add_child(node)
23
+ node
24
+ end
25
+
26
+ # Execute block with new container
27
+ def with_container(container_node, &block)
28
+ add_node(container_node)
29
+ @current_container_stack.push(container_node)
30
+ instance_exec(&block) if block_given?
31
+ @current_container_stack.pop
32
+ container_node
33
+ end
34
+
35
+ # DSL Methods
36
+
37
+ # Add tabs container
38
+ def tabs(type: :horizontal, **attributes, &block)
39
+ tabs_node = Nodes::Tabs.new(attributes.merge(type: type))
40
+ add_node(tabs_node)
41
+
42
+ if block_given?
43
+ @current_container_stack.push(tabs_node)
44
+ tabs_builder = TabsBuilder.new(self, tabs_node)
45
+ tabs_builder.instance_exec(&block)
46
+ @current_container_stack.pop
47
+ end
48
+
49
+ tabs_node
50
+ end
51
+
52
+ # Add section
53
+ def section(title = nil, **attributes, &block)
54
+ section_node = Nodes::Section.new(title, attributes)
55
+
56
+ if block_given?
57
+ with_container(section_node, &block)
58
+ else
59
+ add_node(section_node)
60
+ end
61
+
62
+ section_node
63
+ end
64
+
65
+ # Add grid layout
66
+ def grid(columns: 2, **attributes, &block)
67
+ grid_node = Nodes::Grid.new(attributes.merge(columns: columns))
68
+
69
+ if block_given?
70
+ with_container(grid_node, &block)
71
+ else
72
+ add_node(grid_node)
73
+ end
74
+
75
+ grid_node
76
+ end
77
+
78
+ # Add field
79
+ def field(name, **options)
80
+ field_node = Nodes::FieldNode.new(name, options)
81
+ add_node(field_node)
82
+ end
83
+
84
+ # Add multiple fields at once
85
+ def fields(*names, **common_options)
86
+ names.each do |name|
87
+ field(name, **common_options)
88
+ end
89
+ end
90
+
91
+ # Render custom component
92
+ def render(component_class, **props)
93
+ render_node = Nodes::RenderNode.new(component_class, props)
94
+ add_node(render_node)
95
+ end
96
+
97
+ # Add divider
98
+ def divider(**attributes)
99
+ add_node(Nodes::Divider.new(attributes))
100
+ end
101
+
102
+ # Add spacer
103
+ def spacer(size: 4, **attributes)
104
+ add_node(Nodes::Spacer.new(attributes.merge(size: size)))
105
+ end
106
+
107
+ # Build and return the AST
108
+ def build
109
+ @root_node
110
+ end
111
+
112
+ # Allow custom DSL extensions
113
+ def method_missing(method, *args, &block)
114
+ # Check if it's a registered component type
115
+ if EasyAdmin::Layouts.registered_components.key?(method)
116
+ component_class = EasyAdmin::Layouts.registered_components[method]
117
+ render(component_class, *args)
118
+ else
119
+ super
120
+ end
121
+ end
122
+
123
+ def respond_to_missing?(method, include_private = false)
124
+ EasyAdmin::Layouts.registered_components.key?(method) ||
125
+ field_method?(method) ||
126
+ super
127
+ end
128
+
129
+ # Field DSL delegation methods - delegate to resource class
130
+
131
+ def id_field(**options)
132
+ delegate_to_resource(:id_field, **options)
133
+ field(:id, type: :number, **options)
134
+ end
135
+
136
+ def text_field(name, **options)
137
+ delegate_to_resource(:text_field, name, **options)
138
+ field(name, type: :string, **options)
139
+ end
140
+
141
+ def textarea_field(name, **options)
142
+ delegate_to_resource(:textarea_field, name, **options)
143
+ field(name, type: :text, **options)
144
+ end
145
+
146
+ def number_field(name, **options)
147
+ delegate_to_resource(:number_field, name, **options)
148
+ field(name, type: :number, **options)
149
+ end
150
+
151
+ def email_field(name, **options)
152
+ delegate_to_resource(:email_field, name, **options)
153
+ field(name, type: :email, **options)
154
+ end
155
+
156
+ def date_field(name, **options)
157
+ delegate_to_resource(:date_field, name, **options)
158
+ field(name, type: :date, **options)
159
+ end
160
+
161
+ def datetime_field(name, **options)
162
+ delegate_to_resource(:datetime_field, name, **options)
163
+ field(name, type: :datetime, **options)
164
+ end
165
+
166
+ def boolean_field(name, **options)
167
+ delegate_to_resource(:boolean_field, name, **options)
168
+ field(name, type: :boolean, **options)
169
+ end
170
+
171
+ def select_field(name, **options)
172
+ delegate_to_resource(:select_field, name, **options)
173
+ field(name, type: :select, **options)
174
+ end
175
+
176
+ def belongs_to_field(name, **options)
177
+ delegate_to_resource(:belongs_to_field, name, **options)
178
+ field(name, type: :belongs_to, **options)
179
+ end
180
+
181
+ def has_many_field(name, **options)
182
+ delegate_to_resource(:has_many_field, name, **options)
183
+ field(name, type: :has_many, **options)
184
+ end
185
+
186
+ def file_field(name, **options)
187
+ delegate_to_resource(:file_field, name, **options)
188
+ field(name, type: :file, **options)
189
+ end
190
+
191
+ def json_field(name, **options)
192
+ delegate_to_resource(:json_field, name, **options)
193
+ field(name, type: :json, **options)
194
+ end
195
+
196
+ def password_field(name = :password, **options)
197
+ delegate_to_resource(:password_field, name, **options)
198
+ field(name, type: :password, **options)
199
+ end
200
+
201
+ # Delegate content method calls
202
+ def content(&block)
203
+ content_node = Nodes::Content.new(block: block)
204
+ add_node(content_node)
205
+ end
206
+
207
+ private
208
+
209
+ def field_method?(method)
210
+ method.to_s.end_with?('_field') && resource_class&.respond_to?(method)
211
+ end
212
+
213
+ def delegate_to_resource(method, *args, **options)
214
+ if resource_class
215
+ resource_class.send(method, *args, **options)
216
+ else
217
+ raise "Resource class not available for field delegation"
218
+ end
219
+ end
220
+ end
221
+
222
+ # Specialized builder for tabs
223
+ class TabsBuilder
224
+ def initialize(parent_builder, tabs_node)
225
+ @parent_builder = parent_builder
226
+ @tabs_node = tabs_node
227
+ end
228
+
229
+ def tab(name, **attributes, &block)
230
+ tab_node = Nodes::Tab.new(name, attributes)
231
+ @tabs_node.add_child(tab_node)
232
+
233
+ if block_given?
234
+ # Push tab as current container and execute block in parent builder context
235
+ @parent_builder.current_container_stack.push(tab_node)
236
+ @parent_builder.instance_exec(&block)
237
+ @parent_builder.current_container_stack.pop
238
+ end
239
+
240
+ tab_node
241
+ end
242
+ end
243
+ end
244
+ end
245
+ end