Module: Controllers::Attachable

Constant Summary collapse

PUBLICATIONS_PER_PAGE =
20

Instance Method Summary collapse

Instance Method Details

#attachObject



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'app/concerns/controllers/attachable.rb', line 6

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_modalObject

Render publication picker offcanvas on-demand into Turbo Frame
This avoids nested forms by loading the offcanvas separately



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'app/concerns/controllers/attachable.rb', line 162

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_attachmentObject



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'app/concerns/controllers/attachable.rb', line 42

def remove_attachment
  @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)
    perform_remove_attachment(@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_libraryObject



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
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
# File 'app/concerns/controllers/attachable.rb', line 67

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