replace admin rail with unified bottom sheet editor
All checks were successful
deploy / deploy (push) Successful in 1m30s
All checks were successful
deploy / deploy (push) Successful in 1m30s
- add editor sheet component anchored bottom (mobile) / right (desktop) - admin cog moves to header, always visible for admins - remove Done button from editor header, keep only Save - add editor_at_defaults tracking to disable Reset when at defaults - sheet collapses on click outside or Escape, stays in edit mode - dirty indicator + beforeunload warning for unsaved changes - keyboard shortcuts: Ctrl+Z undo, Ctrl+Shift+Z redo - WCAG compliant: aria-expanded, live region, focus management Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -120,10 +120,18 @@ defmodule BerrypodWeb.Shop.CustomPageTest do
|
||||
:ok
|
||||
end
|
||||
|
||||
test "editing works with ?edit=true", %{conn: conn, user: user} do
|
||||
test "editing works with edit toggle", %{conn: conn, user: user} do
|
||||
conn = log_in_user(conn, user)
|
||||
{:ok, view, _html} = live(conn, "/editable?edit=true")
|
||||
assert has_element?(view, ".page-editor-sidebar")
|
||||
{:ok, view, _html} = live(conn, "/editable")
|
||||
|
||||
# Editor sheet should be visible for admins
|
||||
assert has_element?(view, ".editor-sheet")
|
||||
|
||||
# Click the edit button in the sheet to enter edit mode
|
||||
view |> element(".editor-sheet-edit-btn") |> render_click()
|
||||
|
||||
# Now the editor sheet content should be visible (sheet state changes to open)
|
||||
assert has_element?(view, ".editor-sheet-content")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -29,26 +29,28 @@ defmodule BerrypodWeb.PageEditorHookTest do
|
||||
end
|
||||
|
||||
describe "non-admin cannot access edit mode" do
|
||||
test "visiting with ?edit=true shows normal page, no sidebar", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, "/?edit=true")
|
||||
test "editor sheet is not shown for non-admins", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, "/")
|
||||
|
||||
refute html =~ "page-editor-sidebar"
|
||||
refute html =~ "page-editor-live"
|
||||
refute html =~ "editor-sheet"
|
||||
refute html =~ "Edit page"
|
||||
end
|
||||
end
|
||||
|
||||
describe "edit button visibility" do
|
||||
test "admin sees edit pencil in shop header", %{conn: conn, user: user} do
|
||||
describe "editor sheet visibility" do
|
||||
test "admin sees editor sheet with edit button", %{conn: conn, user: user} do
|
||||
conn = log_in_user(conn, user)
|
||||
{:ok, _view, html} = live(conn, "/")
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
assert html =~ "Edit page"
|
||||
assert has_element?(view, ".editor-sheet")
|
||||
assert has_element?(view, "button", "Edit page")
|
||||
end
|
||||
|
||||
test "non-admin does not see edit pencil", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, "/")
|
||||
test "non-admin does not see editor sheet", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
refute html =~ "Edit page"
|
||||
refute has_element?(view, ".editor-sheet")
|
||||
refute has_element?(view, "button", "Edit page")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -57,69 +59,45 @@ defmodule BerrypodWeb.PageEditorHookTest do
|
||||
%{conn: log_in_user(conn, user)}
|
||||
end
|
||||
|
||||
test "admin enters edit mode with ?edit=true", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/?edit=true")
|
||||
test "clicking edit button enters edit mode", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
assert has_element?(view, ".page-editor-sidebar")
|
||||
assert has_element?(view, ".page-editor-content")
|
||||
# Sheet starts collapsed
|
||||
assert has_element?(view, ".editor-sheet[data-state='collapsed']")
|
||||
|
||||
# Click edit button (the specific edit button class)
|
||||
view |> element(".editor-sheet-edit-btn") |> render_click()
|
||||
|
||||
# Now editing, sheet expanded
|
||||
assert has_element?(view, ".editor-sheet[data-editing='true']")
|
||||
assert has_element?(view, ".editor-sheet-content")
|
||||
assert has_element?(view, ".block-card")
|
||||
end
|
||||
|
||||
test "sidebar shows the page title", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/?edit=true")
|
||||
test "sheet shows the page title when editing", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
assert has_element?(view, ".page-editor-sidebar-title", "Home page")
|
||||
view |> element(".editor-sheet-edit-btn") |> render_click()
|
||||
|
||||
assert has_element?(view, ".editor-sheet-page-title", "Home page")
|
||||
end
|
||||
|
||||
test "done button exits edit mode", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/?edit=true")
|
||||
test "sheet state changes when entering edit mode and collapsing", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# editor_done uses push_navigate, which causes a redirect
|
||||
{:error, {:live_redirect, %{to: "/"}}} =
|
||||
view |> element("button[phx-click='editor_done']") |> render_click()
|
||||
end
|
||||
# Starts collapsed
|
||||
assert has_element?(view, ".editor-sheet[data-state='collapsed']")
|
||||
|
||||
test "toggle sidebar hides and shows via header pencil", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/?edit=true")
|
||||
# Enter edit mode - expands to open
|
||||
view |> element(".editor-sheet-edit-btn") |> render_click()
|
||||
assert has_element?(view, ".editor-sheet[data-state='open']")
|
||||
|
||||
# Sidebar starts open, pencil button in header is hidden
|
||||
assert has_element?(view, "[data-sidebar-open='true']")
|
||||
# Collapse sheet (still in edit mode, just previewing)
|
||||
render_click(view, "editor_set_sheet_state", %{"state" => "collapsed"})
|
||||
|
||||
refute has_element?(
|
||||
view,
|
||||
"button[phx-click='editor_toggle_sidebar'][aria-label='Show editor sidebar']"
|
||||
)
|
||||
|
||||
# Close the sidebar via the backdrop
|
||||
view |> element(".page-editor-backdrop") |> render_click()
|
||||
|
||||
assert has_element?(view, "[data-sidebar-open='false']")
|
||||
# Pencil button appears in header to re-open
|
||||
assert has_element?(
|
||||
view,
|
||||
"button[phx-click='editor_toggle_sidebar'][aria-label='Show editor sidebar']"
|
||||
)
|
||||
|
||||
# Re-open via pencil in header
|
||||
view
|
||||
|> element("button[phx-click='editor_toggle_sidebar'][aria-label='Show editor sidebar']")
|
||||
|> render_click()
|
||||
|
||||
assert has_element?(view, "[data-sidebar-open='true']")
|
||||
end
|
||||
|
||||
test "clicking backdrop hides the sidebar", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/?edit=true")
|
||||
|
||||
# Backdrop present when sidebar is open
|
||||
assert has_element?(view, ".page-editor-backdrop")
|
||||
|
||||
# Click backdrop to dismiss
|
||||
view |> element(".page-editor-backdrop") |> render_click()
|
||||
|
||||
assert has_element?(view, "[data-sidebar-open='false']")
|
||||
# Backdrop gone when sidebar is hidden
|
||||
refute has_element?(view, ".page-editor-backdrop")
|
||||
assert has_element?(view, ".editor-sheet[data-state='collapsed']")
|
||||
# Still in edit mode
|
||||
assert has_element?(view, ".editor-sheet[data-editing='true']")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -129,7 +107,10 @@ defmodule BerrypodWeb.PageEditorHookTest do
|
||||
end
|
||||
|
||||
test "move block down reorders the list", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/?edit=true")
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# Enter edit mode
|
||||
view |> element(".editor-sheet-edit-btn") |> render_click()
|
||||
|
||||
# Home page default: hero is first block
|
||||
first_card = view |> element(".block-card:first-child")
|
||||
@@ -150,9 +131,12 @@ defmodule BerrypodWeb.PageEditorHookTest do
|
||||
end
|
||||
|
||||
test "dirty indicator appears after changes", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/?edit=true")
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
refute has_element?(view, ".admin-badge-warning", "Unsaved changes")
|
||||
# Enter edit mode
|
||||
view |> element(".editor-sheet-edit-btn") |> render_click()
|
||||
|
||||
refute has_element?(view, ".editor-sheet-dirty")
|
||||
|
||||
# Move a block to trigger dirty state
|
||||
blocks = Pages.get_page("home").blocks
|
||||
@@ -162,7 +146,7 @@ defmodule BerrypodWeb.PageEditorHookTest do
|
||||
|> element("button[phx-click='editor_move_up'][phx-value-id='#{second_id}']")
|
||||
|> render_click()
|
||||
|
||||
assert has_element?(view, ".admin-badge-warning", "Unsaved changes")
|
||||
assert has_element?(view, ".editor-sheet-dirty")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -172,7 +156,10 @@ defmodule BerrypodWeb.PageEditorHookTest do
|
||||
end
|
||||
|
||||
test "save persists block changes", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/?edit=true")
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# Enter edit mode
|
||||
view |> element(".editor-sheet-edit-btn") |> render_click()
|
||||
|
||||
# Move a block to make changes
|
||||
blocks = Pages.get_page("home").blocks
|
||||
@@ -186,30 +173,43 @@ defmodule BerrypodWeb.PageEditorHookTest do
|
||||
# Save
|
||||
view |> element("button[phx-click='editor_save']") |> render_click()
|
||||
|
||||
assert has_element?(view, "#shop-flash-info", "Page saved")
|
||||
|
||||
# Verify persistence
|
||||
updated = Pages.get_page("home")
|
||||
refute List.first(updated.blocks)["type"] == original_first_type
|
||||
end
|
||||
|
||||
test "reset restores default blocks", %{conn: conn} do
|
||||
# First, save a modified page
|
||||
test "reset restores default blocks and is undoable", %{conn: conn} do
|
||||
# First, save a modified page (reverse block order)
|
||||
original = Pages.get_page("home")
|
||||
reordered = Enum.reverse(original.blocks)
|
||||
Pages.save_page("home", %{title: original.title, blocks: reordered})
|
||||
PageCache.invalidate_all()
|
||||
|
||||
{:ok, view, _html} = live(conn, "/?edit=true")
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# Enter edit mode
|
||||
view |> element(".editor-sheet-edit-btn") |> render_click()
|
||||
|
||||
# Get the first block type before reset (should be reversed, so last default)
|
||||
first_before_reset = view |> element(".block-card:first-child") |> render()
|
||||
|
||||
# Reset
|
||||
view |> element("button[phx-click='editor_reset_defaults']") |> render_click()
|
||||
|
||||
assert has_element?(view, "#shop-flash-info", "Page reset to defaults")
|
||||
# First block should now be the default first (Hero)
|
||||
first_after_reset = view |> element(".block-card:first-child") |> render()
|
||||
assert first_after_reset =~ "Hero"
|
||||
refute first_after_reset == first_before_reset
|
||||
|
||||
# Verify the blocks are back to defaults
|
||||
reset_page = Pages.get_page("home")
|
||||
assert List.first(reset_page.blocks)["type"] == List.first(original.blocks)["type"]
|
||||
# Reset should be undoable
|
||||
refute has_element?(view, "button[phx-click='editor_undo'][disabled]")
|
||||
|
||||
# Undo the reset
|
||||
view |> element("button[phx-click='editor_undo']") |> render_click()
|
||||
|
||||
# Should be back to the reversed order
|
||||
first_after_undo = view |> element(".block-card:first-child") |> render()
|
||||
refute first_after_undo =~ "Hero"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -219,7 +219,10 @@ defmodule BerrypodWeb.PageEditorHookTest do
|
||||
end
|
||||
|
||||
test "undo reverts the last change", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/?edit=true")
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# Enter edit mode
|
||||
view |> element(".editor-sheet-edit-btn") |> render_click()
|
||||
|
||||
blocks = Pages.get_page("home").blocks
|
||||
second_id = Enum.at(blocks, 1)["id"]
|
||||
@@ -241,7 +244,10 @@ defmodule BerrypodWeb.PageEditorHookTest do
|
||||
end
|
||||
|
||||
test "redo restores an undone change", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/?edit=true")
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# Enter edit mode
|
||||
view |> element(".editor-sheet-edit-btn") |> render_click()
|
||||
|
||||
blocks = Pages.get_page("home").blocks
|
||||
second_id = Enum.at(blocks, 1)["id"]
|
||||
@@ -259,7 +265,10 @@ defmodule BerrypodWeb.PageEditorHookTest do
|
||||
end
|
||||
|
||||
test "history clears on save", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/?edit=true")
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# Enter edit mode
|
||||
view |> element(".editor-sheet-edit-btn") |> render_click()
|
||||
|
||||
blocks = Pages.get_page("home").blocks
|
||||
second_id = Enum.at(blocks, 1)["id"]
|
||||
@@ -275,7 +284,10 @@ defmodule BerrypodWeb.PageEditorHookTest do
|
||||
end
|
||||
|
||||
test "undo/redo buttons reflect stack state", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/?edit=true")
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# Enter edit mode
|
||||
view |> element(".editor-sheet-edit-btn") |> render_click()
|
||||
|
||||
# Initially both disabled
|
||||
assert has_element?(view, "button[phx-click='editor_undo'][disabled]")
|
||||
@@ -295,16 +307,22 @@ defmodule BerrypodWeb.PageEditorHookTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "content pages (deferred init)" do
|
||||
describe "content pages" do
|
||||
setup %{conn: conn, user: user} do
|
||||
%{conn: log_in_user(conn, user)}
|
||||
end
|
||||
|
||||
test "editing works on about page via deferred init", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/about?edit=true")
|
||||
test "editing works on about page", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/about")
|
||||
|
||||
assert has_element?(view, ".page-editor-sidebar")
|
||||
assert has_element?(view, ".page-editor-sidebar-title", "About")
|
||||
# Editor sheet visible for admin
|
||||
assert has_element?(view, ".editor-sheet")
|
||||
|
||||
# Enter edit mode
|
||||
view |> element(".editor-sheet-edit-btn") |> render_click()
|
||||
|
||||
assert has_element?(view, ".editor-sheet-content")
|
||||
assert has_element?(view, ".editor-sheet-page-title", "About")
|
||||
assert has_element?(view, ".block-card")
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user