233 lines
6.4 KiB
Ruby
233 lines
6.4 KiB
Ruby
# Display a widget for uploading, editing and deleting a file attachment
|
|
class Attachment::EditComponent < ApplicationComponent
|
|
attr_reader :champ
|
|
attr_reader :attachment
|
|
attr_reader :user_can_destroy
|
|
alias user_can_destroy? user_can_destroy
|
|
attr_reader :as_multiple
|
|
alias as_multiple? as_multiple
|
|
|
|
EXTENSIONS_ORDER = ['jpeg', 'png', 'pdf', 'zip'].freeze
|
|
|
|
def initialize(champ: nil, auto_attach_url: nil, attached_file:, direct_upload: true, index: 0, as_multiple: false, view_as: :link, user_can_destroy: true, **kwargs)
|
|
@as_multiple = as_multiple
|
|
@attached_file = attached_file
|
|
@auto_attach_url = auto_attach_url
|
|
@champ = champ
|
|
@direct_upload = direct_upload
|
|
@index = index
|
|
@view_as = view_as
|
|
@user_can_destroy = user_can_destroy
|
|
|
|
# attachment passed by kwarg because we don't want a default (nil) value.
|
|
@attachment = if kwargs.key?(:attachment)
|
|
kwargs.delete(:attachment)
|
|
elsif attached_file.respond_to?(:attachment)
|
|
attached_file.attachment
|
|
else
|
|
fail ArgumentError, "You must pass an `attachment` kwarg when not using as single attachment like in #{attached_file.name}. Set it to nil for a new attachment."
|
|
end
|
|
|
|
# When parent form has nested attributes, pass the form builder object_name
|
|
# to correctly infer the input attribute name.
|
|
@form_object_name = kwargs.delete(:form_object_name)
|
|
|
|
verify_initialization!(kwargs)
|
|
end
|
|
|
|
def first?
|
|
@index.zero?
|
|
end
|
|
|
|
def max_file_size
|
|
return if file_size_validator.nil?
|
|
|
|
file_size_validator.options[:less_than]
|
|
end
|
|
|
|
def attachment_id
|
|
@attachment_id ||= (attachment&.id || SecureRandom.uuid)
|
|
end
|
|
|
|
def attachment_path(**args)
|
|
helpers.attachment_path attachment.id, args.merge(signed_id: attachment.blob.signed_id)
|
|
end
|
|
|
|
def destroy_attachment_path
|
|
attachment_path(champ_id: champ&.public_id)
|
|
end
|
|
|
|
def attachment_input_class
|
|
"attachment-input-#{attachment_id}"
|
|
end
|
|
|
|
def file_field_options
|
|
track_issue_with_missing_validators if missing_validators?
|
|
{
|
|
class: class_names("fr-upload attachment-input": true, "#{attachment_input_class}": true, "hidden": persisted?),
|
|
direct_upload: @direct_upload,
|
|
id: input_id,
|
|
aria: { describedby: champ&.describedby_id },
|
|
data: {
|
|
auto_attach_url:,
|
|
turbo_force: :server
|
|
}.merge(has_file_size_validator? ? { max_file_size: } : {})
|
|
}.merge(has_content_type_validator? ? { accept: accept_content_type } : {})
|
|
end
|
|
|
|
def poll_url
|
|
if champ.present?
|
|
auto_attach_url
|
|
else
|
|
attachment_path(user_can_edit: true, view_as: @view_as, auto_attach_url: @auto_attach_url, direct_upload: @direct_upload)
|
|
end
|
|
end
|
|
|
|
def poll_context
|
|
return :dossier if champ.present?
|
|
|
|
nil
|
|
end
|
|
|
|
def field_name(object_name = nil, method_name = nil, *method_names, multiple: false, index: nil)
|
|
helpers.field_name(@form_object_name || ActiveModel::Naming.param_key(@attached_file.record), attribute_name)
|
|
end
|
|
|
|
def attribute_name
|
|
@attached_file.name
|
|
end
|
|
|
|
def remove_button_options
|
|
{
|
|
role: 'button',
|
|
data: { turbo: "true", turbo_method: :delete }
|
|
}
|
|
end
|
|
|
|
def replace_button_options
|
|
{
|
|
type: 'button',
|
|
data: {
|
|
action: "click->replace-attachment#open",
|
|
auto_attach_url: auto_attach_url
|
|
}.compact
|
|
}
|
|
end
|
|
|
|
def retry_button_options
|
|
{
|
|
type: 'button',
|
|
class: 'fr-btn fr-btn--sm fr-btn--tertiary fr-mt-1w attachment-upload-error-retry',
|
|
data: { input_target: ".#{attachment_input_class}", action: 'autosave#onClickRetryButton' }
|
|
}
|
|
end
|
|
|
|
def persisted?
|
|
!!attachment&.persisted?
|
|
end
|
|
|
|
def downloadable?
|
|
return false unless @view_as == :download
|
|
|
|
viewable?
|
|
end
|
|
|
|
def viewable?
|
|
return false if attachment.virus_scanner_error?
|
|
return false if attachment.watermark_pending?
|
|
|
|
true
|
|
end
|
|
|
|
def error?
|
|
attachment.virus_scanner_error?
|
|
end
|
|
|
|
def error_message
|
|
case
|
|
when attachment.virus_scanner.infected?
|
|
t(".errors.virus_infected")
|
|
when attachment.virus_scanner.corrupt?
|
|
t(".errors.corrupted_file")
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def input_id
|
|
if champ.present?
|
|
# There is always a single input by champ, its id must match the label "for" attribute.
|
|
return champ.input_id
|
|
end
|
|
|
|
helpers.field_id(@form_object_name || @attached_file.record, attribute_name)
|
|
end
|
|
|
|
def auto_attach_url
|
|
return @auto_attach_url if @auto_attach_url.present?
|
|
return helpers.auto_attach_url(@champ) if @champ.present?
|
|
|
|
nil
|
|
end
|
|
|
|
def file_size_validator
|
|
@attached_file.record
|
|
._validators[attribute_name.to_sym]
|
|
.find { |validator| validator.class == ActiveStorageValidations::SizeValidator }
|
|
end
|
|
|
|
def content_type_validator
|
|
@attached_file.record
|
|
._validators[attribute_name.to_sym]
|
|
.find { |validator| validator.class == ActiveStorageValidations::ContentTypeValidator }
|
|
end
|
|
|
|
def accept_content_type
|
|
list = content_type_validator.options[:in].dup
|
|
list << ".acidcsa" if list.include?("application/octet-stream")
|
|
list.join(', ')
|
|
end
|
|
|
|
def allowed_formats
|
|
@allowed_formats ||= begin
|
|
formats = content_type_validator.options[:in].filter_map do |content_type|
|
|
MiniMime.lookup_by_content_type(content_type)&.extension
|
|
end.uniq.sort_by { EXTENSIONS_ORDER.index(_1) || 999 }
|
|
|
|
# When too many formats are allowed, consider instead manually indicating
|
|
# above the input a more comprehensive of formats allowed, like "any image", or a simplified list.
|
|
formats.size > 5 ? [] : formats
|
|
end
|
|
end
|
|
|
|
def has_content_type_validator?
|
|
!content_type_validator.nil?
|
|
end
|
|
|
|
def has_file_size_validator?
|
|
!file_size_validator.nil?
|
|
end
|
|
|
|
def missing_validators?
|
|
return true if !has_file_size_validator?
|
|
return true if !has_content_type_validator?
|
|
return false
|
|
end
|
|
|
|
def verify_initialization!(kwargs)
|
|
fail ArgumentError, "Unknown kwarg #{kwargs.keys.join(', ')}" unless kwargs.empty?
|
|
|
|
fail ArgumentError, "Invalid view_as:#{@view_as}, must be :download or :link" if [:download, :link].exclude?(@view_as)
|
|
end
|
|
|
|
def track_issue_with_missing_validators
|
|
Sentry.capture_message(
|
|
"Strange case of missing validator",
|
|
extra: {
|
|
champ: champ,
|
|
field_name: field_name,
|
|
attachment_id: attachment_id
|
|
}
|
|
)
|
|
end
|
|
end
|