Skip to content

Attachments & Uploads

The Crm::AttachmentsComponent is a modern, consolidated ViewComponent that replaces the legacy three-panel attachment system with a single, clean interface.

  • Single Panel Design: Consolidates upload form, publication search, and attachments grid into one compact panel
  • Turbo-Powered: Uses Turbo Streams and Turbo Frames for seamless updates without page refreshes
  • Modal Publication Picker: Clean modal interface for searching and attaching publications
  • Multiple File Upload: Support for selecting and uploading multiple files at once
  • Responsive Grid: Bootstrap 5 responsive grid showing 1-6 columns depending on screen size
  • Reusable: Can be used with any model that has the attachable concern
Crm::AttachmentsComponent
├── Action Buttons (Upload File, Add Publication)
├── Hidden File Upload Form (with Stimulus controller)
├── Turbo Frame: Attachments Grid
│ └── Individual Upload Cards (Crm::UploadCardComponent)
└── Publication Picker Modal (Turbo Frame for search results)
<%= render Crm::AttachmentsComponent.new(context_object: @communication) %>
<%= render Crm::AttachmentsComponent.new(
context_object: @communication,
wrapped: true, # Wrap in simple_panel (default: true)
skip_publications: false, # Hide publication picker (default: false)
multiple_files_allowed: true, # Allow multiple file selection (default: true)
template_options_for_select: nil, # Optional template radio buttons
category_options_for_select: nil # Optional category radio buttons
) %>

For embedding in custom layouts:

<%= render Crm::AttachmentsComponent.new(
context_object: @customer,
wrapped: false
) %>
<%= attachable_panel(@communication) %>
<%= attachable_form @communication %>

This generated:

  • Upload File panel with file input and preview
  • Search Library panel with search form
  • Attachments grid (separate)
<%= render Crm::AttachmentsComponent.new(context_object: @communication) %>

This generates:

  • Single “Attachments” panel
  • Two action buttons (Upload File, Add Publication) in header
  • Hidden file input (triggered by button, no preview, no base64 encoding)
  • Modal for publication search (lazy-loaded)
  • Same responsive attachments grid
  1. No Image Preview: The component intentionally does NOT use the image-preview Stimulus controller or SimpleForm’s image_file input type. This prevents:

    • Base64 encoding of images in the HTML
    • Unnecessary DOM bloat
    • Memory issues with large files
    • Complexity in the upload flow
  2. Plain File Input: Uses file_field_tag instead of SimpleForm’s f.input :attachment, as: :image_file to avoid automatic preview generation.

  3. Dual Controller Pattern:

    • crm-attachments handles UI interactions (button clicks)
    • file-upload handles the actual upload logic
    • This separation of concerns makes the code more maintainable
  4. Capture Phase Event Handling: The file-upload controller uses addEventListener(..., true) to ensure it processes file selection before any other handlers that might interfere.

app/components/crm/
├── attachments_component.rb # Main component class
├── attachments_component.html.erb # Component template
└── upload_card_component.rb # Individual upload card
└── upload_card_component.html.erb
app/views/crm/attachments/
└── _publication_modal.html.erb # Modal with Turbo Frame
app/javascript/controllers/
├── crm_attachments_controller.js # Handles upload button click
└── file_upload_controller.js # Handles file selection & upload
app/views/attachable/
├── _picker.html.erb # Publication search results
├── attach.turbo_stream.erb # Response for file uploads
└── remove_attachment.turbo_stream.erb # Response for removals
  1. Attachments Frame: attachments-frame-{id}

    • Wraps the entire attachments grid
    • Enables Turbo even though Turbo Drive is globally disabled
  2. Publication Picker Frame: publication-picker-frame-{id}

    • Loads publication search interface lazily when modal opens
    • Updates with search results without closing modal
  1. Upload Response: attach.turbo_stream.erb

    • Appends new upload card(s) to attachments grid
    • Shows error flash messages for failed uploads
  2. Remove Response: remove_attachment.turbo_stream.erb

    • Removes upload card from DOM by ID

Purpose: Manages the attachment panel interactions (button clicks only)

Targets:

  • fileInput: The hidden file input element

Actions:

  • openFileUpload: Triggers click on hidden file input to open file picker

Example:

<button data-action="click->crm-attachments#openFileUpload">
Upload File
</button>

Note: This controller only handles opening the file picker. The actual upload is delegated to the file-upload controller.

