mono/packages/ui/docs/page-commands.md
2026-02-19 09:24:43 +01:00

3.8 KiB

Page Commands & Undo/Redo System Proposal

Overview

To support robust Undo/Redo functionality for the User Page Builder, we propose implementing the Command Pattern. Every modification to the page layout (add, remove, move, resize, update settings) will be encapsulated as a Command object.

Command Interface

interface Command {
  id: string;
  type: string;
  timestamp: number;
  execute(): Promise<void>;
  undo(): Promise<void>;
}

Command Stack

We will maintain two stacks in the LayoutContext or a new HistoryContext:

  • past: Command[]
  • future: Command[]

Proposed Commands

1. AddWidgetCommand

  • Execute: Adds a widget to a specific container/index.
  • Undo: Removes the widget with the specific ID.

2. RemoveWidgetCommand

  • Execute: Removes a widget. Store the widget's state (props, ID, location) before removal.
  • Undo: Restores the widget to its original container/index with preserved props.

3. MoveWidgetCommand

  • Execute: Moves widget from (Container A, Index X) to (Container B, Index Y).
  • Undo: Moves widget back to (Container A, Index X).

4. UpdateWidgetSettingsCommand

  • Execute: Updates widget.props with new values. Store previousProps.
  • Undo: Reverts widget.props to previousProps.

5. AddContainerCommand / RemoveContainerCommand

  • Similar logic to widgets but for layout containers.

6. ResizeContainerCommand

  • Execute: Updates container column sizes.
  • Undo: Reverts to previous column sizes.

Implementation Strategy

  1. Refactor useLayout: Move direct state mutations into specific command classes or factory functions.
  2. Action Dispatcher: Create a dispatch(action) function that:
    • Creates the appropriate Command.
    • Executes command.execute().
    • Pushes command to past stack.
    • Clears future stack.
  3. Hotkeys: Bind Ctrl+Z (Undo) and Ctrl+Y / Ctrl+Shift+Z (Redo).

Storage Boundaries & Persistence

1. In-Memory Store (Primary)

  • Scope: Current Browser Tab / Session.
  • Implementation: React State or useReducer within LayoutContext.
  • Behavior: fast, synchronous updates. Cleared on page reload or navigation.

2. Browser Storage (localStorage)

  • Role: Crash Recovery & Session Continuity.
  • Strategy:
    • Persist the currentLayout state to localStorage on every change (debounced).
    • Proposed: Persist the past and future command stacks to localStorage as well.
    • Constraint: All Command objects must be strictly JSON-serializable (no function references).
    • Key: page_editor_history_${pageId}.
  • Benefit: Users can refresh the page and still Undo their last action.

3. Server State (Database)

  • Role: Permanent Storage & Collaboration Source of Truth.
  • Interaction:
    • "Save" commits the current state to Supabase.
    • History Clearance: Typically, saving does not clear the Undo history (allowing "Undo Save"), but navigating away does.
    • Dirty State: If past.length > lastSavedIndex, the UI shows "Unsaved Changes".

4. Boundary Enforcement

  • Serialization: Commands must store copies of data (e.g., previousProps), not references to live objects.
  • Isolation: Undo operations must not trigger side effects (like API calls) other than updating the local layout state, unless explicitly designed (e.g., re-uploading a deleted image is complex; usually we just restore the reference to the image URL).

Edge Cases

  • Multi-user editing: Simple command history assumes single-player mode. Implementation complexity increases significantly with real-time collaboration (requiring OT or CRDTs). For now, we assume last-write-wins or locking.
  • Failed operations: If execute() fails, the command stack should not update.