mono/packages/ui/src/modules/layout/commands.ts
2026-02-25 10:11:54 +01:00

939 lines
34 KiB
TypeScript

import { Command, CommandContext } from './types';
// Force rebuild
import { WidgetInstance, PageLayout, LayoutContainer, UnifiedLayoutManager } from '@/modules/layout/LayoutManager';
// Helper to find a container by ID in the layout tree
const findContainer = (containers: LayoutContainer[], id: string): LayoutContainer | null => {
for (const container of containers) {
if (container.id === id) return container;
if (container.children) {
const found = findContainer(container.children, id);
if (found) return found;
}
}
return null;
};
// Helper to finding parent container
const findParentContainer = (containers: LayoutContainer[], childId: string): LayoutContainer | null => {
for (const container of containers) {
if (container.children.some(c => c.id === childId)) return container;
const found = findParentContainer(container.children, childId);
if (found) return found;
}
return null;
}
// Helper to find widget location
const findWidgetLocation = (containers: LayoutContainer[], widgetId: string): { container: LayoutContainer, index: number, widget: WidgetInstance } | null => {
for (const container of containers) {
const idx = container.widgets.findIndex(w => w.id === widgetId);
if (idx !== -1) {
return { container, index: idx, widget: container.widgets[idx] };
}
if (container.children) {
const found = findWidgetLocation(container.children, widgetId);
if (found) return found;
}
}
return null;
};
// --- Add Widget Command ---
export class AddWidgetCommand implements Command {
id: string;
type = 'ADD_WIDGET';
timestamp: number;
private pageId: string;
private containerId: string;
private widget: WidgetInstance;
private index: number;
constructor(pageId: string, containerId: string, widget: WidgetInstance, index: number = -1) {
this.id = crypto.randomUUID();
this.timestamp = Date.now();
this.pageId = pageId;
this.containerId = containerId;
this.widget = widget;
this.index = index;
}
async execute(context: CommandContext): Promise<void> {
const layout = context.layouts.get(this.pageId);
if (!layout) throw new Error(`Layout not found: ${this.pageId}`);
// Clone layout to avoid mutation
const newLayout = JSON.parse(JSON.stringify(layout)) as PageLayout;
const container = findContainer(newLayout.containers, this.containerId);
if (!container) throw new Error(`Container not found: ${this.containerId}`);
if (this.index === -1) {
container.widgets.push(this.widget);
} else {
container.widgets.splice(this.index, 0, this.widget);
}
newLayout.updatedAt = Date.now();
context.updateLayout(this.pageId, newLayout);
}
async undo(context: CommandContext): Promise<void> {
const layout = context.layouts.get(this.pageId);
if (!layout) throw new Error(`Layout not found: ${this.pageId}`);
const newLayout = JSON.parse(JSON.stringify(layout)) as PageLayout;
// Find widget anywhere (robust against moves)
const location = findWidgetLocation(newLayout.containers, this.widget.id);
if (location) {
location.container.widgets.splice(location.index, 1);
newLayout.updatedAt = Date.now();
context.updateLayout(this.pageId, newLayout);
} else {
console.warn(`Widget ${this.widget.id} not found for undo add`);
}
}
}
// --- Remove Widget Command ---
export class RemoveWidgetCommand implements Command {
id: string;
type = 'REMOVE_WIDGET';
timestamp: number;
private pageId: string;
private widgetId: string;
// State capture for undo
private containerId: string | null = null;
private index: number = -1;
private widget: WidgetInstance | null = null;
constructor(pageId: string, widgetId: string) {
this.id = crypto.randomUUID();
this.timestamp = Date.now();
this.pageId = pageId;
this.widgetId = widgetId;
}
async execute(context: CommandContext): Promise<void> {
const layout = context.layouts.get(this.pageId);
if (!layout) throw new Error(`Layout not found: ${this.pageId}`);
// 1. Capture state BEFORE modification
const location = findWidgetLocation(layout.containers, this.widgetId);
if (!location) {
console.warn(`Widget ${this.widgetId} not found for removal`);
return;
}
this.containerId = location.container.id;
this.index = location.index;
this.widget = location.widget;
// 2. Perform Removal
const newLayout = JSON.parse(JSON.stringify(layout)) as PageLayout;
const newLocation = findWidgetLocation(newLayout.containers, this.widgetId);
if (newLocation) {
newLocation.container.widgets.splice(newLocation.index, 1);
newLayout.updatedAt = Date.now();
context.updateLayout(this.pageId, newLayout);
}
}
async undo(context: CommandContext): Promise<void> {
if (!this.containerId || !this.widget || this.index === -1) {
throw new Error("Cannot undo remove: State was not captured correctly");
}
const layout = context.layouts.get(this.pageId);
if (!layout) throw new Error(`Layout not found: ${this.pageId}`);
const newLayout = JSON.parse(JSON.stringify(layout)) as PageLayout;
const container = findContainer(newLayout.containers, this.containerId);
if (!container) throw new Error(`Original container ${this.containerId} not found`);
// Restore widget at original index
// Ensure index is valid
if (this.index > container.widgets.length) {
container.widgets.push(this.widget);
} else {
container.widgets.splice(this.index, 0, this.widget);
}
newLayout.updatedAt = Date.now();
context.updateLayout(this.pageId, newLayout);
}
}
// --- Update Widget Settings Command ---
export class UpdateWidgetSettingsCommand implements Command {
id: string;
type = 'UPDATE_WIDGET_SETTINGS';
timestamp: number;
private pageId: string;
private widgetId: string;
private newSettings: Record<string, any>; // Store full new settings or partial? Partial is what comes in.
// State capture for undo
private oldSettings: Record<string, any> | null = null;
constructor(pageId: string, widgetId: string, newSettings: Record<string, any>) {
this.id = crypto.randomUUID();
this.timestamp = Date.now();
this.pageId = pageId;
this.widgetId = widgetId;
this.newSettings = newSettings;
}
async execute(context: CommandContext): Promise<void> {
const layout = context.layouts.get(this.pageId);
if (!layout) throw new Error(`Layout not found: ${this.pageId}`);
// 1. Capture state (snapshot of current props) BEFORE modification
// We need to find the widget in the CURRENT layout to get old props
const location = findWidgetLocation(layout.containers, this.widgetId);
if (!location) {
console.warn(`Widget ${this.widgetId} not found for update`);
return;
}
// Only capture oldSettings the FIRST time execute is called (or if we want to support re-execution)
// Since we create a NEW command instance for every user action, this is fine.
// But for Redo, we don't want to recapture 'oldSettings' from the *already updated* state if we were in a weird state.
// Actually, Redo implies we are going from Undo -> Redo.
// Undo restored oldSettings. So Redo will see oldSettings.
// So capturing it again is fine, OR we check if it's null.
if (!this.oldSettings) {
this.oldSettings = JSON.parse(JSON.stringify(location.widget.props || {}));
}
// 2. Perform Update
const newLayout = JSON.parse(JSON.stringify(layout)) as PageLayout;
const newLocation = findWidgetLocation(newLayout.containers, this.widgetId);
if (newLocation) {
// merge
newLocation.widget.props = { ...newLocation.widget.props, ...this.newSettings };
newLayout.updatedAt = Date.now();
context.updateLayout(this.pageId, newLayout);
}
}
async undo(context: CommandContext): Promise<void> {
if (!this.oldSettings) {
console.warn("Cannot undo update: State was not captured");
return;
}
const layout = context.layouts.get(this.pageId);
if (!layout) throw new Error(`Layout not found: ${this.pageId}`);
const newLayout = JSON.parse(JSON.stringify(layout)) as PageLayout;
const location = findWidgetLocation(newLayout.containers, this.widgetId);
if (location) {
// Restore exact old props
location.widget.props = this.oldSettings;
newLayout.updatedAt = Date.now();
context.updateLayout(this.pageId, newLayout);
} else {
console.warn(`Widget ${this.widgetId} not found for undo update`);
}
}
}
// --- Update Page Parent Command ---
export class UpdatePageParentCommand implements Command {
id: string;
type = 'UPDATE_PAGE_PARENT';
timestamp: number;
private pageId: string;
private oldParent: { id: string | null, title?: string, slug?: string } | null;
private newParent: { id: string | null, title?: string, slug?: string } | null;
private onUpdate?: (parent: { id: string | null, title?: string, slug?: string } | null) => void;
constructor(
pageId: string,
oldParent: { id: string | null, title?: string, slug?: string } | null,
newParent: { id: string | null, title?: string, slug?: string } | null,
onUpdate?: (parent: { id: string | null, title?: string, slug?: string } | null) => void
) {
this.id = crypto.randomUUID();
this.timestamp = Date.now();
this.pageId = pageId;
this.oldParent = oldParent;
this.newParent = newParent;
this.onUpdate = onUpdate;
}
async execute(context: CommandContext): Promise<void> {
// Update local metadata state through context
context.updatePageMetadata(this.pageId, { parent: this.newParent?.id ?? null });
if (this.onUpdate) this.onUpdate(this.newParent);
}
async undo(context: CommandContext): Promise<void> {
// Revert local metadata state
context.updatePageMetadata(this.pageId, { parent: this.oldParent?.id ?? null });
if (this.onUpdate) this.onUpdate(this.oldParent);
}
}
// --- Update Page Meta Command ---
export class UpdatePageMetaCommand implements Command {
id: string;
type = 'UPDATE_PAGE_META';
timestamp: number;
private pageId: string;
private oldMeta: Record<string, any>;
private newMeta: Record<string, any>;
private onUpdate?: (meta: Record<string, any>) => void;
constructor(
pageId: string,
oldMeta: Record<string, any>,
newMeta: Record<string, any>,
onUpdate?: (meta: Record<string, any>) => void
) {
this.id = crypto.randomUUID();
this.timestamp = Date.now();
this.pageId = pageId;
this.oldMeta = oldMeta;
this.newMeta = newMeta;
this.onUpdate = onUpdate;
}
async execute(context: CommandContext): Promise<void> {
context.updatePageMetadata(this.pageId, this.newMeta);
if (this.onUpdate) this.onUpdate(this.newMeta);
}
async undo(context: CommandContext): Promise<void> {
context.updatePageMetadata(this.pageId, this.oldMeta);
if (this.onUpdate) this.onUpdate(this.oldMeta);
}
}
// --- Add Container Command ---
export class AddContainerCommand implements Command {
id: string;
type = 'ADD_CONTAINER';
timestamp: number;
private pageId: string;
private parentContainerId?: string;
private container: LayoutContainer;
constructor(pageId: string, container: LayoutContainer, parentContainerId?: string) {
this.id = crypto.randomUUID();
this.timestamp = Date.now();
this.pageId = pageId;
this.container = container;
this.parentContainerId = parentContainerId;
}
async execute(context: CommandContext): Promise<void> {
const layout = context.layouts.get(this.pageId);
if (!layout) throw new Error(`Layout not found: ${this.pageId}`);
const newLayout = JSON.parse(JSON.stringify(layout)) as PageLayout;
if (this.parentContainerId) {
const parent = findContainer(newLayout.containers, this.parentContainerId);
if (parent) {
this.container.order = parent.children.length;
parent.children.push(this.container);
}
} else {
this.container.order = newLayout.containers.length;
newLayout.containers.push(this.container);
}
newLayout.updatedAt = Date.now();
context.updateLayout(this.pageId, newLayout);
}
async undo(context: CommandContext): Promise<void> {
const layout = context.layouts.get(this.pageId);
if (!layout) throw new Error(`Layout not found: ${this.pageId}`);
const newLayout = JSON.parse(JSON.stringify(layout)) as PageLayout;
const remove = (containers: LayoutContainer[]): boolean => {
const idx = containers.findIndex(c => c.id === this.container.id);
if (idx !== -1) {
containers.splice(idx, 1);
containers.forEach((c, i) => c.order = i);
return true;
}
for (const c of containers) {
if (remove(c.children)) return true;
}
return false;
};
remove(newLayout.containers);
newLayout.updatedAt = Date.now();
context.updateLayout(this.pageId, newLayout);
}
}
// --- Batch Add Containers Command ---
// Adds multiple containers in a single execute() against one layout snapshot.
// Avoids React state-batching issue where sequential AddContainerCommand calls
// each read the same stale snapshot and only the last one survives.
export class BatchAddContainersCommand implements Command {
id: string;
type = 'BATCH_ADD_CONTAINERS';
timestamp: number;
private pageId: string;
private containers: LayoutContainer[];
constructor(pageId: string, containers: LayoutContainer[]) {
this.id = crypto.randomUUID();
this.timestamp = Date.now();
this.pageId = pageId;
this.containers = containers;
}
async execute(context: CommandContext): Promise<void> {
const layout = context.layouts.get(this.pageId);
if (!layout) throw new Error(`Layout not found: ${this.pageId}`);
const newLayout = JSON.parse(JSON.stringify(layout)) as PageLayout;
for (const container of this.containers) {
container.order = newLayout.containers.length;
newLayout.containers.push(container);
}
newLayout.updatedAt = Date.now();
context.updateLayout(this.pageId, newLayout);
}
async undo(context: CommandContext): Promise<void> {
const layout = context.layouts.get(this.pageId);
if (!layout) throw new Error(`Layout not found: ${this.pageId}`);
const newLayout = JSON.parse(JSON.stringify(layout)) as PageLayout;
const idsToRemove = new Set(this.containers.map(c => c.id));
newLayout.containers = newLayout.containers.filter(c => !idsToRemove.has(c.id));
newLayout.containers.forEach((c, i) => c.order = i);
newLayout.updatedAt = Date.now();
context.updateLayout(this.pageId, newLayout);
}
}
// --- Remove Container Command ---
export class RemoveContainerCommand implements Command {
id: string;
type = 'REMOVE_CONTAINER';
timestamp: number;
private pageId: string;
private containerId: string;
// State capture
private parentId: string | null = null;
private index: number = -1;
private container: LayoutContainer | null = null;
constructor(pageId: string, containerId: string) {
this.id = crypto.randomUUID();
this.timestamp = Date.now();
this.pageId = pageId;
this.containerId = containerId;
}
async execute(context: CommandContext): Promise<void> {
const layout = context.layouts.get(this.pageId);
if (!layout) throw new Error(`Layout not found: ${this.pageId}`);
// Capture state
const findAndCapture = (containers: LayoutContainer[], parentId: string | null) => {
for (let i = 0; i < containers.length; i++) {
if (containers[i].id === this.containerId) {
this.parentId = parentId;
this.index = i;
this.container = JSON.parse(JSON.stringify(containers[i])); // Deep clone
return true;
}
if (findAndCapture(containers[i].children, containers[i].id)) return true;
}
return false;
};
if (findAndCapture(layout.containers, null)) {
const newLayout = JSON.parse(JSON.stringify(layout)) as PageLayout;
newLayout.updatedAt = Date.now();
UnifiedLayoutManager.removeContainer(newLayout, this.containerId, true);
context.updateLayout(this.pageId, newLayout);
} else {
console.warn(`Container ${this.containerId} not found for removal`);
}
}
async undo(context: CommandContext): Promise<void> {
if (!this.container || this.index === -1) {
console.warn("Cannot undo remove container: State not captured");
return;
}
const layout = context.layouts.get(this.pageId);
if (!layout) throw new Error(`Layout not found: ${this.pageId}`);
const newLayout = JSON.parse(JSON.stringify(layout)) as PageLayout;
if (this.parentId === null) {
// Root container
if (this.index >= newLayout.containers.length) {
newLayout.containers.push(this.container);
} else {
newLayout.containers.splice(this.index, 0, this.container);
}
newLayout.containers.forEach((c, i) => c.order = i);
} else {
const parent = findContainer(newLayout.containers, this.parentId);
if (parent) {
if (this.index >= parent.children.length) {
parent.children.push(this.container);
} else {
parent.children.splice(this.index, 0, this.container);
}
parent.children.forEach((c, i) => c.order = i);
}
}
newLayout.updatedAt = Date.now();
context.updateLayout(this.pageId, newLayout);
}
}
// --- Move Container Command ---
export class MoveContainerCommand implements Command {
id: string;
type = 'MOVE_CONTAINER';
timestamp: number;
private pageId: string;
private containerId: string;
private direction: 'up' | 'down';
constructor(pageId: string, containerId: string, direction: 'up' | 'down') {
this.id = crypto.randomUUID();
this.timestamp = Date.now();
this.pageId = pageId;
this.containerId = containerId;
this.direction = direction;
}
async execute(context: CommandContext): Promise<void> {
const layout = context.layouts.get(this.pageId);
if (!layout) return;
const newLayout = JSON.parse(JSON.stringify(layout)) as PageLayout;
if (UnifiedLayoutManager.moveRootContainer(newLayout, this.containerId, this.direction)) {
newLayout.updatedAt = Date.now();
context.updateLayout(this.pageId, newLayout);
}
}
async undo(context: CommandContext): Promise<void> {
const layout = context.layouts.get(this.pageId);
if (!layout) return;
const reverseDirection = this.direction === 'up' ? 'down' : 'up';
const newLayout = JSON.parse(JSON.stringify(layout)) as PageLayout;
if (UnifiedLayoutManager.moveRootContainer(newLayout, this.containerId, reverseDirection)) {
newLayout.updatedAt = Date.now();
context.updateLayout(this.pageId, newLayout);
}
}
}
// --- Move Widget Command ---
export class MoveWidgetCommand implements Command {
id: string;
type = 'MOVE_WIDGET';
timestamp: number;
private pageId: string;
private widgetId: string;
private direction: 'up' | 'down' | 'left' | 'right';
constructor(pageId: string, widgetId: string, direction: 'up' | 'down' | 'left' | 'right') {
this.id = crypto.randomUUID();
this.timestamp = Date.now();
this.pageId = pageId;
this.widgetId = widgetId;
this.direction = direction;
}
async execute(context: CommandContext): Promise<void> {
const layout = context.layouts.get(this.pageId);
if (!layout) return;
const newLayout = JSON.parse(JSON.stringify(layout)) as PageLayout;
if (UnifiedLayoutManager.moveWidgetInContainer(newLayout, this.widgetId, this.direction)) {
newLayout.updatedAt = Date.now();
context.updateLayout(this.pageId, newLayout);
}
}
async undo(context: CommandContext): Promise<void> {
const layout = context.layouts.get(this.pageId);
if (!layout) return;
const newLayout = JSON.parse(JSON.stringify(layout)) as PageLayout;
let reverseDirection: 'up' | 'down' | 'left' | 'right' = 'up';
switch (this.direction) {
case 'up': reverseDirection = 'down'; break;
case 'down': reverseDirection = 'up'; break;
case 'left': reverseDirection = 'right'; break;
case 'right': reverseDirection = 'left'; break;
}
if (UnifiedLayoutManager.moveWidgetInContainer(newLayout, this.widgetId, reverseDirection)) {
newLayout.updatedAt = Date.now();
context.updateLayout(this.pageId, newLayout);
}
}
}
// --- Update Container Columns Command ---
export class UpdateContainerColumnsCommand implements Command {
id: string;
type = 'UPDATE_CONTAINER_COLUMNS';
timestamp: number;
private pageId: string;
private containerId: string;
private newColumns: number;
private oldColumns: number | null = null;
constructor(pageId: string, containerId: string, columns: number) {
this.id = crypto.randomUUID();
this.timestamp = Date.now();
this.pageId = pageId;
this.containerId = containerId;
this.newColumns = columns;
}
async execute(context: CommandContext): Promise<void> {
const layout = context.layouts.get(this.pageId);
if (!layout) return;
// Capture old
if (this.oldColumns === null) {
const container = findContainer(layout.containers, this.containerId);
if (container) this.oldColumns = container.columns;
}
const newLayout = JSON.parse(JSON.stringify(layout)) as PageLayout;
if (UnifiedLayoutManager.updateContainerColumns(newLayout, this.containerId, this.newColumns)) {
newLayout.updatedAt = Date.now();
context.updateLayout(this.pageId, newLayout);
}
}
async undo(context: CommandContext): Promise<void> {
if (this.oldColumns === null) return;
const layout = context.layouts.get(this.pageId);
if (!layout) return;
const newLayout = JSON.parse(JSON.stringify(layout)) as PageLayout;
if (UnifiedLayoutManager.updateContainerColumns(newLayout, this.containerId, this.oldColumns)) {
newLayout.updatedAt = Date.now();
context.updateLayout(this.pageId, newLayout);
}
}
}
// --- Update Container Settings Command ---
export class UpdateContainerSettingsCommand implements Command {
id: string;
type = 'UPDATE_CONTAINER_SETTINGS';
timestamp: number;
private pageId: string;
private containerId: string;
private newSettings: Partial<LayoutContainer['settings']>;
private oldSettings: Partial<LayoutContainer['settings']> | null = null;
constructor(pageId: string, containerId: string, settings: Partial<LayoutContainer['settings']>) {
this.id = crypto.randomUUID();
this.timestamp = Date.now();
this.pageId = pageId;
this.containerId = containerId;
this.newSettings = settings;
}
async execute(context: CommandContext): Promise<void> {
const layout = context.layouts.get(this.pageId);
if (!layout) return;
// Capture old
if (this.oldSettings === null) {
const container = findContainer(layout.containers, this.containerId);
if (container) this.oldSettings = { ...container.settings };
}
const newLayout = JSON.parse(JSON.stringify(layout)) as PageLayout;
if (UnifiedLayoutManager.updateContainerSettings(newLayout, this.containerId, this.newSettings)) {
newLayout.updatedAt = Date.now();
context.updateLayout(this.pageId, newLayout);
}
}
async undo(context: CommandContext): Promise<void> {
if (this.oldSettings === null) return;
const layout = context.layouts.get(this.pageId);
if (!layout) return;
const newLayout = JSON.parse(JSON.stringify(layout)) as PageLayout;
if (UnifiedLayoutManager.updateContainerSettings(newLayout, this.containerId, this.oldSettings)) {
newLayout.updatedAt = Date.now();
context.updateLayout(this.pageId, newLayout);
}
}
}
// --- Rename Widget Command ---
export class RenameWidgetCommand implements Command {
id: string;
type = 'RENAME_WIDGET';
timestamp: number;
private pageId: string;
private oldId: string;
private newId: string;
constructor(pageId: string, oldId: string, newId: string) {
this.id = crypto.randomUUID();
this.timestamp = Date.now();
this.pageId = pageId;
this.oldId = oldId;
this.newId = newId;
}
async execute(context: CommandContext): Promise<void> {
const layout = context.layouts.get(this.pageId);
if (!layout) throw new Error(`Layout not found: ${this.pageId}`);
const newLayout = JSON.parse(JSON.stringify(layout)) as PageLayout;
if (UnifiedLayoutManager.renameWidget(newLayout, this.oldId, this.newId)) {
newLayout.updatedAt = Date.now();
context.updateLayout(this.pageId, newLayout);
} else {
throw new Error(`Failed to rename widget: ${this.newId} might already exist or widget not found`);
}
}
async undo(context: CommandContext): Promise<void> {
const layout = context.layouts.get(this.pageId);
if (!layout) return;
const newLayout = JSON.parse(JSON.stringify(layout)) as PageLayout;
if (UnifiedLayoutManager.renameWidget(newLayout, this.newId, this.oldId)) {
newLayout.updatedAt = Date.now();
context.updateLayout(this.pageId, newLayout);
}
}
}
// --- Replace Layout Command ---
export class ReplaceLayoutCommand implements Command {
id: string;
type = 'REPLACE_LAYOUT';
timestamp: number;
private pageId: string;
private newLayout: PageLayout;
private oldLayout: PageLayout | null = null;
constructor(pageId: string, newLayout: PageLayout) {
this.id = crypto.randomUUID();
this.timestamp = Date.now();
this.pageId = pageId;
this.newLayout = newLayout;
}
async execute(context: CommandContext): Promise<void> {
const currentLayout = context.layouts.get(this.pageId);
// If current layout not found, we might be importing for the first time or initializing.
// But for undo history, we need something to undo TO.
// If no current layout, maybe we just set it.
// But context usually requires existing key.
// Let's assume layout exists if we are in editor.
if (currentLayout) {
this.oldLayout = JSON.parse(JSON.stringify(currentLayout));
}
// Apply new layout
const layoutToApply = JSON.parse(JSON.stringify(this.newLayout)) as PageLayout;
layoutToApply.id = this.pageId;
layoutToApply.updatedAt = Date.now();
context.updateLayout(this.pageId, layoutToApply);
}
async undo(context: CommandContext): Promise<void> {
if (this.oldLayout) {
this.oldLayout.updatedAt = Date.now();
context.updateLayout(this.pageId, this.oldLayout);
}
}
}
// --- Paste Widgets Command ---
export class PasteWidgetsCommand implements Command {
id: string;
type = 'PASTE_WIDGETS';
timestamp: number;
private pageId: string;
private containerId: string;
private widgets: WidgetInstance[];
private pastedIds: string[] = [];
constructor(pageId: string, containerId: string, widgets: WidgetInstance[]) {
this.id = crypto.randomUUID();
this.timestamp = Date.now();
this.pageId = pageId;
this.containerId = containerId;
// Deep clone and assign new IDs
this.widgets = widgets.map(w => {
const suffix = crypto.randomUUID().slice(0, 6);
const newId = `${w.id.replace(/-copy-[a-f0-9]+$/, '')}-copy-${suffix}`;
return { ...JSON.parse(JSON.stringify(w)), id: newId };
});
this.pastedIds = this.widgets.map(w => w.id);
}
async execute(context: CommandContext): Promise<void> {
const layout = context.layouts.get(this.pageId);
if (!layout) throw new Error(`Layout not found: ${this.pageId}`);
const newLayout = JSON.parse(JSON.stringify(layout)) as PageLayout;
const container = findContainer(newLayout.containers, this.containerId);
if (!container) throw new Error(`Container not found: ${this.containerId}`);
for (const widget of this.widgets) {
container.widgets.push(JSON.parse(JSON.stringify(widget)));
}
newLayout.updatedAt = Date.now();
context.updateLayout(this.pageId, newLayout);
}
async undo(context: CommandContext): Promise<void> {
const layout = context.layouts.get(this.pageId);
if (!layout) throw new Error(`Layout not found: ${this.pageId}`);
const newLayout = JSON.parse(JSON.stringify(layout)) as PageLayout;
for (const pastedId of this.pastedIds) {
const location = findWidgetLocation(newLayout.containers, pastedId);
if (location) {
location.container.widgets.splice(location.index, 1);
}
}
newLayout.updatedAt = Date.now();
context.updateLayout(this.pageId, newLayout);
}
}
// --- Paste Containers Command ---
export class PasteContainersCommand implements Command {
id: string;
type = 'PASTE_CONTAINERS';
timestamp: number;
private pageId: string;
private containers: LayoutContainer[];
private insertIndex: number; // -1 means append
private pastedIds: string[] = [];
constructor(pageId: string, containers: LayoutContainer[], insertIndex: number = -1) {
this.id = crypto.randomUUID();
this.timestamp = Date.now();
this.pageId = pageId;
this.insertIndex = insertIndex;
// Deep clone containers with new IDs
const cloneContainer = (c: LayoutContainer): LayoutContainer => {
const suffix = crypto.randomUUID().slice(0, 6);
return {
...JSON.parse(JSON.stringify(c)),
id: `${c.id.replace(/-copy-[a-f0-9]+$/, '')}-copy-${suffix}`,
widgets: c.widgets.map(w => ({
...JSON.parse(JSON.stringify(w)),
id: `${w.id.replace(/-copy-[a-f0-9]+$/, '')}-copy-${crypto.randomUUID().slice(0, 6)}`
})),
children: c.children ? c.children.map(cloneContainer) : []
};
};
this.containers = containers.map(cloneContainer);
this.pastedIds = this.containers.map(c => c.id);
}
async execute(context: CommandContext): Promise<void> {
const layout = context.layouts.get(this.pageId);
if (!layout) throw new Error(`Layout not found: ${this.pageId}`);
const newLayout = JSON.parse(JSON.stringify(layout)) as PageLayout;
if (this.insertIndex >= 0 && this.insertIndex < newLayout.containers.length) {
// Insert after the given index
newLayout.containers.splice(this.insertIndex + 1, 0, ...this.containers.map(c => JSON.parse(JSON.stringify(c))));
} else {
// Append at end
for (const container of this.containers) {
newLayout.containers.push(JSON.parse(JSON.stringify(container)));
}
}
// Reorder
newLayout.containers.forEach((c, i) => c.order = i);
newLayout.updatedAt = Date.now();
context.updateLayout(this.pageId, newLayout);
}
async undo(context: CommandContext): Promise<void> {
const layout = context.layouts.get(this.pageId);
if (!layout) throw new Error(`Layout not found: ${this.pageId}`);
const newLayout = JSON.parse(JSON.stringify(layout)) as PageLayout;
const idsToRemove = new Set(this.pastedIds);
newLayout.containers = newLayout.containers.filter(c => !idsToRemove.has(c.id));
newLayout.containers.forEach((c, i) => c.order = i);
newLayout.updatedAt = Date.now();
context.updateLayout(this.pageId, newLayout);
}
}