Purpose: Handles file selection and AJAX upload (attached to the form element)

Actions:

  • Listens for file input change events (using capture phase to prevent conflicts)
  • Prevents duplicate uploads with isUploading flag
  • Shows progress spinners for each file being uploaded
  • Sends files via fetch with Accept: text/vnd.turbo-stream.html
  • Processes Turbo Stream responses to append uploaded files to grid
  • Cleans up progress spinners and resets form after upload

Important:

  • The form must NOT include the image-preview controller to avoid base64 encoding
  • Uses plain file_field_tag instead of SimpleForm’s image_file input type
  • Event listener uses capture phase (addEventListener(..., true)) to ensure it runs before any other handlers
  • Lazy Loading: Content loaded only when modal is opened
  • Turbo Frame: Search form and results stay within modal
  • Pagination: Navigate through results without closing modal
  • Responsive Grid: 1-2 columns on mobile/desktop
  • Already Attached: Shows checkmark for publications already attached
  • Turbo Stream Attach: Clicking “Attach” adds publication without closing modal
  1. User clicks “Add Publication” button
  2. Modal opens, Turbo Frame loads search interface (search_library action)
  3. User enters search term and submits
  4. Turbo Frame updates with results (stays in modal)
  5. User clicks “Attach” on a publication
  6. Turbo Stream appends publication to attachments grid
  7. Publication shows checkmark in modal

The Crm::UploadCardComponent renders each individual attachment:

  • 1:1 Aspect Ratio: Perfect square images using Bootstrap ratio-1x1
  • Object-fit Cover: Images fill the square without distortion
  • Responsive: Works in 1-6 column grids
  • Literature Support: Shows publication cover images for PDFs
  • Footer Actions: Consistent button placement with Remove button
<div class="col" id="upload_{id}">
<div class="card h-100">
<input type="hidden" name="...upload_ids[]" value="{id}">
<div class="ratio ratio-1x1 overflow-hidden">
<a href="{preview_url}">
<img src="{thumbnail}" class="w-100 h-100 object-fit-cover">
</a>
</div>
<div class="card-footer text-center">
<div class="small text-truncate">{filename}</div>
<button type="submit" class="btn btn-sm btn-outline-danger">
Remove
</button>
</div>
</div>
</div>

The Attachable concern provides the necessary controller actions:

concern :attachable do
collection do
post :attach # Upload files or attach publications
delete :remove_attachment # Remove attachment
get :search_library # Search publications (for modal)
end
end
resources :communications, concerns: [:attachable]

This generates:

  • POST /communications/attach
  • DELETE /communications/remove_attachment
  • GET /communications/search_library
  • Before: 3 separate panels, ~500px vertical space
  • After: 1 panel with modal, ~300px vertical space
  • Savings: ~40% less vertical space
  • Before:

    • Scroll between upload form and library search
    • File preview takes up space
    • Search results inline, push content down
  • After:

    • All actions accessible from one panel header
    • No preview, direct upload
    • Search in modal, doesn’t affect page layout
    • Cleaner, more professional appearance
  • Before: All panels load on page load
  • After: Publication search lazy-loaded only when needed
  • Before: Logic spread across helpers, partials, and legacy JS
  • After: Encapsulated in ViewComponent with clear responsibilities

To test the component:

  1. Upload Files:

    • Click “Upload File” button
    • Select one or multiple files
    • Verify files appear in grid without page refresh
    • Verify progress spinners show during upload
  2. Attach Publications:

    • Click “Add Publication” button
    • Modal should open with search interface
    • Search for a publication
    • Click “Attach” on a result
    • Verify publication appears in grid
    • Verify publication shows checkmark in modal
  3. Remove Attachments:

    • Click “Remove” on any attachment
    • Confirm deletion
    • Verify attachment disappears without page refresh
  4. New Records (no ID yet):

    • Test on /communications/new
    • Upload files should work
    • Remove should work (deletes upload from DB)
  • Drag & drop file upload
  • Reordering attachments
  • Bulk delete
  • Download all as ZIP
  • Image cropping/editing
  • Attachment categories/tags
  • Version history for attachments

