From 157438adb17461b9aa969b61a290d2b78cef7e9b Mon Sep 17 00:00:00 2001 From: Alexander Don Date: Mon, 17 Nov 2025 20:28:53 +0200 Subject: [PATCH 01/40] Added username to the user editing page and custom fields. --- lib/phoenix_kit/users/auth/user.ex | 3 +- lib/phoenix_kit/users/custom_fields.ex | 45 ++++- .../live/settings/users.html.heex | 36 ++++ lib/phoenix_kit_web/users/settings.ex | 29 ++- lib/phoenix_kit_web/users/settings.html.heex | 180 ++++++++++++++++++ 5 files changed, 288 insertions(+), 5 deletions(-) diff --git a/lib/phoenix_kit/users/auth/user.ex b/lib/phoenix_kit/users/auth/user.ex index 421f35462..2349ff848 100644 --- a/lib/phoenix_kit/users/auth/user.ex +++ b/lib/phoenix_kit/users/auth/user.ex @@ -333,11 +333,12 @@ defmodule PhoenixKit.Users.Auth.User do """ def profile_changeset(user, attrs, opts \\ []) do user - |> cast(attrs, [:first_name, :last_name, :email, :username, :user_timezone]) + |> cast(attrs, [:first_name, :last_name, :email, :username, :user_timezone, :custom_fields]) |> validate_names() |> validate_email(opts) |> validate_username(opts) |> validate_user_timezone() + |> validate_custom_fields() end @doc """ diff --git a/lib/phoenix_kit/users/custom_fields.ex b/lib/phoenix_kit/users/custom_fields.ex index cb8170384..a01d1f16f 100644 --- a/lib/phoenix_kit/users/custom_fields.ex +++ b/lib/phoenix_kit/users/custom_fields.ex @@ -17,6 +17,7 @@ defmodule PhoenixKit.Users.CustomFields do - `required` - Whether the field is required (boolean) - `position` - Display order (integer) - `enabled` - Whether the field is active (boolean) + - `user_accessible` - Whether users can edit this field from their settings page (boolean, default: true) - `validation` - Optional validation rules (map) - `default` - Default value (string) - `options` - For select/radio/checkbox types (list of strings) @@ -101,6 +102,33 @@ defmodule PhoenixKit.Users.CustomFields do |> Enum.sort_by(&(&1["position"] || 0)) end + @doc """ + Returns only enabled field definitions that are user-accessible, sorted by position. + + These are fields that users can view and edit from their own settings page. + Admins can always see and edit all fields regardless of this setting. + + Legacy fields without the `user_accessible` key default to `true` (accessible). + + ## Examples + + iex> list_user_accessible_field_definitions() + [%{"key" => "phone", "enabled" => true, "user_accessible" => true, ...}] + """ + def list_user_accessible_field_definitions do + list_field_definitions() + |> Enum.filter(fn field -> + # Field must be enabled + enabled = field["enabled"] == true + + # user_accessible defaults to true if not set (for legacy fields) + user_accessible = Map.get(field, "user_accessible", true) + + enabled && user_accessible + end) + |> Enum.sort_by(&(&1["position"] || 0)) + end + @doc """ Gets a single field definition by key. @@ -336,15 +364,28 @@ defmodule PhoenixKit.Users.CustomFields do "key" => field_def["key"], "label" => field_def["label"] || field_def["key"], "type" => field_def["type"] || "text", - "required" => field_def["required"] || false, + "required" => normalize_boolean(field_def["required"], false), "position" => field_def["position"] || 0, - "enabled" => Map.get(field_def, "enabled", true), + "enabled" => normalize_boolean(Map.get(field_def, "enabled"), true), + "user_accessible" => normalize_boolean(Map.get(field_def, "user_accessible"), true), "validation" => field_def["validation"] || %{}, "default" => field_def["default"] || "", "options" => field_def["options"] || [] } end + # Convert string boolean values to actual booleans + defp normalize_boolean(value, default) do + case value do + true -> true + false -> false + "true" -> true + "false" -> false + nil -> default + _ -> default + end + end + defp ensure_unique_key(key) do if get_field_definition(key) do {:error, "Field with key '#{key}' already exists"} diff --git a/lib/phoenix_kit_web/live/settings/users.html.heex b/lib/phoenix_kit_web/live/settings/users.html.heex index dc0e04928..ebfc4bf1e 100644 --- a/lib/phoenix_kit_web/live/settings/users.html.heex +++ b/lib/phoenix_kit_web/live/settings/users.html.heex @@ -401,6 +401,7 @@ <.table_default_header_cell>Type <.table_default_header_cell>Required <.table_default_header_cell>Status + <.table_default_header_cell>User Access <.table_default_header_cell>Actions @@ -431,6 +432,13 @@ Disabled <% end %> + <.table_default_cell> + <%= if Map.get(field, "user_accessible", true) do %> + User Editable + <% else %> + Admin Only + <% end %> + <.table_default_cell>
+ <%!-- User Accessible Checkbox --%> +
+ +
+ + When checked, users can view and edit this field on their settings page. When unchecked, only admins can edit it. + +
+
+ <%!-- Position --%>
+ <%!-- Avatar Upload Section --%> +
+ + +
+ <%!-- Avatar Preview --%> +
+ <%= if get_in(@phoenix_kit_current_user.custom_fields, ["avatar_file_id"]) do %> + <% avatar_url = PhoenixKit.Storage.URLSigner.signed_url(get_in(@phoenix_kit_current_user.custom_fields, ["avatar_file_id"]), "thumbnail") %> + Avatar + <% else %> +
+ + {String.upcase(String.at(@phoenix_kit_current_user.email, 0))} + +
+ <% end %> +
+ + <%!-- Upload Controls --%> +
+ <.file_upload + upload={@uploads.avatar} + variant="button" + label="Choose Profile Picture" + /> + +

+ Upload a profile picture (max 10MB). Accepts JPG, PNG, GIF. +

+ + <%!-- Success Message --%> + <%= if @last_uploaded_avatar_id do %> +
+ + Avatar uploaded successfully! +
+ <% end %> +
+
+ + <%!-- Divider after avatar section --%> +
+
+ <.simple_form for={@profile_form} id="profile_form" From aad7dfe7c86b09a3b0cfffe69d52e728dfcb9358 Mon Sep 17 00:00:00 2001 From: Alexander Don Date: Tue, 18 Nov 2025 19:57:37 +0200 Subject: [PATCH 03/40] Updated user admin edit page to have media selector and not uploader --- .../live/components/media_selector_modal.ex | 9 +- lib/phoenix_kit_web/users/user_form.ex | 137 +++--------------- lib/phoenix_kit_web/users/user_form.html.heex | 64 +++++--- 3 files changed, 69 insertions(+), 141 deletions(-) diff --git a/lib/phoenix_kit_web/live/components/media_selector_modal.ex b/lib/phoenix_kit_web/live/components/media_selector_modal.ex index 81f6fa1ae..971b67de8 100644 --- a/lib/phoenix_kit_web/live/components/media_selector_modal.ex +++ b/lib/phoenix_kit_web/live/components/media_selector_modal.ex @@ -52,7 +52,6 @@ defmodule PhoenixKitWeb.Live.Components.MediaSelectorModal do socket = socket |> assign(assigns) - |> assign_new(:selected_ids, fn -> MapSet.new(assigns[:selected_ids] || []) end) |> assign_new(:file_type_filter, fn -> :all end) |> assign_new(:search_query, fn -> "" end) |> assign_new(:current_page, fn -> 1 end) @@ -62,6 +61,14 @@ defmodule PhoenixKitWeb.Live.Components.MediaSelectorModal do |> assign_new(:total_pages, fn -> 0 end) |> maybe_allow_upload() + # Convert selected_ids to MapSet if it's a list + socket = + if socket.assigns[:selected_ids] && is_list(socket.assigns.selected_ids) do + assign(socket, :selected_ids, MapSet.new(socket.assigns.selected_ids)) + else + assign_new(socket, :selected_ids, fn -> MapSet.new([]) end) + end + # Load files if modal is shown socket = if assigns[:show] do diff --git a/lib/phoenix_kit_web/users/user_form.ex b/lib/phoenix_kit_web/users/user_form.ex index a9ec32c21..1a8924d70 100644 --- a/lib/phoenix_kit_web/users/user_form.ex +++ b/lib/phoenix_kit_web/users/user_form.ex @@ -42,12 +42,6 @@ defmodule PhoenixKitWeb.Users.UserForm do socket = socket - |> allow_upload(:avatar, - accept: ["image/*"], - max_entries: 1, - max_file_size: 10_000_000, - auto_upload: true - ) |> assign(:current_locale, locale) |> assign(:mode, mode) |> assign(:user_id, user_id) @@ -60,28 +54,15 @@ defmodule PhoenixKitWeb.Users.UserForm do |> assign(:pending_roles, []) |> assign(:default_role, default_role) |> assign(:timezone_options, timezone_options) - |> assign(:last_uploaded_avatar_id, nil) + |> assign(:show_media_selector, false) |> load_user_data(mode, user_id) |> load_form_data() {:ok, socket} end - def handle_event("validate", %{"_target" => ["avatar"]}, socket) do - # Avatar file selection event - files will auto-upload - entries = socket.assigns.uploads.avatar.entries - Logger.info("avatar validate event: entries=#{length(entries)}") - - if entries != [] do - Logger.info("avatar validate: scheduling check_uploads_complete") - Process.send_after(self(), :check_avatar_uploads_complete, 500) - end - - {:noreply, socket} - end - - def handle_event("cancel_upload", %{"ref" => ref}, socket) do - {:noreply, cancel_upload(socket, :avatar, ref)} + def handle_event("open_media_selector", _params, socket) do + {:noreply, assign(socket, :show_media_selector, true)} end def handle_event("validate_user", %{"user" => user_params}, socket) do @@ -695,119 +676,41 @@ defmodule PhoenixKitWeb.Users.UserForm do end end - def handle_info(:check_avatar_uploads_complete, socket) do - entries = socket.assigns.uploads.avatar.entries - - Logger.info( - "check_avatar_uploads_complete: entries=#{length(entries)}, done?=#{inspect(Enum.map(entries, & &1.done?))}" - ) + def handle_info({:media_selected, file_ids}, socket) do + # Get the first selected file ID (single selection mode) + avatar_file_id = List.first(file_ids) - # Check if all entries are done uploading - if entries != [] && Enum.all?(entries, & &1.done?) do - Logger.info("Avatar uploads done! Processing...") - # All done - process them - process_avatar_uploads(socket) - else - # Still uploading - check again later - Logger.info("Still uploading avatar, checking again...") - Process.send_after(self(), :check_avatar_uploads_complete, 500) - {:noreply, socket} - end - end - - defp process_avatar_uploads(socket) do - # Process uploaded avatar files - uploaded_avatars = - consume_uploaded_entries(socket, :avatar, fn %{path: path}, entry -> - # Get file info - ext = Path.extname(entry.client_name) |> String.replace_leading(".", "") - - # Get current user - current_user = socket.assigns.phoenix_kit_current_user - user_id = if current_user, do: current_user.id, else: 1 - - # Get file size - {:ok, stat} = Elixir.File.stat(path) - file_size = stat.size - - # Calculate hash - file_hash = Auth.calculate_file_hash(path) - - # Store file in storage - case PhoenixKit.Storage.store_file_in_buckets( - path, - "image", - user_id, - file_hash, - ext, - entry.client_name - ) do - {:ok, file, :duplicate} -> - Logger.info("Avatar file is duplicate with ID: #{file.id}") - - {:ok, - %{ - file_id: file.id, - filename: entry.client_name, - size: file_size, - duplicate: true - }} - - {:ok, file} -> - Logger.info("Avatar file stored with ID: #{file.id}") - - # Note: ProcessFileJob is now automatically queued in Storage.store_file_in_buckets - - {:ok, - %{ - file_id: file.id, - filename: entry.client_name, - size: file_size - }} - - {:error, reason} -> - Logger.error("Storage Error: #{inspect(reason)}") - {:error, reason} - end - end) - - # Extract file IDs for use - Logger.info("Uploaded avatars: #{inspect(uploaded_avatars)}") - avatar_file_ids = Enum.map(uploaded_avatars, &get_avatar_file_id/1) - Logger.info("Avatar file IDs: #{inspect(avatar_file_ids)}") - avatar_file_id = List.first(avatar_file_ids) - Logger.info("First avatar file ID: #{inspect(avatar_file_id)}") + # Update user with new avatar + user = socket.assigns.user - # Save the avatar file ID to the user's custom fields socket = - if avatar_file_id && avatar_file_id != nil do - user = socket.assigns.user - + if avatar_file_id && user do case Auth.update_user_fields(user, %{"avatar_file_id" => avatar_file_id}) do {:ok, updated_user} -> - Logger.info("Avatar file ID saved: #{avatar_file_id}") + Logger.info("Avatar updated to file_id: #{avatar_file_id}") socket |> assign(:user, updated_user) - |> assign(:last_uploaded_avatar_id, avatar_file_id) - |> put_flash(:info, "Avatar uploaded successfully!") + |> assign(:show_media_selector, false) + |> put_flash(:info, "Avatar updated successfully!") {:error, changeset} -> - Logger.error("Failed to save avatar file ID: #{inspect(changeset)}") + Logger.error("Failed to update avatar: #{inspect(changeset)}") socket - |> assign(:last_uploaded_avatar_id, avatar_file_id) - |> put_flash(:error, "Avatar uploaded but failed to save to profile") + |> assign(:show_media_selector, false) + |> put_flash(:error, "Failed to update avatar") end else socket - |> put_flash(:error, "Failed to upload avatar") + |> assign(:show_media_selector, false) end {:noreply, socket} end - defp get_avatar_file_id(%{file_id: file_id}), do: file_id - defp get_avatar_file_id({:ok, %{file_id: file_id}}), do: file_id - defp get_avatar_file_id(_), do: nil + def handle_info({:media_selector_closed}, socket) do + {:noreply, assign(socket, :show_media_selector, false)} + end + end diff --git a/lib/phoenix_kit_web/users/user_form.html.heex b/lib/phoenix_kit_web/users/user_form.html.heex index 39d786229..14e4b368d 100644 --- a/lib/phoenix_kit_web/users/user_form.html.heex +++ b/lib/phoenix_kit_web/users/user_form.html.heex @@ -38,10 +38,8 @@
<%!-- Avatar Preview (Left) --%>
- <% avatar_file_id = - @last_uploaded_avatar_id || - (@user.custom_fields && @user.custom_fields["avatar_file_id"]) %> - <% variant = if @last_uploaded_avatar_id, do: "original", else: "medium" %> + <% avatar_file_id = @user.custom_fields && @user.custom_fields["avatar_file_id"] %> + <% variant = "medium" %>
<%= if avatar_file_id do %> <% avatar_url = @@ -73,34 +71,33 @@
- <%!-- Upload Controls (Right) --%> + <%!-- Media Selector Controls (Right) --%>

- Supports JPG, PNG, WebP formats up to 10MB + Select an image from your media library

- <%!-- Avatar Upload Component (Separate form to avoid validate_user conflict) --%> - <.file_upload - upload={@uploads.avatar} - label="Choose Avatar" - icon="hero-cloud-arrow-up" - variant="button" - /> - - <%!-- Success Message --%> - <%= if @last_uploaded_avatar_id do %> -
- <.icon name="hero-check-circle" class="w-5 h-5" /> -
-

Avatar uploaded successfully!

-

{@last_uploaded_avatar_id}

-
+ <%!-- Media Selector Button --%> + + + <%!-- Current Avatar Info --%> + <%= if avatar_file_id do %> +
+ <.icon name="hero-check-circle" class="w-4 h-4 inline text-success" /> + Avatar selected
<% end %>
@@ -685,4 +682,25 @@
<% end %>
+ + <%!-- Media Selector Modal --%> + <%= if @mode == :edit do %> + <% current_avatar_id = @user.custom_fields && @user.custom_fields["avatar_file_id"] %> + <%= if current_avatar_id do %> + <.live_component + module={PhoenixKitWeb.Live.Components.MediaSelectorModal} + id="media-selector-modal" + show={@show_media_selector} + mode={:single} + selected_ids={[current_avatar_id]} + /> + <% else %> + <.live_component + module={PhoenixKitWeb.Live.Components.MediaSelectorModal} + id="media-selector-modal" + show={@show_media_selector} + mode={:single} + /> + <% end %> + <% end %> From 5262a4f43f74a201603c00acdec96f6e7913923b Mon Sep 17 00:00:00 2001 From: Alexander Don Date: Tue, 18 Nov 2025 22:15:17 +0200 Subject: [PATCH 04/40] Changed default bucket to priv/media from priv/uploads --- lib/phoenix_kit/migrations/postgres/v20.ex | 4 ++-- lib/phoenix_kit/storage/providers/local.ex | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/phoenix_kit/migrations/postgres/v20.ex b/lib/phoenix_kit/migrations/postgres/v20.ex index d7ea12b14..d4f8245f8 100644 --- a/lib/phoenix_kit/migrations/postgres/v20.ex +++ b/lib/phoenix_kit/migrations/postgres/v20.ex @@ -357,8 +357,8 @@ defmodule PhoenixKit.Migrations.Postgres.V20 do execute """ INSERT INTO #{prefix_table_name("phoenix_kit_buckets", prefix)} - (id, name, provider, enabled, priority, inserted_at, updated_at) - VALUES ('#{id}', 'Local Storage', 'local', true, 0, '#{now}', '#{now}') + (id, name, provider, endpoint, enabled, priority, inserted_at, updated_at) + VALUES ('#{id}', 'Local Storage', 'local', 'priv/media', true, 0, '#{now}', '#{now}') """ end diff --git a/lib/phoenix_kit/storage/providers/local.ex b/lib/phoenix_kit/storage/providers/local.ex index 83b5e0db4..934968d44 100644 --- a/lib/phoenix_kit/storage/providers/local.ex +++ b/lib/phoenix_kit/storage/providers/local.ex @@ -11,7 +11,7 @@ defmodule PhoenixKit.Storage.Providers.Local do @impl true def store_file(bucket, source_path, destination_path, _opts \\ []) do # Build the full destination path - full_destination = Path.join(bucket.endpoint || "priv/uploads", destination_path) + full_destination = Path.join(bucket.endpoint || "priv/media", destination_path) # Ensure directory exists destination_dir = Path.dirname(full_destination) @@ -35,7 +35,7 @@ defmodule PhoenixKit.Storage.Providers.Local do @impl true def retrieve_file(bucket, file_path, destination_path) do - full_source = Path.join(bucket.endpoint || "priv/uploads", file_path) + full_source = Path.join(bucket.endpoint || "priv/media", file_path) # Ensure destination directory exists destination_dir = Path.dirname(destination_path) @@ -56,7 +56,7 @@ defmodule PhoenixKit.Storage.Providers.Local do @impl true def delete_file(bucket, file_path) do - full_path = Path.join(bucket.endpoint || "priv/uploads", file_path) + full_path = Path.join(bucket.endpoint || "priv/media", file_path) case File.rm(full_path) do :ok -> :ok @@ -70,7 +70,7 @@ defmodule PhoenixKit.Storage.Providers.Local do @impl true def file_exists?(bucket, file_path) do - full_path = Path.join(bucket.endpoint || "priv/uploads", file_path) + full_path = Path.join(bucket.endpoint || "priv/media", file_path) File.exists?(full_path) end @@ -83,7 +83,7 @@ defmodule PhoenixKit.Storage.Providers.Local do @impl true def test_connection(bucket) do - base_path = bucket.endpoint || "priv/uploads" + base_path = bucket.endpoint || "priv/media" # Test if we can create the directory case File.mkdir_p(base_path) do From 07e481e2da8771b38a1cbfb2dde3252d844f796c Mon Sep 17 00:00:00 2001 From: Alexander Don Date: Wed, 19 Nov 2025 00:52:35 +0200 Subject: [PATCH 05/40] Showing error box when tyring to upload without any buckets enabled --- lib/phoenix_kit/storage.ex | 26 +++ .../live/components/media_selector_modal.ex | 87 +++++----- .../components/media_selector_modal.html.heex | 149 +++++++++++------- lib/phoenix_kit_web/live/users/media.ex | 72 ++++++--- .../live/users/media.html.heex | 38 +++-- lib/phoenix_kit_web/users/settings.html.heex | 6 +- lib/phoenix_kit_web/users/user_form.ex | 1 - lib/phoenix_kit_web/users/user_form.html.heex | 2 +- 8 files changed, 253 insertions(+), 128 deletions(-) diff --git a/lib/phoenix_kit/storage.ex b/lib/phoenix_kit/storage.ex index 6ecd379df..63f13997f 100644 --- a/lib/phoenix_kit/storage.ex +++ b/lib/phoenix_kit/storage.ex @@ -897,6 +897,32 @@ defmodule PhoenixKit.Storage do ext, original_filename \\ nil ) do + # Check if any enabled buckets exist + case list_enabled_buckets() do + [] -> + {:error, :no_buckets_configured} + + _buckets -> + # Proceed with storage + store_file_with_buckets_available( + source_path, + file_type, + user_id, + file_checksum, + ext, + original_filename + ) + end + end + + defp store_file_with_buckets_available( + source_path, + file_type, + user_id, + file_checksum, + ext, + original_filename + ) do # Calculate user-specific hash for duplicate detection user_file_checksum = calculate_user_file_checksum(user_id, file_checksum) diff --git a/lib/phoenix_kit_web/live/components/media_selector_modal.ex b/lib/phoenix_kit_web/live/components/media_selector_modal.ex index 971b67de8..a26d9b42c 100644 --- a/lib/phoenix_kit_web/live/components/media_selector_modal.ex +++ b/lib/phoenix_kit_web/live/components/media_selector_modal.ex @@ -49,9 +49,14 @@ defmodule PhoenixKitWeb.Live.Components.MediaSelectorModal do @per_page 30 def update(assigns, socket) do + # Check if any enabled buckets exist + enabled_buckets = Storage.list_enabled_buckets() + has_buckets = length(enabled_buckets) > 0 + socket = socket |> assign(assigns) + |> assign(:has_buckets, has_buckets) |> assign_new(:file_type_filter, fn -> :all end) |> assign_new(:search_query, fn -> "" end) |> assign_new(:current_page, fn -> 1 end) @@ -59,7 +64,7 @@ defmodule PhoenixKitWeb.Live.Components.MediaSelectorModal do |> assign_new(:uploaded_files, fn -> [] end) |> assign_new(:total_count, fn -> 0 end) |> assign_new(:total_pages, fn -> 0 end) - |> maybe_allow_upload() + |> maybe_allow_upload(has_buckets) # Convert selected_ids to MapSet if it's a list socket = @@ -86,16 +91,22 @@ defmodule PhoenixKitWeb.Live.Components.MediaSelectorModal do {:ok, socket} end - defp maybe_allow_upload(socket) do - if socket.assigns[:uploads] do - socket - else - allow_upload(socket, :media_files, - accept: :any, - max_entries: 10, - auto_upload: true, - progress: &handle_progress/3 - ) + defp maybe_allow_upload(socket, has_buckets) do + cond do + socket.assigns[:uploads] -> + socket + + has_buckets -> + allow_upload(socket, :media_files, + accept: :any, + max_entries: 10, + auto_upload: true, + progress: &handle_progress/3 + ) + + true -> + # No buckets - don't allow upload + socket end end @@ -223,33 +234,33 @@ defmodule PhoenixKitWeb.Live.Components.MediaSelectorModal do process_upload(socket, path, entry) end) - # Extract the file ID from the result - consume_uploaded_entry returns [{:ok, file_id}] - new_file_id = - case uploaded_results do - [{:ok, file_id}] when is_binary(file_id) -> file_id - _ -> nil - end - - # Reload files to show the newly uploaded file - {files, total_count} = load_files(socket, socket.assigns.current_page) - total_pages = ceil(total_count / socket.assigns.per_page) - - # Auto-select the newly uploaded file - selected_ids = - if new_file_id do - case socket.assigns.mode do - :single -> MapSet.new([new_file_id]) - :multiple -> MapSet.put(socket.assigns.selected_ids, new_file_id) - end - else - socket.assigns.selected_ids - end - - socket - |> assign(:uploaded_files, files) - |> assign(:total_count, total_count) - |> assign(:total_pages, total_pages) - |> assign(:selected_ids, selected_ids) + # Check if upload failed and handle error + case uploaded_results do + [{:ok, file_id}] when is_binary(file_id) -> + # Success - reload files and auto-select + {files, total_count} = load_files(socket, socket.assigns.current_page) + total_pages = ceil(total_count / socket.assigns.per_page) + + selected_ids = + case socket.assigns.mode do + :single -> MapSet.new([file_id]) + :multiple -> MapSet.put(socket.assigns.selected_ids, file_id) + end + + socket + |> assign(:uploaded_files, files) + |> assign(:total_count, total_count) + |> assign(:total_pages, total_pages) + |> assign(:selected_ids, selected_ids) + + _ -> + # Upload failed - show error message + socket + |> put_flash( + :error, + "Upload failed: No storage buckets configured. Please configure at least one storage bucket before uploading files." + ) + end else socket end diff --git a/lib/phoenix_kit_web/live/components/media_selector_modal.html.heex b/lib/phoenix_kit_web/live/components/media_selector_modal.html.heex index f44909140..5fb4a72c5 100644 --- a/lib/phoenix_kit_web/live/components/media_selector_modal.html.heex +++ b/lib/phoenix_kit_web/live/components/media_selector_modal.html.heex @@ -59,70 +59,111 @@ <%!-- Scrollable Content Area --%>
+ <%!-- No Buckets Warning --%> + <%= if !@has_buckets do %> +
+ <.icon name="hero-exclamation-triangle" class="stroke-current shrink-0 h-6 w-6" /> +
+

No Storage Buckets Configured

+
+ You need to configure at least one storage bucket before you can upload files. + <.link + navigate={PhoenixKit.Utils.Routes.path("/admin/settings/storage")} + class="link link-primary font-semibold" + > + Configure Storage Buckets + +
+
+
+ <% end %> <%!-- Upload Section --%> -
+

<.icon name="hero-arrow-up-tray" class="w-4 h-4" /> Upload New Files

-
- <%!-- Drag and Drop Zone --%> -
-