PhoenixKitCRM.Attachments (PhoenixKitCRM v0.2.4)

Copy Markdown View Source

Folder-scoped media attachments for a CRM contact, backed by core PhoenixKit.Modules.Storage (the same per-resource-folder convention phoenix_kit_staff/phoenix_kit_catalogue use — no module-owned table, no migration).

Each contact owns a deterministic root folder crm-contact-<uuid> for generic files, with a nested Images subfolder for images — all of a contact's files in one folder, images in a folder inside it. Folders are resolved by name on every read (never cached on Contact, so renaming/deleting the folder in /admin/media can't strand a dangling uuid) and created lazily on first upload; the core [:name, :parent_uuid] unique index makes find-or-create race-safe.

Files live in core phoenix_kit_files under the folder; uploading/browsing is done by MediaSelectorModal (scoped to the folder), so this module only resolves folders, lists their files, (un)links picked files, and removes them — soft-trash a sole-owner file, unlink a shared one. It never hard-deletes a possibly-shared asset.

Summary

Types

Which CRM record a folder belongs to.

Functions

Ensures file_uuid is attached to folder_uuid: a no-op if already home there (the modal's scoped uploads land here directly); adopts an orphan file as home; otherwise adds a FolderLink so a file picked from elsewhere appears here without being moved from its owner.

Whether file_uuid is one of the record's own Images-folder image files (home or linked, excluding trashed) — the authorization basis for set_avatar/3.

The record's avatar File struct, or nil if unset / missing / trashed.

Thumbnail URL for the record's avatar (or nil).

The record's avatar file uuid (from metadata), or nil.

Clears the record's avatar pointer.

Removes a file from folder_uuid. Home here and not linked elsewhere → soft-trash (recoverable in the media trash). Home here but also linked elsewhere → promote a link to home. Here only via a FolderLink → drop the link. Never hard-deletes a shared asset.

Public download URL for a file (nil-safe).

Find-or-create the folder for kind, returning {:ok, uuid} or {:error, reason}. Race-safe: a lost create (unique [:name, :parent_uuid]) re-resolves the winner. Call when an action needs the folder to exist (opening the picker / handling a selection).

Find-or-create an interaction's attachment folder.

Heroicon name for a file based on its Storage type / mime.

Resolves the folder uuid for kind (:files → root, :images → the nested Images subfolder) without creating it. Returns the uuid or nil (used on render so viewing a tab doesn't spawn empty folders).

Human-readable byte count. Nil-safe.

Fetch a File struct by uuid (nil-safe), for the composer's staged list.

Whether the file with this uuid is an image (by Storage file_type).

Deterministic root folder name for an interaction's attachments.

Resolve an interaction's attachment folder uuid (no create), or nil.

Files attached to folder_uuid (home-folder files plus those linked in via FolderLink), newest first, excluding trashed. :only narrows by type: :images (file_type == "image"), :non_images, or :all (default). Defensive — keeps a tab showing only its own kind even if a stray file landed in the folder.

Files for many interactions at once → %{interaction_uuid => [File.t()]} (only interactions that have files appear). Two queries total (folders, then files); used to render the timeline without an N+1. Compose-time uploads land home in the interaction folder, so home-folder files are sufficient (no FolderLinks).

Files attached to an interaction (newest first, excluding trashed).

Purge an interaction's attachment folder subtree (best-effort).

Permanently purges a record's media — deletes the root folder and its whole subtree (the nested Images folder + every file) via core's cascading delete_folder_completely/1. Best-effort: logs and returns :ok on any failure so it never blocks a deletion. Call only on a permanent delete (soft-trash keeps the files).

Deterministic root folder name for a record's files (crm-<resource>-<uuid>).

Points the record's avatar at file_uuid (server-owned metadata write).

Thumbnail URL for an image file, falling back to the original (nil-safe).

Types

resource()

@type resource() :: :contact | :company

Which CRM record a folder belongs to.

Functions

attach(file_uuid, folder_uuid)

@spec attach(binary(), binary()) :: :ok

Ensures file_uuid is attached to folder_uuid: a no-op if already home there (the modal's scoped uploads land here directly); adopts an orphan file as home; otherwise adds a FolderLink so a file picked from elsewhere appears here without being moved from its owner.

avatar_candidate?(resource, record_uuid, file_uuid)

@spec avatar_candidate?(resource(), binary(), binary()) :: boolean()

Whether file_uuid is one of the record's own Images-folder image files (home or linked, excluding trashed) — the authorization basis for set_avatar/3.

avatar_file(record)

@spec avatar_file(struct()) :: PhoenixKit.Modules.Storage.File.t() | nil

The record's avatar File struct, or nil if unset / missing / trashed.

avatar_url(record)

@spec avatar_url(struct()) :: String.t() | nil

Thumbnail URL for the record's avatar (or nil).

avatar_uuid(arg1)

@spec avatar_uuid(struct()) :: binary() | nil

The record's avatar file uuid (from metadata), or nil.

clear_avatar(record)

@spec clear_avatar(struct()) :: {:ok, struct()} | {:error, term()}

Clears the record's avatar pointer.

detach(file_uuid, folder_uuid)

@spec detach(binary(), binary() | nil) :: :ok | {:error, term()}

Removes a file from folder_uuid. Home here and not linked elsewhere → soft-trash (recoverable in the media trash). Home here but also linked elsewhere → promote a link to home. Here only via a FolderLink → drop the link. Never hard-deletes a shared asset.

download_url(file)

@spec download_url(map()) :: String.t() | nil

Public download URL for a file (nil-safe).

ensure_folder(resource, uuid, atom, actor_uuid)

@spec ensure_folder(resource(), binary(), :files | :images, binary() | nil) ::
  {:ok, binary()} | {:error, term()}

Find-or-create the folder for kind, returning {:ok, uuid} or {:error, reason}. Race-safe: a lost create (unique [:name, :parent_uuid]) re-resolves the winner. Call when an action needs the folder to exist (opening the picker / handling a selection).

ensure_interaction_folder(interaction_uuid, actor_uuid)

@spec ensure_interaction_folder(binary(), binary() | nil) ::
  {:ok, binary()} | {:error, term()}

Find-or-create an interaction's attachment folder.

file_icon(arg1)

@spec file_icon(map()) :: String.t()

Heroicon name for a file based on its Storage type / mime.

folder_uuid(resource, uuid, atom)

@spec folder_uuid(resource(), binary(), :files | :images) :: binary() | nil

Resolves the folder uuid for kind (:files → root, :images → the nested Images subfolder) without creating it. Returns the uuid or nil (used on render so viewing a tab doesn't spawn empty folders).

format_file_size(bytes)

@spec format_file_size(integer() | nil) :: String.t()

Human-readable byte count. Nil-safe.

get_file(uuid)

@spec get_file(binary()) :: PhoenixKit.Modules.Storage.File.t() | nil

Fetch a File struct by uuid (nil-safe), for the composer's staged list.

image?(file_uuid)

@spec image?(binary()) :: boolean()

Whether the file with this uuid is an image (by Storage file_type).

interaction_folder_name(interaction_uuid)

@spec interaction_folder_name(binary()) :: binary()

Deterministic root folder name for an interaction's attachments.

interaction_folder_uuid(interaction_uuid)

@spec interaction_folder_uuid(binary()) :: binary() | nil

Resolve an interaction's attachment folder uuid (no create), or nil.

list_files(folder_uuid, opts)

@spec list_files(
  binary() | nil,
  keyword()
) :: [PhoenixKit.Modules.Storage.File.t()]

Files attached to folder_uuid (home-folder files plus those linked in via FolderLink), newest first, excluding trashed. :only narrows by type: :images (file_type == "image"), :non_images, or :all (default). Defensive — keeps a tab showing only its own kind even if a stray file landed in the folder.

list_files_by_interaction(interaction_uuids)

@spec list_files_by_interaction([binary()]) :: %{
  required(binary()) => [PhoenixKit.Modules.Storage.File.t()]
}

Files for many interactions at once → %{interaction_uuid => [File.t()]} (only interactions that have files appear). Two queries total (folders, then files); used to render the timeline without an N+1. Compose-time uploads land home in the interaction folder, so home-folder files are sufficient (no FolderLinks).

list_interaction_files(interaction_uuid)

@spec list_interaction_files(binary()) :: [PhoenixKit.Modules.Storage.File.t()]

Files attached to an interaction (newest first, excluding trashed).

purge_interaction_media(interaction_uuid)

@spec purge_interaction_media(binary()) :: :ok

Purge an interaction's attachment folder subtree (best-effort).

purge_media(resource, uuid)

@spec purge_media(resource(), binary()) :: :ok

Permanently purges a record's media — deletes the root folder and its whole subtree (the nested Images folder + every file) via core's cascading delete_folder_completely/1. Best-effort: logs and returns :ok on any failure so it never blocks a deletion. Call only on a permanent delete (soft-trash keeps the files).

root_folder_name(resource, uuid)

@spec root_folder_name(resource(), binary()) :: binary()

Deterministic root folder name for a record's files (crm-<resource>-<uuid>).

set_avatar(resource, record, file_uuid)

@spec set_avatar(resource(), struct(), binary()) :: {:ok, struct()} | {:error, term()}

Points the record's avatar at file_uuid (server-owned metadata write).

Authorizes the pointer: file_uuid must be an image that actually lives in (or is linked into) this record's Images folder — a forged event can't point the avatar at an arbitrary file elsewhere in storage. Refuses a trashed record ({:error, :record_trashed}) and a non-candidate file ({:error, :not_record_image}); clearing is unguarded.

thumb_url(file)

@spec thumb_url(map()) :: String.t() | nil

Thumbnail URL for an image file, falling back to the original (nil-safe).