<%# Simple usage - wrapped in card panel %>
<%= render Crm::AttachmentsComponent.new(context_object: @communication) %>
<%# Unwrapped - no card panel %>
<%= render Crm::AttachmentsComponent.new(
context_object: @customer,
wrapped: false
) %>
<%# With options %>
<%= render Crm::AttachmentsComponent.new(
context_object: @room_configuration,
skip_publications: true, # Hide publication picker
multiple_files_allowed: false, # Single file only
template_options_for_select: [ # PDF template options
['Letter Portrait', 'letter_portrait'],
['Letter Landscape', 'letter_landscape']
],
category_options_for_select: [ # File categories
['Quote', 'quote'],
['Invoice', 'invoice']
]
) %>

Your controller must include the Attachable concern:

class CommunicationsController < ApplicationController
include Controllers::Attachable
# ... your actions ...
end
resources :communications, concerns: [:attachable]

This generates:

  • POST /communications/attach - Upload files or attach publications
  • DELETE /communications/remove_attachment - Remove attachment
  • GET /communications/search_library - Search publications
ParameterTypeDefaultDescription
context_objectActiveRecordrequiredThe object to attach files to
wrappedBooleantrueWrap in card panel with header
skip_publicationsBooleanfalseHide publication picker button
multiple_files_allowedBooleantrueAllow multiple file selection
template_options_for_selectArraynilPDF template radio buttons
category_options_for_selectArraynilFile category radio buttons

Your model must have the attachable concern:

class Communication < ApplicationRecord
include Models::Attachable
# This provides:
# - has_many :uploads
# - upload_ids getter/setter
end
  • Purpose: Opens file picker
  • Location: app/javascript/controllers/crm_attachments_controller.js
  • Actions: openFileUpload
  • Purpose: Handles file upload
  • Location: app/javascript/controllers/file_upload_controller.js
  • Auto-attached: Yes (via form data-controller)
<% @uploads.each do |upload| %>
<%= turbo_stream.append "attachments-#{@context_object.id}" do %>
<%= render partial: '/attachable/upload', locals: {
upload: upload,
context_object: @context_object,
context_class: @context_class
} %>
<% end %>
<% end %>

Remove Response (remove_attachment.turbo_stream.erb)

Section titled “Remove Response (remove_attachment.turbo_stream.erb)”
<%= turbo_stream.remove "upload_#{@upload.id}" %>

Cause: JavaScript not compiled Solution: Run yarn build

Cause: Turbo Stream response not being processed Solution: Check that controller action responds with format.turbo_stream

Cause: Using SimpleForm’s image_file input type Solution: Use plain file_field_tag (already fixed in component)

Cause: isUploading flag not working Solution: Check that file-upload controller is properly connected

Cause: Missing search_library action or route Solution: Ensure controller includes Attachable concern and routes include :attachable

The controllers already have debug logging:

// In browser console, you'll see:
📎 CRM Attachments controller connected
📎 File upload controller connected
📎 Opening file picker
📎 Files selected: 2
📎 Submitting upload...
📎 Upload response received 200
📎 Upload complete

In Network tab, look for the POST to /attach:

  • Response should have Content-Type: text/vnd.turbo-stream.html
  • Response body should contain <turbo-stream action="append">
# In rails console
Communication.find(123).uploads
# Should show the uploaded files

The component uses Bootstrap 5 classes:

<!-- Card panel (when wrapped=true) -->
<div class="card w-100 attachments-panel">
<div class="card-header">...</div>
<div class="card-body">
<!-- Responsive grid -->
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-6 g-4">
<!-- Upload cards -->
</div>
</div>
</div>
  • Mobile: 1 column
  • Small (576px+): 2 columns
  • Medium (768px+): 3 columns
  • Large (992px+): 4 columns
  • XL (1200px+): 6 columns
  1. Don’t use image previews: The component intentionally avoids base64 previews to keep HTML lightweight
  2. Lazy load publications: The publication picker is lazy-loaded only when the modal opens
  3. Use Turbo Streams: Avoid full page refreshes by using Turbo Stream responses
  4. Limit file sizes: Set max_file_size in your form validation
  1. Authorization: The Attachable concern uses CanCanCan to check permissions
  2. File validation: Upload model should validate file types and sizes
  3. CSRF protection: Automatically handled by Rails and included in fetch requests
  4. Sanitization: File names are escaped to prevent XSS
