WIDGET POSITION PERSISTENCE FIX

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

Widget Position Persistence Fix

Problem Summary

Widget positions were not persisting across browsers after drag-and-drop operations. When a widget was moved in one browser, it would revert to its original position when viewed in another browser or after page refresh.

Root Causes Identified

1. Missing Backend Sync

2. Breakpoint Double-Encoding

3. Position Comparison Logic

Solutions Implemented

1. Automatic Backend Sync (editor.service.ts)

Added debounced backend sync that automatically saves widgets to the backend after 1 second of inactivity:

// Setup automatic backend sync
private setupBackendSync(): void {
  this.widgetChangeSubject
    .pipe(
      debounceTime(1000), // Wait 1 second after last change
      distinctUntilChanged((a: Widget[], b: Widget[]) => {
        // Only sync if widgets actually changed
        const createSignature = (widgets: Widget[]) => {
          return widgets
            .map(w => `${w.id}:${JSON.stringify(w.position)}`)
            .sort()
            .join('|');
        };
        return createSignature(a) === createSignature(b);
      }),
      takeUntilDestroyed(this.destroyRef)
    )
    .subscribe({
      next: (widgets) => {
        // Trigger backend save via action handler
        this.actionHandler.execute('editor:save-current-page', {
          page: currentPage,
          widgets: widgets
        }).subscribe({...});
      }
    });
}

Key Features:

2. Breakpoint Cleaning (responsive-draggable.directive.ts)

Added breakpoint normalization to handle double-encoded strings:

private updateWidgetPositionModel(deltaX: number, deltaY: number): void {
  let breakpoint: BreakpointKey = this.activeBreakpoint();
  
  // Clean breakpoint if it's double-encoded
  if (typeof breakpoint === 'string') {
    let cleanedBreakpointStr: string = breakpoint;
    
    // Remove JSON encoding artifacts (handle double/triple encoding)
    // Example: "\"\\\"BASE\\\"\"" -> "BASE" -> "base"
    while (cleanedBreakpointStr.startsWith('"') && cleanedBreakpointStr.endsWith('"')) {
      try {
        const parsed = JSON.parse(cleanedBreakpointStr);
        if (typeof parsed === 'string') {
          cleanedBreakpointStr = parsed;
        } else {
          break;
        }
      } catch {
        cleanedBreakpointStr = cleanedBreakpointStr.slice(1, -1);
      }
    }
    
    // Normalize to lowercase and validate
    cleanedBreakpointStr = cleanedBreakpointStr.toLowerCase().trim();
    const validBreakpoints: BreakpointKey[] = ['base', 'sm', 'md', 'lg', 'xl', '2xl'];
    breakpoint = validBreakpoints.includes(cleanedBreakpointStr as BreakpointKey) 
      ? (cleanedBreakpointStr as BreakpointKey)
      : ('base' as BreakpointKey);
  }
  
  // Use cleaned breakpoint for position updates
  responsivePosition[breakpoint] = newPosition;
}

Key Features:

3. Position Recalculation (widget-drag-handler.service.ts)

Fixed logic to always recalculate position when movement occurs:

handleDragEnd(widget: Widget, event: ResponsiveDragEndEvent, activeBreakpoint: BreakpointKey, zoom: number): Widget {
  // Only skip recalculation if there's NO movement
  if (breakpointPos && 
      typeof breakpointPos.x === 'number' && 
      typeof breakpointPos.y === 'number' &&
      event.deltaX === 0 && 
      event.deltaY === 0) {
    // No movement - position is already correct
    return widget;
  }
  
  // There was movement - MUST recalculate to ensure position is updated
  const currentPosition = this.responsiveWidgetService.getWidgetPosition(widget, activeBreakpoint);
  const newPosition = currentPosition && ...
  return this.responsiveWidgetService.setWidgetPropertyForBreakpoint(...);
}

Key Features:

4. Signal Update Timing (responsive-draggable.directive.ts)

Added queueMicrotask to ensure signal updates propagate:

this.updateWidgetPositionModel(deltaX, deltaY);

queueMicrotask(() => {
  const updatedWidget = this.widget(); // Get widget AFTER signal update
  this.dragEnd.emit({
    widget: updatedWidget, // This now has the updated position
    breakpoint: breakpoint,
  });
});

Key Features:

Data Flow

Before Fix:

1. User drags widget
2. updateWidgetPositionModel() updates widget signal
3. handleDragEnd() returns widget WITHOUT recalculating (bug!)
4. Widget position NOT updated in backend
5. Other browsers load OLD position from backend

After Fix:

1. User drags widget
2. updateWidgetPositionModel() updates widget signal (with cleaned breakpoint)
3. queueMicrotask ensures signal propagates
4. handleDragEnd() ALWAYS recalculates when movement detected
5. Widget position updated in flash-persistence (instant)
6. After 1 second, auto-sync saves to backend
7. Other browsers load NEW position from backend ✅

Testing Checklist

Related Files

Future Improvements

  1. Reduce debounce time for faster sync (currently 1 second)
  2. Add conflict resolution for concurrent edits
  3. Optimize position comparison to avoid deep JSON.stringify
  4. Add position update batching for multiple widgets