Class: Crm::UploadCardComponent

Inherits:
ViewComponent::Base
  • Object
show all
Defined in:
app/components/crm/upload_card_component.rb

Overview

ViewComponent: renders the upload card block.

Shows a single Upload as a card with a thumbnail, attachment name,
file size, a Fancybox preview link, a hidden +upload_ids[]+ field for
form submission, and an optional remove button.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(upload:, context_object: nil, context_class: nil, controller_path: nil, disable_delete: false, parent_form_id: nil) ⇒ UploadCardComponent

Returns a new instance of UploadCardComponent.

Parameters:

  • upload (Upload)

    the upload to render

  • context_object (Object, nil) (defaults to: nil)

    the record the upload is attached to

  • context_class (Class, String, nil) (defaults to: nil)

    the context class; defaults to
    +context_object+'s class, and is required when +context_object+ is nil

  • controller_path (String, nil) (defaults to: nil)

    controller path for the remove URL;
    defaults to the tableized +context_class+ name

  • disable_delete (Boolean) (defaults to: false)

    when true, hides the remove button

  • parent_form_id (String, nil) (defaults to: nil)

    DOM id of an external form to
    associate the hidden field with via the +form+ attribute

Raises:

  • (ArgumentError)

    when +context_class+ cannot be inferred



27
28
29
30
31
32
33
34
35
36
37
# File 'app/components/crm/upload_card_component.rb', line 27

def initialize(upload:, context_object: nil, context_class: nil, controller_path: nil, disable_delete: false, parent_form_id: nil)
  super()
  @upload = upload
  @context_object = context_object
  @context_class = context_class || context_object&.class
  raise ArgumentError, 'context_class is required when context_object is nil' if @context_class.nil?

  @controller_path = controller_path || (@context_class.is_a?(Class) ? @context_class.name.tableize : @context_class.to_s.tableize)
  @disable_delete = disable_delete
  @parent_form_id = parent_form_id
end

Instance Attribute Details

#context_classUpload, ... (readonly)

Returns:

  • (Upload)

    the upload rendered by this card

  • (Object, nil)

    the record the upload is attached to

  • (Class, String)

    class (or class name) used to scope upload params and routes

  • (String)

    controller path used to build the remove-attachment URL

  • (Boolean)

    whether the remove button is suppressed

  • (String, nil)

    DOM id of an external form the hidden field belongs to



15
16
17
# File 'app/components/crm/upload_card_component.rb', line 15

def context_class
  @context_class
end

#context_objectUpload, ... (readonly)

Returns:

  • (Upload)

    the upload rendered by this card

  • (Object, nil)

    the record the upload is attached to

  • (Class, String)

    class (or class name) used to scope upload params and routes

  • (String)

    controller path used to build the remove-attachment URL

  • (Boolean)

    whether the remove button is suppressed

  • (String, nil)

    DOM id of an external form the hidden field belongs to



15
16
17
# File 'app/components/crm/upload_card_component.rb', line 15

def context_object
  @context_object
end

#controller_pathUpload, ... (readonly)

Returns:

  • (Upload)

    the upload rendered by this card

  • (Object, nil)

    the record the upload is attached to

  • (Class, String)

    class (or class name) used to scope upload params and routes

  • (String)

    controller path used to build the remove-attachment URL

  • (Boolean)

    whether the remove button is suppressed

  • (String, nil)

    DOM id of an external form the hidden field belongs to



15
16
17
# File 'app/components/crm/upload_card_component.rb', line 15

def controller_path
  @controller_path
end

#disable_deleteUpload, ... (readonly)

Returns:

  • (Upload)

    the upload rendered by this card

  • (Object, nil)

    the record the upload is attached to

  • (Class, String)

    class (or class name) used to scope upload params and routes

  • (String)

    controller path used to build the remove-attachment URL

  • (Boolean)

    whether the remove button is suppressed

  • (String, nil)

    DOM id of an external form the hidden field belongs to



15
16
17
# File 'app/components/crm/upload_card_component.rb', line 15

def disable_delete
  @disable_delete
end

#parent_form_idUpload, ... (readonly)

Returns:

  • (Upload)

    the upload rendered by this card

  • (Object, nil)

    the record the upload is attached to

  • (Class, String)

    class (or class name) used to scope upload params and routes

  • (String)

    controller path used to build the remove-attachment URL

  • (Boolean)

    whether the remove button is suppressed

  • (String, nil)

    DOM id of an external form the hidden field belongs to



15
16
17
# File 'app/components/crm/upload_card_component.rb', line 15

def parent_form_id
  @parent_form_id
end

#uploadUpload, ... (readonly)

Returns:

  • (Upload)

    the upload rendered by this card

  • (Object, nil)

    the record the upload is attached to

  • (Class, String)

    class (or class name) used to scope upload params and routes

  • (String)

    controller path used to build the remove-attachment URL

  • (Boolean)

    whether the remove button is suppressed

  • (String, nil)

    DOM id of an external form the hidden field belongs to



15
16
17
# File 'app/components/crm/upload_card_component.rb', line 15

def upload
  @upload
end

Instance Method Details

