408 lines
16 KiB
TypeScript
408 lines
16 KiB
TypeScript
import React, { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react';
|
|
import { UnifiedLayoutManager, PageLayout, WidgetInstance, LayoutContainer } from '@/modules/layout/LayoutManager';
|
|
import { HistoryManager } from '@/modules/layout/HistoryManager';
|
|
import { Command } from '@/modules/layout/types';
|
|
import {
|
|
AddWidgetCommand,
|
|
RemoveWidgetCommand,
|
|
UpdateWidgetSettingsCommand,
|
|
AddContainerCommand,
|
|
RemoveContainerCommand,
|
|
MoveContainerCommand,
|
|
MoveWidgetCommand,
|
|
UpdateContainerColumnsCommand,
|
|
UpdateContainerSettingsCommand,
|
|
RenameWidgetCommand,
|
|
ReplaceLayoutCommand
|
|
} from '@/modules/layout/commands';
|
|
|
|
|
|
interface LayoutContextType {
|
|
// Generic page management
|
|
loadPageLayout: (pageId: string, defaultName?: string) => Promise<void>;
|
|
getLoadedPageLayout: (pageId: string) => PageLayout | null;
|
|
clearPageLayout: (pageId: string) => Promise<void>;
|
|
|
|
// Generic page actions
|
|
addWidgetToPage: (pageId: string, containerId: string, widgetId: string, targetColumn?: number, initialProps?: Record<string, any>) => Promise<WidgetInstance>;
|
|
removeWidgetFromPage: (pageId: string, widgetInstanceId: string) => Promise<void>;
|
|
moveWidgetInPage: (pageId: string, widgetInstanceId: string, direction: 'up' | 'down' | 'left' | 'right') => Promise<void>;
|
|
updatePageContainerColumns: (pageId: string, containerId: string, columns: number) => Promise<void>;
|
|
updatePageContainerSettings: (pageId: string, containerId: string, settings: Partial<LayoutContainer['settings']>) => Promise<void>;
|
|
addPageContainer: (pageId: string, parentContainerId?: string) => Promise<LayoutContainer>;
|
|
removePageContainer: (pageId: string, containerId: string) => Promise<void>;
|
|
movePageContainer: (pageId: string, containerId: string, direction: 'up' | 'down') => Promise<void>;
|
|
updateWidgetProps: (pageId: string, widgetInstanceId: string, props: Record<string, any>) => Promise<void>;
|
|
renameWidget: (pageId: string, widgetInstanceId: string, newId: string) => Promise<boolean>;
|
|
exportPageLayout: (pageId: string) => Promise<string>;
|
|
importPageLayout: (pageId: string, jsonData: string) => Promise<PageLayout>;
|
|
hydratePageLayout: (pageId: string, layout: PageLayout) => void;
|
|
|
|
// Manual save
|
|
saveToApi: () => Promise<boolean>;
|
|
|
|
// History Actions
|
|
undo: () => Promise<void>;
|
|
redo: () => Promise<void>;
|
|
canUndo: boolean;
|
|
canRedo: boolean;
|
|
clearHistory: () => void;
|
|
executeCommand: (command: Command) => Promise<void>;
|
|
|
|
// State
|
|
isLoading: boolean;
|
|
loadedPages: Map<string, PageLayout>;
|
|
}
|
|
|
|
const LayoutContext = createContext<LayoutContextType | undefined>(undefined);
|
|
|
|
interface LayoutProviderProps {
|
|
children: ReactNode;
|
|
}
|
|
|
|
export const LayoutProvider: React.FC<LayoutProviderProps> = ({ children }) => {
|
|
const [loadedPages, setLoadedPages] = useState<Map<string, PageLayout>>(new Map());
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
// History State
|
|
const [canUndo, setCanUndo] = useState(false);
|
|
const [canRedo, setCanRedo] = useState(false);
|
|
|
|
// Pending Metadata State
|
|
const [pendingMetadata, setPendingMetadata] = useState<Map<string, Record<string, any>>>(new Map());
|
|
|
|
const updateLayoutCallback = useCallback((pageId: string, layout: PageLayout) => {
|
|
setLoadedPages(prev => new Map(prev).set(pageId, layout));
|
|
// UnifiedLayoutManager.savePageLayout(layout).catch(e => console.error("Failed to persist layout update", e));
|
|
}, []);
|
|
|
|
const updatePageMetadataCallback = useCallback((pageId: string, metadata: Record<string, any>) => {
|
|
setPendingMetadata(prev => {
|
|
const newMap = new Map(prev);
|
|
const existing = newMap.get(pageId) || {};
|
|
newMap.set(pageId, { ...existing, ...metadata });
|
|
return newMap;
|
|
});
|
|
}, []);
|
|
|
|
const [historyManager] = useState(() => new HistoryManager({
|
|
pageId: '',
|
|
layouts: loadedPages,
|
|
updateLayout: updateLayoutCallback,
|
|
pageMetadata: pendingMetadata,
|
|
updatePageMetadata: updatePageMetadataCallback
|
|
}));
|
|
|
|
// Update history manager context when state changes
|
|
useEffect(() => {
|
|
// We need to update the context inside historyManager so it has access to latest state
|
|
// but HistoryManager doesn't preserve context reference, it uses it per execute/undo/redo.
|
|
// However, for 'executeCommand' prop below, we construct a fresh context.
|
|
}, [loadedPages, pendingMetadata, updateLayoutCallback, updatePageMetadataCallback]);
|
|
|
|
|
|
const updateHistoryState = useCallback(() => {
|
|
setCanUndo(historyManager.canUndo());
|
|
setCanRedo(historyManager.canRedo());
|
|
}, [historyManager]);
|
|
|
|
useEffect(() => {
|
|
updateHistoryState();
|
|
}, [historyManager, loadedPages, pendingMetadata]);
|
|
|
|
// Load layout
|
|
const loadPageLayout = useCallback(async (pageId: string, defaultName?: string) => {
|
|
try {
|
|
const layout = await UnifiedLayoutManager.getPageLayout(pageId, defaultName);
|
|
setLoadedPages(prev => new Map(prev).set(pageId, layout));
|
|
} catch (e) {
|
|
console.error("Failed to load page layout", e);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
const getLoadedPageLayout = useCallback((pageId: string) => {
|
|
return loadedPages.get(pageId) || null;
|
|
}, [loadedPages]);
|
|
|
|
const clearPageLayout = useCallback(async (pageId: string) => {
|
|
setLoadedPages(prev => {
|
|
const next = new Map(prev);
|
|
next.delete(pageId);
|
|
return next;
|
|
});
|
|
}, []);
|
|
|
|
// Widget Actions
|
|
const addWidgetToPage = useCallback(async (pageId: string, containerId: string, widgetId: string, targetColumn?: number, initialProps?: Record<string, any>) => {
|
|
const layout = loadedPages.get(pageId);
|
|
if (!layout) throw new Error("Layout not loaded");
|
|
|
|
const widgetInstance = UnifiedLayoutManager.createWidgetInstance(widgetId);
|
|
if (initialProps) {
|
|
widgetInstance.props = { ...widgetInstance.props, ...initialProps };
|
|
}
|
|
|
|
// Use Command for Undo/Redo
|
|
const command = new AddWidgetCommand(pageId, containerId, widgetInstance, -1); // Index handling might need improvement if strict positioning required
|
|
await historyManager.execute(command, {
|
|
pageId,
|
|
layouts: loadedPages,
|
|
updateLayout: updateLayoutCallback,
|
|
pageMetadata: pendingMetadata,
|
|
updatePageMetadata: updatePageMetadataCallback
|
|
});
|
|
updateHistoryState();
|
|
return widgetInstance;
|
|
}, [loadedPages, historyManager, updateLayoutCallback, pendingMetadata, updatePageMetadataCallback, updateHistoryState]);
|
|
|
|
const removeWidgetFromPage = useCallback(async (pageId: string, widgetInstanceId: string) => {
|
|
const command = new RemoveWidgetCommand(pageId, widgetInstanceId);
|
|
await historyManager.execute(command, {
|
|
pageId,
|
|
layouts: loadedPages,
|
|
updateLayout: updateLayoutCallback,
|
|
pageMetadata: pendingMetadata,
|
|
updatePageMetadata: updatePageMetadataCallback
|
|
});
|
|
updateHistoryState();
|
|
}, [loadedPages, historyManager, updateLayoutCallback, pendingMetadata, updatePageMetadataCallback, updateHistoryState]);
|
|
|
|
const updateWidgetProps = useCallback(async (pageId: string, widgetInstanceId: string, props: Record<string, any>) => {
|
|
const command = new UpdateWidgetSettingsCommand(pageId, widgetInstanceId, props);
|
|
await historyManager.execute(command, {
|
|
pageId,
|
|
layouts: loadedPages,
|
|
updateLayout: updateLayoutCallback,
|
|
pageMetadata: pendingMetadata,
|
|
updatePageMetadata: updatePageMetadataCallback
|
|
});
|
|
updateHistoryState();
|
|
}, [loadedPages, historyManager, updateLayoutCallback, pendingMetadata, updatePageMetadataCallback, updateHistoryState]);
|
|
|
|
// Other Actions (Direct Update for now)
|
|
const moveWidgetInPage = useCallback(async (pageId: string, widgetInstanceId: string, direction: 'up' | 'down' | 'left' | 'right') => {
|
|
const layout = loadedPages.get(pageId);
|
|
if (!layout) return;
|
|
|
|
const command = new MoveWidgetCommand(pageId, widgetInstanceId, direction);
|
|
await historyManager.execute(command, {
|
|
pageId,
|
|
layouts: loadedPages,
|
|
updateLayout: updateLayoutCallback,
|
|
pageMetadata: pendingMetadata,
|
|
updatePageMetadata: updatePageMetadataCallback
|
|
});
|
|
updateHistoryState();
|
|
}, [loadedPages, historyManager, updateLayoutCallback, pendingMetadata, updatePageMetadataCallback, updateHistoryState]);
|
|
|
|
const updatePageContainerColumns = useCallback(async (pageId: string, containerId: string, columns: number) => {
|
|
const layout = loadedPages.get(pageId);
|
|
if (!layout) return;
|
|
const command = new UpdateContainerColumnsCommand(pageId, containerId, columns);
|
|
await historyManager.execute(command, {
|
|
pageId,
|
|
layouts: loadedPages,
|
|
updateLayout: updateLayoutCallback,
|
|
pageMetadata: pendingMetadata,
|
|
updatePageMetadata: updatePageMetadataCallback
|
|
});
|
|
updateHistoryState();
|
|
}, [loadedPages, historyManager, updateLayoutCallback, pendingMetadata, updatePageMetadataCallback, updateHistoryState]);
|
|
|
|
const updatePageContainerSettings = useCallback(async (pageId: string, containerId: string, settings: Partial<LayoutContainer['settings']>) => {
|
|
const layout = loadedPages.get(pageId);
|
|
if (!layout) return;
|
|
const command = new UpdateContainerSettingsCommand(pageId, containerId, settings);
|
|
await historyManager.execute(command, {
|
|
pageId,
|
|
layouts: loadedPages,
|
|
updateLayout: updateLayoutCallback,
|
|
pageMetadata: pendingMetadata,
|
|
updatePageMetadata: updatePageMetadataCallback
|
|
});
|
|
updateHistoryState();
|
|
}, [loadedPages, historyManager, updateLayoutCallback, pendingMetadata, updatePageMetadataCallback, updateHistoryState]);
|
|
|
|
const addPageContainer = useCallback(async (pageId: string, parentContainerId?: string) => {
|
|
const layout = loadedPages.get(pageId);
|
|
if (!layout) throw new Error("Layout not loaded");
|
|
// Create container instance first (pure)
|
|
// UnifiedLayoutManager.addContainer mutates, checking if we can generate detached container?
|
|
// We can't easily generate detached container with UnifiedLayoutManager.addContainer without a layout.
|
|
// So we manually create it here or use a helper that doesn't attach.
|
|
// UnifiedLayoutManager.generateContainerId is static.
|
|
|
|
// Manual creation to follow command pattern:
|
|
const newContainer: LayoutContainer = {
|
|
id: UnifiedLayoutManager.generateContainerId(),
|
|
type: 'container',
|
|
columns: 1,
|
|
gap: 16,
|
|
widgets: [],
|
|
children: [],
|
|
order: 0 // Command execution will set order
|
|
};
|
|
|
|
const command = new AddContainerCommand(pageId, newContainer, parentContainerId);
|
|
await historyManager.execute(command, {
|
|
pageId,
|
|
layouts: loadedPages,
|
|
updateLayout: updateLayoutCallback,
|
|
pageMetadata: pendingMetadata,
|
|
updatePageMetadata: updatePageMetadataCallback
|
|
});
|
|
updateHistoryState();
|
|
return newContainer;
|
|
}, [loadedPages, historyManager, updateLayoutCallback, pendingMetadata, updatePageMetadataCallback, updateHistoryState]);
|
|
|
|
const removePageContainer = useCallback(async (pageId: string, containerId: string) => {
|
|
const layout = loadedPages.get(pageId);
|
|
if (!layout) return;
|
|
const command = new RemoveContainerCommand(pageId, containerId);
|
|
await historyManager.execute(command, {
|
|
pageId,
|
|
layouts: loadedPages,
|
|
updateLayout: updateLayoutCallback,
|
|
pageMetadata: pendingMetadata,
|
|
updatePageMetadata: updatePageMetadataCallback
|
|
});
|
|
updateHistoryState();
|
|
}, [loadedPages, historyManager, updateLayoutCallback, pendingMetadata, updatePageMetadataCallback, updateHistoryState]);
|
|
|
|
const movePageContainer = useCallback(async (pageId: string, containerId: string, direction: 'up' | 'down') => {
|
|
const layout = loadedPages.get(pageId);
|
|
if (!layout) return;
|
|
const command = new MoveContainerCommand(pageId, containerId, direction);
|
|
await historyManager.execute(command, {
|
|
pageId,
|
|
layouts: loadedPages,
|
|
updateLayout: updateLayoutCallback,
|
|
pageMetadata: pendingMetadata,
|
|
updatePageMetadata: updatePageMetadataCallback
|
|
});
|
|
updateHistoryState();
|
|
}, [loadedPages, historyManager, updateLayoutCallback, pendingMetadata, updatePageMetadataCallback, updateHistoryState]);
|
|
|
|
const renameWidget = useCallback(async (pageId: string, widgetInstanceId: string, newId: string) => {
|
|
const layout = loadedPages.get(pageId);
|
|
if (!layout) return false;
|
|
const command = new RenameWidgetCommand(pageId, widgetInstanceId, newId);
|
|
await historyManager.execute(command, {
|
|
pageId,
|
|
layouts: loadedPages,
|
|
updateLayout: updateLayoutCallback,
|
|
pageMetadata: pendingMetadata,
|
|
updatePageMetadata: updatePageMetadataCallback
|
|
});
|
|
updateHistoryState();
|
|
return true; // Command execution is async void, we assume success if no throw
|
|
}, [loadedPages, historyManager, updateLayoutCallback, pendingMetadata, updatePageMetadataCallback, updateHistoryState]);
|
|
|
|
const exportPageLayout = useCallback(async (pageId: string) => {
|
|
return UnifiedLayoutManager.exportPageLayout(pageId);
|
|
}, []);
|
|
|
|
const importPageLayout = useCallback(async (pageId: string, jsonData: string) => {
|
|
// Use parseLayoutJSON (pure) then execute ReplaceLayoutCommand
|
|
const parsedLayout = UnifiedLayoutManager.parseLayoutJSON(jsonData, pageId);
|
|
|
|
const command = new ReplaceLayoutCommand(pageId, parsedLayout);
|
|
await historyManager.execute(command, {
|
|
pageId,
|
|
layouts: loadedPages,
|
|
updateLayout: updateLayoutCallback,
|
|
pageMetadata: pendingMetadata,
|
|
updatePageMetadata: updatePageMetadataCallback
|
|
});
|
|
updateHistoryState();
|
|
return parsedLayout;
|
|
}, [loadedPages, historyManager, updateLayoutCallback, pendingMetadata, updatePageMetadataCallback, updateHistoryState]);
|
|
|
|
const hydratePageLayout = useCallback((pageId: string, layout: PageLayout) => {
|
|
setLoadedPages(prev => new Map(prev).set(pageId, layout));
|
|
}, []);
|
|
|
|
const saveToApi = useCallback(async (): Promise<boolean> => {
|
|
console.warn("LayoutContext.saveToApi is deprecated. Storage logic has moved to UserPageEdit and db.ts.");
|
|
return true;
|
|
}, []);
|
|
|
|
const undo = async () => {
|
|
await historyManager.undo({
|
|
pageId: '',
|
|
layouts: loadedPages,
|
|
updateLayout: updateLayoutCallback,
|
|
pageMetadata: pendingMetadata,
|
|
updatePageMetadata: updatePageMetadataCallback
|
|
});
|
|
updateHistoryState();
|
|
};
|
|
|
|
const redo = async () => {
|
|
await historyManager.redo({
|
|
pageId: '',
|
|
layouts: loadedPages,
|
|
updateLayout: updateLayoutCallback,
|
|
pageMetadata: pendingMetadata,
|
|
updatePageMetadata: updatePageMetadataCallback
|
|
});
|
|
updateHistoryState();
|
|
};
|
|
|
|
const clearHistory = useCallback(() => {
|
|
historyManager.clear();
|
|
updateHistoryState();
|
|
}, [historyManager, updateHistoryState]);
|
|
|
|
return (
|
|
<LayoutContext.Provider value={{
|
|
// ... existing values ...
|
|
loadPageLayout,
|
|
getLoadedPageLayout,
|
|
clearPageLayout,
|
|
addWidgetToPage,
|
|
removeWidgetFromPage,
|
|
moveWidgetInPage,
|
|
updatePageContainerColumns,
|
|
updatePageContainerSettings,
|
|
addPageContainer,
|
|
removePageContainer,
|
|
movePageContainer,
|
|
updateWidgetProps,
|
|
renameWidget,
|
|
saveToApi,
|
|
exportPageLayout,
|
|
importPageLayout,
|
|
hydratePageLayout,
|
|
isLoading,
|
|
loadedPages,
|
|
undo,
|
|
redo,
|
|
canUndo,
|
|
canRedo,
|
|
clearHistory,
|
|
executeCommand: async (command: Command) => {
|
|
await historyManager.execute(command, {
|
|
pageId: '',
|
|
layouts: loadedPages,
|
|
updateLayout: updateLayoutCallback,
|
|
pageMetadata: pendingMetadata,
|
|
updatePageMetadata: updatePageMetadataCallback
|
|
});
|
|
updateHistoryState();
|
|
}
|
|
}}>
|
|
{children}
|
|
</LayoutContext.Provider>
|
|
);
|
|
};
|
|
|
|
export const useLayout = (): LayoutContextType => {
|
|
const context = useContext(LayoutContext);
|
|
if (context === undefined) {
|
|
throw new Error('useLayout must be used within a LayoutProvider');
|
|
}
|
|
return context;
|
|
}; |