elaine_crud 0.1.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 (55) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/LICENSE +21 -0
  4. data/README.md +225 -0
  5. data/Rakefile +9 -0
  6. data/TODO.md +496 -0
  7. data/app/controllers/elaine_crud/base_controller.rb +228 -0
  8. data/app/helpers/elaine_crud/base_helper.rb +787 -0
  9. data/app/helpers/elaine_crud/search_helper.rb +132 -0
  10. data/app/javascript/controllers/dropdown_controller.js +18 -0
  11. data/app/views/elaine_crud/base/_edit_row.html.erb +60 -0
  12. data/app/views/elaine_crud/base/_export_button.html.erb +88 -0
  13. data/app/views/elaine_crud/base/_foreign_key_select_refresh.html.erb +52 -0
  14. data/app/views/elaine_crud/base/_form.html.erb +45 -0
  15. data/app/views/elaine_crud/base/_form_fields.html.erb +45 -0
  16. data/app/views/elaine_crud/base/_index_table.html.erb +58 -0
  17. data/app/views/elaine_crud/base/_modal.html.erb +71 -0
  18. data/app/views/elaine_crud/base/_pagination.html.erb +110 -0
  19. data/app/views/elaine_crud/base/_per_page_selector.html.erb +30 -0
  20. data/app/views/elaine_crud/base/_search_bar.html.erb +75 -0
  21. data/app/views/elaine_crud/base/_show_details.html.erb +29 -0
  22. data/app/views/elaine_crud/base/_view_row.html.erb +96 -0
  23. data/app/views/elaine_crud/base/edit.html.erb +51 -0
  24. data/app/views/elaine_crud/base/index.html.erb +74 -0
  25. data/app/views/elaine_crud/base/new.html.erb +12 -0
  26. data/app/views/elaine_crud/base/new_modal.html.erb +37 -0
  27. data/app/views/elaine_crud/base/not_found.html.erb +49 -0
  28. data/app/views/elaine_crud/base/show.html.erb +32 -0
  29. data/docs/ARCHITECTURE.md +410 -0
  30. data/docs/CSS_GRID_LAYOUT.md +126 -0
  31. data/docs/DEMO.md +693 -0
  32. data/docs/DSL_EXAMPLES.md +313 -0
  33. data/docs/FOREIGN_KEY_EXAMPLE.rb +100 -0
  34. data/docs/FOREIGN_KEY_SUPPORT.md +197 -0
  35. data/docs/HAS_MANY_IMPLEMENTATION.md +154 -0
  36. data/docs/LAYOUT_EXAMPLES.md +301 -0
  37. data/docs/TROUBLESHOOTING.md +170 -0
  38. data/elaine_crud.gemspec +46 -0
  39. data/lib/elaine_crud/dsl_methods.rb +348 -0
  40. data/lib/elaine_crud/engine.rb +37 -0
  41. data/lib/elaine_crud/export_handling.rb +164 -0
  42. data/lib/elaine_crud/field_configuration.rb +422 -0
  43. data/lib/elaine_crud/field_configuration_methods.rb +152 -0
  44. data/lib/elaine_crud/layout_calculation.rb +55 -0
  45. data/lib/elaine_crud/parameter_handling.rb +48 -0
  46. data/lib/elaine_crud/record_fetching.rb +150 -0
  47. data/lib/elaine_crud/relationship_handling.rb +220 -0
  48. data/lib/elaine_crud/routing.rb +33 -0
  49. data/lib/elaine_crud/search_and_filtering.rb +285 -0
  50. data/lib/elaine_crud/sorting_concern.rb +65 -0
  51. data/lib/elaine_crud/version.rb +5 -0
  52. data/lib/elaine_crud.rb +25 -0
  53. data/lib/tasks/demo.rake +111 -0
  54. data/lib/tasks/spec.rake +26 -0
  55. metadata +264 -0
