feat(transaction): add support for file attachments using Active Storage (#713)

* feat(transaction): add support for file attachments using Active Storage

* feat(attachments): implement transaction attachments with upload, show, and delete functionality

* feat(attachments): enhance attachment upload functionality to support multiple files and improved error handling

* feat(attachments): add attachment upload form and display functionality in transaction views

* feat(attachments): implement attachment validation for count, size, and content type; enhance upload form with validation hints

* fix(attachments): use correct UI components

* feat(attachments): Implement Turbo Stream responses for creating and deleting transaction attachments.

* fix(attachments): include auth in activestorage controller

* test(attachments): add test coverage for turbostream and auth

* feat(attachments): extract strings to i18n

* fix(attachments): ensure only newly added attachments are purged when transaction validation fails.

* fix(attachments): validate attachment params

* refactor(attachments): use stimulus declarative actions

* fix(attachments): add auth for other representations

* refactor(attachments): use Browse component for attachment uploads

* fix(attachments): reject empty values on attachment upload

* fix(attachments): hide the upload form if reached max uploads

* fix(attachments): correctly purge only newly added attachments on upload failure

* fix(attachments): ensure attachment count limit is respected within a transaction lock

* fix(attachments): update attachment parameter handling to avoid `ParameterMissing` errors.

* fix(components): adjust icon_only logic for buttonish

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
This commit is contained in:
Ellion Blessan
2026-03-15 05:56:27 +07:00
committed by GitHub
parent 3a869c760e
commit 98ae6782dc
16 changed files with 675 additions and 2 deletions

View File

@@ -0,0 +1,63 @@
import { Controller } from "@hotwired/stimulus"
export default class AttachmentUploadController extends Controller {
static targets = ["fileInput", "submitButton", "fileName", "uploadText"]
static values = {
maxFiles: Number,
maxSize: Number
}
connect() {
this.updateSubmitButton()
}
triggerFileInput() {
this.fileInputTarget.click()
}
updateSubmitButton() {
const files = Array.from(this.fileInputTarget.files)
const hasFiles = files.length > 0
// Basic validation hints (server validates definitively)
let isValid = hasFiles
let errorMessage = ""
if (hasFiles) {
if (this.hasUploadTextTarget) this.uploadTextTarget.classList.add("hidden")
if (this.hasFileNameTarget) {
const filenames = files.map(f => f.name).join(", ")
const textElement = this.fileNameTarget.querySelector("p")
if (textElement) textElement.textContent = filenames
this.fileNameTarget.classList.remove("hidden")
}
// Check file count
if (files.length > this.maxFilesValue) {
isValid = false
errorMessage = `Too many files (max ${this.maxFilesValue})`
}
// Check file sizes
const oversizedFiles = files.filter(file => file.size > this.maxSizeValue)
if (oversizedFiles.length > 0) {
isValid = false
errorMessage = `File too large (max ${Math.round(this.maxSizeValue / 1024 / 1024)}MB)`
}
} else {
if (this.hasUploadTextTarget) this.uploadTextTarget.classList.remove("hidden")
if (this.hasFileNameTarget) this.fileNameTarget.classList.add("hidden")
}
this.submitButtonTarget.disabled = !isValid
if (hasFiles && isValid) {
const count = files.length
this.submitButtonTarget.textContent = count === 1 ? "Upload 1 file" : `Upload ${count} files`
} else if (errorMessage) {
this.submitButtonTarget.textContent = errorMessage
} else {
this.submitButtonTarget.textContent = "Upload"
}
}
}