This commit is contained in:
carla 2026-01-23 12:54:48 +01:00 committed by Moritz
parent 04b0916c1e
commit bf7e47ce5c
Signed by: moritz
GPG key ID: 1020A035E5DD0824
7 changed files with 147 additions and 126 deletions

View file

@ -48,6 +48,7 @@ defmodule MvWeb.GlobalSettingsLive do
alias Mv.Membership
alias Mv.Membership.Import.MemberCSV
alias MvWeb.Authorization
alias Mv.Config
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
@ -140,14 +141,6 @@ defmodule MvWeb.GlobalSettingsLive do
"Use the custom field name as the CSV column header (same normalization as member fields applies)"
)}
</p>
<div class="mt-2">
<.link
navigate={~p"/custom_field_values"}
class="link link-primary"
>
{gettext("Manage Custom Fields")}
</.link>
</div>
</div>
</div>
@ -209,7 +202,8 @@ defmodule MvWeb.GlobalSettingsLive do
phx-disable-with={gettext("Starting import...")}
variant="primary"
disabled={
Enum.empty?(@uploads.csv_file.entries) or
@import_status == :running or
Enum.empty?(@uploads.csv_file.entries) or
@uploads.csv_file.entries |> List.first() |> then(&(&1 && not &1.done?))
}
data-testid="start-import-button"
@ -226,7 +220,7 @@ defmodule MvWeb.GlobalSettingsLive do
class="mt-4"
data-testid="import-progress-container"
>
<%= if @import_status == :running do %>
<%= if @import_progress.status == :running do %>
<p class="text-sm" data-testid="import-progress-text">
{gettext("Processing chunk %{current} of %{total}...",
current: @import_progress.current_chunk,
@ -235,7 +229,7 @@ defmodule MvWeb.GlobalSettingsLive do
</p>
<% end %>
<%= if @import_status == :done do %>
<%= if @import_progress.status == :done do %>
<section class="space-y-4" data-testid="import-results-panel">
<h2 class="text-lg font-semibold">
{gettext("Import Results")}
@ -372,51 +366,85 @@ defmodule MvWeb.GlobalSettingsLive do
def handle_event("start_import", _params, socket) do
# Server-side admin check
if Authorization.can?(socket.assigns[:current_user], :create, Mv.Membership.Member) do
# Check if upload is completed
upload_entries = socket.assigns.uploads.csv_file.entries
if Enum.empty?(upload_entries) do
# Prevent concurrent imports
if socket.assigns.import_status == :running do
{:noreply,
put_flash(
socket,
:error,
gettext("Please select a CSV file to import.")
gettext("Import is already running. Please wait for it to complete.")
)}
else
entry = List.first(upload_entries)
# Check if upload is completed
upload_entries = socket.assigns.uploads.csv_file.entries
if entry.done? do
with {:ok, content} <- consume_and_read_csv(socket) do
case MemberCSV.prepare(content) do
{:ok, import_state} ->
total_chunks = length(import_state.chunks)
if Enum.empty?(upload_entries) do
{:noreply,
put_flash(
socket,
:error,
gettext("Please select a CSV file to import.")
)}
else
entry = List.first(upload_entries)
progress = %{
inserted: 0,
failed: 0,
errors: [],
warnings: import_state.warnings || [],
status: :running,
current_chunk: 0,
total_chunks: total_chunks
}
if entry.done? do
with {:ok, content} <- consume_and_read_csv(socket) do
case MemberCSV.prepare(content) do
{:ok, import_state} ->
total_chunks = length(import_state.chunks)
socket =
socket
|> assign(:import_state, import_state)
|> assign(:import_progress, progress)
|> assign(:import_status, :running)
progress = %{
inserted: 0,
failed: 0,
errors: [],
warnings: import_state.warnings || [],
status: :running,
current_chunk: 0,
total_chunks: total_chunks,
errors_truncated?: false
}
send(self(), {:process_chunk, 0})
socket =
socket
|> assign(:import_state, import_state)
|> assign(:import_progress, progress)
|> assign(:import_status, :running)
{:noreply, socket}
send(self(), {:process_chunk, 0})
{:noreply, socket}
{:error, error} ->
error_message =
case error do
%{message: msg} when is_binary(msg) -> msg
%{errors: errors} when is_list(errors) -> inspect(errors)
reason when is_binary(reason) -> reason
other -> inspect(other)
end
{:noreply,
put_flash(
socket,
:error,
gettext("Failed to prepare CSV import: %{error}", error: error_message)
)}
end
else
{:error, reason} when is_binary(reason) ->
{:noreply,
put_flash(
socket,
:error,
gettext("Failed to prepare CSV import: %{reason}", reason: reason)
)}
{:error, error} ->
error_message =
case error do
%{message: msg} when is_binary(msg) -> msg
%{errors: errors} when is_list(errors) -> inspect(errors)
reason when is_binary(reason) -> reason
other -> inspect(other)
end
@ -428,36 +456,13 @@ defmodule MvWeb.GlobalSettingsLive do
)}
end
else
{:error, reason} when is_binary(reason) ->
{:noreply,
put_flash(
socket,
:error,
gettext("Failed to prepare CSV import: %{reason}", reason: reason)
)}
{:error, error} ->
error_message =
case error do
%{message: msg} when is_binary(msg) -> msg
%{errors: errors} when is_list(errors) -> inspect(errors)
other -> inspect(other)
end
{:noreply,
put_flash(
socket,
:error,
gettext("Failed to prepare CSV import: %{error}", error: error_message)
)}
{:noreply,
put_flash(
socket,
:error,
gettext("Please wait for the file upload to complete before starting the import.")
)}
end
else
{:noreply,
put_flash(
socket,
:error,
gettext("Please wait for the file upload to complete before starting the import.")
)}
end
end
else
@ -576,6 +581,7 @@ defmodule MvWeb.GlobalSettingsLive do
end
# Starts async task to process a chunk
# In tests (SQL sandbox mode), runs synchronously to avoid Ecto Sandbox issues
defp start_chunk_processing_task(socket, import_state, progress, idx) do
chunk = Enum.at(import_state.chunks, idx)
actor = socket.assigns[:current_user]
@ -589,21 +595,33 @@ defmodule MvWeb.GlobalSettingsLive do
actor: actor
]
# Start async task to process chunk
Task.Supervisor.async_nolink(Mv.TaskSupervisor, fn ->
case MemberCSV.process_chunk(
chunk,
import_state.column_map,
import_state.custom_field_map,
opts
) do
{:ok, chunk_result} ->
send(live_view_pid, {:chunk_done, idx, chunk_result})
if Config.sql_sandbox?() do
# Run synchronously in tests to avoid Ecto Sandbox issues with async tasks
{:ok, chunk_result} =
MemberCSV.process_chunk(
chunk,
import_state.column_map,
import_state.custom_field_map,
opts
)
{:error, reason} ->
send(live_view_pid, {:chunk_error, idx, reason})
end
end)
# In test mode, send the message - it will be processed when render() is called
# in the test. The test helper wait_for_import_completion() handles message processing
send(live_view_pid, {:chunk_done, idx, chunk_result})
else
# Start async task to process chunk in production
Task.Supervisor.async_nolink(Mv.TaskSupervisor, fn ->
{:ok, chunk_result} =
MemberCSV.process_chunk(
chunk,
import_state.column_map,
import_state.custom_field_map,
opts
)
send(live_view_pid, {:chunk_done, idx, chunk_result})
end)
end
{:noreply, socket}
end
@ -670,10 +688,10 @@ defmodule MvWeb.GlobalSettingsLive do
end
end)
|> case do
[{_name, {:ok, content}}] when is_binary(content) ->
[{:ok, content}] when is_binary(content) ->
{:ok, content}
[{_name, {:error, reason}}] ->
[{:error, reason}] ->
{:error, gettext("Failed to read file: %{reason}", reason: reason)}
[] ->