EDITOR SAVE IMPLEMENTATION

Ecosistema completo para organización y gestión de contenido con interfaces espaciales

Editor Save Implementation

📋 Overview

This document details the implementation of the page save functionality in the editor-web-lib. The save system properly syncs widgets from the EditorService to the PagesService storage.

🏗️ Architecture

Data Flow

EditorService (Flash Persistence)
  ├─ widgetsByPage[pageId] → Current page widgets
  ├─ page → Current page metadata
  │
  ↓ Save Action Triggered
  │
SaveCurrentPageAction
  ├─ Reads current page from EditorService
  ├─ Reads current widgets from EditorService
  ├─ Merges widgets into page object
  │
  ↓ Persist to Storage
  │
PagesService (Flash Persistence)
  └─ pages[] → Persistent page storage with widgets

Key Components

1. SaveCurrentPageAction (@libs-structural/editor-web-lib/actions)

Purpose: Public action that saves the current page being edited

Responsibilities:

Implementation:

execute(params, context): Observable<SaveCurrentPageOutput> {
  // 1. Get current page from editor
  const currentPage = this.editorService.page();
  
  // 2. Get current widgets for this page
  const currentWidgets = this.editorService.widgets();
  
  // 3. Create updated page with widgets
  const pageToSave = {
    ...currentPage,
    widgets: currentWidgets,
    updatedAt: new Date().toISOString(),
  };
  
  // 4. Save via PagesService
  return from(this.pagesService.updatePage(currentPage.id, pageToSave));
}

Action ID: editor:save-current-page

Parameters: None (uses current editor state)

Returns:

{
  success: boolean;
  error?: string;
}

2. EditorLayoutComponent (@libs-structural/editor-web-lib/components)

Purpose: Main editor toolbar with save button

Save Button Implementation:

<button class="btn btn-primary" (click)="saveProject()" [disabled]="isSaving()">
  @if (isSaving()) {
    <i class="fas fa-spinner fa-spin"></i> Saving...
  } @else {
    <i class="fas fa-save"></i> Save
  }
</button>

Save Method:

saveProject(): void {
  if (this.isSaving()) return;
  
  this.isSaving.set(true);
  
  this.actionRegistry.execute('editor:save-current-page', {}).subscribe({
    next: (result) => {
      if (result.success) {
        this.logger.info('✅ Page saved successfully');
      } else {
        this.logger.error('❌ Save failed:', result.error);
      }
      this.isSaving.set(false);
    },
    error: (error) => {
      this.logger.error('❌ Save action failed:', error);
      this.isSaving.set(false);
    }
  });
}

3. EditorService (@libs-ui/editor-ui/services)

Purpose: Manages editor state with per-page widget storage

Flash Persistence State:

readonly editorState = autoFlashState('editor-state', {
  page: null as Page | null,
  widgets: [] as Widget[], // Legacy (backward compat)
  widgetsByPage: {} as { [pageId: string]: Widget[] }, // 🔥 NEW
  // ... other state
});

Page-Aware Widgets:

readonly widgets = computed(() => {
  const state = this.editorState();
  const currentPageId = state.page?.id;
  
  if (!currentPageId) {
    return state.widgets || []; // Fallback to legacy
  }
  
  const widgetsByPage = state.widgetsByPage || {};
  return widgetsByPage[currentPageId] || [];
});

4. PagesService (@libs-foundational/core-angular)

Purpose: Manages persistent storage of pages

Storage Key: pages

Update Method:

async updatePage(pageId: string, updates: Partial<World>): Promise<World | undefined> {
  const pages = this._pages();
  const index = pages.findIndex(p => p.id === pageId);
  
  if (index === -1) return undefined;
  
  const updatedPage: World = {
    ...pages[index],
    ...updates,
    id: pageId, // Preserve ID
    updatedAt: new Date().toISOString(),
    version: (pages[index].version || 1) + 1
  };
  
  const newPages = [...pages];
  newPages[index] = updatedPage;
  
  await this.savePagesToStorage(newPages);
  this._pages.set(newPages);
  
  return updatedPage;
}

🔄 Data Synchronization

Editor → Pages Flow

User adds/modifies widgets in editor
  ↓
EditorService.addWidget() / updateWidget()
  ↓
Updates widgetsByPage[currentPageId]
  ↓
Flash Persistence automatically syncs
  ↓
User clicks "Save" button
  ↓
SaveCurrentPageAction.execute()
  ↓
Reads widgets from EditorService
  ↓
Merges into page object
  ↓
PagesService.updatePage()
  ↓
