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,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElaineCrud
4
+ # Base controller providing CRUD functionality for ActiveRecord models
5
+ #
6
+ # Usage:
7
+ # class PeopleController < ElaineCrud::BaseController
8
+ # model Person
9
+ # permit_params :name, :email, :phone
10
+ # end
11
+ class BaseController < ActionController::Base
12
+ # Include all concerns
13
+ include ElaineCrud::SortingConcern
14
+ include ElaineCrud::SearchAndFiltering
15
+ include ElaineCrud::DSLMethods
16
+ include ElaineCrud::FieldConfigurationMethods
17
+ include ElaineCrud::LayoutCalculation
18
+ include ElaineCrud::RelationshipHandling
19
+ include ElaineCrud::RecordFetching
20
+ include ElaineCrud::ParameterHandling
21
+ include ElaineCrud::ExportHandling
22
+
23
+ # Include view helpers so they're available in lambda contexts
24
+ include ActionView::Helpers::TagHelper
25
+ include ActionView::Helpers::UrlHelper
26
+ include ActionView::Helpers::TextHelper
27
+ include ActionView::Helpers::NumberHelper
28
+ include ActionView::Helpers::DateHelper
29
+
30
+ protect_from_forgery with: :exception
31
+ # No layout specified - host app controllers should set their own layout
32
+
33
+ # Handle record not found errors with custom 404 page
34
+ rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
35
+
36
+ # Standard CRUD actions
37
+ def index
38
+ @records = fetch_records
39
+ @model_name = crud_model.name
40
+ @columns = determine_columns
41
+
42
+ # Search/filter metadata
43
+ @search_query = search_query
44
+ @active_filters = filters
45
+ @searchable_columns = determine_searchable_columns
46
+ @filterable_columns = determine_filterable_columns
47
+ @total_count = total_unfiltered_count if search_active?
48
+ end
49
+
50
+ def show
51
+ @record = find_record
52
+ @model_name = crud_model.name
53
+ @columns = determine_columns
54
+ render 'elaine_crud/base/show'
55
+ end
56
+
57
+ def new
58
+ @record = crud_model.new
59
+ @model_name = crud_model.name
60
+
61
+ # Pre-populate with parent relationship if filtering by parent
62
+ populate_parent_relationships(@record)
63
+
64
+ apply_field_defaults(@record)
65
+ render 'elaine_crud/base/new'
66
+ end
67
+
68
+ # Action for rendering new form in modal (for nested creates)
69
+ def new_modal
70
+ @record = crud_model.new
71
+ @model_name = crud_model.name
72
+ @modal_mode = true
73
+ @return_field = params[:return_field] # The foreign key field to update
74
+
75
+ apply_field_defaults(@record)
76
+ render 'elaine_crud/base/new_modal', layout: false
77
+ end
78
+
79
+ def create
80
+ @record = crud_model.new(record_params)
81
+
82
+ # Ensure parent relationship is maintained
83
+ populate_parent_relationships(@record) if @record.errors.any?
84
+
85
+ if @record.save
86
+ # Handle modal mode (nested create)
87
+ if params[:modal_mode] == 'true' && params[:return_field].present?
88
+ # Get field configuration from the parent controller
89
+ parent_controller_class = "#{params[:parent_model].pluralize.camelize}Controller".constantize
90
+ parent_controller = parent_controller_class.new
91
+ parent_field_config = parent_controller.send(:field_config_for, params[:return_field].to_sym)
92
+
93
+ render turbo_stream: turbo_stream.replace(
94
+ "#{params[:return_field]}_select_wrapper",
95
+ partial: 'elaine_crud/base/foreign_key_select_refresh',
96
+ locals: {
97
+ field_name: params[:return_field],
98
+ selected_id: @record.id,
99
+ field_config: parent_field_config,
100
+ parent_model: params[:parent_model]
101
+ }
102
+ )
103
+ else
104
+ # Redirect back to filtered view if came from parent context
105
+ redirect_to redirect_after_create_path, notice: "#{crud_model.name} was successfully created.", status: :see_other
106
+ end
107
+ else
108
+ @model_name = crud_model.name
109
+ @modal_mode = params[:modal_mode] == 'true'
110
+
111
+ if @modal_mode
112
+ render 'elaine_crud/base/new_modal', layout: false, status: :unprocessable_entity
113
+ else
114
+ render 'elaine_crud/base/new', status: :unprocessable_entity
115
+ end
116
+ end
117
+ end
118
+
119
+ def edit
120
+ @record = find_record
121
+ @records = fetch_records # Fetch all records for the edit page
122
+ @model_name = crud_model.name
123
+ @columns = determine_columns
124
+
125
+ # Set search/filter metadata for the edit page (which includes search bar)
126
+ @search_query = search_query
127
+ @active_filters = filters
128
+ @searchable_columns = determine_searchable_columns
129
+ @filterable_columns = determine_filterable_columns
130
+ @total_count = total_unfiltered_count if search_active?
131
+
132
+ # If Turbo is disabled, always render the full edit page
133
+ if turbo_disabled?
134
+ render 'elaine_crud/base/edit'
135
+ elsif turbo_frame_request?
136
+ # For Turbo Frame requests, return just the edit row partial
137
+ render partial: 'elaine_crud/base/edit_row', locals: { record: @record, columns: @columns }
138
+ else
139
+ # For direct access, render the full edit page showing all records with this one in edit mode
140
+ render 'elaine_crud/base/edit'
141
+ end
142
+ end
143
+
144
+ def update
145
+ @record = find_record
146
+ @model_name = crud_model.name
147
+ @columns = determine_columns
148
+
149
+ update_params = record_params
150
+
151
+ # Debug logging for development
152
+ if Rails.env.development?
153
+ Rails.logger.info "ElaineCrud: Attempting to update with params: #{update_params.inspect}"
154
+ Rails.logger.info "ElaineCrud: Record before update: #{@record.attributes.inspect}"
155
+ end
156
+
157
+ if @record.update(update_params)
158
+ Rails.logger.info "ElaineCrud: Record updated successfully: #{@record.attributes.inspect}" if Rails.env.development?
159
+
160
+ # For Turbo Frame requests, return the view row partial
161
+ if turbo_frame_request?
162
+ # Use bg-white for consistency when returning from edit (alternating colors handled by full page refresh)
163
+ render partial: 'elaine_crud/base/view_row', locals: { record: @record, columns: @columns, row_bg_class: 'bg-white', is_last_record: false }
164
+ else
165
+ redirect_to polymorphic_path(@record), notice: "#{crud_model.name} was successfully updated.", status: :see_other
166
+ end
167
+ else
168
+ # Handle validation errors
169
+ Rails.logger.warn "ElaineCrud: Record update failed with errors: #{@record.errors.full_messages}" if Rails.env.development?
170
+
171
+ if turbo_frame_request?
172
+ # Return edit row partial with errors
173
+ render partial: 'elaine_crud/base/edit_row', locals: { record: @record, columns: @columns }, status: :unprocessable_entity
174
+ else
175
+ # Render full edit page with errors - need search/filter metadata
176
+ @records = fetch_records
177
+ @search_query = search_query
178
+ @active_filters = filters
179
+ @searchable_columns = determine_searchable_columns
180
+ @filterable_columns = determine_filterable_columns
181
+ @total_count = total_unfiltered_count if search_active?
182
+ render 'elaine_crud/base/edit', status: :unprocessable_entity
183
+ end
184
+ end
185
+ end
186
+
187
+ def cancel_edit
188
+ @record = find_record
189
+ @model_name = crud_model.name
190
+ @columns = determine_columns
191
+
192
+ # For Turbo Frame requests, return just the view row partial
193
+ if turbo_frame_request?
194
+ header_layout = calculate_layout_header(@columns.map(&:to_sym))
195
+ render partial: 'elaine_crud/base/view_row', locals: { record: @record, columns: @columns, header_layout: header_layout }
196
+ else
197
+ # For direct access, redirect to index
198
+ redirect_to action: :index
199
+ end
200
+ end
201
+
202
+ def destroy
203
+ @record = find_record
204
+ @record.destroy
205
+ redirect_to polymorphic_path(crud_model), notice: "#{crud_model.name} was successfully deleted.", status: :see_other
206
+ end
207
+
208
+ private
209
+
210
+ # Check if the request is coming from a Turbo Frame
211
+ def turbo_frame_request?
212
+ request.headers['Turbo-Frame'].present?
213
+ end
214
+
215
+ # Handle ActiveRecord::RecordNotFound with custom 404 page
216
+ def record_not_found(exception)
217
+ @model_name = crud_model.name
218
+ @model_class = crud_model
219
+ @resource_id = params[:id]
220
+ @exception_message = exception.message
221
+
222
+ respond_to do |format|
223
+ format.html { render 'elaine_crud/base/not_found', status: :not_found }
224
+ format.json { render json: { error: 'Record not found' }, status: :not_found }
225
+ end
226
+ end
227
+ end
228
+ end