class CommunicationsControllerAttachTest < ActionDispatch::IntegrationTest
test "attaches file to communication" do
communication = create(:communication)
file = fixture_file_upload("test.pdf", "application/pdf")
post attach_path, params: {
context_object_id: communication.id,
context_class: "Communication",
upload: { attachment: [file] }
}
assert_equal 1, communication.uploads.count
assert_response :success
end
end
class AttachmentUploadTest < ApplicationSystemTestCase
test "user uploads file" do
communication = create(:communication)
visit edit_communication_path(communication)
click_button "Upload File"
attach_file "upload[attachment][]", "test/fixtures/files/test.pdf", make_visible: true
assert_text "test.pdf"
assert_equal 1, communication.reload.uploads.count
end
end
<%= attachable_panel(@communication) %>
<%= attachable_form @communication %>
<%= render Crm::AttachmentsComponent.new(context_object: @communication) %>
  • 40% less vertical space
  • Cleaner UI
  • Better performance
  • Easier to maintain

┌─────────────────────────────────────────────────────────────────┐
│ Crm::AttachmentsComponent │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Card Header │ │
│ │ ┌──────────────────┐ ┌──────────────────────────────┐ │ │
│ │ │ Upload File Btn │ │ Add Publication Btn │ │ │
│ │ │ (crm-attachments)│ │ (opens modal) │ │ │
│ │ └──────────────────┘ └──────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Hidden Form (file-upload controller) │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ <input type="file" multiple> │ │ │
│ │ │ NO image-preview controller │ │ │
│ │ │ NO base64 encoding │ │ │
│ │ └──────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Turbo Frame: attachments-frame-{id} │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ Attachments Grid: #attachments-{id} │ │ │
│ │ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ │ │
│ │ │ │ Card │ │ Card │ │ Card │ │ Card │ ... │ │ │
│ │ │ └──────┘ └──────┘ └──────┘ └──────┘ │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌──────────┐ ┌──────────────────┐ ┌─────────────┐ ┌────────────┐
│ User │ │ crm-attachments │ │ file-upload │ │ Server │
│ │ │ Controller │ │ Controller │ │ │
└────┬─────┘ └────────┬─────────┘ └──────┬──────┘ └─────┬──────┘
│ │ │ │
│ 1. Click "Upload │ │ │
│ File" button │ │ │
├────────────────────>│ │ │
│ │ │ │
│ │ 2. Find file input │ │
│ │ and trigger click()│ │
│ ├──────────────────────>│ │
│ │ │ │
│ 3. OS file picker │ │ │
│ opens │ │ │
│<───────────────────── │ │
│ │ │ │
│ 4. User selects │ │ │
│ file(s) │ │ │
│─────────────────────────────────────────────>│ │
│ │ │ │
│ │ │ 5. handleFileSelection()
│ │ │ (capture phase) │
│ │ │ │
│ │ │ 6. Show progress │
│ │ │ spinners │
│ │ │ │
│ 7. Progress │ │ │
│ spinners appear │ │ │
│<─────────────────────────────────────────────┤ │
│ │ │ │
│ │ │ 8. Submit via fetch│
│ │ │ with FormData │
│ │ ├────────────────────>│
│ │ │ │
│ │ │ │ 9. Create Upload
│ │ │ │ records
│ │ │ │
│ │ │ 10. Turbo Stream │
│ │ │ response │
│ │ │<────────────────────┤
│ │ │ │
│ │ │ 11. Render stream │
│ │ │ (append cards) │
│ │ │ │
│ 12. Upload cards │ │ │
│ appear in grid │ │ │
│<─────────────────────────────────────────────┤ │
│ │ │ │
│ │ │ 13. Remove progress│
│ │ │ spinners │
│ │ │ │
│ │ │ 14. Reset form │
│ │ │ │
│ 15. Ready for │ │ │
│ next upload │ │ │
│<─────────────────────────────────────────────┤ │
│ │ │ │
// ONLY handles UI interactions
openFileUpload(event) {
event.preventDefault()
const fileInput = this.element.querySelector('input[type="file"]')
fileInput.click() // Opens OS file picker
}
// Handles ALL upload logic
connect() {
this.setupFileUpload()
}
setupFileUpload() {
// Use capture phase to run before other handlers
fileInput.addEventListener('change', handler, true)
}
handleFileSelection(event) {
// 1. Prevent duplicates
// 2. Show progress UI
// 3. Submit via fetch
// 4. Process Turbo Stream response
// 5. Clean up
}

