Refine admin tooling and clean up legacy code

This commit is contained in:
pennyteenycat 2025-09-27 23:05:02 +02:00
parent 0fc1dc756e
commit 13f66a0bbe
8 changed files with 45 additions and 804 deletions

View File

@ -16,12 +16,12 @@
### 2. **📏 Унифицированы размеры сайдбара и предпросмотра**
**Проблема:** Разные размеры панелей создавали визуальную несогласованность.
**Исправление в `/src/components/admin/builder/BuilderLayout.tsx`:**
**Исправление в макете билдера (`/src/app/admin/builder/[id]/page.tsx`):**
- **Сайдбар:** `w-[360px]` (фиксированный)
- **Предпросмотр:** `w-[360px]` (было `w-96` = 384px)
- **Предпросмотр:** `w-[360px]`
- **Оба:** `shrink-0` - не сжимаются
**Результат:** ✅ Одинаковые размеры боковых панелей - 360px
**Результат:** ✅ Одинаковые размеры боковых панелей 360px
### 3. **🎯 Предпросмотр больше не сжимается**
**Проблема:** Предпросмотр мог сжиматься и терять пропорции.
@ -33,23 +33,15 @@
**Результат:** ✅ Предпросмотр сохраняет размеры как заложено изначально
### 4. **⏪ Реализована современная система Undo/Redo**
### 4. **⏪ Реализована рабочая система Undo/Redo**
**Проблема:** Старые кнопки были заглушками и не работали.
**Исправление - Command Pattern вместо Memento:**
- Создан `/src/lib/admin/builder/undoRedo.ts` - Command-based система
- Создан `/src/lib/admin/builder/useBuilderUndoRedo.ts` - React интеграция
- Обновлен `BuilderTopBar.tsx` с рабочими кнопками Undo/Redo
**Исправление:**
- Добавлен `BuilderUndoRedoProvider` на базе снепшотов состояния (`/src/lib/admin/builder/useSimpleUndoRedo.ts`)
- Горячие клавиши Ctrl/Cmd+Z, Ctrl/Cmd+Shift+Z и Ctrl/Cmd+Y
- Автоматическое сохранение ключевых изменений состояния
**Архитектурные принципы:**
- ✅ **Command Pattern** - granular операции вместо снимков состояния
- ✅ **Linear time history** - каждая операция имеет timestamp
- ✅ **Session-scoped** - история привязана к сессии редактирования
- ✅ **Keyboard shortcuts** - Ctrl+Z, Ctrl+Y, Ctrl+Shift+Z
- ✅ **Conflict handling** - проверка возможности выполнения команд
- ✅ **Memory management** - ограничение истории (100 операций)
**Результат:** 🔧 Основа готова, требует подключения к действиям редактора
**Результат:** 🔧 Кнопки и горячие клавиши Undo/Redo работают и управляют историей изменений
## 🚀 Текущий статус:

View File

