Module: Plutonium::Wizard::Attachments

Defined in:
lib/plutonium/wizard/attachments.rb

Overview

Bridges a wizard's staged attachment value to the displayable attachment the Uppy input and Display::Components::Attachment render — model-free and backend-agnostic, for a bare attribute :photo, :string + input :photo, as: :uppy, direct_upload: true field (no using: model needed).

A wizard stages plain strings, so an attachment field holds its backend's own direct-upload token: ActiveStorage's signed_id (an opaque signed string) or active_shrine/Shrine's cached-file data (a JSON object). Those are the only two shapes, and they're distinguishable — a Shrine token parses as JSON, an AS signed_id doesn't — so we revive each through its own backend with no model and no per-field configuration.

Resolution is display-only: staging and execute (which assigns the token straight to a model attachment — both AS and active_shrine accept it) never call it. The two backends' native objects answer DIFFERENT method names (filename/content_type vs original_filename/mime_type), so a resolved token is wrapped in Resolved, a uniform view exposing exactly what the display + preview components call. See the wizard-attachments design spec.

Defined Under Namespace

Classes: Resolved

Class Method Summary collapse

Class Method Details

.attachment_backendObject

The default server-side staging backend: the configured one, else auto-detected (active_shrine loaded → Shrine, else ActiveStorage).



75
76
77
78
# File 'lib/plutonium/wizard/attachments.rb', line 75

def attachment_backend
  Plutonium.configuration.wizards.attachment_backend ||
    (defined?(ActiveShrine) ? :shrine : :active_storage)
end

.field?(input_options) ⇒ Boolean

Whether a step input renders as an attachment (its as: is a file alias), so its staged token should be resolved for display. Keys off the form's canonical file-input alias set, so the two never drift.

Returns:

  • (Boolean)


40
41
42
43
# File 'lib/plutonium/wizard/attachments.rb', line 40

def field?(input_options)
  as = input_options&.dig(:options, :as) || input_options&.dig(:as)
  Plutonium::UI::Form::Base::Builder::FILE_INPUT_TYPES.include?(as&.to_sym)
end

.resolve(value) ⇒ Array<Resolved>

Resolve a staged attachment token (or array of them) into uniform Resolved view(s).

Parameters:

  • value (String, Array, nil)

    the staged token(s).

Returns:

  • (Array<Resolved>)

    resolved attachments; blank, tampered, or unrecognized tokens are dropped (never raised), so a bad token can't 500 the form or the review.



33
34
35
# File 'lib/plutonium/wizard/attachments.rb', line 33

def resolve(value)
  Array(value).filter_map { |token| resolve_token(token) }
end

.stage_upload(value, backend: nil, uploader: nil) ⇒ Object

SERVER-SIDE staging: turn a submitted attachment value into a token string to stage in data, minting one from an uploaded file when needed.

Handles every shape a step POST can carry for an attachment field:

  • an already-minted token String (direct upload, or the hidden preview field on re-submit) → kept verbatim;
  • an uploaded file (IO-like) → uploaded to the backend's cache, returning its token (an AS signed_id, or Shrine cached-file JSON);
  • blank / no selection → nil (the caller drops the key so the previously staged token survives a Back/re-submit);
  • an Array (multiple) → each element mapped, blanks dropped.

Parameters:

  • backend (Symbol, nil) (defaults to: nil)

    per-field override; nil → the configured default.

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

    a Shrine uploader to cache through (:shrine backend only) — its cache-stage plugins (mime/dimension extraction, generate_location, validations) run instead of base Shrine's. The minted token stays uploader-agnostic, so display + execute promotion are unaffected. Ignored shape for ActiveStorage (raises if given).



63
64
65
66
67
68
69
70
71
# File 'lib/plutonium/wizard/attachments.rb', line 63

def stage_upload(value, backend: nil, uploader: nil)
  if value.is_a?(Array)
    value.filter_map { |v| stage_upload(v, backend:, uploader:) }.presence
  elsif value.is_a?(String)
    value.presence
  elsif value.respond_to?(:read)
    upload_to_cache(value, backend || attachment_backend, uploader:)
  end
end

.validation_errors(value, backend: nil, uploader: nil) ⇒ Array<String>

Run the EFFECTIVE Shrine uploader's attacher validations against a staged token (or array of them), returning the validation messages — so a file that violates the uploader's validate_* rules is rejected at the STEP (stage phase), not deferred to execute's model assignment.

The effective uploader is the field's uploader: if given, else base Shrine — both of which may carry Attacher.validate rules. Returns [] when the field isn't Shrine-backed (ActiveStorage has no attacher here), when nothing is staged, or when the effective uploader declares no validations.

Parameters:

  • value (String, Array, nil)

    the staged token(s).

  • backend (Symbol, nil) (defaults to: nil)

    per-field override; nil → the configured default.

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

    the field's uploader: option.

Returns:

  • (Array<String>)

    validation messages (empty ⇒ valid).



94
95
96
97
98
99
100
101
102
103
104
# File 'lib/plutonium/wizard/attachments.rb', line 94

def validation_errors(value, backend: nil, uploader: nil)
  return [] unless (backend || attachment_backend).to_sym == :shrine

  klass = shrine_uploader(uploader)
  # Shrine's `validation` plugin is OPTIONAL — without it (or `validation_helpers`)
  # the Attacher has no `#errors` and nothing to enforce. Detect it up front so a
  # plugin-less app is a clean no-op, not a per-step rescued NoMethodError.
  return [] unless klass::Attacher.method_defined?(:errors)

  Array(value).flat_map { |token| token_validation_errors(klass, token) }
end