From 13f66a0bbe1d1092d1cd94a11af553c84ba9db84 Mon Sep 17 00:00:00 2001 From: pennyteenycat Date: Sat, 27 Sep 2025 23:05:02 +0200 Subject: [PATCH] Refine admin tooling and clean up legacy code --- FIXES-SUMMARY.md | 26 +- src/app/admin/page.tsx | 22 +- .../admin/builder/BuilderLayout.tsx | 85 ----- .../admin/builder/BuilderTopBar.tsx | 17 +- src/lib/admin/builder/undoRedo.ts.disabled | 331 ------------------ .../builder/useBuilderUndoRedo.ts.disabled | 142 -------- src/lib/admin/builder/useSimpleUndoRedo.ts | 18 +- src/lib/admin/builder/useUndoRedo.ts | 208 ----------- 8 files changed, 45 insertions(+), 804 deletions(-) delete mode 100644 src/components/admin/builder/BuilderLayout.tsx delete mode 100644 src/lib/admin/builder/undoRedo.ts.disabled delete mode 100644 src/lib/admin/builder/useBuilderUndoRedo.ts.disabled delete mode 100644 src/lib/admin/builder/useUndoRedo.ts diff --git a/FIXES-SUMMARY.md b/FIXES-SUMMARY.md index 28a82f3..f99b7f4 100644 --- a/FIXES-SUMMARY.md +++ b/FIXES-SUMMARY.md @@ -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 Ρ€Π°Π±ΠΎΡ‚Π°ΡŽΡ‚ ΠΈ ΡƒΠΏΡ€Π°Π²Π»ΡΡŽΡ‚ историСй ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ ## πŸš€ Π’Π΅ΠΊΡƒΡ‰ΠΈΠΉ статус: diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 31f3d32..c165f1f 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -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([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const router = useRouter(); // Π€ΠΈΠ»ΡŒΡ‚Ρ€Ρ‹ ΠΈ поиск const [searchQuery, setSearchQuery] = useState(''); @@ -68,7 +70,7 @@ export default function AdminCatalogPage() { // const [selectedFunnels, setSelectedFunnels] = useState>(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'); diff --git a/src/components/admin/builder/BuilderLayout.tsx b/src/components/admin/builder/BuilderLayout.tsx deleted file mode 100644 index 4cf9466..0000000 --- a/src/components/admin/builder/BuilderLayout.tsx +++ /dev/null @@ -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 ( -
- {topBar &&
{topBar}
} -
- {sidebar && ( - - )} -
-
-
- {canvas} -
-
- {showPreview && preview && ( -
-
-
-

ΠŸΡ€Π΅Π΄ΠΏΡ€ΠΎΡΠΌΠΎΡ‚Ρ€

- {onTogglePreview && ( - - )} -
-
{preview}
-
-
- )} - {!showPreview && onTogglePreview && ( -
- -
- )} -
-
-
- ); -} diff --git a/src/components/admin/builder/BuilderTopBar.tsx b/src/components/admin/builder/BuilderTopBar.tsx index 9ba82c2..a794244 100644 --- a/src/components/admin/builder/BuilderTopBar.tsx +++ b/src/components/admin/builder/BuilderTopBar.tsx @@ -25,18 +25,16 @@ interface BuilderTopBarProps { saving?: boolean; funnelInfo?: FunnelInfo; onLoadError?: (error: string) => void; - onSaveSuccess?: () => void; // КоллбСк для сброса isDirty послС ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎΠ³ΠΎ сохранСния } -export function BuilderTopBar({ - onNew, +export function BuilderTopBar({ + onNew, onSave, onPublish, 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); } diff --git a/src/lib/admin/builder/undoRedo.ts.disabled b/src/lib/admin/builder/undoRedo.ts.disabled deleted file mode 100644 index 5774672..0000000 --- a/src/lib/admin/builder/undoRedo.ts.disabled +++ /dev/null @@ -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, 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, 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, - }; -} diff --git a/src/lib/admin/builder/useBuilderUndoRedo.ts.disabled b/src/lib/admin/builder/useBuilderUndoRedo.ts.disabled deleted file mode 100644 index ff559ae..0000000 --- a/src/lib/admin/builder/useBuilderUndoRedo.ts.disabled +++ /dev/null @@ -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, position: number): void; - removeScreen(screenId: string, screenData: Record, 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, position: number) => { - const command = undoRedo.commands.addScreen(screen, position); - undoRedo.execute(command); - }, - - removeScreen: (screenId: string, screenData: Record, 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; -} diff --git a/src/lib/admin/builder/useSimpleUndoRedo.ts b/src/lib/admin/builder/useSimpleUndoRedo.ts index fee0972..848e638 100644 --- a/src/lib/admin/builder/useSimpleUndoRedo.ts +++ b/src/lib/admin/builder/useSimpleUndoRedo.ts @@ -6,6 +6,14 @@ import { useState, useCallback, useEffect } from 'react'; import type { BuilderState } from './context'; +function cloneState(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]; @@ -49,9 +57,9 @@ export function useSimpleUndoRedo( if (!canUndo) return; 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 @@ -65,9 +73,9 @@ export function useSimpleUndoRedo( if (!canRedo) return; 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 diff --git a/src/lib/admin/builder/useUndoRedo.ts b/src/lib/admin/builder/useUndoRedo.ts deleted file mode 100644 index 0fcf874..0000000 --- a/src/lib/admin/builder/useUndoRedo.ts +++ /dev/null @@ -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({ - past: [], - present: initialState, - future: [] - }); - - // Ref для отслСТивания Π±Π°Π·ΠΎΠ²Ρ‹Ρ… Ρ‚ΠΎΡ‡Π΅ΠΊ (сохранСно Π² Π‘Π”) - const baselineIndices = useRef>(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() - }; -}