Problem: SimpleForm’s image_file input automatically adds image-preview controller which:

  • Reads files as base64 data URLs
  • Embeds large base64 strings in HTML
  • Causes DOM bloat and memory issues

Solution: Use plain file_field_tag without any preview controller

Problem: Multiple event listeners on the same input can conflict

Solution: Use capture phase (addEventListener(..., true)) to ensure file-upload controller runs first

// Capture phase (runs first, from root to target)
fileInput.addEventListener('change', handler, true)
// Bubble phase (runs after, from target to root)
fileInput.addEventListener('change', handler, false) // default

Problem: Mixing UI interactions with upload logic creates complex, hard-to-maintain code

Solution: Separate concerns:

  • crm-attachments: UI interactions (button clicks)
  • file-upload: Upload logic (file handling, XHR, Turbo Streams)

Server responds with:

<% @uploads.each do |upload| %>
<%= turbo_stream.append "attachments-#{@context_object.id}" do %>
<%= render partial: '/attachable/upload', locals: {
upload: upload,
context_object: @context_object
} %>
<% end %>
<% end %>

This appends new upload cards to the grid without page refresh.

While uploading, temporary progress elements are shown:

<div class="col upload-in-progress">
<div class="card h-100">
<div class="card-body text-center">
<i class="fa-solid fa-spinner fa-spin fa-2x mb-2"></i>
<p class="small mb-0">Uploading filename.pdf...</p>
</div>
</div>
</div>

These are removed when the Turbo Stream response arrives with the actual upload cards.

  1. File Selection Cancelled: No action taken
  2. Upload in Progress: Duplicate submissions prevented by isUploading flag
  3. Server Error: Progress spinners removed, alert shown to user
  4. Network Error: Caught by fetch .catch(), alert shown to user
  1. Open browser console
  2. Navigate to a page with the attachment component
  3. Click “Upload File”
  4. Watch console logs:
    📎 CRM Attachments controller connected
    📎 File upload controller connected
    📎 Opening file picker
    📎 handleFileSelection triggered
    📎 Files selected: 2
    📎 Submitting upload...
    📎 Upload response received 200
    📎 Upload complete
  1. Upload a file
  2. View page source (Cmd+U)
  3. Search for “data:image” or “base64”
  4. Should find NONE in the attachments section
  • 1 MB image → ~1.3 MB base64 in HTML
  • Multiple images → Megabytes of DOM bloat
  • Slow page rendering
  • High memory usage
  • 1 MB image → ~200 bytes in HTML (just the upload card)
  • Multiple images → Minimal DOM impact
  • Fast page rendering
  • Low memory usage
  • ✅ Chrome/Edge (Chromium)
  • ✅ Firefox
  • ✅ Safari
  • ✅ Mobile browsers (iOS Safari, Chrome Mobile)

Requires:

  • fetch API (all modern browsers)
  • FormData API (all modern browsers)
  • Turbo 7+ (included in project)

Presigned URL Uploads with Uppy and Dragonfly

Section titled “Presigned URL Uploads with Uppy and Dragonfly”

This document explains how to use the new presigned URL upload functionality that allows Uppy to upload files directly to Wasabi S3 storage while still using Dragonfly and the Upload model for backend processing.

The presigned URL upload system provides:

  • Direct uploads to Wasabi S3 storage (bypassing Rails server for file transfer)
  • Better performance for large files and multiple uploads
  • Reduced server load during file uploads
  • Maintained compatibility with existing Dragonfly and Upload model workflows
  1. Client requests presigned URL from Rails backend
  2. Rails generates presigned URL using AWS SDK for Wasabi S3
  3. Uppy uploads file directly to Wasabi using presigned URL
  4. Rails creates Upload record and processes file with Dragonfly after successful upload

Use the dedicated S3 uploader controller for new implementations:

<%= uppy_s3_uploader(
presigned_url_endpoint: presigned_url_uploads_path,
upload_complete_endpoint: upload_complete_uploads_path,
max_files: 5,
max_file_size: 50.megabytes,
allowed_file_types: ['image/*'],
hidden_field_name: 'upload_ids',
resource_type: 'Customer',
resource_id: @customer.id,
category: 'photo'
) %>

Use the existing uploader with S3 mode for backward compatibility:

