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
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.
Options
:files_grid(defaulttrue) — set tofalseto skip theassign_files_state/1work (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.
Opens the media selector modal scoped to the resource's folder.
Removes the file from this resource. Three cases:
- File's home folder is this resource AND it's only here → trash it (single-owner case; same effect as before folder-links).
- 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.
- 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.
Translates LiveView upload error atoms to user-facing text.
Returns the upload ref name used for the inline files dropzone.