@@ -33,6 +33,19 @@ export interface UndoManagerOptions {
3333 maxHistory ?: number ;
3434}
3535
36+ /** Type guard validating the required shape of a persisted UndoableOperation. */
37+ function isValidOperation ( op : unknown ) : op is UndoableOperation {
38+ if ( typeof op !== 'object' || op === null ) return false ;
39+ const o = op as Record < string , unknown > ;
40+ return (
41+ typeof o . id === 'string' &&
42+ typeof o . type === 'string' &&
43+ typeof o . objectName === 'string' &&
44+ typeof o . recordId === 'string' &&
45+ typeof o . timestamp === 'number'
46+ ) ;
47+ }
48+
3649/**
3750 * Manages undo/redo stacks for CRUD operations.
3851 *
@@ -110,6 +123,91 @@ export class UndoManager {
110123 /** Get a shallow copy of the undo history (for developer tools). */
111124 getHistory ( ) : UndoableOperation [ ] { return [ ...this . undoStack ] ; }
112125
126+ /** Get a shallow copy of the redo history (for developer tools). */
127+ getRedoHistory ( ) : UndoableOperation [ ] { return [ ...this . redoStack ] ; }
128+
129+ // ---------------------------------------------------------------------------
130+ // Batch operations
131+ // ---------------------------------------------------------------------------
132+
133+ /** Push multiple operations as one atomic unit. Clears the redo stack. */
134+ pushBatch ( operations : UndoableOperation [ ] ) : void {
135+ if ( operations . length === 0 ) return ;
136+ this . undoStack . push ( ...operations ) ;
137+ // Trim from the front if we exceed maxHistory
138+ if ( this . undoStack . length > this . maxHistory ) {
139+ this . undoStack . splice ( 0 , this . undoStack . length - this . maxHistory ) ;
140+ }
141+ this . redoStack = [ ] ;
142+ this . notify ( ) ;
143+ }
144+
145+ /** Pop `count` operations from the undo stack and move them to redo (LIFO order). */
146+ popUndoBatch ( count : number ) : UndoableOperation [ ] {
147+ const actual = Math . min ( count , this . undoStack . length ) ;
148+ if ( actual === 0 ) return [ ] ;
149+ const ops = this . undoStack . splice ( this . undoStack . length - actual , actual ) ;
150+ // Preserve LIFO order on the redo stack (last undone goes on top)
151+ this . redoStack . push ( ...ops ) ;
152+ this . notify ( ) ;
153+ return ops ;
154+ }
155+
156+ /** Pop `count` operations from the redo stack and move them to undo (LIFO order). */
157+ popRedoBatch ( count : number ) : UndoableOperation [ ] {
158+ const actual = Math . min ( count , this . redoStack . length ) ;
159+ if ( actual === 0 ) return [ ] ;
160+ const ops = this . redoStack . splice ( this . redoStack . length - actual , actual ) ;
161+ this . undoStack . push ( ...ops ) ;
162+ this . notify ( ) ;
163+ return ops ;
164+ }
165+
166+ // ---------------------------------------------------------------------------
167+ // Persistence (localStorage)
168+ // ---------------------------------------------------------------------------
169+
170+ private static readonly STORAGE_KEY = 'objectui:undo-history' ;
171+
172+ /** Persist the current undo/redo stacks to localStorage. */
173+ saveToStorage ( ) : void {
174+ try {
175+ const payload = JSON . stringify ( {
176+ undoStack : this . undoStack ,
177+ redoStack : this . redoStack ,
178+ } ) ;
179+ localStorage . setItem ( UndoManager . STORAGE_KEY , payload ) ;
180+ } catch {
181+ // localStorage may be unavailable (SSR, quota exceeded, etc.)
182+ }
183+ }
184+
185+ /** Restore undo/redo stacks from localStorage (no-op when unavailable). */
186+ loadFromStorage ( ) : void {
187+ try {
188+ if ( typeof localStorage === 'undefined' ) return ;
189+ const raw = localStorage . getItem ( UndoManager . STORAGE_KEY ) ;
190+ if ( ! raw ) return ;
191+ const parsed = JSON . parse ( raw ) as {
192+ undoStack ?: UndoableOperation [ ] ;
193+ redoStack ?: UndoableOperation [ ] ;
194+ } ;
195+ if ( Array . isArray ( parsed . undoStack ) ) {
196+ this . undoStack = parsed . undoStack . filter ( isValidOperation ) ;
197+ }
198+ if ( Array . isArray ( parsed . redoStack ) ) {
199+ this . redoStack = parsed . redoStack . filter ( isValidOperation ) ;
200+ }
201+ // Enforce maxHistory in case persisted state used a different limit
202+ if ( this . undoStack . length > this . maxHistory ) {
203+ this . undoStack . splice ( 0 , this . undoStack . length - this . maxHistory ) ;
204+ }
205+ this . notify ( ) ;
206+ } catch {
207+ // Silently ignore parse errors or missing storage
208+ }
209+ }
210+
113211 private notify ( ) : void { this . listeners . forEach ( ( fn ) => fn ( ) ) ; }
114212}
115213
0 commit comments