complete editor panel reorganisation polish
All checks were successful
deploy / deploy (push) Successful in 6m49s
All checks were successful
deploy / deploy (push) Successful in 6m49s
- fix Site tab not loading theme state on direct URL navigation - fix nav editor showing "Custom URL" for page links (detect by URL match) - add Home option to nav page picker - mark editor-reorganisation plan as complete - add dynamic-url-customisation and draft-publish-workflow plans Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,15 +12,24 @@ Current editing workflow uses explicit save — changes are either saved (immedi
|
||||
|
||||
## Solution
|
||||
|
||||
Implement a draft-then-publish model where all edits auto-save as drafts, visitors see only published versions, and explicit "Publish" commits changes live.
|
||||
Implement a draft-then-publish model with **site-level publishing** and **version history**:
|
||||
|
||||
1. **Site starts unpublished** — New sites are in draft mode until first publish
|
||||
2. **Auto-save drafts** — All edits auto-save, no risk of lost work
|
||||
3. **Visitors see published only** — No accidental exposure of drafts
|
||||
4. **Version history** — Each publish creates a snapshot, enabling rollback
|
||||
5. **URLs are versioned** — URL slugs are part of version data (ties into dynamic URL customisation)
|
||||
|
||||
## Design principles
|
||||
|
||||
1. **Drafts are per-user** — each admin sees their own pending changes
|
||||
2. **Visitors always see published** — no accidental exposure of drafts
|
||||
3. **Auto-save constantly** — no risk of lost work
|
||||
4. **Clear visual distinction** — obvious when viewing draft vs published
|
||||
5. **Single publish action** — one button publishes all pending changes
|
||||
1. **Site-level lifecycle** — Site has a `first_published_at` timestamp; until then, visitors see "coming soon"
|
||||
2. **Drafts are per-user** — each admin sees their own pending changes
|
||||
3. **Visitors always see published** — no accidental exposure of drafts
|
||||
4. **Auto-save constantly** — no risk of lost work
|
||||
5. **Clear visual distinction** — obvious when viewing draft vs published
|
||||
6. **Single publish action** — one button publishes all pending changes
|
||||
7. **Version snapshots** — each publish creates a version record for rollback
|
||||
8. **Atomic rollback** — rolling back restores content AND URL (redirect chain updates automatically)
|
||||
|
||||
## Scope
|
||||
|
||||
@@ -34,47 +43,95 @@ Three types of editable content:
|
||||
|
||||
## Data model
|
||||
|
||||
### Option A: Draft columns (recommended for simplicity)
|
||||
### Site-level publishing
|
||||
|
||||
```elixir
|
||||
# Migration: add draft columns to pages
|
||||
alter table(:pages) do
|
||||
add :draft_blocks, :map
|
||||
add :draft_user_id, references(:users, type: :binary_id, on_delete: :nilify_all)
|
||||
add :draft_updated_at, :utc_datetime_usec
|
||||
end
|
||||
# Add to settings or dedicated sites table
|
||||
# Option 1: Settings key
|
||||
%{"site_first_published_at" => nil} # nil = unpublished, datetime = published
|
||||
|
||||
# New settings keys
|
||||
# - "draft_theme_settings" → JSON blob
|
||||
# - "draft_site_name" → string
|
||||
# - "draft_header_nav" → JSON
|
||||
# etc.
|
||||
# Option 2: Dedicated sites table (if multi-tenant later)
|
||||
create table(:sites, primary_key: false) do
|
||||
add :id, :binary_id, primary_key: true
|
||||
add :first_published_at, :utc_datetime_usec # nil until first publish
|
||||
timestamps(type: :utc_datetime_usec)
|
||||
end
|
||||
```
|
||||
|
||||
### Option B: Separate drafts table (more flexible)
|
||||
### Page versions (recommended approach)
|
||||
|
||||
Rather than draft columns, use a proper version table. Each publish creates a version snapshot.
|
||||
|
||||
```elixir
|
||||
create table(:drafts, primary_key: false) do
|
||||
create table(:page_versions, primary_key: false) do
|
||||
add :id, :binary_id, primary_key: true
|
||||
add :user_id, references(:users, type: :binary_id, on_delete: :delete_all), null: false
|
||||
add :entity_type, :string, null: false # "page", "theme", "settings"
|
||||
add :entity_id, :string # page id, or nil for global
|
||||
add :data, :map, null: false
|
||||
add :page_id, references(:pages, type: :binary_id, on_delete: :delete_all), null: false
|
||||
add :version_number, :integer, null: false
|
||||
add :blocks, :map, null: false
|
||||
add :url_slug, :string # URL is part of version data
|
||||
add :meta_title, :string
|
||||
add :meta_description, :string
|
||||
add :published_at, :utc_datetime_usec
|
||||
add :published_by_id, references(:users, type: :binary_id, on_delete: :nilify_all)
|
||||
timestamps(type: :utc_datetime_usec)
|
||||
end
|
||||
|
||||
create unique_index(:drafts, [:user_id, :entity_type, :entity_id])
|
||||
create index(:page_versions, [:page_id, :version_number])
|
||||
create unique_index(:page_versions, [:page_id, :version_number])
|
||||
```
|
||||
|
||||
**Recommendation:** Start with Option A (simpler), migrate to Option B if multi-user or more complex draft needs emerge.
|
||||
```elixir
|
||||
# Pages table changes
|
||||
alter table(:pages) do
|
||||
add :published_version_id, references(:page_versions, type: :binary_id, on_delete: :nilify_all)
|
||||
add :draft_blocks, :map
|
||||
add :draft_url_slug, :string
|
||||
add :draft_user_id, references(:users, type: :binary_id, on_delete: :nilify_all)
|
||||
add :draft_updated_at, :utc_datetime_usec
|
||||
end
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
- `pages.draft_*` fields hold current work-in-progress
|
||||
- On publish: create new `page_versions` record, update `pages.published_version_id`
|
||||
- On rollback: set `published_version_id` to older version, update redirect chain
|
||||
- Visitors always see the version pointed to by `published_version_id`
|
||||
|
||||
### URL versioning and redirects
|
||||
|
||||
When a page version has a different URL than the current published version:
|
||||
1. The redirect system tracks URL history
|
||||
2. All old URLs redirect to current canonical URL
|
||||
3. On rollback, old URL becomes canonical again, redirect chain updates
|
||||
|
||||
```elixir
|
||||
# Example: Page starts at /about, renamed to /about-us, then rolled back
|
||||
|
||||
# Version 1: url_slug = "about" <- created
|
||||
# Version 2: url_slug = "about-us" <- published, redirect /about → /about-us created
|
||||
# Rollback to v1: redirect /about-us → /about created (or /about redirect deleted)
|
||||
```
|
||||
|
||||
This ties directly into the [dynamic URL customisation plan](dynamic-url-customisation.md).
|
||||
|
||||
## Implementation
|
||||
|
||||
### Phase 1: Page drafts
|
||||
### Phase 0: Site-level publishing
|
||||
|
||||
| Task | Est | Notes |
|
||||
|------|-----|-------|
|
||||
| Add draft columns to pages table | 30m | Migration |
|
||||
| Add `site_first_published_at` setting | 15m | nil until first publish |
|
||||
| Update coming soon page to check this setting | 30m | Show until site is published |
|
||||
| Add "Publish site" button to admin dashboard | 30m | First-time publish action |
|
||||
| Hide "Publish site" after first publish | 15m | One-time action |
|
||||
|
||||
### Phase 1: Page versions and drafts
|
||||
|
||||
| Task | Est | Notes |
|
||||
|------|-----|-------|
|
||||
| Create page_versions table | 30m | Migration with version data |
|
||||
| Add draft columns and published_version_id to pages | 30m | Migration |
|
||||
| Create PageVersions context module | 1.5h | `create_version/2`, `rollback_to/2`, `list_versions/1` |
|
||||
| Update Pages context with draft functions | 1h | `save_draft/2`, `publish_draft/1`, `discard_draft/1`, `has_draft?/1` |
|
||||
| Modify PageEditorHook to auto-save drafts | 1h | Debounced save on every change |
|
||||
| Add "Publish" and "Discard" buttons to editor | 1h | Replace current "Save" |
|
||||
@@ -100,16 +157,33 @@ create unique_index(:drafts, [:user_id, :entity_type, :entity_id])
|
||||
| Update settings editor to use drafts | 1h | |
|
||||
| Unified "Publish all" option | 1h | Publish page + theme + settings together |
|
||||
|
||||
### Phase 4: Polish
|
||||
### Phase 4: Version history and rollback
|
||||
|
||||
| Task | Est | Notes |
|
||||
|------|-----|-------|
|
||||
| Version history panel in editor | 1.5h | List of published versions with timestamps |
|
||||
| Rollback action with confirmation | 1h | "Revert to version 3?" |
|
||||
| URL redirect chain update on rollback | 1h | Update redirects to point to restored URL |
|
||||
| Version comparison view (stretch) | 2h | Side-by-side diff |
|
||||
|
||||
### Phase 5: Polish
|
||||
|
||||
| Task | Est | Notes |
|
||||
|------|-----|-------|
|
||||
| Draft age indicator ("Last saved 5 mins ago") | 30m | |
|
||||
| Draft conflict handling (stale draft warning) | 1h | Edge case: published changed since draft created |
|
||||
| Version history / rollback (stretch goal) | 3h | Store previous published versions |
|
||||
| Version pruning (keep last N versions) | 1h | Prevent unbounded growth |
|
||||
|
||||
## UX flow
|
||||
|
||||
### First-time site publish
|
||||
|
||||
1. New shop completes setup wizard
|
||||
2. Site is in "coming soon" mode — visitors see placeholder
|
||||
3. Admin edits pages, theme, settings (all auto-saved as drafts)
|
||||
4. Admin clicks "Publish site" on dashboard
|
||||
5. Site goes live — `first_published_at` set, all current drafts become version 1
|
||||
|
||||
### Editing a page
|
||||
|
||||
1. Admin navigates to page, clicks "Edit"
|
||||
@@ -117,8 +191,17 @@ create unique_index(:drafts, [:user_id, :entity_type, :entity_id])
|
||||
3. Admin makes changes — **auto-saved as draft every few seconds**
|
||||
4. Admin can navigate away freely — draft persists
|
||||
5. Admin returns later — draft loaded automatically
|
||||
6. "Publish" button commits draft → published
|
||||
7. "Discard" button deletes draft, reverts to published
|
||||
6. "Publish" button commits draft → new version
|
||||
7. "Discard" button deletes draft, reverts to current published version
|
||||
|
||||
### Rolling back a page
|
||||
|
||||
1. Admin opens version history in editor
|
||||
2. Sees list: "Version 3 (current) — 2 hours ago", "Version 2 — yesterday", etc.
|
||||
3. Clicks "Revert to version 2"
|
||||
4. Confirmation: "This will restore version 2 content and URL. Continue?"
|
||||
5. On confirm: `published_version_id` updated, redirect chain adjusted
|
||||
6. Current draft (if any) is NOT affected — can discard or keep editing
|
||||
|
||||
### Visual indicators
|
||||
|
||||
@@ -163,22 +246,30 @@ end
|
||||
- **Multi-user drafts** — currently single draft per page, could extend to per-user drafts
|
||||
- **Draft preview link** — shareable URL that shows draft to non-admins (for review)
|
||||
- **Scheduled publishing** — "Publish at 9am tomorrow"
|
||||
- **Draft comparison** — side-by-side diff of draft vs published
|
||||
- **Theme/settings versioning** — extend version history to theme and shop settings
|
||||
- **Bulk rollback** — "Revert entire site to yesterday" across all pages
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Unified editing mode (phases 1-7) must be complete first
|
||||
- This work builds on the editor panel UI established there
|
||||
|
||||
## Related Plans
|
||||
|
||||
- [dynamic-url-customisation.md](dynamic-url-customisation.md) — URLs are versioned data; these plans integrate cleanly
|
||||
- Can be implemented in either order, but implementing together gives the cleanest result
|
||||
|
||||
## Estimates
|
||||
|
||||
| Phase | Est |
|
||||
|-------|-----|
|
||||
| Phase 1: Page drafts | 5h |
|
||||
| Phase 0: Site-level publishing | 1.5h |
|
||||
| Phase 1: Page versions and drafts | 7h |
|
||||
| Phase 2: Theme drafts | 4h |
|
||||
| Phase 3: Settings drafts | 4h |
|
||||
| Phase 4: Polish | 5h |
|
||||
| **Total** | **18h** |
|
||||
| Phase 4: Version history and rollback | 5.5h |
|
||||
| Phase 5: Polish | 2.5h |
|
||||
| **Total** | **24.5h** |
|
||||
|
||||
## References
|
||||
|
||||
|
||||
Reference in New Issue
Block a user