PhoenixKitCatalogue.Attachments (PhoenixKitCatalogue v0.1.14)

Copy Markdown View Source

Folder-scoped file attachments + featured image for catalogue resources (items and catalogues share the exact same pattern).

Each resource owns a phoenix_kit_media_folders row keyed by a deterministic name derived from the resource struct and UUID. Files belong to the resource via phoenix_kit_files.folder_uuid, queried on mount and refreshed after uploads. An optional featured image is a single UUID pointer on resource.data["featured_image_uuid"].

Usage

The owning LiveView calls mount_attachments/2 in mount/3 and allow_attachment_upload/1 in the same chain. Its event/info clauses delegate to the matching functions here:

# Mount
socket
|> Attachments.mount_attachments(item_or_catalogue)
|> Attachments.allow_attachment_upload()

# Events (one-liner bodies)
def handle_event("open_featured_image_picker", _, s),
  do: Attachments.open_featured_image_picker(s)

def handle_event("close_media_selector", _, s),
  do: {:noreply, Attachments.close_media_selector(s)}

def handle_event("cancel_upload", %{"ref" => ref}, s),
  do: Attachments.cancel_attachment_upload(s, ref)

def handle_event("clear_featured_image", _, s),
  do: Attachments.clear_featured_image(s)

def handle_event("remove_file", %{"uuid" => uuid}, s),
  do: Attachments.trash_file(s, uuid)

def handle_info({:media_selected, uuids}, s),
  do: Attachments.handle_media_selected(s, uuids)

def handle_info({:media_selector_closed}, s),
  do: {:noreply, Attachments.close_media_selector(s)}

On save, weave attachment state into params:

params = Attachments.inject_attachment_data(params, socket)

And after a :new save succeeds, rename the pending folder:

:ok = Attachments.maybe_rename_pending_folder(socket, saved_resource)

Resource shape

The module pattern-matches on the resource struct to derive the folder name prefix. Add a new clause to folder_name_for/1 to support additional resource types.

Summary

Functions

Registers the file input :attachment_files with a 20-file, 100MB ceiling and auto-upload. Progress is consumed by handle_progress/3 which this module captures for the caller.

Cancels an in-flight upload entry by ref.

Nulls the featured image pointer in socket state (save persists).

Clears the media-selector assigns; returns the plain socket.

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

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

Routes the :media_selected reply by :media_selector_target. Featured-image target promotes the first selected UUID; files target is a no-op (modal already set folder_uuid). Both refresh the grid from the folder.

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

After a :new save, renames the pending (random-named) folder to the deterministic name now that the resource has a UUID. Non-fatal: rename failures log and return :ok so the save flow isn't blocked.

Populates the attachment-related assigns on the socket. Accepts the owning resource (Item or Catalogue or Category). Stashes the resource at :attachments_resource so later callbacks (progress, events) can reach it without plumbing.

Opens the media selector modal scoped to the resource's folder.

Removes the file from this resource. Three cases

Translates LiveView upload error atoms to user-facing text.

Returns the upload ref name used for the inline files dropzone.

Functions

allow_attachment_upload(socket)

Registers the file input :attachment_files with a 20-file, 100MB ceiling and auto-upload. Progress is consumed by handle_progress/3 which this module captures for the caller.

cancel_attachment_upload(socket, ref)

Cancels an in-flight upload entry by ref.

close_media_selector(socket)

Clears the media-selector assigns; returns the plain socket.

file_icon(arg1)

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

format_file_size(bytes)

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

handle_media_selected(socket, file_uuids)

Routes the :media_selected reply by :media_selector_target. Featured-image target promotes the first selected UUID; files target is a no-op (modal already set folder_uuid). Both refresh the grid from the folder.

inject_attachment_data(params, socket)

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

maybe_rename_pending_folder(socket, resource)

After a :new save, renames the pending (random-named) folder to the deterministic name now that the resource has a UUID. Non-fatal: rename failures log and return :ok so the save flow isn't blocked.

mount_attachments(socket, resource, opts \\ [])

Populates the attachment-related assigns on the socket. Accepts the owning resource (Item or Catalogue or Category). Stashes the resource at :attachments_resource so later callbacks (progress, events) can reach it without plumbing.

Options

  • :files_grid (default true) — set to false to skip the assign_files_state/1 work (and the per-mount DB query that enumerates the folder's files). The CategoryFormLive uses this because its UI only renders the featured-image card; it has no files grid, so the file list query was wasted.

trash_file(socket, uuid)

Removes the file from this resource. Three cases:

  1. File's home folder is this resource AND it's only here → trash it (single-owner case; same effect as before folder-links).
  2. File's home folder is this resource AND it's also linked elsewhere → promote one link to home, delete the promoted link. The file stays alive under its new owner.
  3. File was here via a FolderLink (home is another resource) → delete the link. Other owners keep their reference untouched.

Also clears the featured pointer if the removed file was featured.

upload_error_message(other)

Translates LiveView upload error atoms to user-facing text.

upload_name()

Returns the upload ref name used for the inline files dropzone.