Show upload progress and submission confirmation on /submit

Add a per-entry progress bar (with cancel) while a CSV streams in, and
replace the small post-commit alert with a prominent "X contacts
submitted" confirmation card.
This commit is contained in:
Graham McIntire 2026-04-09 13:46:27 -05:00
parent 912f161874
commit f21277cf4f
2 changed files with 71 additions and 27 deletions

View file

@ -109,6 +109,10 @@ defmodule MicrowavepropWeb.SubmitLive do
end
end
def handle_event("cancel_csv_upload", %{"ref" => ref}, socket) do
{:noreply, cancel_upload(socket, :csv, ref)}
end
def handle_event("confirm_csv", _params, socket) do
case socket.assigns.csv_preview do
%{valid: valid_rows} when valid_rows != [] ->
@ -117,7 +121,7 @@ defmodule MicrowavepropWeb.SubmitLive do
{:noreply,
socket
|> assign(csv_preview: nil, csv_result: commit_result)
|> put_flash(:info, "Imported #{length(commit_result.imported)} contacts.")}
|> put_flash(:info, "#{length(commit_result.imported)} contacts submitted.")}
_ ->
{:noreply, put_flash(socket, :error, "Nothing to import.")}
@ -164,6 +168,11 @@ defmodule MicrowavepropWeb.SubmitLive do
end
end
defp error_to_string(:too_large), do: "File is too large"
defp error_to_string(:too_many_files), do: "Only one file allowed"
defp error_to_string(:not_accepted), do: "Only .csv files are accepted"
defp error_to_string(other), do: to_string(other)
@impl true
def render(assigns) do
~H"""
@ -348,6 +357,37 @@ defmodule MicrowavepropWeb.SubmitLive do
</label>
</div>
<div :for={entry <- @uploads.csv.entries} class="space-y-1">
<div class="flex items-center justify-between text-xs tabular-nums">
<span class="truncate pr-2">{entry.client_name}</span>
<span class="flex items-center gap-2 shrink-0">
<span>{entry.progress}%</span>
<button
type="button"
class="btn btn-ghost btn-xs"
phx-click="cancel_csv_upload"
phx-value-ref={entry.ref}
aria-label="Cancel upload"
>
<.icon name="hero-x-mark" class="w-4 h-4" />
</button>
</span>
</div>
<progress
class="progress progress-primary w-full"
value={entry.progress}
max="100"
>
</progress>
<div :for={err <- upload_errors(@uploads.csv, entry)} class="text-xs text-error">
{error_to_string(err)}
</div>
</div>
<div :for={err <- upload_errors(@uploads.csv)} class="text-xs text-error">
{error_to_string(err)}
</div>
<%= if @current_user do %>
<input type="hidden" name="submitter_email" value={@current_user.email} />
<% else %>
@ -559,24 +599,32 @@ defmodule MicrowavepropWeb.SubmitLive do
defp duplicate_source(_), do: "Duplicate"
defp csv_results(assigns) do
~H"""
<div class="space-y-4">
<%= if length(@result.imported) > 0 do %>
<div class="alert alert-success">
<.icon name="hero-check-circle" class="w-5 h-5" />
<span>
{length(@result.imported)} {if length(@result.imported) == 1,
do: "contact",
else: "contacts"} imported successfully
</span>
</div>
<% end %>
assigns =
assign(assigns,
imported_count: length(assigns.result.imported),
error_count: length(assigns.result.errors)
)
<%= if length(@result.errors) > 0 do %>
~H"""
<div class="space-y-6">
<div
:if={@imported_count > 0}
class="rounded-box border border-success/40 bg-success/10 p-8 text-center"
>
<.icon name="hero-check-circle" class="w-16 h-16 text-success mx-auto mb-3" />
<div class="text-3xl font-bold tabular-nums">
{@imported_count} {if @imported_count == 1, do: "contact", else: "contacts"} submitted
</div>
<p class="text-sm opacity-70 mt-2">
Weather, terrain, and propagation data will be attached shortly.
</p>
</div>
<div :if={@error_count > 0} class="space-y-2">
<div class="alert alert-error">
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
<span>
{length(@result.errors)} {if length(@result.errors) == 1, do: "row", else: "rows"} had errors at insert time
{@error_count} {if @error_count == 1, do: "row", else: "rows"} had errors at insert time
</span>
</div>
@ -589,20 +637,16 @@ defmodule MicrowavepropWeb.SubmitLive do
</tr>
</thead>
<tbody>
<%= for {row_num, messages} <- @result.errors do %>
<tr>
<td class="font-mono">Row {row_num}</td>
<td>
<%= for msg <- messages do %>
<div>{msg}</div>
<% end %>
</td>
</tr>
<% end %>
<tr :for={{row_num, messages} <- @result.errors}>
<td class="font-mono">Row {row_num}</td>
<td>
<div :for={msg <- messages}>{msg}</div>
</td>
</tr>
</tbody>
</table>
</div>
<% end %>
</div>
<button type="button" class="btn btn-outline" phx-click="reset_csv">
Upload another

View file

@ -102,7 +102,7 @@ defmodule MicrowavepropWeb.SubmitLiveTest do
|> element("button[phx-click=confirm_csv]")
|> render_click()
assert confirm_html =~ "1 contact imported"
assert confirm_html =~ "1 contact submitted"
assert Repo.aggregate(Contact, :count) == 1
end