Packages

phoenix_kit

1.7.113
1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib phoenix_kit_web components media_gallery.ex
Raw

lib/phoenix_kit_web/components/media_gallery.ex

defmodule PhoenixKitWeb.Components.MediaGallery do
@moduledoc """
A general-purpose LiveComponent for selecting, ordering, previewing and removing
a set of images.
Generalizes the gallery + picker pattern so any PhoenixKit consumer can embed it
instead of re-implementing the plumbing. The component manages an ordered list of
file UUIDs and reports changes to the parent LiveView.
## Usage
<.live_component
module={PhoenixKitWeb.Components.MediaGallery}
id="my-gallery"
selected={@image_uuids}
phoenix_kit_current_user={@current_user}
/>
# Receive changes in parent LiveView
def handle_info({PhoenixKitWeb.Components.MediaGallery, "my-gallery", {:changed, uuids}}, socket) do
{:noreply, assign(socket, :image_uuids, uuids)}
end
## Attrs
- `id` — required; used for element IDs and change notifications
- `title` — optional heading above the gallery
- `selected` — ordered list of file UUIDs (current selection); default `[]`
- `mode``:single` or `:multiple` (default `:multiple`)
- `cols` — number of grid columns for the thumbnail layout (1..6, default `4`).
Plumbed straight through to `<.draggable_list>`.
- `featured_first` — when `true`, the first item in `:selected` renders a
"Featured" badge in the top-left corner. Matches the
`phoenix_kit_posts` post-creation convention where the first image is the
featured one and drag-reordering changes the feature. Default `false` so
existing consumers aren't surprised.
- `scope_folder_id` — folder scope passed to the picker
- `phoenix_kit_current_user` — required for upload in the picker
- `readonly` — when `true`, hides the pick button, remove buttons, and DnD;
preview (lightbox) still works; default `false`
## Change notifications
After any change (pick / remove / reorder), the component sends:
{PhoenixKitWeb.Components.MediaGallery, id, {:changed, ordered_uuids}}
to the parent LiveView via `send/2`.
## Reorder event contract
The thumbnail grid uses the canonical `<.draggable_list>` primitive
(`PhoenixKitWeb.Components.Core.DraggableList`), which fires the
`"reorder_images"` event with payload `%{"ordered_ids" => uuids}`.
Because `MediaGallery` is a LiveComponent, the grid is rendered with
`target={"#\#{@id}"}` so the `SortableGrid` hook routes the event via
`pushEventTo` to this component's own `handle_event/3` — not the host
LiveView. Each gallery's `id` is unique, so multiple galleries on the same
page never cross-deliver reorder events.
"""
use PhoenixKitWeb, :live_component
require Logger
alias PhoenixKit.Modules.Storage
alias PhoenixKitWeb.Live.Components.MediaSelectorModal
import PhoenixKit.Modules.Shared.Components.ImageSet
@impl true
def update(%{media_viewer_closed: true}, socket) do
{:ok, assign(socket, :preview_uuid, nil)}
end
def update(%{media_selector_closed: true}, socket) do
{:ok, assign(socket, :show_picker, false)}
end
def update(%{media_selected: uuids} = _assigns, socket) do
uuids = uuids || []
# Guard: if component hasn't been fully initialised yet these are nil-safe defaults.
current = socket.assigns[:selected] || []
mode = socket.assigns[:mode] || :multiple
new_selected = apply_selection(current, uuids, mode)
socket =
socket
|> assign_new(:show_picker, fn -> false end)
|> assign(:selected, new_selected)
|> load_files()
|> assign(:show_picker, false)
notify_parent(socket)
{:ok, socket}
end
def update(assigns, socket) do
socket =
socket
|> assign(assigns)
|> assign_new(:selected, fn -> [] end)
|> assign_new(:mode, fn -> :multiple end)
|> assign_new(:cols, fn -> 4 end)
|> assign_new(:featured_first, fn -> false end)
|> assign_new(:scope_folder_id, fn -> nil end)
|> assign_new(:phoenix_kit_current_user, fn -> nil end)
|> assign_new(:readonly, fn -> false end)
|> assign_new(:title, fn -> nil end)
|> assign_new(:show_picker, fn -> false end)
|> assign_new(:preview_uuid, fn -> nil end)
|> assign_new(:files, fn -> [] end)
|> assign_new(:variants_map, fn -> %{} end)
|> load_files()
{:ok, socket}
end
@impl true
def handle_event("open_picker", _params, socket) do
{:noreply, assign(socket, :show_picker, true)}
end
def handle_event("remove_image", %{"uuid" => uuid}, socket) do
new_selected = Enum.reject(socket.assigns.selected, &(&1 == uuid))
socket = socket |> assign(:selected, new_selected) |> load_files()
notify_parent(socket)
{:noreply, socket}
end
def handle_event("preview_image", %{"uuid" => uuid}, socket) do
{:noreply, assign(socket, :preview_uuid, uuid)}
end
# Reorder event from <.draggable_list>: payload uses `ordered_ids` and the
# event is routed to this component by the SortableGrid hook via
# `pushEventTo` (the grid is rendered with `target={"##{@id}"}`).
def handle_event("reorder_images", %{"ordered_ids" => ids}, socket) do
current = socket.assigns.selected
new_selected = Enum.filter(ids, &(&1 in current))
# Append any current uuids that weren't in the payload (defensive; the
# hook should always include every visible item).
leftovers = Enum.reject(current, &(&1 in ids))
new_selected = new_selected ++ leftovers
socket = socket |> assign(:selected, new_selected) |> load_files()
notify_parent(socket)
{:noreply, socket}
end
# ── Private helpers ────────────────────────────────────────────────────
defp load_files(socket) do
selected = socket.assigns.selected
# Skip the DB round-trip when the selection list hasn't changed.
if selected == socket.assigns[:selected_loaded] do
socket
else
do_load_files(socket, selected)
end
end
defp do_load_files(socket, []) do
assign(socket, files: [], variants_map: %{}, selected_loaded: [])
end
defp do_load_files(socket, selected) do
files = Storage.get_files(selected)
variants_map = Storage.list_image_set_variants_for_files(selected)
assign(socket, files: files, variants_map: variants_map, selected_loaded: selected)
rescue
# Defensive degradation at the UI boundary — if Storage can't be reached
# (DB outage, missing connection, sandbox unavailable in tests, cast on a
# malformed UUID), render an empty gallery instead of crashing the
# LiveView. The cases we explicitly anticipate: DBConnection.ConnectionError
# (network/pool), Ecto.Query.CastError (bad UUID in the list), and
# DBConnection.OwnershipError (Ecto.Adapters.SQL.Sandbox in tests that
# exercise update/handle_event paths without checking out a connection).
e in [
DBConnection.ConnectionError,
DBConnection.OwnershipError,
Ecto.Query.CastError
] ->
Logger.warning("MediaGallery: could not load files — #{Exception.message(e)}")
assign(socket, files: [], variants_map: %{}, selected_loaded: nil)
end
defp apply_selection(_current, uuids, :single) do
case uuids do
[uuid | _] -> [uuid]
[] -> []
end
end
defp apply_selection(_current, uuids, _multiple), do: uuids
defp notify_parent(socket) do
parent = self()
id = socket.assigns.id
selected = socket.assigns.selected
send(parent, {PhoenixKitWeb.Components.MediaGallery, id, {:changed, selected}})
end
end