migrate admin forms to inline feedback
All checks were successful
deploy / deploy (push) Successful in 1m26s
All checks were successful
deploy / deploy (push) Successful in 1m26s
Replace put_flash calls with inline feedback for form saves: - Email settings: "Now send a test email" after saving - Settings: from address and signing secret saves - Page editor: save button shows "Saved" checkmark Inline feedback appears next to save buttons and auto-clears after 3 seconds. Banners (put_flash) remain for page-level outcomes like deletions, state changes, and async operations. Task 3 of notification overhaul. Theme editor skipped as it auto-saves. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
bd07c9c7d9
commit
42542ac177
@ -69,13 +69,13 @@ Based on usability testing (March 2026). Reworks the entire setup flow into a si
|
|||||||
|
|
||||||
### Notification system overhaul ([plan](docs/plans/notification-overhaul.md))
|
### Notification system overhaul ([plan](docs/plans/notification-overhaul.md))
|
||||||
|
|
||||||
Replace floating toast/flash messages with inline feedback and persistent top banners. 110+ flash messages across 28 files to migrate.
|
Replace floating toast/flash messages with inline feedback and persistent top banners. ~140 flash messages across 28 files to migrate. Inline feedback for form saves, banners for page-level outcomes.
|
||||||
|
|
||||||
| # | Task | Est | Status |
|
| # | Task | Est | Status |
|
||||||
|---|------|-----|--------|
|
|---|------|-----|--------|
|
||||||
| 1 | Build inline feedback component | 1.5h | planned |
|
| 1 | Build inline feedback component | 1.5h | done |
|
||||||
| 2 | Build persistent top banner component (replaces flash) | 1.5h | planned |
|
| 2 | Build persistent top banner component (replaces flash) | 1.5h | done |
|
||||||
| 3 | Migrate admin forms to inline feedback (theme, pages, settings, email, providers) | 3h | planned |
|
| 3 | Migrate admin forms to inline feedback (theme, pages, settings, email, providers) | 3h | in progress |
|
||||||
| 4 | Migrate remaining admin pages (media, products, activity, newsletter, redirects, nav) | 2h | planned |
|
| 4 | Migrate remaining admin pages (media, products, activity, newsletter, redirects, nav) | 2h | planned |
|
||||||
| 5 | Migrate shop pages (cart, contact, checkout, auth) | 2h | planned |
|
| 5 | Migrate shop pages (cart, contact, checkout, auth) | 2h | planned |
|
||||||
| 6 | Migrate setup wizard notifications | 1h | planned |
|
| 6 | Migrate setup wizard notifications | 1h | planned |
|
||||||
|
|||||||
@ -37,6 +37,7 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
|||||||
|> assign(:test_retryable, false)
|
|> assign(:test_retryable, false)
|
||||||
|> assign(:from_checklist, false)
|
|> assign(:from_checklist, false)
|
||||||
|> assign(:field_errors, flash_errors)
|
|> assign(:field_errors, flash_errors)
|
||||||
|
|> assign(:save_status, :idle)
|
||||||
|> assign(:form, to_form(%{}, as: :email))}
|
|> assign(:form, to_form(%{}, as: :email))}
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -93,6 +94,8 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
|||||||
socket.assigns.current_scope.user.email
|
socket.assigns.current_scope.user.email
|
||||||
) do
|
) do
|
||||||
{:ok, _adapter_info} ->
|
{:ok, _adapter_info} ->
|
||||||
|
Process.send_after(self(), :clear_save_status, 3000)
|
||||||
|
|
||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|> assign(:adapter_key, adapter_key)
|
|> assign(:adapter_key, adapter_key)
|
||||||
@ -102,7 +105,7 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
|||||||
|> assign(:field_errors, %{})
|
|> assign(:field_errors, %{})
|
||||||
|> assign(:test_result, nil)
|
|> assign(:test_result, nil)
|
||||||
|> assign(:test_error, nil)
|
|> assign(:test_error, nil)
|
||||||
|> put_flash(:info, "Settings saved — send a test email to check it works")}
|
|> assign(:save_status, :saved)}
|
||||||
|
|
||||||
{:error, field_errors} when is_map(field_errors) ->
|
{:error, field_errors} when is_map(field_errors) ->
|
||||||
{:noreply, assign(socket, :field_errors, field_errors)}
|
{:noreply, assign(socket, :field_errors, field_errors)}
|
||||||
@ -142,6 +145,10 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
|||||||
# Swoosh test adapter sends {:email, ...} messages — ignore them
|
# Swoosh test adapter sends {:email, ...} messages — ignore them
|
||||||
def handle_info({:email, _}, socket), do: {:noreply, socket}
|
def handle_info({:email, _}, socket), do: {:noreply, socket}
|
||||||
|
|
||||||
|
def handle_info(:clear_save_status, socket) do
|
||||||
|
{:noreply, assign(socket, :save_status, :idle)}
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
@ -235,6 +242,7 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
|||||||
values={@all_values[adapter.key] || %{}}
|
values={@all_values[adapter.key] || %{}}
|
||||||
field_errors={if(@adapter_key == adapter.key, do: @field_errors, else: %{})}
|
field_errors={if(@adapter_key == adapter.key, do: @field_errors, else: %{})}
|
||||||
env_locked={@env_locked}
|
env_locked={@env_locked}
|
||||||
|
save_status={if(@adapter_key == adapter.key, do: @save_status, else: :idle)}
|
||||||
/>
|
/>
|
||||||
</.form>
|
</.form>
|
||||||
</section>
|
</section>
|
||||||
@ -336,6 +344,7 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
|||||||
attr :values, :map, required: true
|
attr :values, :map, required: true
|
||||||
attr :field_errors, :map, required: true
|
attr :field_errors, :map, required: true
|
||||||
attr :env_locked, :boolean, required: true
|
attr :env_locked, :boolean, required: true
|
||||||
|
attr :save_status, :atom, default: :idle
|
||||||
|
|
||||||
defp adapter_config(assigns) do
|
defp adapter_config(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
@ -377,6 +386,7 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
|||||||
<.button phx-disable-with="Saving...">
|
<.button phx-disable-with="Saving...">
|
||||||
Save settings
|
Save settings
|
||||||
</.button>
|
</.button>
|
||||||
|
<.inline_feedback status={@save_status} message="Now send a test email" />
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -32,6 +32,7 @@ defmodule BerrypodWeb.Admin.Pages.Editor do
|
|||||||
|> assign(:history, [])
|
|> assign(:history, [])
|
||||||
|> assign(:future, [])
|
|> assign(:future, [])
|
||||||
|> assign(:dirty, false)
|
|> assign(:dirty, false)
|
||||||
|
|> assign(:save_status, :idle)
|
||||||
|> assign(:show_picker, false)
|
|> assign(:show_picker, false)
|
||||||
|> assign(:picker_filter, "")
|
|> assign(:picker_filter, "")
|
||||||
|> assign(:expanded, MapSet.new())
|
|> assign(:expanded, MapSet.new())
|
||||||
@ -383,13 +384,18 @@ defmodule BerrypodWeb.Admin.Pages.Editor do
|
|||||||
|
|
||||||
case Pages.save_page(slug, %{title: page_data.title, blocks: blocks}) do
|
case Pages.save_page(slug, %{title: page_data.title, blocks: blocks}) do
|
||||||
{:ok, _page} ->
|
{:ok, _page} ->
|
||||||
|
Process.send_after(self(), :clear_save_status, 3000)
|
||||||
|
|
||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|> assign(:dirty, false)
|
|> assign(:dirty, false)
|
||||||
|> put_flash(:info, "Page saved")}
|
|> assign(:save_status, :saved)}
|
||||||
|
|
||||||
{:error, _changeset} ->
|
{:error, _changeset} ->
|
||||||
{:noreply, put_flash(socket, :error, "Failed to save page")}
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:save_status, :error)
|
||||||
|
|> put_flash(:error, "Failed to save page")}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -515,6 +521,13 @@ defmodule BerrypodWeb.Admin.Pages.Editor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# ── Handle info ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info(:clear_save_status, socket) do
|
||||||
|
{:noreply, assign(socket, :save_status, :idle)}
|
||||||
|
end
|
||||||
|
|
||||||
# ── Render ───────────────────────────────────────────────────────
|
# ── Render ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
@ -586,6 +599,7 @@ defmodule BerrypodWeb.Admin.Pages.Editor do
|
|||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
|
<.inline_feedback status={@save_status} />
|
||||||
</:actions>
|
</:actions>
|
||||||
</.header>
|
</.header>
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,8 @@ defmodule BerrypodWeb.Admin.Settings do
|
|||||||
|> assign(:site_live, Settings.site_live?())
|
|> assign(:site_live, Settings.site_live?())
|
||||||
|> assign(:cart_recovery_enabled, Settings.abandoned_cart_recovery_enabled?())
|
|> assign(:cart_recovery_enabled, Settings.abandoned_cart_recovery_enabled?())
|
||||||
|> assign(:from_address, Settings.get_setting("email_from_address") || user.email)
|
|> assign(:from_address, Settings.get_setting("email_from_address") || user.email)
|
||||||
|
|> assign(:from_address_status, :idle)
|
||||||
|
|> assign(:signing_secret_status, :idle)
|
||||||
|> assign_stripe_state()
|
|> assign_stripe_state()
|
||||||
|> assign_products_state()
|
|> assign_products_state()
|
||||||
|> assign_account_state(user)}
|
|> assign_account_state(user)}
|
||||||
@ -116,11 +118,12 @@ defmodule BerrypodWeb.Admin.Settings do
|
|||||||
|
|
||||||
if address != "" do
|
if address != "" do
|
||||||
Settings.put_setting("email_from_address", address)
|
Settings.put_setting("email_from_address", address)
|
||||||
|
Process.send_after(self(), :clear_from_address_status, 3000)
|
||||||
|
|
||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|> assign(:from_address, address)
|
|> assign(:from_address, address)
|
||||||
|> put_flash(:info, "From address saved")}
|
|> assign(:from_address_status, :saved)}
|
||||||
else
|
else
|
||||||
{:noreply, put_flash(socket, :error, "From address can't be blank")}
|
{:noreply, put_flash(socket, :error, "From address can't be blank")}
|
||||||
end
|
end
|
||||||
@ -181,11 +184,12 @@ defmodule BerrypodWeb.Admin.Settings do
|
|||||||
{:noreply, put_flash(socket, :error, "Please enter a signing secret")}
|
{:noreply, put_flash(socket, :error, "Please enter a signing secret")}
|
||||||
else
|
else
|
||||||
StripeSetup.save_signing_secret(secret)
|
StripeSetup.save_signing_secret(secret)
|
||||||
|
Process.send_after(self(), :clear_signing_secret_status, 3000)
|
||||||
|
|
||||||
socket =
|
socket =
|
||||||
socket
|
socket
|
||||||
|> assign_stripe_state()
|
|> assign_stripe_state()
|
||||||
|> put_flash(:info, "Webhook signing secret saved")
|
|> assign(:signing_secret_status, :saved)
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
@ -289,6 +293,17 @@ defmodule BerrypodWeb.Admin.Settings do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# -- Clear status messages --
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info(:clear_from_address_status, socket) do
|
||||||
|
{:noreply, assign(socket, :from_address_status, :idle)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info(:clear_signing_secret_status, socket) do
|
||||||
|
{:noreply, assign(socket, :signing_secret_status, :idle)}
|
||||||
|
end
|
||||||
|
|
||||||
# -- Render --
|
# -- Render --
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
@ -364,6 +379,7 @@ defmodule BerrypodWeb.Admin.Settings do
|
|||||||
stripe_has_signing_secret={@stripe_has_signing_secret}
|
stripe_has_signing_secret={@stripe_has_signing_secret}
|
||||||
secret_form={@secret_form}
|
secret_form={@secret_form}
|
||||||
advanced_open={@stripe_advanced_open}
|
advanced_open={@stripe_advanced_open}
|
||||||
|
signing_secret_status={@signing_secret_status}
|
||||||
/>
|
/>
|
||||||
<% end %>
|
<% end %>
|
||||||
</section>
|
</section>
|
||||||
@ -448,6 +464,7 @@ defmodule BerrypodWeb.Admin.Settings do
|
|||||||
placeholder="noreply@yourshop.com"
|
placeholder="noreply@yourshop.com"
|
||||||
/>
|
/>
|
||||||
<.button phx-disable-with="Saving...">Save</.button>
|
<.button phx-disable-with="Saving...">Save</.button>
|
||||||
|
<.inline_feedback status={@from_address_status} />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -704,6 +721,7 @@ defmodule BerrypodWeb.Admin.Settings do
|
|||||||
/>
|
/>
|
||||||
<div class="admin-form-actions-sm">
|
<div class="admin-form-actions-sm">
|
||||||
<.button phx-disable-with="Saving...">Save signing secret</.button>
|
<.button phx-disable-with="Saving...">Save signing secret</.button>
|
||||||
|
<.inline_feedback status={@signing_secret_status} />
|
||||||
</div>
|
</div>
|
||||||
</.form>
|
</.form>
|
||||||
<% else %>
|
<% else %>
|
||||||
@ -733,6 +751,7 @@ defmodule BerrypodWeb.Admin.Settings do
|
|||||||
/>
|
/>
|
||||||
<div class="admin-form-actions-sm">
|
<div class="admin-form-actions-sm">
|
||||||
<.button phx-disable-with="Saving...">Save signing secret</.button>
|
<.button phx-disable-with="Saving...">Save signing secret</.button>
|
||||||
|
<.inline_feedback status={@signing_secret_status} />
|
||||||
</div>
|
</div>
|
||||||
</.form>
|
</.form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -97,7 +97,7 @@ defmodule BerrypodWeb.Admin.EmailSettingsTest do
|
|||||||
})
|
})
|
||||||
|> render_submit()
|
|> render_submit()
|
||||||
|
|
||||||
assert html =~ "Settings saved"
|
assert html =~ "Now send a test email"
|
||||||
assert Settings.get_setting("email_adapter") == "brevo"
|
assert Settings.get_setting("email_adapter") == "brevo"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -208,7 +208,7 @@ defmodule BerrypodWeb.Admin.PagesTest do
|
|||||||
|
|
||||||
render_click(view, "save")
|
render_click(view, "save")
|
||||||
|
|
||||||
assert render(view) =~ "Page saved"
|
assert has_element?(view, ".admin-inline-feedback-saved")
|
||||||
|
|
||||||
saved = Pages.get_page("home")
|
saved = Pages.get_page("home")
|
||||||
assert length(saved.blocks) == 3
|
assert length(saved.blocks) == 3
|
||||||
|
|||||||
@ -113,7 +113,7 @@ defmodule BerrypodWeb.Admin.SettingsTest do
|
|||||||
})
|
})
|
||||||
|> render_submit()
|
|> render_submit()
|
||||||
|
|
||||||
assert html =~ "Webhook signing secret saved"
|
assert has_element?(view, ".admin-inline-feedback-saved")
|
||||||
assert html =~ "whsec_te•••456"
|
assert html =~ "whsec_te•••456"
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -261,7 +261,7 @@ defmodule BerrypodWeb.Admin.SettingsTest do
|
|||||||
|> form("form[phx-submit=\"save_from_address\"]", %{from_address: "shop@example.com"})
|
|> form("form[phx-submit=\"save_from_address\"]", %{from_address: "shop@example.com"})
|
||||||
|> render_submit()
|
|> render_submit()
|
||||||
|
|
||||||
assert html =~ "From address saved"
|
assert has_element?(view, ".admin-inline-feedback-saved")
|
||||||
assert Settings.get_setting("email_from_address") == "shop@example.com"
|
assert Settings.get_setting("email_from_address") == "shop@example.com"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user