#attachment_nameString

Human-readable attachment name.

Returns:

  • (String)

    the attachment name



80
81
82
# File 'app/components/crm/upload_card_component.rb', line 80

def attachment_name
  helpers.attachment_name(upload)
end

URL for the download link.

Returns:

  • (String)

    the upload download path



100
101
102
# File 'app/components/crm/upload_card_component.rb', line 100

def download_link
  helpers.upload_path(upload)
end

#fancybox_groupString

Fancybox group name; unique per upload to prevent gallery behavior.

Returns:

  • (String)

    e.g. +"upload-42"+



121
122
123
# File 'app/components/crm/upload_card_component.rb', line 121

def fancybox_group
  "upload-#{upload.id}"
end

#fancybox_typeString

Fancybox content type derived from the upload's MIME type.

Returns:

  • (String)

    +'image'+, +'pdf'+, or +'iframe'+



106
107
108
109
110
111
112
113
114
115
116
117
# File 'app/components/crm/upload_card_component.rb', line 106

def fancybox_type
  # Use cached mime_type from database (fast) instead of attachment.mime_type
  # which can trigger network calls
  mime = upload.attachment_mime_type || upload.mime_type
  if mime&.start_with?('image')
    'image'
  elsif mime == 'application/pdf'
    'pdf'
  else
    'iframe'
  end
end

#file_sizeString

Human-readable file size.

Returns:

  • (String)

    e.g. +"1.2 MB"+



86
87
88
# File 'app/components/crm/upload_card_component.rb', line 86

def file_size
  helpers.number_to_human_size(upload.attachment_size)
end

#hidden_field_tag_htmlActiveSupport::SafeBuffer

Hidden +upload_ids[]+ field linking this upload to the context's form.

Returns:

  • (ActiveSupport::SafeBuffer)

    the hidden field tag HTML



41
42
43
44
45
46
47
48
49
50
51
52
# File 'app/components/crm/upload_card_component.rb', line 41

def hidden_field_tag_html
  # Handle both Class and String inputs for context_class
  class_name = context_class.is_a?(String) ? context_class : context_class.name
  param_name = "#{class_name.tableize.singularize}[upload_ids][]"

  # If parent_form_id is specified, associate the hidden field with that form
  if parent_form_id.present?
    helpers.hidden_field_tag param_name, upload.id, form: parent_form_id
  else
    helpers.hidden_field_tag param_name, upload.id
  end
end

#image_urlString?

Thumbnail URL for the card image, or +nil+ for non-thumbnailable files.

Returns:

  • (String, nil)

    cover/presigned/thumbnail URL, or +nil+ on error



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'app/components/crm/upload_card_component.rb', line 56

def image_url
  # For publications: use ImageKit CDN cover image (fast, no network calls)
  # For images: use presigned URL directly (browser scales, no Dragonfly processing)
  # For PDFs: use Dragonfly/libvips generated thumbnail
  # For other files: no thumbnail
  if upload.publication
    upload.publication.cover_image_url(width: 400)
  elsif upload.is_image?
    # Use presigned URL directly - browser can display and scale the image
    # This avoids slow Dragonfly thumbnail generation
    upload.presigned_url(expires_in: 6.days)
  elsif upload.is_pdf?
    # Use Dragonfly libvips for PDF thumbnail generation (first page)
    upload.thumbnail_url(dimensions: '300x300>')
  else
    nil # Skip for non-thumbnailable files (e.g., .doc, .xls)
  end
rescue StandardError => e
  Rails.logger.debug { "[UploadCardComponent] image_url error: #{e.message}" }
  nil
end

#preview_urlString

URL for the Fancybox preview link.

Returns:

  • (String)

    a presigned URL valid for 6 days



92
93
94
95
96
# File 'app/components/crm/upload_card_component.rb', line 92

def preview_url
  # Use local presigned URL generation (fast, no S3 network call)
  # No fallback to slow remote_url - if presigned fails, link will be broken but page loads fast
  upload.presigned_url(expires_in: 6.days)
end

#remove_buttonActiveSupport::SafeBuffer

Remove button linking to the context controller's +remove_attachment+ action.

Returns:

  • (ActiveSupport::SafeBuffer)

    the link HTML



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
# File 'app/components/crm/upload_card_component.rb', line 127

def remove_button
  # Use controller_path explicitly for route generation
  # This handles irregular controllers (e.g., article_trainings vs Article model)
  remove_params = {
    upload_id: upload.id,
    context_object_id: context_object&.id || 0,
    context_class: context_class.name,
    format: :turbo_stream
  }

  # Use url_for with explicit controller_path for route generation
  remove_url = helpers.url_for(controller: controller_path,
                                action: :remove_attachment,
                                **remove_params,
                                only_path: true)

  helpers.link_to(
    remove_url,
    class: 'btn btn-sm btn-outline-danger w-100',
    'aria-label': "Remove #{attachment_name}",
    data: {
      turbo_method: :delete,
      turbo_stream: true,
      turbo_confirm: 'Are you sure you want to delete this attachment?'
    }
  ) do
    fa_icon('trash', text: 'Remove')
  end
end