# 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: ```elixir |> 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: 1. Update `theme_editor_settings` in memory (for preview) 2. Regenerate CSS for live preview 3. Set `theme_dirty = true` 4. **Do NOT call `Settings.update_theme_settings()`** #### 1.3 Store original theme state When loading theme state in `load_theme_state/1`: ```elixir |> 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 ```elixir |> assign(:site_dirty, false) |> assign(:site_editor_original, nil) ``` #### 2.2 Modify site event handlers Change `handle_site_update/2` to: 1. Update `site_*` assigns in memory (for preview) 2. Set `site_dirty = true` 3. **Do NOT call `Site.put_announcement()` or `Site.put_footer_content()`** #### 2.3 Store original site state When loading site state in `load_site_state/1`: ```elixir 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 ```elixir 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_all` event #### 3.3 Implement unified save handler ```elixir 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: ```heex ``` CSS: ```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: ```heex
``` ### Phase 5: Navigation Warning Flow (1h) #### 5.1 Update EditorKeyboard JS hook Modify the navigation intercept in `assets/js/app.js`: ```javascript // 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 ```heex

Unsaved changes

You have unsaved changes. What would you like to do?

``` #### 5.3 Handle modal events ```elixir 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 ```elixir 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 1. **Page tab**: Edit blocks → switch tabs → switch back → verify changes preserved 2. **Theme tab**: Change color → switch tabs → verify preview still shows new color 3. **Site tab**: Edit announcement → switch tabs → verify preview shows new text 4. **Save all**: Make changes in all tabs → click Save → verify all persisted 5. **Navigate away**: Make changes → click link → verify warning modal 6. **Save & continue**: Warning modal → Save & continue → verify saved and navigated 7. **Discard & continue**: Warning modal → Discard → verify reverted and navigated 8. **Cancel**: Warning modal → Cancel → verify sheet opens, changes preserved 9. **Browser refresh**: Make changes → F5 → verify beforeunload warning 10. **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