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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/LICENSE +21 -0
- data/README.md +225 -0
- data/Rakefile +9 -0
- data/TODO.md +496 -0
- data/app/controllers/elaine_crud/base_controller.rb +228 -0
- data/app/helpers/elaine_crud/base_helper.rb +787 -0
- data/app/helpers/elaine_crud/search_helper.rb +132 -0
- data/app/javascript/controllers/dropdown_controller.js +18 -0
- data/app/views/elaine_crud/base/_edit_row.html.erb +60 -0
- data/app/views/elaine_crud/base/_export_button.html.erb +88 -0
- data/app/views/elaine_crud/base/_foreign_key_select_refresh.html.erb +52 -0
- data/app/views/elaine_crud/base/_form.html.erb +45 -0
- data/app/views/elaine_crud/base/_form_fields.html.erb +45 -0
- data/app/views/elaine_crud/base/_index_table.html.erb +58 -0
- data/app/views/elaine_crud/base/_modal.html.erb +71 -0
- data/app/views/elaine_crud/base/_pagination.html.erb +110 -0
- data/app/views/elaine_crud/base/_per_page_selector.html.erb +30 -0
- data/app/views/elaine_crud/base/_search_bar.html.erb +75 -0
- data/app/views/elaine_crud/base/_show_details.html.erb +29 -0
- data/app/views/elaine_crud/base/_view_row.html.erb +96 -0
- data/app/views/elaine_crud/base/edit.html.erb +51 -0
- data/app/views/elaine_crud/base/index.html.erb +74 -0
- data/app/views/elaine_crud/base/new.html.erb +12 -0
- data/app/views/elaine_crud/base/new_modal.html.erb +37 -0
- data/app/views/elaine_crud/base/not_found.html.erb +49 -0
- data/app/views/elaine_crud/base/show.html.erb +32 -0
- data/docs/ARCHITECTURE.md +410 -0
- data/docs/CSS_GRID_LAYOUT.md +126 -0
- data/docs/DEMO.md +693 -0
- data/docs/DSL_EXAMPLES.md +313 -0
- data/docs/FOREIGN_KEY_EXAMPLE.rb +100 -0
- data/docs/FOREIGN_KEY_SUPPORT.md +197 -0
- data/docs/HAS_MANY_IMPLEMENTATION.md +154 -0
- data/docs/LAYOUT_EXAMPLES.md +301 -0
- data/docs/TROUBLESHOOTING.md +170 -0
- data/elaine_crud.gemspec +46 -0
- data/lib/elaine_crud/dsl_methods.rb +348 -0
- data/lib/elaine_crud/engine.rb +37 -0
- data/lib/elaine_crud/export_handling.rb +164 -0
- data/lib/elaine_crud/field_configuration.rb +422 -0
- data/lib/elaine_crud/field_configuration_methods.rb +152 -0
- data/lib/elaine_crud/layout_calculation.rb +55 -0
- data/lib/elaine_crud/parameter_handling.rb +48 -0
- data/lib/elaine_crud/record_fetching.rb +150 -0
- data/lib/elaine_crud/relationship_handling.rb +220 -0
- data/lib/elaine_crud/routing.rb +33 -0
- data/lib/elaine_crud/search_and_filtering.rb +285 -0
- data/lib/elaine_crud/sorting_concern.rb +65 -0
- data/lib/elaine_crud/version.rb +5 -0
- data/lib/elaine_crud.rb +25 -0
- data/lib/tasks/demo.rake +111 -0
- data/lib/tasks/spec.rake +26 -0
- metadata +264 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# ElaineCrud DSL Examples
|
|
2
|
+
|
|
3
|
+
This document shows comprehensive examples of the ElaineCrud field DSL framework that has been implemented. All examples include TODO comments since the actual functionality is not yet implemented.
|
|
4
|
+
|
|
5
|
+
## Basic Field Configuration
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
class PeopleController < ElaineCrud::BaseController
|
|
9
|
+
model Person
|
|
10
|
+
permit_params :name, :email, :role, :active, :company_id, :salary
|
|
11
|
+
|
|
12
|
+
# Simple hash-style configuration
|
|
13
|
+
field :name, title: "Full Name", description: "Enter first and last name"
|
|
14
|
+
|
|
15
|
+
# Block-style configuration for complex setups
|
|
16
|
+
field :email do |f|
|
|
17
|
+
f.title "Email Address"
|
|
18
|
+
f.description "Primary contact email"
|
|
19
|
+
f.display_as { |value| mail_to(value) if value.present? }
|
|
20
|
+
f.edit_as { |value| email_field_tag(field_name, value, class: "form-input") }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Dropdown Options
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
class ProductController < ElaineCrud::BaseController
|
|
29
|
+
model Product
|
|
30
|
+
permit_params :name, :status, :category
|
|
31
|
+
|
|
32
|
+
# Array of options
|
|
33
|
+
field :status,
|
|
34
|
+
title: "Product Status",
|
|
35
|
+
options: ["draft", "published", "archived"]
|
|
36
|
+
|
|
37
|
+
# Hash mapping display => value
|
|
38
|
+
field :category,
|
|
39
|
+
title: "Product Category",
|
|
40
|
+
options: {
|
|
41
|
+
"Consumer Electronics" => "electronics",
|
|
42
|
+
"Home & Garden" => "home_garden",
|
|
43
|
+
"Books & Media" => "books"
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Foreign Key Relationships
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
class EmployeeController < ElaineCrud::BaseController
|
|
52
|
+
model Employee
|
|
53
|
+
permit_params :name, :email, :company_id, :department_id
|
|
54
|
+
|
|
55
|
+
# Basic foreign key - displays with to_s
|
|
56
|
+
field :company_id do |f|
|
|
57
|
+
f.title "Company"
|
|
58
|
+
f.description "Select the company this employee works for"
|
|
59
|
+
f.foreign_key model: Company
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Foreign key with custom display and scoping
|
|
63
|
+
field :department_id do |f|
|
|
64
|
+
f.title "Department"
|
|
65
|
+
f.foreign_key model: Department,
|
|
66
|
+
display: ->(dept) { "#{dept.name} (#{dept.location})" },
|
|
67
|
+
scope: -> { Department.active.order(:name) },
|
|
68
|
+
null_option: "Select a department..."
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Custom Display and Edit Callbacks
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
class TransactionController < ElaineCrud::BaseController
|
|
77
|
+
model Transaction
|
|
78
|
+
permit_params :amount, :description, :transaction_type, :processed_at
|
|
79
|
+
|
|
80
|
+
# Method reference for display callback
|
|
81
|
+
field :amount,
|
|
82
|
+
title: "Transaction Amount",
|
|
83
|
+
display_as: :format_currency,
|
|
84
|
+
edit_as: :currency_input
|
|
85
|
+
|
|
86
|
+
# Inline block for display
|
|
87
|
+
field :transaction_type do |f|
|
|
88
|
+
f.title "Type"
|
|
89
|
+
f.display_as { |value|
|
|
90
|
+
case value
|
|
91
|
+
when 'credit' then content_tag(:span, 'Credit', class: 'text-green-600 font-semibold')
|
|
92
|
+
when 'debit' then content_tag(:span, 'Debit', class: 'text-red-600 font-semibold')
|
|
93
|
+
else value.to_s.titleize
|
|
94
|
+
end
|
|
95
|
+
}
|
|
96
|
+
f.options ["credit", "debit", "transfer"]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def format_currency(value, record)
|
|
102
|
+
number_to_currency(value) if value
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def currency_input(value, record, form)
|
|
106
|
+
form.number_field(:amount, step: 0.01, class: "form-input", placeholder: "0.00")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Custom Partial Rendering
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
class AudienceController < ElaineCrud::BaseController
|
|
115
|
+
model Audience
|
|
116
|
+
permit_params :name, :audience_query, :state
|
|
117
|
+
|
|
118
|
+
# Use a custom partial for complex field rendering
|
|
119
|
+
field :audience_query do |f|
|
|
120
|
+
f.title "Audience Query"
|
|
121
|
+
f.description "Build your audience targeting criteria using the visual query builder"
|
|
122
|
+
f.edit_partial "audience_query_builder"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Hash-style partial configuration
|
|
126
|
+
field :rich_content,
|
|
127
|
+
title: "Rich Content Editor",
|
|
128
|
+
description: "WYSIWYG editor for rich text content",
|
|
129
|
+
edit_partial: "shared/rich_text_editor"
|
|
130
|
+
end
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Partial Template Example
|
|
134
|
+
|
|
135
|
+
Then create the partial file `app/views/audience_browser/_audience_query_builder.html.erb`:
|
|
136
|
+
|
|
137
|
+
## Read-Only Fields with Defaults
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
class OrderController < ElaineCrud::BaseController
|
|
141
|
+
model Order
|
|
142
|
+
permit_params :customer_name, :total_amount, :status, :order_number, :created_by
|
|
143
|
+
|
|
144
|
+
# Read-only with static default
|
|
145
|
+
field :order_number,
|
|
146
|
+
title: "Order Number",
|
|
147
|
+
description: "Automatically generated unique identifier",
|
|
148
|
+
readonly: true,
|
|
149
|
+
default_value: -> { "ORD-#{SecureRandom.alphanumeric(8).upcase}" }
|
|
150
|
+
|
|
151
|
+
# Read-only timestamp with custom formatting
|
|
152
|
+
field :created_at,
|
|
153
|
+
title: "Order Date",
|
|
154
|
+
readonly: true,
|
|
155
|
+
display_as: :format_order_date
|
|
156
|
+
|
|
157
|
+
# Read-only field populated from session/context
|
|
158
|
+
field :created_by,
|
|
159
|
+
title: "Created By",
|
|
160
|
+
readonly: true,
|
|
161
|
+
default_value: -> { current_user&.name }
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
def format_order_date(value, record)
|
|
166
|
+
value&.strftime("%B %d, %Y at %I:%M %p")
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def current_user
|
|
170
|
+
# In this app, user info comes from reverse proxy headers
|
|
171
|
+
OpenStruct.new(name: request.headers['X-Forwarded-User'])
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Complex Real-World Example
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
class UserController < ElaineCrud::BaseController
|
|
180
|
+
model User
|
|
181
|
+
permit_params :name, :email, :role, :company_id, :department_id, :salary, :active, :hire_date
|
|
182
|
+
|
|
183
|
+
# Basic text field with validation
|
|
184
|
+
field :name do |f|
|
|
185
|
+
f.title "Full Name"
|
|
186
|
+
f.description "Enter the employee's first and last name"
|
|
187
|
+
f.display_as { |value| content_tag(:strong, value) }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Email with custom display and validation
|
|
191
|
+
field :email do |f|
|
|
192
|
+
f.title "Email Address"
|
|
193
|
+
f.description "Primary work email address"
|
|
194
|
+
f.display_as { |value| mail_to(value, value, class: "text-blue-600") }
|
|
195
|
+
f.edit_as { |value, record, form|
|
|
196
|
+
form.email_field(:email, value: value, required: true, class: "form-input")
|
|
197
|
+
}
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Dropdown with predefined options
|
|
201
|
+
field :role,
|
|
202
|
+
title: "Job Role",
|
|
203
|
+
description: "Employee's primary role in the organization",
|
|
204
|
+
options: ["Developer", "Designer", "Manager", "Admin", "HR"]
|
|
205
|
+
|
|
206
|
+
# Foreign key to Company with custom display
|
|
207
|
+
field :company_id do |f|
|
|
208
|
+
f.title "Company"
|
|
209
|
+
f.description "Select the company this employee works for"
|
|
210
|
+
f.foreign_key model: Company,
|
|
211
|
+
display: ->(company) { "#{company.name} (#{company.city})" },
|
|
212
|
+
scope: -> { Company.active.includes(:address) },
|
|
213
|
+
null_option: "Select a company..."
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Foreign key to Department filtered by company
|
|
217
|
+
field :department_id do |f|
|
|
218
|
+
f.title "Department"
|
|
219
|
+
f.description "Select the department within the company"
|
|
220
|
+
f.foreign_key model: Department,
|
|
221
|
+
display: :name_with_code,
|
|
222
|
+
scope: -> { Department.joins(:company).where(company: company_scope) }
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Currency field with formatting
|
|
226
|
+
field :salary do |f|
|
|
227
|
+
f.title "Annual Salary"
|
|
228
|
+
f.description "Base annual salary in USD"
|
|
229
|
+
f.display_as :format_salary
|
|
230
|
+
f.edit_as { |value, record, form|
|
|
231
|
+
form.number_field(:salary, value: value, step: 1000, min: 0,
|
|
232
|
+
class: "form-input", placeholder: "Enter amount")
|
|
233
|
+
}
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Boolean with custom display
|
|
237
|
+
field :active,
|
|
238
|
+
title: "Active Employee",
|
|
239
|
+
description: "Is this person currently employed?",
|
|
240
|
+
display_as: ->(value) {
|
|
241
|
+
if value
|
|
242
|
+
content_tag(:span, "✅ Active", class: "text-green-600 font-semibold")
|
|
243
|
+
else
|
|
244
|
+
content_tag(:span, "❌ Inactive", class: "text-red-600 font-semibold")
|
|
245
|
+
end
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
# Date field with custom formatting
|
|
249
|
+
field :hire_date do |f|
|
|
250
|
+
f.title "Hire Date"
|
|
251
|
+
f.description "Employee's first day of work"
|
|
252
|
+
f.display_as { |value| value&.strftime("%B %d, %Y") }
|
|
253
|
+
f.default_value -> { Date.current }
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
private
|
|
257
|
+
|
|
258
|
+
def format_salary(value, record)
|
|
259
|
+
return "Not disclosed" if value.blank?
|
|
260
|
+
number_to_currency(value, precision: 0)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def company_scope
|
|
264
|
+
# This would be used to filter departments by selected company
|
|
265
|
+
# Implementation would need to be dynamic based on form state
|
|
266
|
+
Company.all
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Field Configuration Reference
|
|
272
|
+
|
|
273
|
+
### Available Options
|
|
274
|
+
|
|
275
|
+
| Option | Type | Description | Example |
|
|
276
|
+
|--------|------|-------------|---------|
|
|
277
|
+
| `title` | String | Human-readable field title | `"Full Name"` |
|
|
278
|
+
| `description` | String | Help text for forms | `"Enter first and last name"` |
|
|
279
|
+
| `readonly` | Boolean | Prevents editing | `true` |
|
|
280
|
+
| `default_value` | Value/Proc | Default for new records | `-> { Date.current }` |
|
|
281
|
+
| `display_as` | Symbol/Proc | Custom display rendering | `:format_currency` |
|
|
282
|
+
| `edit_as` | Symbol/Proc | Custom form field rendering | `{ |v| email_field(...) }` |
|
|
283
|
+
| `edit_partial` | String | Custom partial for field rendering | `"audience_query_builder"` |
|
|
284
|
+
| `options` | Array/Hash | Dropdown options | `["option1", "option2"]` |
|
|
285
|
+
| `foreign_key` | Hash | Foreign key configuration | `{ model: Company, display: :name }` |
|
|
286
|
+
|
|
287
|
+
### Foreign Key Configuration
|
|
288
|
+
|
|
289
|
+
```ruby
|
|
290
|
+
field :company_id do |f|
|
|
291
|
+
f.foreign_key model: Company, # Required: target model
|
|
292
|
+
display: :name, # Optional: how to display (Symbol/Proc)
|
|
293
|
+
scope: -> { Company.active }, # Optional: filter records
|
|
294
|
+
null_option: "Select company..." # Optional: placeholder text
|
|
295
|
+
end
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Callback Signatures
|
|
299
|
+
|
|
300
|
+
```ruby
|
|
301
|
+
# Display callbacks
|
|
302
|
+
display_as: :method_name # Calls controller.method_name(value, record)
|
|
303
|
+
display_as: { |value, record| ... } # Inline block
|
|
304
|
+
|
|
305
|
+
# Edit callbacks
|
|
306
|
+
edit_as: :method_name # Calls controller.method_name(value, record, form)
|
|
307
|
+
edit_as: { |value, record, form| ... } # Inline block
|
|
308
|
+
|
|
309
|
+
# Default value callbacks
|
|
310
|
+
default_value: -> { Time.current } # Called in controller context
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
All of these examples are currently placeholders with TODO comments in the actual implementation. Each feature needs to be implemented one by one in the FieldConfiguration and BaseController classes.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Example: Meeting Controller with Foreign Key Support
|
|
2
|
+
#
|
|
3
|
+
# This example shows how ElaineCrud automatically handles foreign key relationships
|
|
4
|
+
# for a Meeting model that belongs_to a MeetingRoom.
|
|
5
|
+
#
|
|
6
|
+
# Models (example structure):
|
|
7
|
+
#
|
|
8
|
+
# class Meeting < ApplicationRecord
|
|
9
|
+
# belongs_to :meeting_room
|
|
10
|
+
#
|
|
11
|
+
# validates :title, presence: true
|
|
12
|
+
# validates :start_time, presence: true
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# class MeetingRoom < ApplicationRecord
|
|
16
|
+
# has_many :meetings
|
|
17
|
+
#
|
|
18
|
+
# validates :name, presence: true
|
|
19
|
+
# end
|
|
20
|
+
|
|
21
|
+
class MeetingsController < ElaineCrud::BaseController
|
|
22
|
+
# Set the model - this automatically detects belongs_to relationships
|
|
23
|
+
model Meeting
|
|
24
|
+
|
|
25
|
+
# Specify the fields you want to permit for forms
|
|
26
|
+
# Foreign keys (meeting_room_id) are automatically included
|
|
27
|
+
permit_params :title, :description, :start_time, :end_time
|
|
28
|
+
|
|
29
|
+
# Optional: Customize foreign key display and behavior
|
|
30
|
+
field :meeting_room_id do
|
|
31
|
+
title "Meeting Room"
|
|
32
|
+
foreign_key(
|
|
33
|
+
model: MeetingRoom,
|
|
34
|
+
display: :name, # Show the 'name' field from MeetingRoom
|
|
35
|
+
null_option: "Choose a room",
|
|
36
|
+
scope: -> { MeetingRoom.available } # Optional: only show available rooms
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Optional: Customize other fields
|
|
41
|
+
field :title do
|
|
42
|
+
title "Meeting Title"
|
|
43
|
+
description "Enter a descriptive title for the meeting"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
field :start_time do
|
|
47
|
+
title "Start Time"
|
|
48
|
+
description "When the meeting begins"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
field :end_time do
|
|
52
|
+
title "End Time"
|
|
53
|
+
description "When the meeting ends"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Optional: Hide certain fields from display
|
|
57
|
+
field :description do
|
|
58
|
+
visible false # Won't show in index listing
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Optional: Make fields readonly
|
|
62
|
+
field :created_at do
|
|
63
|
+
visible true
|
|
64
|
+
readonly true
|
|
65
|
+
title "Created"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# What happens automatically:
|
|
70
|
+
#
|
|
71
|
+
# 1. ElaineCrud detects that Meeting belongs_to :meeting_room
|
|
72
|
+
# 2. It automatically configures meeting_room_id as a foreign key field
|
|
73
|
+
# 3. In the index view, instead of showing "1, 2, 3" for meeting_room_id,
|
|
74
|
+
# it shows the related MeetingRoom's display field (usually name, title, etc.)
|
|
75
|
+
# 4. In edit forms, meeting_room_id becomes a dropdown with all MeetingRoom options
|
|
76
|
+
# 5. The foreign key is automatically included in permitted_params
|
|
77
|
+
# 6. Database queries are optimized with includes() to avoid N+1 queries
|
|
78
|
+
#
|
|
79
|
+
# Manual configuration options:
|
|
80
|
+
#
|
|
81
|
+
# field :meeting_room_id do
|
|
82
|
+
# foreign_key(
|
|
83
|
+
# model: MeetingRoom, # Required: the related model class
|
|
84
|
+
# display: :name, # Field to show (default: auto-detected)
|
|
85
|
+
# scope: -> { MeetingRoom.active }, # Optional: filter available options
|
|
86
|
+
# null_option: "Select room" # Optional: placeholder text
|
|
87
|
+
# )
|
|
88
|
+
# end
|
|
89
|
+
#
|
|
90
|
+
# Alternative display options:
|
|
91
|
+
#
|
|
92
|
+
# field :meeting_room_id do
|
|
93
|
+
# foreign_key(
|
|
94
|
+
# model: MeetingRoom,
|
|
95
|
+
# display: ->(room) { "#{room.name} (#{room.capacity} seats)" } # Proc for complex display
|
|
96
|
+
# )
|
|
97
|
+
# end
|
|
98
|
+
|
|
99
|
+
# Routes (add to your routes.rb):
|
|
100
|
+
# resources :meetings
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Foreign Key Support in ElaineCrud
|
|
2
|
+
|
|
3
|
+
ElaineCrud now provides comprehensive automatic support for `belongs_to` relationships in ActiveRecord models. This feature eliminates the need for manual configuration in most cases while still providing flexibility for customization.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
### Automatic Detection
|
|
8
|
+
- Automatically detects all `belongs_to` relationships in your ActiveRecord model
|
|
9
|
+
- Auto-configures foreign key fields without requiring manual setup
|
|
10
|
+
- Includes foreign keys in permitted parameters automatically
|
|
11
|
+
- Optimizes database queries with automatic `includes()` to prevent N+1 queries
|
|
12
|
+
|
|
13
|
+
### Smart Display Field Detection
|
|
14
|
+
- Automatically determines the best display field for related models
|
|
15
|
+
- Prefers common fields like `:name`, `:title`, `:display_name`, `:full_name`, `:label`, `:description`
|
|
16
|
+
- Falls back to first string/text column if common fields aren't found
|
|
17
|
+
- Uses `:id` as final fallback
|
|
18
|
+
|
|
19
|
+
### Display Behavior
|
|
20
|
+
- **Index View**: Shows the related record's display field instead of the foreign key ID
|
|
21
|
+
- **Edit Forms**: Renders a dropdown with all available options from the related model, with the current value pre-selected
|
|
22
|
+
- **Error Handling**: Gracefully handles missing records and configuration errors
|
|
23
|
+
|
|
24
|
+
## Basic Usage
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
class MeetingsController < ElaineCrud::BaseController
|
|
28
|
+
model Meeting # Automatically detects belongs_to :meeting_room
|
|
29
|
+
permit_params :title, :start_time, :end_time # meeting_room_id included automatically
|
|
30
|
+
end
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
That's it! ElaineCrud will automatically:
|
|
34
|
+
1. Detect the `belongs_to :meeting_room` relationship
|
|
35
|
+
2. Configure `meeting_room_id` as a foreign key field
|
|
36
|
+
3. Show meeting room names in the index instead of IDs
|
|
37
|
+
4. Provide a dropdown in forms with all meeting rooms
|
|
38
|
+
5. Include `meeting_room_id` in permitted parameters
|
|
39
|
+
|
|
40
|
+
## Customization Options
|
|
41
|
+
|
|
42
|
+
### Basic Customization
|
|
43
|
+
```ruby
|
|
44
|
+
field :meeting_room_id do
|
|
45
|
+
title "Conference Room"
|
|
46
|
+
foreign_key(
|
|
47
|
+
model: MeetingRoom,
|
|
48
|
+
display: :name,
|
|
49
|
+
null_option: "Choose a room"
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Advanced Display Logic
|
|
55
|
+
```ruby
|
|
56
|
+
field :meeting_room_id do
|
|
57
|
+
foreign_key(
|
|
58
|
+
model: MeetingRoom,
|
|
59
|
+
display: ->(room) { "#{room.name} (#{room.capacity} seats)" }
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Scoped Options
|
|
65
|
+
```ruby
|
|
66
|
+
field :meeting_room_id do
|
|
67
|
+
foreign_key(
|
|
68
|
+
model: MeetingRoom,
|
|
69
|
+
display: :name,
|
|
70
|
+
scope: -> { MeetingRoom.available.order(:name) }
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Custom Method Display
|
|
76
|
+
```ruby
|
|
77
|
+
# In MeetingRoom model
|
|
78
|
+
class MeetingRoom < ApplicationRecord
|
|
79
|
+
def display_name
|
|
80
|
+
"#{name} - #{building.name}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# In controller
|
|
85
|
+
field :meeting_room_id do
|
|
86
|
+
foreign_key(
|
|
87
|
+
model: MeetingRoom,
|
|
88
|
+
display: :display_name
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Configuration Options
|
|
94
|
+
|
|
95
|
+
| Option | Type | Description | Default |
|
|
96
|
+
|--------|------|-------------|---------|
|
|
97
|
+
| `model` | Class | The related ActiveRecord model | Auto-detected from belongs_to |
|
|
98
|
+
| `display` | Symbol/Proc | Field or method to display | Auto-detected (name, title, etc.) |
|
|
99
|
+
| `scope` | Proc | Limits available options | `-> { Model.all }` |
|
|
100
|
+
| `null_option` | String | Placeholder text for blank option | "Select [relationship name]" |
|
|
101
|
+
|
|
102
|
+
## Implementation Details
|
|
103
|
+
|
|
104
|
+
### Automatic Model Analysis
|
|
105
|
+
When you call `model ModelClass`, ElaineCrud:
|
|
106
|
+
1. Inspects all `belongs_to` reflections on the model
|
|
107
|
+
2. Creates `FieldConfiguration` objects for each foreign key
|
|
108
|
+
3. Determines the best display field for each related model
|
|
109
|
+
4. Adds foreign keys to the permitted parameters list
|
|
110
|
+
|
|
111
|
+
### Query Optimization
|
|
112
|
+
The `fetch_records` method automatically includes belongs_to associations to prevent N+1 queries:
|
|
113
|
+
```ruby
|
|
114
|
+
# Instead of this (N+1 queries):
|
|
115
|
+
meetings.each { |meeting| puts meeting.meeting_room.name }
|
|
116
|
+
|
|
117
|
+
# ElaineCrud automatically does this:
|
|
118
|
+
Meeting.includes(:meeting_room).each { |meeting| puts meeting.meeting_room.name }
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Display Field Detection Logic
|
|
122
|
+
1. Check for common display fields: `name`, `title`, `display_name`, `full_name`, `label`, `description`
|
|
123
|
+
2. If none found, use first string/text column (excluding id, created_at, updated_at)
|
|
124
|
+
3. Fall back to `:id` if no suitable field found
|
|
125
|
+
|
|
126
|
+
## Error Handling
|
|
127
|
+
|
|
128
|
+
ElaineCrud handles various error scenarios gracefully:
|
|
129
|
+
- **Missing Related Record**: Shows "Not found (ID: X)" message
|
|
130
|
+
- **Configuration Errors**: Shows error message in development, falls back to ID in production
|
|
131
|
+
- **Missing Display Field**: Falls back to `to_s` method
|
|
132
|
+
- **Database Errors**: Gracefully handles and logs errors
|
|
133
|
+
|
|
134
|
+
## Migration from Manual Configuration
|
|
135
|
+
|
|
136
|
+
If you previously configured foreign keys manually, ElaineCrud will respect your existing configuration:
|
|
137
|
+
```ruby
|
|
138
|
+
# Your existing configuration takes precedence
|
|
139
|
+
field :meeting_room_id do
|
|
140
|
+
foreign_key(model: MeetingRoom, display: :custom_name)
|
|
141
|
+
end
|
|
142
|
+
# Auto-detection skips this field since it's already configured
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Performance Considerations
|
|
146
|
+
|
|
147
|
+
- **Automatic Includes**: Foreign key relationships are automatically included in queries
|
|
148
|
+
- **Lazy Loading**: Related records are only loaded when actually displayed
|
|
149
|
+
- **Caching**: Consider adding Rails caching for frequently accessed dropdown options
|
|
150
|
+
|
|
151
|
+
## Future Enhancements
|
|
152
|
+
|
|
153
|
+
The current implementation focuses on `belongs_to` relationships. Future versions may include:
|
|
154
|
+
- `has_many` relationship support
|
|
155
|
+
- `has_and_belongs_to_many` relationship support
|
|
156
|
+
- Polymorphic relationship support
|
|
157
|
+
- Search/autocomplete for large datasets
|
|
158
|
+
- Nested attribute support
|
|
159
|
+
|
|
160
|
+
## Example Models
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
class Meeting < ApplicationRecord
|
|
164
|
+
belongs_to :meeting_room
|
|
165
|
+
belongs_to :organizer, class_name: 'User'
|
|
166
|
+
|
|
167
|
+
validates :title, presence: true
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
class MeetingRoom < ApplicationRecord
|
|
171
|
+
has_many :meetings
|
|
172
|
+
validates :name, presence: true
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
class User < ApplicationRecord
|
|
176
|
+
has_many :organized_meetings, class_name: 'Meeting', foreign_key: 'organizer_id'
|
|
177
|
+
end
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
With these models, ElaineCrud automatically handles both `meeting_room_id` and `organizer_id` foreign keys in your `MeetingsController`.
|
|
181
|
+
|
|
182
|
+
## Troubleshooting
|
|
183
|
+
|
|
184
|
+
### Foreign Key Not Showing as Dropdown
|
|
185
|
+
- Ensure your model has a `belongs_to` relationship defined
|
|
186
|
+
- Check that the foreign key column exists in the database
|
|
187
|
+
- Verify the related model class is accessible
|
|
188
|
+
|
|
189
|
+
### Wrong Display Field Being Used
|
|
190
|
+
- Manually configure the `display` option in field configuration
|
|
191
|
+
- Add a custom display method to your model
|
|
192
|
+
- Check model column names match expected patterns
|
|
193
|
+
|
|
194
|
+
### Performance Issues
|
|
195
|
+
- Add database indexes on foreign key columns
|
|
196
|
+
- Consider using scopes to limit dropdown options
|
|
197
|
+
- Implement caching for frequently accessed dropdowns
|