- Add Site context for managing site-wide content (social links, nav items, announcement bar, footer content) - Add SocialLink schema with URL normalization and platform auto-detection supporting 40+ platforms via host and 25+ via URI scheme - Add NavItem schema for header/footer navigation (editor UI coming next) - Add SiteEditor component with collapsible sections for each content type - Wire social links card block and footer to use database data - Filter empty URLs from display in shop components - Add DetailsPreserver hook to preserve collapsible section state - Add comprehensive tests for Site context and SocialLink functions - Remove unused helper functions from onboarding to fix compiler warnings - Move sync_edit_url_param helper to group handle_editor_event clauses Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
9.1 KiB
Unified Editor Session
Status: Complete Depends on: Editor panel reorganisation (Phase 1-2 complete) Estimated effort: 4-5 hours
Overview
Unify the editing experience across all three tabs (Page, Theme, Site) so they behave consistently:
- Instant preview — all changes show immediately on the shop
- Explicit save — changes only persist to database when you click Save
- Free tab switching — no warnings when switching between tabs
- Accumulated changes — edits across all tabs are saved together
- Navigation warning — warn on page navigation or refresh if any tab has unsaved changes
Current State
| Tab | Dirty Tracking | Save Method | Preview |
|---|---|---|---|
| Page | editor_dirty |
Manual Save | Instant (in-memory blocks) |
| Theme | None | Auto-save | Instant (CSS regenerated) |
| Site | None | Auto-save | Instant (assigns updated) |
Target State
| Tab | Dirty Tracking | Save Method | Preview |
|---|---|---|---|
| Page | page_dirty |
Unified Save | Instant (in-memory blocks) |
| Theme | theme_dirty |
Unified Save | Instant (in-memory settings) |
| Site | site_dirty |
Unified Save | Instant (in-memory settings) |
Unified dirty flag: editor_dirty = page_dirty || theme_dirty || site_dirty
Implementation Tasks
Phase 1: Theme Tab — Preview Without Auto-Save (1.5h)
Currently Theme auto-saves every change. We need to decouple preview from persistence.
1.1 Add theme dirty tracking
In page_editor_hook.ex, add assigns:
|> assign(:theme_dirty, false)
|> assign(:theme_editor_original, nil) # snapshot at load time
1.2 Modify theme event handlers
Change handlers like theme_update_setting, theme_update_color, etc. to:
- Update
theme_editor_settingsin memory (for preview) - Regenerate CSS for live preview
- Set
theme_dirty = true - Do NOT call
Settings.update_theme_settings()
1.3 Store original theme state
When loading theme state in load_theme_state/1:
|> assign(:theme_editor_original, theme_settings)
1.4 Add theme revert logic
On discard/close with dirty state, restore from theme_editor_original.
Phase 2: Site Tab — Preview Without Auto-Save (1h)
2.1 Add site dirty tracking
|> assign(:site_dirty, false)
|> assign(:site_editor_original, nil)
2.2 Modify site event handlers
Change handle_site_update/2 to:
- Update
site_*assigns in memory (for preview) - Set
site_dirty = true - Do NOT call
Site.put_announcement()orSite.put_footer_content()
2.3 Store original site state
When loading site state in load_site_state/1:
original = %{
announcement_text: site_settings.announcement_text,
announcement_link: site_settings.announcement_link,
announcement_style: site_settings.announcement_style,
footer_about: site_settings.footer_about,
footer_copyright: site_settings.footer_copyright,
show_newsletter: site_settings.show_newsletter
}
|> assign(:site_editor_original, original)
Phase 3: Unified Save Button (1h)
3.1 Compute unified dirty state
defp compute_editor_dirty(socket) do
page_dirty = socket.assigns[:editor_dirty] || false
theme_dirty = socket.assigns[:theme_dirty] || false
site_dirty = socket.assigns[:site_dirty] || false
settings_dirty = socket.assigns[:settings_dirty] || false
assign(socket, :any_dirty, page_dirty || theme_dirty || site_dirty || settings_dirty)
end
3.2 Update Save button
In editor_sheet header, change Save button to:
- Enabled when
@any_dirty - On click:
editor_save_allevent
3.3 Implement unified save handler
defp handle_editor_event("editor_save_all", _params, socket) do
socket = save_all_tabs(socket)
{:halt, socket}
end
defp save_all_tabs(socket) do
socket
|> maybe_save_page()
|> maybe_save_theme()
|> maybe_save_site()
|> maybe_save_settings()
|> assign(:editor_save_status, :saved)
|> schedule_save_status_clear()
end
defp maybe_save_page(socket) do
if socket.assigns[:editor_dirty] do
# existing page save logic
else
socket
end
end
defp maybe_save_theme(socket) do
if socket.assigns[:theme_dirty] do
Settings.update_theme_settings(socket.assigns.theme_editor_settings)
assign(socket, :theme_dirty, false)
else
socket
end
end
defp maybe_save_site(socket) do
if socket.assigns[:site_dirty] do
site = socket.assigns
Site.put_announcement(site.site_announcement_text, site.site_announcement_link, site.site_announcement_style)
Site.put_footer_content(site.site_footer_about, site.site_footer_copyright, site.site_footer_show_newsletter)
assign(socket, :site_dirty, false)
else
socket
end
end
Phase 4: Visual Indicators (0.5h)
4.1 Dirty dots on tab labels
In editor_sheet tablist:
<button role="tab" ...>
Page
<span :if={@page_dirty} class="editor-tab-dirty-dot" aria-label="unsaved changes"></span>
</button>
CSS:
.editor-tab-dirty-dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--color-warning);
margin-left: 4px;
}
4.2 Update EditorKeyboard hook
Pass unified dirty state to the hook:
<div id="editor-sheet-inner"
phx-hook="EditorKeyboard"
data-dirty={to_string(@any_dirty)}>
Phase 5: Navigation Warning Flow (1h)
5.1 Update EditorKeyboard JS hook
Modify the navigation intercept in assets/js/app.js:
// On navigation attempt when dirty
if (this.el.dataset.dirty === "true") {
e.preventDefault();
e.stopImmediatePropagation();
// Send event to LiveView to show modal
this.pushEvent("editor_nav_blocked", { href: href });
}
5.2 Add navigation modal component
<dialog :if={@editor_nav_blocked} class="editor-nav-modal" open>
<div class="editor-nav-modal-content">
<h3>Unsaved changes</h3>
<p>You have unsaved changes. What would you like to do?</p>
<div class="editor-nav-modal-actions">
<button phx-click="editor_save_and_navigate">Save & continue</button>
<button phx-click="editor_discard_and_navigate">Discard & continue</button>
<button phx-click="editor_cancel_navigate">Cancel</button>
</div>
</div>
</dialog>
5.3 Handle modal events
defp handle_editor_event("editor_nav_blocked", %{"href" => href}, socket) do
{:halt, assign(socket, editor_nav_blocked: href)}
end
defp handle_editor_event("editor_save_and_navigate", _params, socket) do
socket = save_all_tabs(socket)
{:halt, push_navigate(socket, to: socket.assigns.editor_nav_blocked)}
end
defp handle_editor_event("editor_discard_and_navigate", _params, socket) do
socket = revert_all_tabs(socket)
{:halt, push_navigate(socket, to: socket.assigns.editor_nav_blocked)}
end
defp handle_editor_event("editor_cancel_navigate", _params, socket) do
# Re-open sheet if collapsed
socket =
socket
|> assign(:editor_nav_blocked, nil)
|> assign(:editor_sheet_state, :open)
{:halt, socket}
end
5.4 Add revert logic
defp revert_all_tabs(socket) do
socket
|> maybe_revert_page()
|> maybe_revert_theme()
|> maybe_revert_site()
|> assign(:any_dirty, false)
end
defp maybe_revert_theme(socket) do
if socket.assigns[:theme_dirty] do
original = socket.assigns.theme_editor_original
# Regenerate CSS from original
# Reset theme_editor_settings to original
else
socket
end
end
Files to Modify
| File | Changes |
|---|---|
lib/berrypod_web/page_editor_hook.ex |
Dirty tracking, save handlers, revert logic |
lib/berrypod_web/components/shop_components/layout.ex |
Tab dirty dots, Save button, nav modal |
lib/berrypod_web/page_renderer.ex |
Pass unified dirty state to editor_sheet |
assets/js/app.js |
EditorKeyboard nav intercept |
assets/css/admin/components.css |
Dirty dot styles, nav modal styles |
Testing Plan
- Page tab: Edit blocks → switch tabs → switch back → verify changes preserved
- Theme tab: Change color → switch tabs → verify preview still shows new color
- Site tab: Edit announcement → switch tabs → verify preview shows new text
- Save all: Make changes in all tabs → click Save → verify all persisted
- Navigate away: Make changes → click link → verify warning modal
- Save & continue: Warning modal → Save & continue → verify saved and navigated
- Discard & continue: Warning modal → Discard → verify reverted and navigated
- Cancel: Warning modal → Cancel → verify sheet opens, changes preserved
- Browser refresh: Make changes → F5 → verify beforeunload warning
- Tab close: Make changes → close tab → verify beforeunload warning
Rollback Plan
If issues arise, the immediate fix is to re-enable auto-save for Theme/Site tabs by calling the persistence functions in the event handlers again. The dirty tracking can remain in place.
Future Enhancements
- Undo/redo for Theme/Site: Currently only Page tab has history
- Draft persistence: Save drafts to localStorage for recovery after crash
- Keyboard shortcuts: Cmd+S to save all