Persistent storage updated

Pages → Editor Flow

User navigates to /editor/:pageId
  ↓
EditorWorkspaceComponent.loadPageById()
  ↓
Loads page from PagesService or Space Map
  ↓
Calls EditorService.setPage()
  ↓
EditorService loads widgets from widgetsByPage[pageId]
  ↓
If no widgets in widgetsByPage, initializes from page.widgets
  ↓
Widgets rendered in editor canvas

🎯 Storage Strategy

Flash Persistence Layers

  1. Editor State (editor-state key)

    • Per-page widget storage
    • Editor UI state (zoom, mode, etc.)
    • Undo/redo history
    • Flash synced across tabs
  2. Pages Storage (pages key)

    • Complete page objects with widgets
    • Metadata (title, description, status)
    • Version tracking
    • Persisted to IndexedDB

Data Isolation

📊 Data Model

Page (World) Structure

interface World {
  id: string;                    // Unique page ID
  title: string;                 // Page title
  slug: string;                  // URL-friendly slug
  description?: string;          // Page description
  widgets: Widget[];             // 🎯 WIDGET ARRAY (synced on save)
  layout: PageLayout;            // Layout configuration
  meta: PageMetadata;            // SEO/meta tags
  permissions: PagePermissions;  // Access control
  status: PageStatus;            // DRAFT | PUBLISHED | ARCHIVED
  version: number;               // Auto-incremented on save
  createdAt: string;             // ISO datetime
  updatedAt: string;             // ISO datetime (updated on save)
  cosmos?: PageCosmos;           // Space/Universe association
  ownerId?: string;              // Owner user ID
}

Widget Structure

interface Widget {
  id: string;                    // Unique widget ID
  type: WidgetType;              // button | text | image | etc.
  position: ResponsiveValue<Position>;  // Responsive positioning
  size: ResponsiveValue<Size>;   // Responsive sizing
  styles: ResponsiveValue<WidgetStyles>; // Responsive styles
  properties: Record<string, any>;  // Widget-specific props
  children?: Widget[];           // Nested widgets
  flowId?: string;               // Singularity flow binding
}

🧪 Testing the Save Flow

Manual Test Steps

  1. Navigate to editor: /space/:slug/editor/:pageId
  2. Add widgets to the canvas
  3. Modify widget properties
  4. Click Save button in toolbar
  5. Check console for: ✅ Page saved successfully
  6. Reload the page
  7. Verify widgets persist

Expected Console Logs

[SaveCurrentPageAction] Syncing page with widgets to storage
  pageId: "page-home"
  pageTitle: "Home Page"
  widgetCount: 3

[SaveCurrentPageAction] ✅ Page saved successfully
  pageId: "page-home"
  pageTitle: "Home Page"
  version: 2
  widgetCount: 3

[EditorLayout] ✅ Page saved successfully

Storage Verification

Open DevTools → Application → IndexedDB → gg-storage

Check pages key:

[
  {
    "id": "page-home",
    "title": "Home Page",
    "widgets": [
      {
        "id": "widget-1",
        "type": "button",
        "position": { ... },
        "properties": { ... }
      }
    ],
    "version": 2,
    "updatedAt": "2025-11-24T..."
  }
]

🚀 Next Steps

1. Auto-Save (TODO #3)

Implement automatic save on widget changes with debouncing:

// In EditorService
effect(() => {
  const widgets = this.widgets();
  const page = this.page();
  
  if (!page) return;
  
  // Debounce auto-save (5 seconds)
  this.autoSaveTimer.next();
});

private autoSaveTimer = timer(5000).pipe(
  debounceTime(5000),
  switchMap(() => this.actionRegistry.execute('editor:save-current-page', {}))
);

2. Page Routing (TODO #4)

Implement dynamic routing for pages:

// editor.routes.ts
{
  path: 'editor',
  children: [
    {
      path: ':pageId',
      component: EditorWorkspaceComponent,
      resolve: { page: PageResolver }
    }
  ]
}

3. Draft System (TODO #3)

Add draft/published workflow:

interface DraftSaveOptions {
  isDraft: boolean;
  autoPublish?: boolean;
  schedulePublish?: Date;
}

4. Version History

Track page versions for undo/recovery:

interface PageVersion {
  id: string;
  pageId: string;
  version: number;
  widgets: Widget[];
  createdAt: string;
  createdBy: string;
}

📝 Summary

✅ Completed:

⚠️ TODO:

🔗 Related Documentation


Last Updated: 2025-11-24
Status: ✅ Implemented & Tested
Next Review: After auto-save implementation