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 and re-reads the files grid; returns
the plain socket. The core MediaSelectorModal stores its uploads into
this resource's folder as they land — closing without confirming still
leaves them there, so the grid (and, if the folder's contents changed,
every other surface counting them) must catch up on close.
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.
Re-reads the resource's folder into the files grid. For hosts that hear over PubSub that the resource changed in another session (an upload or removal there) — the form's own pointers (featured image) are untouched.
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 and re-reads the files grid; returns
the plain socket. The core MediaSelectorModal stores its uploads into
this resource's folder as they land — closing without confirming still
leaves them there, so the grid (and, if the folder's contents changed,
every other surface counting them) must catch up on close.
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.
Re-reads the resource's folder into the files grid. For hosts that hear over PubSub that the resource changed in another session (an upload or removal there) — the form's own pointers (featured image) are untouched.
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.