@@ -183,6 +183,26 @@ export function themeGlobalStyleVariations( state = {}, action ) {
183
183
return state ;
184
184
}
185
185
186
+ const withMultiEntityRecordEdits = ( reducer ) => ( state , action ) => {
187
+ if ( action . type === 'UNDO' || action . type === 'REDO' ) {
188
+ const { stackedEdits } = action ;
189
+
190
+ let newState = state ;
191
+ stackedEdits . forEach ( ( { kind, name, recordId, edits } ) => {
192
+ newState = reducer ( newState , {
193
+ type : 'EDIT_ENTITY_RECORD' ,
194
+ kind,
195
+ name,
196
+ recordId,
197
+ edits,
198
+ } ) ;
199
+ } ) ;
200
+ return newState ;
201
+ }
202
+
203
+ return reducer ( state , action ) ;
204
+ } ;
205
+
186
206
/**
187
207
* Higher Order Reducer for a given entity config. It supports:
188
208
*
@@ -196,6 +216,8 @@ export function themeGlobalStyleVariations( state = {}, action ) {
196
216
*/
197
217
function entity ( entityConfig ) {
198
218
return compose ( [
219
+ withMultiEntityRecordEdits ,
220
+
199
221
// Limit to matching action type so we don't attempt to replace action on
200
222
// an unhandled action.
201
223
ifMatchingAction (
@@ -411,8 +433,9 @@ export const entities = ( state = {}, action ) => {
411
433
/**
412
434
* @typedef {Object } UndoStateMeta
413
435
*
414
- * @property {number } offset Where in the undo stack we are.
415
- * @property {Object } [flattenedUndo] Flattened form of undo stack.
436
+ * @property {number } list The undo stack.
437
+ * @property {number } offset Where in the undo stack we are.
438
+ * @property {Object } cache Cache of unpersisted transient edits.
416
439
*/
417
440
418
441
/** @typedef {Array<Object> & UndoStateMeta } UndoState */
@@ -422,10 +445,7 @@ export const entities = ( state = {}, action ) => {
422
445
*
423
446
* @todo Given how we use this we might want to make a custom class for it.
424
447
*/
425
- const UNDO_INITIAL_STATE = Object . assign ( [ ] , { offset : 0 } ) ;
426
-
427
- /** @type {Object } */
428
- let lastEditAction ;
448
+ const UNDO_INITIAL_STATE = { list : [ ] , offset : 0 } ;
429
449
430
450
/**
431
451
* Reducer keeping track of entity edit undo history.
@@ -436,107 +456,103 @@ let lastEditAction;
436
456
* @return {UndoState } Updated state.
437
457
*/
438
458
export function undo ( state = UNDO_INITIAL_STATE , action ) {
459
+ const omitPendingRedos = ( currentState ) => {
460
+ return {
461
+ ...currentState ,
462
+ list : currentState . list . slice (
463
+ 0 ,
464
+ currentState . offset || undefined
465
+ ) ,
466
+ offset : 0 ,
467
+ } ;
468
+ } ;
469
+
470
+ const appendCachedEditsToLastUndo = ( currentState ) => {
471
+ if ( ! currentState . cache ) {
472
+ return currentState ;
473
+ }
474
+
475
+ let nextState = {
476
+ ...currentState ,
477
+ list : [ ...currentState . list ] ,
478
+ } ;
479
+ nextState = omitPendingRedos ( nextState ) ;
480
+ let previousUndoState = nextState . list . pop ( ) ;
481
+ currentState . cache . forEach ( ( edit ) => {
482
+ previousUndoState = appendEditsToStack ( previousUndoState , edit ) ;
483
+ } ) ;
484
+ nextState . list . push ( previousUndoState ) ;
485
+
486
+ return {
487
+ ...nextState ,
488
+ cache : undefined ,
489
+ } ;
490
+ } ;
491
+
492
+ const appendEditsToStack = (
493
+ stack = [ ] ,
494
+ { kind, name, recordId, edits }
495
+ ) => {
496
+ const existingEditIndex = stack ?. findIndex (
497
+ ( { kind : k , name : n , recordId : r } ) => {
498
+ return k === kind && n === name && r === recordId ;
499
+ }
500
+ ) ;
501
+
502
+ const nextStack = stack . filter (
503
+ ( _ , index ) => index !== existingEditIndex
504
+ ) ;
505
+ nextStack . push ( {
506
+ kind,
507
+ name,
508
+ recordId,
509
+ edits : {
510
+ ...( existingEditIndex !== - 1
511
+ ? stack [ existingEditIndex ] . edits
512
+ : { } ) ,
513
+ ...edits ,
514
+ } ,
515
+ } ) ;
516
+ return nextStack ;
517
+ } ;
518
+
439
519
switch ( action . type ) {
440
- case 'EDIT_ENTITY_RECORD' :
441
520
case 'CREATE_UNDO_LEVEL' :
442
- let isCreateUndoLevel = action . type === 'CREATE_UNDO_LEVEL' ;
443
- const isUndoOrRedo =
444
- ! isCreateUndoLevel &&
445
- ( action . meta . isUndo || action . meta . isRedo ) ;
446
- if ( isCreateUndoLevel ) {
447
- action = lastEditAction ;
448
- } else if ( ! isUndoOrRedo ) {
449
- // Don't lose the last edit cache if the new one only has transient edits.
450
- // Transient edits don't create new levels so updating the cache would make
451
- // us skip an edit later when creating levels explicitly.
452
- if (
453
- Object . keys ( action . edits ) . some (
454
- ( key ) => ! action . transientEdits [ key ]
455
- )
456
- ) {
457
- lastEditAction = action ;
458
- } else {
459
- lastEditAction = {
460
- ...action ,
461
- edits : {
462
- ...( lastEditAction && lastEditAction . edits ) ,
463
- ...action . edits ,
464
- } ,
465
- } ;
466
- }
467
- }
521
+ return appendCachedEditsToLastUndo ( state ) ;
468
522
469
- /** @type {UndoState } */
470
- let nextState ;
471
-
472
- if ( isUndoOrRedo ) {
473
- // @ts -ignore we might consider using Object.assign({}, state)
474
- nextState = [ ...state ] ;
475
- nextState . offset =
476
- state . offset + ( action . meta . isUndo ? - 1 : 1 ) ;
477
-
478
- if ( state . flattenedUndo ) {
479
- // The first undo in a sequence of undos might happen while we have
480
- // flattened undos in state. If this is the case, we want execution
481
- // to continue as if we were creating an explicit undo level. This
482
- // will result in an extra undo level being appended with the flattened
483
- // undo values.
484
- // We also have to take into account if the `lastEditAction` had opted out
485
- // of being tracked in undo history, like the action that persists the latest
486
- // content right before saving. In that case we have to update the `lastEditAction`
487
- // to avoid returning early before applying the existing flattened undos.
488
- isCreateUndoLevel = true ;
489
- if ( ! lastEditAction . meta . undo ) {
490
- lastEditAction . meta . undo = {
491
- edits : { } ,
492
- } ;
493
- }
494
- action = lastEditAction ;
495
- } else {
496
- return nextState ;
497
- }
498
- }
523
+ case 'UNDO' :
524
+ case 'REDO' : {
525
+ const nextState = appendCachedEditsToLastUndo ( state ) ;
526
+ return {
527
+ ...nextState ,
528
+ offset : state . offset + ( action . type === 'UNDO' ? - 1 : 1 ) ,
529
+ } ;
530
+ }
499
531
500
- if ( ! action . meta . undo ) {
501
- return state ;
502
- }
532
+ case 'EDIT_ENTITY_RECORD' :
533
+ const isCachedChange = Object . keys ( action . edits ) . every (
534
+ ( key ) => action . transientEdits [ key ]
535
+ ) ;
503
536
504
- // Transient edits don't create an undo level, but are
505
- // reachable in the next meaningful edit to which they
506
- // are merged. They are defined in the entity's config.
507
- if (
508
- ! isCreateUndoLevel &&
509
- ! Object . keys ( action . edits ) . some (
510
- ( key ) => ! action . transientEdits [ key ]
511
- )
512
- ) {
513
- // @ts -ignore we might consider using Object.assign({}, state)
514
- nextState = [ ...state ] ;
515
- nextState . flattenedUndo = {
516
- ...state . flattenedUndo ,
517
- ...action . edits ,
537
+ if ( isCachedChange ) {
538
+ return {
539
+ ...state ,
540
+ cache : appendEditsToStack ( state . cache , action ) ,
518
541
} ;
519
- nextState . offset = state . offset ;
520
- return nextState ;
521
542
}
522
543
523
- // Clear potential redos, because this only supports linear history.
524
- nextState =
525
- // @ts -ignore this needs additional cleanup, probably involving code-level changes
526
- nextState || state . slice ( 0 , state . offset || undefined ) ;
527
- nextState . offset = nextState . offset || 0 ;
528
- nextState . pop ( ) ;
529
- if ( ! isCreateUndoLevel ) {
530
- nextState . push ( {
531
- kind : action . meta . undo . kind ,
532
- name : action . meta . undo . name ,
533
- recordId : action . meta . undo . recordId ,
534
- edits : {
535
- ...state . flattenedUndo ,
536
- ...action . meta . undo . edits ,
537
- } ,
538
- } ) ;
539
- }
544
+ let nextState = omitPendingRedos ( state ) ;
545
+ nextState = appendCachedEditsToLastUndo ( nextState ) ;
546
+ nextState = { ...nextState , list : [ ...nextState . list ] } ;
547
+ const previousUndoState = nextState . list . pop ( ) ;
548
+ nextState . list . push (
549
+ appendEditsToStack ( previousUndoState , {
550
+ kind : action . kind ,
551
+ name : action . name ,
552
+ recordId : action . recordId ,
553
+ edits : action . meta . undo . edits ,
554
+ } )
555
+ ) ;
540
556
// When an edit is a function it's an optimization to avoid running some expensive operation.
541
557
// We can't rely on the function references being the same so we opt out of comparing them here.
542
558
const comparisonUndoEdits = Object . values (
@@ -546,15 +562,16 @@ export function undo( state = UNDO_INITIAL_STATE, action ) {
546
562
( edit ) => typeof edit !== 'function'
547
563
) ;
548
564
if ( ! isShallowEqual ( comparisonUndoEdits , comparisonEdits ) ) {
549
- nextState . push ( {
550
- kind : action . kind ,
551
- name : action . name ,
552
- recordId : action . recordId ,
553
- edits : isCreateUndoLevel
554
- ? { ... state . flattenedUndo , ... action . edits }
555
- : action . edits ,
556
- } ) ;
565
+ nextState . list . push ( [
566
+ {
567
+ kind : action . kind ,
568
+ name : action . name ,
569
+ recordId : action . recordId ,
570
+ edits : action . edits ,
571
+ } ,
572
+ ] ) ;
557
573
}
574
+
558
575
return nextState ;
559
576
}
560
577
0 commit comments