@@ -0,0 +1,422 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElaineCrud
4
+ # Configuration class for individual field customization
5
+ # Supports both hash-style and block-style DSL configuration
6
+ class FieldConfiguration
7
+ attr_accessor :field_name, :title, :description, :readonly, :default_value,
8
+ :display_callback, :edit_callback, :edit_partial, :options, :foreign_key_config,
9
+ :has_many_config, :has_one_config, :habtm_config, :visible, :grid_column_span,
10
+ :searchable, :filterable, :filter_type, :nested_create_config
11
+
12
+ def initialize(field_name, **options)
13
+ @field_name = field_name
14
+
15
+ # Set defaults
16
+ @title = options.fetch(:title, field_name.to_s.humanize)
17
+ @description = options.fetch(:description, nil)
18
+ @readonly = options.fetch(:readonly, false)
19
+ @default_value = options.fetch(:default_value, nil)
20
+ @display_callback = options.fetch(:display_as, nil)
21
+ @edit_callback = options.fetch(:edit_as, nil)
22
+ @edit_partial = options.fetch(:edit_partial, nil)
23
+ @options = options.fetch(:options, nil)
24
+ @foreign_key_config = options.fetch(:foreign_key, nil)
25
+ @has_many_config = options.fetch(:has_many, nil)
26
+ @has_one_config = options.fetch(:has_one, nil)
27
+ @habtm_config = options.fetch(:habtm, nil)
28
+ @visible = options.fetch(:visible, nil)
29
+ @grid_column_span = options.fetch(:grid_column_span, nil)
30
+ @searchable = options.fetch(:searchable, nil)
31
+ @filterable = options.fetch(:filterable, nil)
32
+ @filter_type = options.fetch(:filter_type, nil)
33
+ @nested_create_config = options.fetch(:nested_create, nil)
34
+ end
35
+
36
+ # Block-style DSL methods
37
+ def title(value = nil)
38
+ return @title if value.nil?
39
+ @title = value
40
+ end
41
+
42
+ def description(value = nil)
43
+ return @description if value.nil?
44
+ @description = value
45
+ end
46
+
47
+ def readonly(value = nil)
48
+ return @readonly if value.nil?
49
+ @readonly = value
50
+ end
51
+
52
+ def default_value(value = nil)
53
+ return @default_value if value.nil?
54
+ @default_value = value
55
+ end
56
+
57
+ def display_as(callback = nil, &block)
58
+ return @display_callback if callback.nil? && block.nil?
59
+ @display_callback = callback || block
60
+ end
61
+
62
+ def edit_as(callback = nil, &block)
63
+ return @edit_callback if callback.nil? && block.nil?
64
+ @edit_callback = callback || block
65
+ end
66
+
67
+ def edit_partial(partial_path = nil)
68
+ return @edit_partial if partial_path.nil?
69
+ @edit_partial = partial_path
70
+ end
71
+
72
+ def options(list = nil)
73
+ return @options if list.nil?
74
+ @options = list
75
+ end
76
+
77
+ def foreign_key(**config)
78
+ return @foreign_key_config if config.empty?
79
+ @foreign_key_config = config
80
+ end
81
+
82
+ def has_many(**config)
83
+ return @has_many_config if config.empty?
84
+ @has_many_config = config
85
+ end
86
+
87
+ def has_one(**config)
88
+ return @has_one_config if config.empty?
89
+ @has_one_config = config
90
+ end
91
+
92
+ def habtm(**config)
93
+ return @habtm_config if config.empty?
94
+ @habtm_config ||= {}
95
+ @habtm_config.merge!(config)
96
+ end
97
+
98
+ def visible(value = nil)
99
+ return @visible if value.nil?
100
+ @visible = value
101
+ end
102
+
103
+ def grid_column_span(value = nil)
104
+ return @grid_column_span if value.nil?
105
+ @grid_column_span = value
106
+ end
107
+
108
+ def searchable(value = nil)
109
+ return @searchable if value.nil?
110
+ @searchable = value
111
+ end
112
+
113
+ def filterable(value = nil)
114
+ return @filterable if value.nil?
115
+ @filterable = value
116
+ end
117
+
118
+ def filter_type(value = nil)
119
+ return @filter_type if value.nil?
120
+ @filter_type = value
121
+ end
122
+
123
+ def nested_create(config = nil)
124
+ return @nested_create_config if config.nil?
125
+ # Allow nested_create true as shorthand - use true as the value since {} is not .present?
126
+ @nested_create_config = config == true ? true : config
127
+ end
128
+
129
+ # Helper methods for checking configuration state
130
+ def has_options?
131
+ @options.present?
132
+ end
133
+
134
+ def has_foreign_key?
135
+ @foreign_key_config.present?
136
+ end
137
+
138
+ def has_has_many?
139
+ @has_many_config.present?
140
+ end
141
+
142
+ def has_has_one?
143
+ @has_one_config.present?
144
+ end
145
+
146
+ def has_habtm?
147
+ @habtm_config.present?
148
+ end
149
+
150
+ def has_custom_display?
151
+ @display_callback.present?
152
+ end
153
+
154
+ def has_custom_edit?
155
+ @edit_callback.present?
156
+ end
157
+
158
+ def has_edit_partial?
159
+ @edit_partial.present?
160
+ end
161
+
162
+ def has_nested_create?
163
+ @nested_create_config.present?
164
+ end
165
+
166
+ # Method to execute display callback
167
+ def render_display_value(record, controller_instance)
168
+ # For virtual fields (those that don't exist on the model), use nil as field_value
169
+ field_value = if record.respond_to?(@field_name)
170
+ record.public_send(@field_name)
171
+ else
172
+ nil
173
+ end
174
+
175
+ case @display_callback
176
+ when Symbol
177
+ # Call method on controller instance
178
+ if controller_instance.respond_to?(@display_callback, true)
179
+ result = controller_instance.send(@display_callback, field_value, record)
180
+ else
181
+ raise NoMethodError, "Display callback method '#{@display_callback}' not found on #{controller_instance.class.name}"
182
+ end
183
+ when Proc
184
+ # Call proc with field value and record
185
+ # Execute the lambda with the controller that has access to helpers
186
+ result = controller_instance.instance_exec(field_value, record, &@display_callback)
187
+ else
188
+ raise ArgumentError, "Display callback must be a Symbol or Proc, got #{@display_callback.class.name}"
189
+ end
190
+
191
+ # Ensure result is HTML safe
192
+ result.respond_to?(:html_safe) ? result.html_safe : result.to_s.html_safe
193
+ rescue => e
194
+ # Graceful error handling - show the error in development but fallback in production
195
+ if Rails.env.development?
196
+ %Q{<span class="text-red-500 text-xs">Error: #{e.message}</span>}.html_safe
197
+ else
198
+ field_value.to_s.html_safe
199
+ end
200
+ end
201
+
202
+ # Method to execute edit callback
203
+ def render_edit_field(record, controller_instance, form_builder = nil, view_context = nil)
204
+ field_value = record.public_send(@field_name)
205
+
206
+ case @edit_callback
207
+ when Symbol
208
+ # Call method on controller instance
209
+ if controller_instance.respond_to?(@edit_callback, true)
210
+ result = controller_instance.send(@edit_callback, field_value, record, form_builder)
211
+ else
212
+ raise NoMethodError, "Edit callback method '#{@edit_callback}' not found on #{controller_instance.class.name}"
213
+ end
214
+ when Proc
215
+ # Call proc with field value, record, and form builder
216
+ if view_context
217
+ # Execute the lambda in the view context so it has access to helpers
218
+ result = view_context.instance_exec(field_value, record, form_builder, &@edit_callback)
219
+ else
220
+ # Fallback to direct call for backward compatibility
221
+ result = @edit_callback.call(field_value, record, form_builder)
222
+ end
223
+ else
224
+ raise ArgumentError, "Edit callback must be a Symbol or Proc, got #{@edit_callback.class.name}"
225
+ end
226
+
227
+ # Ensure result is HTML safe
228
+ result.respond_to?(:html_safe) ? result.html_safe : result.to_s.html_safe
229
+ rescue => e
230
+ # Graceful error handling - show the error in development but fallback in production
231
+ if Rails.env.development?
232
+ if view_context
233
+ view_context.content_tag(:span, "Error: #{e.message}", class: "text-red-500 text-xs")
234
+ else
235
+ %Q{<span class="text-red-500 text-xs">Error: #{e.message}</span>}.html_safe
236
+ end
237
+ else
238
+ # Fallback to basic text field
239
+ if form_builder
240
+ field_class = "block w-full border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm"
241
+ form_builder.text_field(@field_name, class: field_class)
242
+ else
243
+ field_value.to_s.html_safe
244
+ end
245
+ end
246
+ end
247
+
248
+ # Method to get foreign key options for dropdowns
249
+ def foreign_key_options(controller_instance)
250
+ return [] unless has_foreign_key?
251
+
252
+ # Validate required configuration
253
+ target_model = @foreign_key_config[:model]
254
+ return [] unless target_model
255
+
256
+ # Get records using scope if provided, otherwise all records
257
+ records = if @foreign_key_config[:scope]
258
+ @foreign_key_config[:scope].call
259
+ else
260
+ target_model.all
261
+ end
262
+
263
+ # Format options for select dropdown
264
+ options = records.map do |record|
265
+ display_value = case @foreign_key_config[:display]
266
+ when Symbol
267
+ record.respond_to?(@foreign_key_config[:display]) ?
268
+ record.public_send(@foreign_key_config[:display]) :
269
+ record.to_s
270
+ when Proc
271
+ @foreign_key_config[:display].call(record)
272
+ else
273
+ record.to_s
274
+ end
275
+
276
+ [display_value, record.id]
277
+ end
278
+
279
+ options
280
+ rescue => e
281
+ # Graceful error handling
282
+ if Rails.env.development?
283
+ [["Error: #{e.message}", ""]]
284
+ else
285
+ []
286
+ end
287
+ end
288
+
289
+ # Method to get default value for new records
290
+ def resolve_default_value(controller_instance)
291
+ # TODO: Implement default value resolution
292
+ # 1. Return nil if @default_value is nil
293
+ # 2. If @default_value is a Proc or Lambda, call it with controller_instance as context
294
+ # 3. If @default_value is any other value, return it directly
295
+ # 4. Handle edge cases and ensure returned value is appropriate for the field type
296
+ return @default_value if @default_value.present? && !@default_value.respond_to?(:call)
297
+ return nil # Placeholder for callable defaults
298
+ end
299
+
300
+ # Helper method to determine if field should be shown in index
301
+ def show_in_index?
302
+ # TODO: Implement index visibility logic
303
+ # Default: show all fields except id and timestamps
304
+ # Could be configurable in future: @show_in_index boolean
305
+ !%w[id created_at updated_at].include?(@field_name.to_s)
306
+ end
307
+
308
+ # Helper method to determine if field should be shown in forms
309
+ def show_in_form?
310
+ # TODO: Implement form visibility logic
311
+ # Default: show all permitted params except readonly fields in edit mode
312
+ # Could be configurable in future: @show_in_form boolean
313
+ !@readonly
314
+ end
315
+
316
+ # Helper method to get field input type for default rendering
317
+ def default_input_type(column_type)
318
+ # TODO: Implement default input type mapping
319
+ # Map ActiveRecord column types to HTML input types:
320
+ # - :string, :text -> text_field / text_area
321
+ # - :integer, :decimal, :float -> number_field
322
+ # - :boolean -> check_box
323
+ # - :date -> date_field
324
+ # - :datetime, :timestamp -> datetime_local_field
325
+ # - etc.
326
+ case column_type
327
+ when :string then :text_field
328
+ when :text then :text_area
329
+ when :integer, :decimal, :float then :number_field
330
+ when :boolean then :check_box
331
+ when :date then :date_field
332
+ when :datetime, :timestamp then :datetime_local_field
333
+ else :text_field
334
+ end
335
+ end
336
+
337
+ # Get options for has_many relationship display
338
+ def has_many_related_records(controller_instance)
339
+ return [] unless has_has_many?
340
+
341
+ model_class = @has_many_config[:model]
342
+ foreign_key = @has_many_config[:foreign_key]
343
+
344
+ # This would be called from a parent record context
345
+ parent_record = controller_instance.instance_variable_get(:@record)
346
+ return [] unless parent_record
347
+
348
+ related_records = parent_record.public_send(@field_name)
349
+
350
+ # Apply scope if specified
351
+ if @has_many_config[:scope]
352
+ related_records = related_records.instance_eval(&@has_many_config[:scope])
353
+ end
354
+
355
+ related_records
356
+ end
357
+
358
+ # Get display value for has_many relationship
359
+ def render_has_many_display(record, controller_instance)
360
+ return "" unless has_has_many?
361
+
362
+ related_records = record.public_send(@field_name)
363
+
364
+ if @has_many_config[:show_count]
365
+ count = related_records.count
366
+
367
+ if @has_many_config[:max_preview_items] && @has_many_config[:max_preview_items] > 0
368
+ preview_records = related_records.limit(@has_many_config[:max_preview_items])
369
+ display_field = @has_many_config[:display] || :id
370
+
371
+ preview_text = preview_records.map do |rel_record|
372
+ begin
373
+ if display_field.is_a?(Proc)
374
+ display_field.call(rel_record)
375
+ else
376
+ result = rel_record.public_send(display_field)
377
+ result || "N/A"
378
+ end
379
+ rescue => e
380
+ Rails.logger.error "ElaineCrud: Error calling #{display_field} on #{rel_record.class.name}##{rel_record.id}: #{e.message}"
381
+ "Error"
382
+ end
383
+ end.join(", ")
384
+
385
+ "#{count} items#{count > 0 ? ": #{preview_text}" : ""}"
386
+ else
387
+ "#{count} items"
388
+ end
389
+ else
390
+ related_records.count.to_s
391
+ end
392
+ rescue => e
393
+ Rails.logger.error "ElaineCrud: Error rendering has_many display: #{e.message}"
394
+ "Error loading relationships"
395
+ end
396
+
397
+ # Get display value for has_one relationship
398
+ def render_has_one_display(record, controller_instance)
399
+ return "" unless has_has_one?
400
+
401
+ related_record = record.public_send(@field_name)
402
+ return "—" if related_record.nil?
403
+
404
+ display_field = @has_one_config[:display] || :id
405
+
406
+ begin
407
+ if display_field.is_a?(Proc)
408
+ display_field.call(related_record)
409
+ else
410
+ result = related_record.public_send(display_field)
411
+ result || "N/A"
412
+ end
413
+ rescue => e
414
+ Rails.logger.error "ElaineCrud: Error calling #{display_field} on #{related_record.class.name}##{related_record.id}: #{e.message}"
415
+ "Error"
416
+ end
417
+ rescue => e
418
+ Rails.logger.error "ElaineCrud: Error rendering has_one display: #{e.message}"
419
+ "Error loading relationship"
420
+ end
421
+ end
422
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElaineCrud
4
+ # Instance methods for accessing and working with field configurations
5
+ # Provides runtime access to field metadata and rendering
6
+ module FieldConfigurationMethods
7
+ extend ActiveSupport::Concern
8
+
9
+ # Get configuration for a specific field
10
+ # @param field_name [Symbol] The field name
11
+ # @return [FieldConfiguration, nil] The configuration or nil if not configured
12
+ def field_config_for(field_name)
13
+ field_configurations&.dig(field_name.to_sym)
14
+ end
15
+
16
+ # Get all configured field names
17
+ # @return [Array<Symbol>] List of configured field names
18
+ def configured_fields
19
+ field_configurations&.keys || []
20
+ end
21
+
22
+ # Check if a field has custom configuration
23
+ # @param field_name [Symbol] The field name
24
+ # @return [Boolean] True if field has custom configuration
25
+ def field_configured?(field_name)
26
+ field_config_for(field_name).present?
27
+ end
28
+
29
+ # Get display title for a field
30
+ # @param field_name [Symbol] The field name
31
+ # @return [String] The display title (configured or humanized)
32
+ def field_title(field_name)
33
+ config = field_config_for(field_name)
34
+ config&.title || field_name.to_s.humanize
35
+ end
36
+
37
+ # Get description for a field
38
+ # @param field_name [Symbol] The field name
39
+ # @return [String, nil] The field description if configured
40
+ def field_description(field_name)
41
+ field_config_for(field_name)&.description
42
+ end
43
+
44
+ # Check if a field is readonly
45
+ # @param field_name [Symbol] The field name
46
+ # @return [Boolean] True if field is readonly
47
+ def field_readonly?(field_name)
48
+ field_config_for(field_name)&.readonly || false
49
+ end
50
+
51
+ # Check if Turbo frames are disabled for this controller
52
+ # @return [Boolean] True if Turbo frames are disabled
53
+ def turbo_disabled?
54
+ disable_turbo_frames == true
55
+ end
56
+
57
+ # Render display value for a field
58
+ # @param record [ActiveRecord::Base] The record
59
+ # @param field_name [Symbol] The field name
60
+ # @return [String] HTML safe string for display
61
+ def render_field_display(record, field_name)
62
+ # Delegate to helper which has access to view context
63
+ helpers.display_field_value(record, field_name)
64
+ end
65
+
66
+ # Render edit field for a form
67
+ # @param record [ActiveRecord::Base] The record
68
+ # @param field_name [Symbol] The field name
69
+ # @param form_builder [ActionView::Helpers::FormBuilder, nil] Optional form builder
70
+ # @return [String] HTML safe string for form field
71
+ def render_field_edit(record, field_name, form_builder = nil)
72
+ config = field_config_for(field_name)
73
+
74
+ return readonly_field_display(record, field_name) if field_readonly?(field_name)
75
+
76
+ if config&.has_custom_edit?
77
+ config.render_edit_field(record, self, form_builder)
78
+ else
79
+ # TODO: Implement fallback to default edit field logic
80
+ # Should generate appropriate form fields based on field type
81
+ # Should handle dropdowns for options, foreign key selects, etc.
82
+ "<!-- TODO: Default edit field for #{field_name} -->" # Placeholder
83
+ end
84
+ end
85
+
86
+ # Render readonly field display (for readonly fields in edit forms)
87
+ # @param record [ActiveRecord::Base] The record
88
+ # @param field_name [Symbol] The field name
89
+ # @return [String] HTML safe string for readonly display
90
+ def readonly_field_display(record, field_name)
91
+ # TODO: Implement readonly field display
92
+ # Should render the display value but in an edit form context
93
+ # Maybe with a different styling to indicate it's not editable
94
+ render_field_display(record, field_name) # Placeholder
95
+ end
96
+
97
+ # Instance method version of determine_display_field_for_model
98
+ # @param model_class [Class] The ActiveRecord model class
99
+ # @return [Symbol] The field to use for display
100
+ def determine_display_field_for_model(model_class)
101
+ # Common field names for display, in order of preference
102
+ display_candidates = [:name, :title, :display_name, :full_name, :label, :description]
103
+
104
+ # Check which columns exist
105
+ column_names = model_class.column_names.map(&:to_sym)
106
+
107
+ # Return the first matching candidate
108
+ display_field = display_candidates.find { |candidate| column_names.include?(candidate) }
109
+
110
+ # If none found, use the first string/text column that's not id, created_at, updated_at
111
+ unless display_field
112
+ string_columns = model_class.columns.select do |col|
113
+ [:string, :text].include?(col.type) &&
114
+ !%w[id created_at updated_at].include?(col.name)
115
+ end
116
+ display_field = string_columns.first&.name&.to_sym
117
+ end
118
+
119
+ # Final fallback to :id
120
+ display_field || :id
121
+ end
122
+
123
+ # Apply default values to a new record based on field configurations
124
+ # @param record [ActiveRecord::Base] The record to apply defaults to
125
+ def apply_field_defaults(record)
126
+ # TODO: Implement default value application
127
+ # Should iterate through field configurations and apply default_value if present
128
+ # Should handle both static values and proc/lambda callbacks
129
+ # Should only apply to new records (record.new_record?)
130
+ return record # Placeholder
131
+ end
132
+
133
+ # Debug method to help troubleshoot configuration issues
134
+ # Call this in your controller in development to see the configuration
135
+ def debug_configuration
136
+ return unless Rails.env.development?
137
+
138
+ puts "\n=== ElaineCrud Configuration Debug ==="
139
+ puts "Model: #{crud_model&.name || 'NOT SET'}"
140
+ puts "Permitted Attributes: #{permitted_attributes&.inspect || 'NOT SET'}"
141
+ puts "Field Configurations: #{field_configurations&.keys&.inspect || 'NONE'}"
142
+
143
+ if crud_model
144
+ reflections = crud_model.reflections.select { |_, r| r.is_a?(ActiveRecord::Reflection::BelongsToReflection) }
145
+ puts "Belongs_to relationships: #{reflections.keys.inspect}"
146
+ puts "Foreign keys: #{reflections.values.map(&:foreign_key).inspect}"
147
+ end
148
+
149
+ puts "====================================\n"
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElaineCrud
4
+ # Methods for calculating and customizing layout and grid structure
5
+ # Handles column widths, row layouts, and field positioning
6
+ module LayoutCalculation
7
+ extend ActiveSupport::Concern
8
+
9
+ # Calculate layout structure for a specific record
10
+ # @param content [ActiveRecord::Base] The record being displayed
11
+ # @param fields [Array<Symbol>] Array of field names to include in layout
12
+ # @return [Array<Array<Hash>>] Nested array where first dimension is rows, second is columns
13
+ # Each column hash can contain: field_name, colspan, rowspan, and future properties
14
+ def calculate_layout(content, fields)
15
+ # Default implementation: single row with all fields, each taking 1 column and 1 row
16
+ row = fields.map do |field_name|
17
+ {
18
+ field_name: field_name,
19
+ colspan: 1,
20
+ rowspan: 1
21
+ }
22
+ end
23
+
24
+ [row] # Return single row
25
+ end
26
+
27
+ # Calculate layout header structure defining column sizes and titles
28
+ # @param fields [Array<Symbol>] Array of field names to include in layout
29
+ # @return [Array<Hash>] Array of header config objects with width, field_name, and/or title
30
+ # Each object can contain:
31
+ # - width: CSS width (required, e.g., "minmax(100px, 1fr)" or "25%")
32
+ # - field_name: Symbol of field to display and enable sorting (optional)
33
+ # - title: Custom column title, overrides field title (optional)
34
+ def calculate_layout_header(fields)
35
+ # Default implementation: flexible columns that can expand to fit content
36
+ # Using minmax() allows columns to grow beyond their base size when content requires it
37
+ # Note: Create a new array to avoid mutating the input
38
+ all_fields = fields + ["ROW-ACTIONS"]
39
+
40
+ all_fields.map do |field_name|
41
+ width = case field_name.to_s
42
+ when 'id' then "max-content"
43
+ when 'email' then "minmax(180px, 2fr)"
44
+ when 'ROW-ACTIONS' then "max-content"
45
+ else "minmax(100px, 1fr)"
46
+ end
47
+
48
+ {
49
+ width: width,
50
+ field_name: field_name
51
+ }
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElaineCrud
4
+ # Methods for handling strong parameters
5
+ # Processes form submissions and enforces permitted attributes
6
+ module ParameterHandling
7
+ extend ActiveSupport::Concern
8
+
9
+ private
10
+
11
+ # Strong parameters
12
+ # @return [ActionController::Parameters] Filtered parameters
13
+ def record_params
14
+ return {} unless permitted_attributes.present?
15
+
16
+ model_param_key = crud_model.name.underscore.to_sym
17
+
18
+ # Debug logging for development
19
+ if Rails.env.development?
20
+ Rails.logger.info "ElaineCrud: Looking for params under key '#{model_param_key}'"
21
+ Rails.logger.info "ElaineCrud: Available param keys: #{params.keys.inspect}"
22
+ Rails.logger.info "ElaineCrud: Permitted attributes: #{permitted_attributes.inspect}"
23
+ Rails.logger.info "ElaineCrud: Model name: #{crud_model.name}"
24
+ Rails.logger.info "ElaineCrud: Model underscore: #{crud_model.name.underscore}"
25
+
26
+ # Show the actual parameter structure
27
+ params.keys.each do |key|
28
+ if params[key].is_a?(ActionController::Parameters) || params[key].is_a?(Hash)
29
+ Rails.logger.info "ElaineCrud: params[#{key}] = #{params[key].inspect}"
30
+ end
31
+ end
32
+ end
33
+
34
+ if params[model_param_key].present?
35
+ filtered_params = params.require(model_param_key).permit(*permitted_attributes)
36
+ Rails.logger.info "ElaineCrud: Successfully filtered params: #{filtered_params.inspect}" if Rails.env.development?
37
+ filtered_params
38
+ else
39
+ Rails.logger.warn "ElaineCrud: No parameters found for model '#{model_param_key}'" if Rails.env.development?
40
+ Rails.logger.info "ElaineCrud: Available top-level param structure:" if Rails.env.development?
41
+ params.each do |key, value|
42
+ Rails.logger.info "ElaineCrud: #{key} => #{value.class} (#{value.is_a?(Hash) || value.is_a?(ActionController::Parameters) ? value.keys.inspect : value.inspect})"
43
+ end
44
+ {}
45
+ end
46
+ end
47
+ end
48
+ end