Module: Controllers::Attachable
- Extended by:
- ActiveSupport::Concern
- Included in:
- ActivitiesController, ArticleProceduresController, ArticleTechnicalsController, ArticleTrainingsController, CommunicationsController, Crm::ExportedCatalogItemPacketsController, Crm::PostCommentsController, Crm::PostsController, Crm::SmsMessagesController, OpportunitiesController, RmasController, RoomConfigurationsController, SupportCasesController, TopicsController, VouchersController
- Defined in:
- app/concerns/controllers/attachable.rb
Overview
Shared controller actions for attaching uploads and library publications
to a context record (Article, Communication, and similar models).
Provides Turbo Stream endpoints to attach uploaded files or existing
publications, remove attachments, search the publications library, and
render the publication-picker offcanvas.
Constant Summary collapse
- PUBLICATIONS_PER_PAGE =
Publications per page.
20
Instance Method Summary collapse
-
#attach ⇒ void
Attaches uploads to the context record, or stages them for a new record.
-
#publication_modal ⇒ void
Renders the publication picker offcanvas on-demand into a Turbo Frame.
-
#remove_attachment ⇒ void
Removes an upload from the context record, deleting it only when safe.
-
#search_library ⇒ void
Searches the publications library for attachable items.
Instance Method Details
#attach ⇒ void
This method returns an undefined value.
Attaches uploads to the context record, or stages them for a new record.
Logic Details
Three scenarios are handled based on params[:context_object_id] /
params[:id]: a new record (no valid ID — uploads are created but not
attached, and are submitted via hidden form fields), an invalid record
(ID given but record not found — sets a flash error), or an existing
record (uploads are attached via #perform_attach).
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'app/concerns/controllers/attachable.rb', line 26 def attach load_context # Set controller_path for route generation in components # This handles irregular controllers (e.g., article_trainings vs Article model) @controller_path = controller_name # Capture parent_form_id if provided (for new records - hidden fields need form attribute) @parent_form_id = params[:parent_form_id] # Determine the scenario: new record, existing record, or invalid record context_object_id = params[:context_object_id] || params[:id] is_new_record = context_object_id.blank? || context_object_id.to_i == 0 is_invalid_record = !is_new_record && @context_object.nil? is_existing_record = !is_new_record && @context_object.present? # Execute appropriate action based on scenario if is_new_record # New record: create uploads but don't attach them (will be submitted via hidden fields) perform_attach(nil) elsif is_invalid_record # Existing record ID provided but record not found - set flash error flash[:error] = 'Unable to attach: Communication not found. Please refresh the page.' @uploads = [] # Ensure @uploads is set for template elsif is_existing_record # Existing record: attach uploads to the record perform_attach(@context_object) end # Single response block for all scenarios # Flash messages are automatically handled by TurboStreamFlashable after_action respond_to do |format| format.turbo_stream { render 'attachable/attach' } end end |
#publication_modal ⇒ void
This method returns an undefined value.
Renders the publication picker offcanvas on-demand into a Turbo Frame.
This avoids nested forms by loading the offcanvas separately.
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'app/concerns/controllers/attachable.rb', line 205 def publication_modal load_context @controller_path = controller_name context_object_id = params[:context_object_id] || @context_object&.id || 0 @offcanvas_id = "publication-picker-#{context_object_id}" render partial: 'attachable/publication_offcanvas', layout: false, locals: { offcanvas_id: @offcanvas_id, context_object: @context_object, context_class_name: params[:context_class], controller_path: @controller_path } end |
#remove_attachment ⇒ void
This method returns an undefined value.
Removes an upload from the context record, deleting it only when safe.
Logic Details
For saved records (params[:context_object_id] present), the upload is
unlinked from the record and only deleted when #perform_remove_attachment
deems it safe. For new/unsaved records, uploaded files can be deleted
(Upload#ok_to_delete? true), but literature/publications should NOT be
deleted. The upload is removed from the DOM via Turbo Stream regardless.
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'app/concerns/controllers/attachable.rb', line 72 def @upload = Upload.find(params[:upload_id]) # Load context if provided (for existing records) if params[:context_object_id].present? load_context # For saved records: unlink the upload from the record, and only delete if safe # (uploaded files can be deleted, but literature/publications should only be unlinked) (@context_object) elsif @upload.ok_to_delete? && can?(:destroy, @upload) # For new/unsaved records (no context_object_id yet): # - Uploaded files: can be safely deleted (ok_to_delete? returns true) # - Literature/publications: should NOT be deleted (ok_to_delete? returns false) # The upload is removed from the DOM via Turbo Stream regardless @upload.destroy end respond_to do |format| format.turbo_stream { render 'attachable/remove_attachment' } format.html { redirect_back_or_to(root_path, notice: 'Attachment removed') } end end |
#search_library ⇒ void
This method returns an undefined value.
Searches the publications library for attachable items.
Logic Details
Uses cursor-based (keyset) pagination on items.id for infinite scroll:
fetches PUBLICATIONS_PER_PAGE + 1 rows to detect a next page and returns
the last item's ID as @cursor. A blank search term yields no results.
The params[:append] flag selects between appending results to the grid
(infinite scroll) and replacing the whole picker frame (fresh search).
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 |
# File 'app/concerns/controllers/attachable.rb', line 108 def search_library load_context context_object_id = @context_object.try(:id).to_i is_append = params[:append].to_s == 'true' if params[:term].present? # Cursor-based pagination using item.id for keyset navigation # Eager load literature and primary_image to avoid N+1 queries publications_query = Item.publications.active .joins(:literature) .includes(:literature, :primary_image) .keywords_search(params[:term]) .order('items.id DESC') # Apply cursor filter if provided (keyset pagination) publications_query = publications_query.where(items: { id: ...params[:cursor].to_i }) if params[:cursor].present? @publications = publications_query.limit(PUBLICATIONS_PER_PAGE + 1).to_a # Check if there are more results @has_more = @publications.size > PUBLICATIONS_PER_PAGE @publications = @publications.first(PUBLICATIONS_PER_PAGE) # Next cursor is the last item's ID @cursor = @publications.last&.id else @publications = [] @has_more = false @cursor = nil end # Get existing upload IDs to show "Already Attached" state existing_upload_ids = @context_object.try(:upload_ids) || [] respond_to do |format| format.turbo_stream do if is_append # Appending more results (infinite scroll) # Use multiple streams: append items to grid, replace trigger with new trigger render 'attachable/search_library_append', locals: { publications: @publications, existing_upload_ids: existing_upload_ids, context_object_id: context_object_id, context_class_name: params[:context_class], controller_path: controller_name, cursor: @cursor, has_more: @has_more, search_term: params[:term] } else # Fresh search - replace entire frame render turbo_stream: turbo_stream.update( "publication-picker-frame-#{context_object_id}", Crm::PublicationPickerComponent.new( offcanvas_id: "publication-picker-#{context_object_id}", context_object: @context_object, publications: @publications, existing_upload_ids: existing_upload_ids, body_only: true, search_term: params[:term], cursor: @cursor, has_more: @has_more ) ) end end format.html do if is_append # Turbo-frame lazy loading with turbo_stream response # Must set content_type so Turbo processes the stream actions render 'attachable/search_library_append', formats: [:turbo_stream], content_type: 'text/vnd.turbo-stream.html', locals: { publications: @publications, existing_upload_ids: existing_upload_ids, context_object_id: context_object_id, context_class_name: params[:context_class], controller_path: controller_name, cursor: @cursor, has_more: @has_more, search_term: params[:term] } elsif request.headers['Turbo-Frame'].present? render 'attachable/search_library', layout: false else render 'attachable/search_library' end end end end |