# `PhoenixKitManufacturing.Attachments`
[🔗](https://github.com/BeamLabEU/phoenix_kit_manufacturing/blob/0.4.1/lib/phoenix_kit_manufacturing/attachments.ex#L1)

Folder-scoped file attachments + featured image for Manufacturing's
`Machine` resource. Designed for **multi-resource** LVs: many Files
cards can live on the same page, each keyed by an opaque string
"scope" — typically the resource's id or a draft id.

This is a 1:1 adaptation of the mechanics in
`PhoenixKitLocations.Attachments` (itself a re-shape of the
single-resource Attachments pattern in `PhoenixKitCatalogue.Attachments`).
The only scope currently wired up is the literal `"machine"` (see
`MachineFormLive`), but the multi-scope mechanic is kept exactly as
in the source — not collapsed to a single-resource shape — so a
future resource on this module (e.g. Operations) can reuse it
without a rewrite. All that state lives in a per-scope map:

    socket.assigns.attachments_by_scope = %{
      "machine" => %{folder_uuid: …, featured_image_uuid: …, files: […], …}
    }

Modal state (`:show_media_selector` and friends) stays shared at
the socket level — only one modal opens at a time — but tracks
`:media_selector_scope` so the picker's result applies to the
right resource.

Uploads use a single shared config (`@upload_name`). The dropzone
in each Files card calls `set_active_upload_scope/2` on click so
`handle_progress/3` knows which scope's folder the file belongs to.
Edge case: clicking dropzone A then dropzone B before either file
picker resolves will route the next-picked file to B — accepted
trade for keeping one upload config instead of N atom-named refs.

## Usage

    # Mount
    socket
    |> Attachments.mount(scope: "machine", resource: machine)
    |> Attachments.allow_attachment_upload()

    # Render — pass scope to the Files card render
    <FilesCard scope="machine" state={Attachments.state(@socket, "machine")} … />

    # Events take scope via phx-value
    def handle_event("open_featured_image_picker", %{"scope" => scope}, s),
      do: Attachments.open_featured_image_picker(s, scope)

    def handle_event("set_active_upload_scope", %{"scope" => scope}, s),
      do: {:noreply, Attachments.set_active_upload_scope(s, scope)}

    # Save-time — inject for each scope
    params = Attachments.inject_attachment_data(params, socket, "machine")

    # After a `:new` resource is persisted, rename its pending folder
    :ok = Attachments.maybe_rename_pending_folder_for(folder_uuid, saved_resource)

## Resource shape

Each scope's resource carries a `data` JSONB with
`files_folder_uuid` and `featured_image_uuid` keys. Add a clause
to `folder_name_for/1` to support additional resource structs.

# `allow_attachment_upload`

Registers the shared file input with a 20-file, 100MB ceiling and
auto-upload. Progress routes to `handle_progress/3` which reads the
active upload scope to figure out the target folder.

# `cancel_attachment_upload`

Cancels an in-flight upload entry by ref.

# `clear_featured_image`

Nulls the featured image pointer in the given scope (save persists).

# `close_media_selector`

Clears modal-state assigns; returns the plain socket.

# `empty_scope_state`

Default empty per-scope state. Returned by `state/2` when the scope
hasn't been mounted yet — keeps render-time templates safe to call
before mount runs.

# `file_icon`

Picks a heroicon name for a file based on its Storage type.

# `folder_name_for`

```elixir
@spec folder_name_for(any()) :: {:ok, String.t()} | :pending
```

Returns `{:ok, "<prefix>-<uuid>"}` for known resource structs.
Public so multi-scope LVs can compute the target name without
re-implementing the prefix scheme.

# `forget_scope`

Drops a scope's state (and clears the active-upload / modal scope
pointers if they were pointing at this scope). Call when a draft
is discarded so the per-scope map doesn't grow unbounded.

# `format_file_size`

Renders a byte count as a human string. Nil-safe.

# `handle_media_selected`

Routes the `:media_selected` reply by the modal's target. Featured-
image target promotes the first selected UUID into the scope tracked
by `:media_selector_scope`.

# `init`

Initializes the per-scope map and shared modal assigns. Idempotent —
safe to call multiple times; existing scope state is preserved.

# `inject_attachment_data`

Merges `files_folder_uuid` and `featured_image_uuid` for `scope`
into `params["data"]`. Call right before passing params to your
context's create/update.

# `maybe_rename_pending_folder_for`

```elixir
@spec maybe_rename_pending_folder_for(String.t() | nil, any()) :: :ok
```

Renames a known pending folder UUID to match the resource's
deterministic name. Non-fatal: rename failures log and return `:ok`.

# `mount`

Populates a single scope's state from `resource.data`. Pulls the
folder uuid (if any) + featured image (if any) + the folder's file
list. Existing other-scope entries are left untouched.

## Options

  * `:files_grid` (default `true`) — set to `false` to skip the
    per-mount DB query that enumerates the folder's files. Useful
    when the card only renders the featured-image control and
    doesn't need the grid.

# `open_featured_image_picker`

Opens the featured-image picker scoped to this resource's folder.
Stores `scope` in `:media_selector_scope` so the picker's reply
knows which scope's `:featured_image_uuid` to update.

# `set_active_upload_scope`

Marks which scope is about to receive the next upload. Wire this
to `phx-click` on each Files card's dropzone so concurrent dropzones
route to the right folder.

# `state`

Returns the per-scope state map (or the empty-state default if the
scope hasn't been mounted). Always safe to call from a template.

# `trash_file`

Removes the file from the scope's folder. See per-case comments in
`do_detach/2` — soft-trash for single-owner home folders, link
deletion when the file is multi-resource. Also clears featured if
the removed file was featured.

# `upload_error_message`

Translates LiveView upload error atoms to user-facing text.

# `upload_name`

Returns the upload ref name used by every Files card on the page.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