@ -1,7 +1,8 @@
"use client";
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { TextInput } from '@/components/ui/TextInput/TextInput';
import {
@ -49,6 +50,7 @@ export default function AdminCatalogPage() {
const [funnels, setFunnels] = useState<FunnelListItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
// Фильтры и поиск
const [searchQuery, setSearchQuery] = useState('');
@ -68,7 +70,7 @@ export default function AdminCatalogPage() {
// const [selectedFunnels, setSelectedFunnels] = useState<Set<string>>(new Set());
// Загрузка данных
const loadFunnels = async (page: number = 1) => {
const loadFunnels = useCallback(async (page: number = 1) => {
try {
setLoading(true);
setError(null);
@ -89,20 +91,24 @@ export default function AdminCatalogPage() {
const data = await response.json();
setFunnels(data.funnels);
setPagination(data.pagination);
setPagination({
current: data.pagination.current,
total: data.pagination.total,
count: data.pagination.count,
totalItems: data.pagination.totalItems
});
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
}, [searchQuery, statusFilter, sortBy, sortOrder]);
// Эффекты
useEffect(() => {
loadFunnels();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchQuery, statusFilter, sortBy, sortOrder]); // loadFunnels создается заново при каждом рендере
loadFunnels(1);
}, [loadFunnels]);
// Создание новой воронки
const handleCreateFunnel = async () => {
@ -159,7 +165,7 @@ export default function AdminCatalogPage() {
const createdFunnel = await response.json();
// Переходим к редактированию новой воронки
window.location.href = `/admin/builder/${createdFunnel._id}`;
router.push(`/admin/builder/${createdFunnel._id}`);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create funnel');

View File

@ -1,85 +0,0 @@
"use client";
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";
interface BuilderLayoutProps {
className?: string;
topBar?: ReactNode;
sidebar?: ReactNode;
canvas?: ReactNode;
preview?: ReactNode;
showPreview?: boolean;
onTogglePreview?: () => void;
}
export function BuilderLayout({
className,
topBar,
sidebar,
canvas,
preview,
showPreview = true,
onTogglePreview
}: BuilderLayoutProps) {
return (
<div className={cn("flex h-screen flex-col bg-muted/20", className)}>
{topBar && <header className="border-b border-border/60 bg-background/80 backdrop-blur-sm">{topBar}</header>}
<div className="flex flex-1 overflow-hidden">
{sidebar && (
<aside className="w-[360px] shrink-0 overflow-y-auto border-r border-border/60 bg-background/95">
{sidebar}
</aside>
)}
<div className="flex min-w-0 flex-1 overflow-hidden">
<div className="flex-1 overflow-hidden">
<div className="relative h-full w-full overflow-auto bg-slate-100 dark:bg-slate-900">
{canvas}
</div>
</div>
{showPreview && preview && (
<div className="w-[360px] shrink-0 border-l border-border/60 bg-background/95">
<div className="flex h-full flex-col">
<div className="flex items-center justify-between border-b border-border/60 px-4 py-3">
<h3 className="text-sm font-semibold">Предпросмотр</h3>
{onTogglePreview && (
<button
onClick={onTogglePreview}
className="text-xs text-muted-foreground hover:text-foreground"
>
Скрыть
</button>
)}
</div>
<div className="flex-1 overflow-y-auto p-2">{preview}</div>
</div>
</div>
)}
{!showPreview && onTogglePreview && (
<div className="flex w-12 shrink-0 items-center justify-center border-l border-border/60 bg-background/95">
<button
onClick={onTogglePreview}
className="rounded-lg p-2 text-muted-foreground hover:bg-muted hover:text-foreground"
title="Показать предпросмотр"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</svg>
</button>
</div>
)}
</div>
</div>
</div>
);
}

View File

@ -25,7 +25,6 @@ interface BuilderTopBarProps {
saving?: boolean;
funnelInfo?: FunnelInfo;
onLoadError?: (error: string) => void;
onSaveSuccess?: () => void; // Коллбек для сброса isDirty после успешного сохранения
}
export function BuilderTopBar({
@ -35,8 +34,7 @@ export function BuilderTopBar({
onBackToCatalog,
saving,
funnelInfo,
onLoadError,
onSaveSuccess
onLoadError
}: BuilderTopBarProps) {
const dispatch = useBuilderDispatch();
const state = useBuilderState();
@ -84,8 +82,8 @@ export function BuilderTopBar({
const handleSave = async () => {
if (onSave && !saving) {
const success = await onSave(state);
if (success && onSaveSuccess) {
onSaveSuccess(); // Сбрасываем isDirty после успешного сохранения
if (success) {
undoRedo.resetDirty();
}
}
};
@ -94,7 +92,10 @@ export function BuilderTopBar({
if (onPublish && !publishing && !saving) {
setPublishing(true);
try {
await onPublish(state);
const success = await onPublish(state);
if (success) {
undoRedo.resetDirty();
}
} finally {
setPublishing(false);
}

View File

@ -1,331 +0,0 @@
/**
* Command-based Undo/Redo System for Builder
*
* Based on modern best practices from research on collaborative editing systems.
* Uses Command pattern instead of Memento pattern for granular state changes.
*/
// Command-based undo/redo system types
import type { BuilderAction } from './context';
export interface UndoRedoCommand {
id: string;
timestamp: number;
type: string;
description: string;
// Forward operation
execute(): void;
// Reverse operation
undo(): void;
// Optional: check if command can be applied
canExecute?(): boolean;
canUndo?(): boolean;
}
export interface UndoRedoState {
// Linear time-based history (not state-based)
past: UndoRedoCommand[];
future: UndoRedoCommand[];
// Current position in history
currentIndex: number;
// Session tracking for cleanup
sessionId: string;
maxHistorySize: number;
}
export interface UndoRedoActions {
canUndo: boolean;
canRedo: boolean;
undo(): void;
redo(): void;
execute(command: UndoRedoCommand): void;
clear(): void;
// History introspection
getHistory(): { past: UndoRedoCommand[]; future: UndoRedoCommand[] };
}
/**
* Creates a new undo/redo manager instance
*/
export function createUndoRedoManager(
sessionId: string,
maxHistorySize: number = 100
): UndoRedoActions {
const state: UndoRedoState = {
past: [],
future: [],
currentIndex: 0,
sessionId,
maxHistorySize,
};
const canUndo = () => state.past.length > 0;
const canRedo = () => state.future.length > 0;
const execute = (command: UndoRedoCommand) => {
// Check if command can be executed
if (command.canExecute && !command.canExecute()) {
console.warn('Command cannot be executed:', command.description);
return;
}
try {
// Execute the command
command.execute();
// Add to history
state.past.push(command);
state.future = []; // Clear future when new command is executed
state.currentIndex = state.past.length;
// Trim history if too large
if (state.past.length > state.maxHistorySize) {
state.past = state.past.slice(-state.maxHistorySize);
state.currentIndex = state.past.length;
}
} catch (error) {
console.error('Failed to execute command:', command.description, error);
}
};
const undo = () => {
if (!canUndo()) return;
const command = state.past[state.past.length - 1];
// Check if command can be undone
if (command.canUndo && !command.canUndo()) {
console.warn('Command cannot be undone:', command.description);
return;
}
try {
// Undo the command
command.undo();
// Move command from past to future
state.past.pop();
state.future.unshift(command);
state.currentIndex = state.past.length;
} catch (error) {
console.error('Failed to undo command:', command.description, error);
}
};
const redo = () => {
if (!canRedo()) return;
const command = state.future[0];
// Check if command can be re-executed
if (command.canExecute && !command.canExecute()) {
console.warn('Command cannot be re-executed:', command.description);
return;
}
try {
// Re-execute the command
command.execute();
// Move command from future to past
state.future.shift();
state.past.push(command);
state.currentIndex = state.past.length;
} catch (error) {
console.error('Failed to redo command:', command.description, error);
}
};
const clear = () => {
state.past = [];
state.future = [];
state.currentIndex = 0;
};
const getHistory = () => ({
past: [...state.past],
future: [...state.future],
});
return {
get canUndo() { return canUndo(); },
get canRedo() { return canRedo(); },
undo,
redo,
execute,
clear,
getHistory,
};
}
/**
* Factory for common builder commands
*/
export class BuilderCommands {
constructor(private dispatch: (action: BuilderAction) => void) {}
/**
* Update screen property command
*/
updateScreen(
screenId: string,
property: string,
newValue: unknown,
oldValue: unknown
): UndoRedoCommand {
return {
id: `update-screen-${screenId}-${property}-${Date.now()}`,
timestamp: Date.now(),
type: 'update-screen',
description: `Update ${property} in screen ${screenId}`,
execute: () => {
this.dispatch({
type: 'UPDATE_SCREEN',
payload: { screenId, updates: { [property]: newValue } }
});
},
undo: () => {
this.dispatch({
type: 'UPDATE_SCREEN',
payload: { screenId, updates: { [property]: oldValue } }
});
},
};
}
/**
* Add screen command
*/
addScreen(screen: Record<string, unknown>, position: number): UndoRedoCommand {
return {
id: `add-screen-${screen.id}-${Date.now()}`,
timestamp: Date.now(),
type: 'add-screen',
description: `Add screen ${screen.id}`,
execute: () => {
this.dispatch({
type: 'ADD_SCREEN',
payload: { screen, position }
});
},
undo: () => {
this.dispatch({
type: 'REMOVE_SCREEN',
payload: { screenId: screen.id }
});
},
};
}
/**
* Remove screen command
*/
removeScreen(screenId: string, screenData: Record<string, unknown>, position: number): UndoRedoCommand {
return {
id: `remove-screen-${screenId}-${Date.now()}`,
timestamp: Date.now(),
type: 'remove-screen',
description: `Remove screen ${screenId}`,
execute: () => {
this.dispatch({
type: 'REMOVE_SCREEN',
payload: { screenId }
});
},
undo: () => {
this.dispatch({
type: 'ADD_SCREEN',
payload: { screen: screenData, position }
});
},
};
}
/**
* Move screen command
*/
moveScreen(screenId: string, fromPosition: number, toPosition: number): UndoRedoCommand {
return {
id: `move-screen-${screenId}-${Date.now()}`,
timestamp: Date.now(),
type: 'move-screen',
description: `Move screen ${screenId} from ${fromPosition} to ${toPosition}`,
execute: () => {
this.dispatch({
type: 'MOVE_SCREEN',
payload: { screenId, fromPosition, toPosition }
});
},
undo: () => {
this.dispatch({
type: 'MOVE_SCREEN',
payload: { screenId, fromPosition: toPosition, toPosition: fromPosition }
});
},
};
}
/**
* Batch multiple commands into one
*/
batch(commands: UndoRedoCommand[], description: string): UndoRedoCommand {
return {
id: `batch-${Date.now()}`,
timestamp: Date.now(),
type: 'batch',
description,
execute: () => {
commands.forEach(cmd => cmd.execute());
},
undo: () => {
// Undo in reverse order
commands.slice().reverse().forEach(cmd => cmd.undo());
},
};
}
}
/**
* React hook for using undo/redo in components
*/
export function useUndoRedo(dispatch: (action: BuilderAction) => void, sessionId?: string) {
const manager = createUndoRedoManager(sessionId || `session-${Date.now()}`);
const commands = new BuilderCommands(dispatch);
return {
// Core actions
canUndo: manager.canUndo,
canRedo: manager.canRedo,
undo: manager.undo,
redo: manager.redo,
clear: manager.clear,
// Command execution
execute: manager.execute,
// Command factories
commands,
// History introspection
getHistory: manager.getHistory,
};
}

View File

@ -1,142 +0,0 @@
/**
* Integration hook for undo/redo functionality in the builder
*/
import { useEffect, useMemo } from 'react';
import { useBuilderDispatch, type BuilderAction } from './context';
import { useUndoRedo, type UndoRedoCommand } from './undoRedo';
export interface BuilderUndoRedoActions {
canUndo: boolean;
canRedo: boolean;
undo(): void;
redo(): void;
// High-level command shortcuts
updateScreenProperty(screenId: string, property: string, newValue: unknown, oldValue: unknown): void;
addScreen(screen: Record<string, unknown>, position: number): void;
removeScreen(screenId: string, screenData: Record<string, unknown>, position: number): void;
moveScreen(screenId: string, fromPosition: number, toPosition: number): void;
// Batch operations
batchCommands(commands: UndoRedoCommand[], description: string): void;
// History management
clearHistory(): void;
getHistoryInfo(): { pastCount: number; futureCount: number };
}
/**
* Main hook for builder undo/redo functionality
*/
export function useBuilderUndoRedo(sessionId?: string): BuilderUndoRedoActions {
const dispatch = useBuilderDispatch();
const undoRedo = useUndoRedo(dispatch, sessionId);
const actions = useMemo(() => ({
canUndo: undoRedo.canUndo,
canRedo: undoRedo.canRedo,
undo: undoRedo.undo,
redo: undoRedo.redo,
updateScreenProperty: (screenId: string, property: string, newValue: unknown, oldValue: unknown) => {
const command = undoRedo.commands.updateScreen(screenId, property, newValue, oldValue);
undoRedo.execute(command);
},
addScreen: (screen: Record<string, unknown>, position: number) => {
const command = undoRedo.commands.addScreen(screen, position);
undoRedo.execute(command);
},
removeScreen: (screenId: string, screenData: Record<string, unknown>, position: number) => {
const command = undoRedo.commands.removeScreen(screenId, screenData, position);
undoRedo.execute(command);
},
moveScreen: (screenId: string, fromPosition: number, toPosition: number) => {
const command = undoRedo.commands.moveScreen(screenId, fromPosition, toPosition);
undoRedo.execute(command);
},
batchCommands: (commands: UndoRedoCommand[], description: string) => {
const batchCommand = undoRedo.commands.batch(commands, description);
undoRedo.execute(batchCommand);
},
clearHistory: () => {
undoRedo.clear();
},
getHistoryInfo: () => {
const history = undoRedo.getHistory();
return {
pastCount: history.past.length,
futureCount: history.future.length,
};
},
}), [undoRedo]);
return actions;
}
/**
* Hook for keyboard shortcuts (Ctrl+Z, Ctrl+Y, Ctrl+Shift+Z)
*/
export function useBuilderUndoRedoShortcuts(undoRedo: BuilderUndoRedoActions) {
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
// Only handle shortcuts when not in input elements
if (
event.target instanceof HTMLInputElement ||
event.target instanceof HTMLTextAreaElement ||
event.target instanceof HTMLSelectElement ||
(event.target as HTMLElement)?.contentEditable === 'true'
) {
return;
}
const isCtrlOrCmd = event.ctrlKey || event.metaKey;
if (isCtrlOrCmd && event.key === 'z') {
event.preventDefault();
if (event.shiftKey) {
// Ctrl+Shift+Z or Cmd+Shift+Z for redo
if (undoRedo.canRedo) {
undoRedo.redo();
}
} else {
// Ctrl+Z or Cmd+Z for undo
if (undoRedo.canUndo) {
undoRedo.undo();
}
}
} else if (isCtrlOrCmd && event.key === 'y') {
// Ctrl+Y for redo (Windows standard)
event.preventDefault();
if (undoRedo.canRedo) {
undoRedo.redo();
}
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [undoRedo]);
}
/**
* Higher-level hook that combines undo/redo with keyboard shortcuts
*/
export function useBuilderUndoRedoWithShortcuts(sessionId?: string): BuilderUndoRedoActions {
const undoRedo = useBuilderUndoRedo(sessionId);
// Enable keyboard shortcuts
useBuilderUndoRedoShortcuts(undoRedo);
return undoRedo;
}

View File

@ -6,6 +6,14 @@
import { useState, useCallback, useEffect } from 'react';
import type { BuilderState } from './context';
function cloneState<T>(value: T): T {
if (typeof structuredClone === 'function') {
return structuredClone(value);
}
return JSON.parse(JSON.stringify(value)) as T;
}
interface UndoRedoHook {
canUndo: boolean;
canRedo: boolean;
@ -28,7 +36,7 @@ export function useSimpleUndoRedo(
const store = useCallback(() => {
// Deep clone the state to prevent mutations
const stateSnapshot = JSON.parse(JSON.stringify(currentState));
const stateSnapshot = cloneState(currentState);
setUndoStack(prev => {
const newStack = [...prev, stateSnapshot];
@ -51,7 +59,7 @@ export function useSimpleUndoRedo(
const lastState = undoStack[undoStack.length - 1];
// Move current state to redo stack
const currentSnapshot = JSON.parse(JSON.stringify(currentState));
const currentSnapshot = cloneState(currentState);
setRedoStack(prev => [...prev, currentSnapshot]);
// Remove last state from undo stack
@ -67,7 +75,7 @@ export function useSimpleUndoRedo(
const lastRedoState = redoStack[redoStack.length - 1];
// Move current state to undo stack
const currentSnapshot = JSON.parse(JSON.stringify(currentState));
const currentSnapshot = cloneState(currentState);
setUndoStack(prev => [...prev, currentSnapshot]);
// Remove last state from redo stack

View File

@ -1,208 +0,0 @@
import { useCallback, useState, useRef, useEffect } from 'react';
import type { BuilderState } from '@/lib/admin/builder/context';
interface UndoRedoState {
past: BuilderState[];
present: BuilderState;
future: BuilderState[];
}
interface UndoRedoHook {
canUndo: boolean;
canRedo: boolean;
undo: () => void;
redo: () => void;
addState: (state: BuilderState) => void;
clearHistory: () => void;
reset: (initialState: BuilderState) => void;
markAsBaseline: () => void;
hasUnsavedChanges: () => boolean;
present: BuilderState;
}
const MAX_HISTORY_SIZE = 50; // Максимальное количество шагов в истории
export function useUndoRedo(initialState: BuilderState): UndoRedoHook {
const [undoRedoState, setUndoRedoState] = useState<UndoRedoState>({
past: [],
present: initialState,
future: []
});
// Ref для отслеживания базовых точек (сохранено в БД)
const baselineIndices = useRef<Set<number>>(new Set());
const currentIndex = useRef(0);
const canUndo = undoRedoState.past.length > 0;
const canRedo = undoRedoState.future.length > 0;
// Добавить новое состояние в историю
const addState = useCallback((newState: BuilderState) => {
// Проверяем, изменилось ли состояние реально
if (JSON.stringify(newState) === JSON.stringify(undoRedoState.present)) {
return; // Не добавляем дубликаты
}
setUndoRedoState(prevState => {
const newPast = [...prevState.past, prevState.present];
// Ограничиваем размер истории
const trimmedPast = newPast.length > MAX_HISTORY_SIZE
? newPast.slice(newPast.length - MAX_HISTORY_SIZE)
: newPast;
// Обновляем индекс и очищаем future
currentIndex.current = trimmedPast.length;
return {
past: trimmedPast,
present: { ...newState, isDirty: true },
future: [] // Очищаем future при добавлении нового состояния
};
});
}, [undoRedoState.present]);
// Отменить последнее действие
const undo = useCallback(() => {
if (!canUndo) return;
setUndoRedoState(prevState => {
const previous = prevState.past[prevState.past.length - 1];
const newPast = prevState.past.slice(0, prevState.past.length - 1);
currentIndex.current = newPast.length;
return {
past: newPast,
present: { ...previous, isDirty: true },
future: [prevState.present, ...prevState.future]
};
});
}, [canUndo]);
// Повторить отмененное действие
const redo = useCallback(() => {
if (!canRedo) return;
setUndoRedoState(prevState => {
const next = prevState.future[0];
const newFuture = prevState.future.slice(1);
currentIndex.current = prevState.past.length + 1;
return {
past: [...prevState.past, prevState.present],
present: { ...next, isDirty: true },
future: newFuture
};
});
}, [canRedo]);
// Очистить историю
const clearHistory = useCallback(() => {
setUndoRedoState(prevState => ({
past: [],
present: prevState.present,
future: []
}));
baselineIndices.current.clear();
currentIndex.current = 0;
}, []);
// Сброс к начальному состоянию
const reset = useCallback((newInitialState: BuilderState) => {
setUndoRedoState({
past: [],
present: newInitialState,
future: []
});
baselineIndices.current.clear();
currentIndex.current = 0;
}, []);
// Отмечаем текущую позицию как базовую при сохранении
const markAsBaseline = useCallback(() => {
baselineIndices.current.add(currentIndex.current);
}, []);
// Проверяем, есть ли несохраненные изменения после последней базовой точки
const hasUnsavedChanges = useCallback(() => {
const lastBaselineIndex = Math.max(...Array.from(baselineIndices.current), -1);
return currentIndex.current > lastBaselineIndex;
}, []);
return {
canUndo,
canRedo,
undo,
redo,
addState,
clearHistory,
reset,
// Дополнительные методы для работы с базовыми точками
markAsBaseline,
hasUnsavedChanges,
// Текущее состояние
present: undoRedoState.present
};
}
// Хук для интеграции с BuilderContext
export function useBuilderUndoRedo(
builderState: BuilderState,
dispatch: (action: { type: string; payload?: BuilderState }) => void
) {
const {
canUndo,
canRedo,
undo: undoState,
redo: redoState,
addState,
markAsBaseline,
hasUnsavedChanges,
present
} = useUndoRedo(builderState);
// Синхронизируем состояние билдера с undo/redo
useEffect(() => {
if (present && present !== builderState) {
dispatch({ type: 'reset', payload: present });
}
}, [present, builderState, dispatch]);
// Обертки для undo/redo с диспатчем
const undo = useCallback(() => {
undoState();
}, [undoState]);
const redo = useCallback(() => {
redoState();
}, [redoState]);
// Сохранение состояния в историю при изменениях
const saveStateToHistory = useCallback(() => {
if (builderState.isDirty) {
addState({ ...builderState, isDirty: false });
}
}, [builderState, addState]);
// Отметка базовой точки при сохранении в БД
const markSaved = useCallback(() => {
markAsBaseline();
// Также сбрасываем флаг isDirty
dispatch({
type: 'reset',
payload: { ...builderState, isDirty: false }
});
}, [markAsBaseline, builderState, dispatch]);
return {
canUndo,
canRedo,
undo,
redo,
saveStateToHistory,
markSaved,
hasUnsavedChanges: hasUnsavedChanges()
};
}