<%= uppy_uploader(
endpoint: upload_uploads_path, # Still needed for XHR fallback
presigned_url_endpoint: presigned_url_uploads_path,
upload_complete_endpoint: upload_complete_uploads_path,
mode: 's3', # Enable S3 mode
max_files: 5,
max_file_size: 50.megabytes,
allowed_file_types: ['image/*'],
hidden_field_name: 'upload_ids',
resource_type: 'Customer',
resource_id: @customer.id,
category: 'photo'
) %>

Use the provided helper methods for common scenarios:

<!-- RMA image uploads with S3 -->
<%= rma_image_uploader_s3(@rma) %>
<!-- Large file uploads with S3 -->
<%= large_file_uploader_s3 %>
<!-- Dedicated S3 helpers -->
<%= rma_image_s3_uploader(@rma) %>
<%= large_file_s3_uploader %>

The system requires the @uppy/aws-s3 package, which has been added to package.json:

{
"@uppy/aws-s3": "^5.0.0"
}

The system uses existing Dragonfly S3 configuration:

  • S3_DATASTORE_ACCESS_KEY
  • S3_DATASTORE_SECRET_KEY
  • Wasabi endpoint and bucket configuration

New routes have been added to both CRM and WWW namespaces:

# CRM routes
resources :uploads do
collection do
get :presigned_url
post :upload_complete
end
end
# WWW routes
namespace :www do
resources :uploads, only: [] do
collection do
get :presigned_url
post :upload_complete
end
end
end

Generates a presigned URL for direct S3 upload.

Parameters:

  • file_name (required): Name of the file to upload
  • content_type (required): MIME type of the file
  • file_size (required): Size of the file in bytes
  • category (optional): Upload category (default: ‘photo’)
  • resource_type (optional): Associated resource type
  • resource_id (optional): Associated resource ID

Response:

{
"presigned_url": "https://s3.us-central-1.wasabisys.com/bucket/key?signature=...",
"key": "secure_assets/development/2024/01/15/10/30/45/uuid/filename.jpg",
"bucket": "heatwave-assets-usc1",
"region": "us-central-1"
}

Creates Upload record after successful S3 upload.

Parameters:

  • key (required): S3 key of the uploaded file
  • file_name (required): Original filename
  • content_type (required): MIME type
  • file_size (required): File size in bytes
  • category (optional): Upload category
  • resource_type (optional): Associated resource type
  • resource_id (optional): Associated resource ID

Response:

{
"message": "Upload completed successfully.",
"files_list": "[123]",
"upload_ids": [123]
}
  • Faster uploads: Files go directly to S3, bypassing Rails server
  • Reduced server load: No file data passes through Rails application
  • Better scalability: S3 handles the heavy lifting for file transfers
  • Resumable uploads: Uppy can resume interrupted uploads
  • Progress tracking: Real-time upload progress
  • Error handling: Comprehensive error handling and retry logic
  • Dragonfly integration: Files are processed by Dragonfly after upload
  • Upload model: Standard Upload records are created
  • Existing workflows: Compatible with existing file management systems
  1. Update helper calls to include S3 parameters:

    <!-- Before -->
    <%= uppy_uploader(endpoint: upload_uploads_path) %>
    <!-- After -->
    <%= uppy_uploader(
    endpoint: upload_uploads_path,
    presigned_url_endpoint: presigned_url_uploads_path,
    upload_complete_endpoint: upload_complete_uploads_path,
    mode: 's3'
    ) %>
  2. Use helper methods for common scenarios:

    <!-- Before -->
    <%= rma_image_uploader(@rma) %>
    <!-- After -->
    <%= rma_image_uploader_s3(@rma) %>
  3. Test thoroughly to ensure compatibility with existing workflows

  1. CORS errors: Ensure Wasabi bucket has proper CORS configuration
  2. Presigned URL errors: Check AWS credentials and permissions
  3. Upload completion failures: Verify backend endpoints are accessible

Enable debug mode in Uppy controllers:

// In the controller
this.uppy = new Uppy({
debug: true, // Enable debug logging
// ... other options
})

Check browser console for detailed error messages and network requests.

  • Presigned URLs expire after 1 hour
  • File size limits are enforced (500MB maximum)
  • Content type validation prevents malicious file uploads
  • CSRF protection is maintained for all endpoints
  • Authorization is required for all upload operations
  • Multipart uploads for very large files
  • Upload progress integration with existing UI
  • Batch upload optimization
  • Custom metadata support
  • Upload validation before S3 upload