@wordpress/core-data 6.11.0 → 6.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/README.md +4 -0
  3. package/build/actions.js +252 -276
  4. package/build/actions.js.map +1 -1
  5. package/build/batch/create-batch.js +8 -16
  6. package/build/batch/create-batch.js.map +1 -1
  7. package/build/batch/default-processor.js +1 -1
  8. package/build/batch/default-processor.js.map +1 -1
  9. package/build/entities.js +16 -25
  10. package/build/entities.js.map +1 -1
  11. package/build/entity-provider.js +12 -17
  12. package/build/entity-provider.js.map +1 -1
  13. package/build/fetch/__experimental-fetch-link-suggestions.js +2 -6
  14. package/build/fetch/__experimental-fetch-link-suggestions.js.map +1 -1
  15. package/build/fetch/__experimental-fetch-url-data.js +1 -2
  16. package/build/fetch/__experimental-fetch-url-data.js.map +1 -1
  17. package/build/hooks/use-entity-record.js +7 -11
  18. package/build/hooks/use-entity-record.js.map +1 -1
  19. package/build/hooks/use-entity-records.js +3 -5
  20. package/build/hooks/use-entity-records.js.map +1 -1
  21. package/build/hooks/use-query-select.js +1 -6
  22. package/build/hooks/use-query-select.js.map +1 -1
  23. package/build/index.js +1 -7
  24. package/build/index.js.map +1 -1
  25. package/build/locks/actions.js +3 -4
  26. package/build/locks/actions.js.map +1 -1
  27. package/build/locks/reducer.js +1 -4
  28. package/build/locks/reducer.js.map +1 -1
  29. package/build/locks/selectors.js +3 -4
  30. package/build/locks/selectors.js.map +1 -1
  31. package/build/locks/utils.js +3 -5
  32. package/build/locks/utils.js.map +1 -1
  33. package/build/private-selectors.js +37 -0
  34. package/build/private-selectors.js.map +1 -0
  35. package/build/queried-data/actions.js +2 -5
  36. package/build/queried-data/actions.js.map +1 -1
  37. package/build/queried-data/reducer.js +17 -47
  38. package/build/queried-data/reducer.js.map +1 -1
  39. package/build/queried-data/selectors.js +4 -11
  40. package/build/queried-data/selectors.js.map +1 -1
  41. package/build/reducer.js +167 -194
  42. package/build/reducer.js.map +1 -1
  43. package/build/resolvers.js +175 -220
  44. package/build/resolvers.js.map +1 -1
  45. package/build/selectors.js +53 -61
  46. package/build/selectors.js.map +1 -1
  47. package/build/utils/forward-resolver.js +4 -11
  48. package/build/utils/forward-resolver.js.map +1 -1
  49. package/build/utils/on-sub-key.js +1 -3
  50. package/build/utils/on-sub-key.js.map +1 -1
  51. package/build-module/actions.js +251 -276
  52. package/build-module/actions.js.map +1 -1
  53. package/build-module/batch/create-batch.js +8 -16
  54. package/build-module/batch/create-batch.js.map +1 -1
  55. package/build-module/batch/default-processor.js +1 -1
  56. package/build-module/batch/default-processor.js.map +1 -1
  57. package/build-module/entities.js +16 -25
  58. package/build-module/entities.js.map +1 -1
  59. package/build-module/entity-provider.js +12 -17
  60. package/build-module/entity-provider.js.map +1 -1
  61. package/build-module/fetch/__experimental-fetch-link-suggestions.js +2 -6
  62. package/build-module/fetch/__experimental-fetch-link-suggestions.js.map +1 -1
  63. package/build-module/fetch/__experimental-fetch-url-data.js +1 -2
  64. package/build-module/fetch/__experimental-fetch-url-data.js.map +1 -1
  65. package/build-module/hooks/use-entity-record.js +7 -11
  66. package/build-module/hooks/use-entity-record.js.map +1 -1
  67. package/build-module/hooks/use-entity-records.js +3 -5
  68. package/build-module/hooks/use-entity-records.js.map +1 -1
  69. package/build-module/hooks/use-query-select.js +1 -6
  70. package/build-module/hooks/use-query-select.js.map +1 -1
  71. package/build-module/index.js +1 -7
  72. package/build-module/index.js.map +1 -1
  73. package/build-module/locks/actions.js +3 -4
  74. package/build-module/locks/actions.js.map +1 -1
  75. package/build-module/locks/reducer.js +1 -4
  76. package/build-module/locks/reducer.js.map +1 -1
  77. package/build-module/locks/selectors.js +3 -4
  78. package/build-module/locks/selectors.js.map +1 -1
  79. package/build-module/locks/utils.js +3 -5
  80. package/build-module/locks/utils.js.map +1 -1
  81. package/build-module/private-selectors.js +28 -0
  82. package/build-module/private-selectors.js.map +1 -0
  83. package/build-module/queried-data/actions.js +2 -5
  84. package/build-module/queried-data/actions.js.map +1 -1
  85. package/build-module/queried-data/reducer.js +17 -47
  86. package/build-module/queried-data/reducer.js.map +1 -1
  87. package/build-module/queried-data/selectors.js +4 -11
  88. package/build-module/queried-data/selectors.js.map +1 -1
  89. package/build-module/reducer.js +168 -194
  90. package/build-module/reducer.js.map +1 -1
  91. package/build-module/resolvers.js +175 -220
  92. package/build-module/resolvers.js.map +1 -1
  93. package/build-module/selectors.js +55 -61
  94. package/build-module/selectors.js.map +1 -1
  95. package/build-module/utils/forward-resolver.js +4 -11
  96. package/build-module/utils/forward-resolver.js.map +1 -1
  97. package/build-module/utils/on-sub-key.js +1 -3
  98. package/build-module/utils/on-sub-key.js.map +1 -1
  99. package/build-types/actions.d.ts.map +1 -1
  100. package/build-types/private-selectors.d.ts +25 -0
  101. package/build-types/private-selectors.d.ts.map +1 -0
  102. package/build-types/reducer.d.ts +6 -2
  103. package/build-types/reducer.d.ts.map +1 -1
  104. package/build-types/selectors.d.ts +16 -4
  105. package/build-types/selectors.d.ts.map +1 -1
  106. package/package.json +12 -12
  107. package/src/actions.js +9 -8
  108. package/src/index.js +0 -1
  109. package/src/private-selectors.ts +30 -0
  110. package/src/reducer.js +130 -104
  111. package/src/selectors.ts +33 -13
  112. package/src/test/reducer.js +89 -54
  113. package/src/test/selectors.js +8 -8
  114. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import type { State, UndoEdit } from './selectors';
5
+
6
+ type Optional< T > = T | undefined;
7
+
8
+ /**
9
+ * Returns the previous edit from the current undo offset
10
+ * for the entity records edits history, if any.
11
+ *
12
+ * @param state State tree.
13
+ *
14
+ * @return The edit.
15
+ */
16
+ export function getUndoEdits( state: State ): Optional< UndoEdit[] > {
17
+ return state.undo.list[ state.undo.list.length - 1 + state.undo.offset ];
18
+ }
19
+
20
+ /**
21
+ * Returns the next edit from the current undo offset
22
+ * for the entity records edits history, if any.
23
+ *
24
+ * @param state State tree.
25
+ *
26
+ * @return The edit.
27
+ */
28
+ export function getRedoEdits( state: State ): Optional< UndoEdit[] > {
29
+ return state.undo.list[ state.undo.list.length + state.undo.offset ];
30
+ }
package/src/reducer.js CHANGED
@@ -183,6 +183,30 @@ 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(
192
+ ( { kind, name, recordId, property, from, to } ) => {
193
+ newState = reducer( newState, {
194
+ type: 'EDIT_ENTITY_RECORD',
195
+ kind,
196
+ name,
197
+ recordId,
198
+ edits: {
199
+ [ property ]: action.type === 'UNDO' ? from : to,
200
+ },
201
+ } );
202
+ }
203
+ );
204
+ return newState;
205
+ }
206
+
207
+ return reducer( state, action );
208
+ };
209
+
186
210
  /**
187
211
  * Higher Order Reducer for a given entity config. It supports:
188
212
  *
@@ -196,6 +220,8 @@ export function themeGlobalStyleVariations( state = {}, action ) {
196
220
  */
197
221
  function entity( entityConfig ) {
198
222
  return compose( [
223
+ withMultiEntityRecordEdits,
224
+
199
225
  // Limit to matching action type so we don't attempt to replace action on
200
226
  // an unhandled action.
201
227
  ifMatchingAction(
@@ -411,8 +437,9 @@ export const entities = ( state = {}, action ) => {
411
437
  /**
412
438
  * @typedef {Object} UndoStateMeta
413
439
  *
414
- * @property {number} offset Where in the undo stack we are.
415
- * @property {Object} [flattenedUndo] Flattened form of undo stack.
440
+ * @property {number} list The undo stack.
441
+ * @property {number} offset Where in the undo stack we are.
442
+ * @property {Object} cache Cache of unpersisted transient edits.
416
443
  */
417
444
 
418
445
  /** @typedef {Array<Object> & UndoStateMeta} UndoState */
@@ -422,10 +449,7 @@ export const entities = ( state = {}, action ) => {
422
449
  *
423
450
  * @todo Given how we use this we might want to make a custom class for it.
424
451
  */
425
- const UNDO_INITIAL_STATE = Object.assign( [], { offset: 0 } );
426
-
427
- /** @type {Object} */
428
- let lastEditAction;
452
+ const UNDO_INITIAL_STATE = { list: [], offset: 0 };
429
453
 
430
454
  /**
431
455
  * Reducer keeping track of entity edit undo history.
@@ -436,107 +460,114 @@ let lastEditAction;
436
460
  * @return {UndoState} Updated state.
437
461
  */
438
462
  export function undo( state = UNDO_INITIAL_STATE, action ) {
463
+ const omitPendingRedos = ( currentState ) => {
464
+ return {
465
+ ...currentState,
466
+ list: currentState.list.slice(
467
+ 0,
468
+ currentState.offset || undefined
469
+ ),
470
+ offset: 0,
471
+ };
472
+ };
473
+
474
+ const appendCachedEditsToLastUndo = ( currentState ) => {
475
+ if ( ! currentState.cache ) {
476
+ return currentState;
477
+ }
478
+
479
+ let nextState = {
480
+ ...currentState,
481
+ list: [ ...currentState.list ],
482
+ };
483
+ nextState = omitPendingRedos( nextState );
484
+ const previousUndoState = nextState.list.pop();
485
+ const updatedUndoState = currentState.cache.reduce(
486
+ appendEditToStack,
487
+ previousUndoState
488
+ );
489
+ nextState.list.push( updatedUndoState );
490
+
491
+ return {
492
+ ...nextState,
493
+ cache: undefined,
494
+ };
495
+ };
496
+
497
+ const appendEditToStack = (
498
+ stack = [],
499
+ { kind, name, recordId, property, from, to }
500
+ ) => {
501
+ const existingEditIndex = stack?.findIndex(
502
+ ( { kind: k, name: n, recordId: r, property: p } ) => {
503
+ return (
504
+ k === kind && n === name && r === recordId && p === property
505
+ );
506
+ }
507
+ );
508
+ const nextStack = [ ...stack ];
509
+ if ( existingEditIndex !== -1 ) {
510
+ // If the edit is already in the stack leave the initial "from" value.
511
+ nextStack[ existingEditIndex ] = {
512
+ ...nextStack[ existingEditIndex ],
513
+ to,
514
+ };
515
+ } else {
516
+ nextStack.push( {
517
+ kind,
518
+ name,
519
+ recordId,
520
+ property,
521
+ from,
522
+ to,
523
+ } );
524
+ }
525
+ return nextStack;
526
+ };
527
+
439
528
  switch ( action.type ) {
440
- case 'EDIT_ENTITY_RECORD':
441
529
  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
- }
530
+ return appendCachedEditsToLastUndo( state );
468
531
 
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
- }
532
+ case 'UNDO':
533
+ case 'REDO': {
534
+ const nextState = appendCachedEditsToLastUndo( state );
535
+ return {
536
+ ...nextState,
537
+ offset: state.offset + ( action.type === 'UNDO' ? -1 : 1 ),
538
+ };
539
+ }
499
540
 
541
+ case 'EDIT_ENTITY_RECORD': {
500
542
  if ( ! action.meta.undo ) {
501
543
  return state;
502
544
  }
503
545
 
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,
546
+ const isCachedChange = Object.keys( action.edits ).every(
547
+ ( key ) => action.transientEdits[ key ]
548
+ );
549
+
550
+ const edits = Object.keys( action.edits ).map( ( key ) => {
551
+ return {
552
+ kind: action.kind,
553
+ name: action.name,
554
+ recordId: action.recordId,
555
+ property: key,
556
+ from: action.meta.undo.edits[ key ],
557
+ to: action.edits[ key ],
518
558
  };
519
- nextState.offset = state.offset;
520
- return nextState;
521
- }
559
+ } );
522
560
 
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
- } );
561
+ if ( isCachedChange ) {
562
+ return {
563
+ ...state,
564
+ cache: edits.reduce( appendEditToStack, state.cache ),
565
+ };
539
566
  }
567
+
568
+ let nextState = omitPendingRedos( state );
569
+ nextState = appendCachedEditsToLastUndo( nextState );
570
+ nextState = { ...nextState, list: [ ...nextState.list ] };
540
571
  // When an edit is a function it's an optimization to avoid running some expensive operation.
541
572
  // We can't rely on the function references being the same so we opt out of comparing them here.
542
573
  const comparisonUndoEdits = Object.values(
@@ -546,16 +577,11 @@ export function undo( state = UNDO_INITIAL_STATE, action ) {
546
577
  ( edit ) => typeof edit !== 'function'
547
578
  );
548
579
  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
- } );
580
+ nextState.list.push( edits );
557
581
  }
582
+
558
583
  return nextState;
584
+ }
559
585
  }
560
586
 
561
587
  return state;
package/src/selectors.ts CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  setNestedValue,
23
23
  } from './utils';
24
24
  import type * as ET from './entity-types';
25
+ import { getUndoEdits, getRedoEdits } from './private-selectors';
25
26
 
26
27
  // This is an incomplete, high-level approximation of the State type.
27
28
  // It makes the selectors slightly more safe, but is intended to evolve
@@ -73,9 +74,18 @@ interface EntityConfig {
73
74
  kind: string;
74
75
  }
75
76
 
76
- interface UndoState extends Array< Object > {
77
- flattenedUndo: unknown;
77
+ export interface UndoEdit {
78
+ name: string;
79
+ kind: string;
80
+ recordId: string;
81
+ from: any;
82
+ to: any;
83
+ }
84
+
85
+ interface UndoState {
86
+ list: Array< UndoEdit[] >;
78
87
  offset: number;
88
+ cache: UndoEdit[];
79
89
  }
80
90
 
81
91
  interface UserState {
@@ -884,24 +894,38 @@ function getCurrentUndoOffset( state: State ): number {
884
894
  * Returns the previous edit from the current undo offset
885
895
  * for the entity records edits history, if any.
886
896
  *
887
- * @param state State tree.
897
+ * @deprecated since 6.3
898
+ *
899
+ * @param state State tree.
888
900
  *
889
901
  * @return The edit.
890
902
  */
891
903
  export function getUndoEdit( state: State ): Optional< any > {
892
- return state.undo[ state.undo.length - 2 + getCurrentUndoOffset( state ) ];
904
+ deprecated( "select( 'core' ).getUndoEdit()", {
905
+ since: '6.3',
906
+ } );
907
+ return state.undo.list[
908
+ state.undo.list.length - 2 + getCurrentUndoOffset( state )
909
+ ]?.[ 0 ];
893
910
  }
894
911
 
895
912
  /**
896
913
  * Returns the next edit from the current undo offset
897
914
  * for the entity records edits history, if any.
898
915
  *
899
- * @param state State tree.
916
+ * @deprecated since 6.3
917
+ *
918
+ * @param state State tree.
900
919
  *
901
920
  * @return The edit.
902
921
  */
903
922
  export function getRedoEdit( state: State ): Optional< any > {
904
- return state.undo[ state.undo.length + getCurrentUndoOffset( state ) ];
923
+ deprecated( "select( 'core' ).getRedoEdit()", {
924
+ since: '6.3',
925
+ } );
926
+ return state.undo.list[
927
+ state.undo.list.length + getCurrentUndoOffset( state )
928
+ ]?.[ 0 ];
905
929
  }
906
930
 
907
931
  /**
@@ -913,7 +937,7 @@ export function getRedoEdit( state: State ): Optional< any > {
913
937
  * @return Whether there is a previous edit or not.
914
938
  */
915
939
  export function hasUndo( state: State ): boolean {
916
- return Boolean( getUndoEdit( state ) );
940
+ return Boolean( getUndoEdits( state ) );
917
941
  }
918
942
 
919
943
  /**
@@ -925,7 +949,7 @@ export function hasUndo( state: State ): boolean {
925
949
  * @return Whether there is a next edit or not.
926
950
  */
927
951
  export function hasRedo( state: State ): boolean {
928
- return Boolean( getRedoEdit( state ) );
952
+ return Boolean( getRedoEdits( state ) );
929
953
  }
930
954
 
931
955
  /**
@@ -1142,11 +1166,7 @@ export const hasFetchedAutosaves = createRegistrySelector(
1142
1166
  export const getReferenceByDistinctEdits = createSelector(
1143
1167
  // This unused state argument is listed here for the documentation generating tool (docgen).
1144
1168
  ( state: State ) => [],
1145
- ( state: State ) => [
1146
- state.undo.length,
1147
- state.undo.offset,
1148
- state.undo.flattenedUndo,
1149
- ]
1169
+ ( state: State ) => [ state.undo.list.length, state.undo.offset ]
1150
1170
  );
1151
1171
 
1152
1172
  /**
@@ -143,28 +143,34 @@ describe( 'entities', () => {
143
143
  } );
144
144
 
145
145
  describe( 'undo', () => {
146
- let lastEdits;
146
+ let lastValues;
147
147
  let undoState;
148
148
  let expectedUndoState;
149
- const createEditActionPart = ( edits ) => ( {
149
+
150
+ const createExpectedDiff = ( property, { from, to } ) => ( {
150
151
  kind: 'someKind',
151
152
  name: 'someName',
152
153
  recordId: 'someRecordId',
153
- edits,
154
+ property,
155
+ from,
156
+ to,
154
157
  } );
155
158
  const createNextEditAction = ( edits, transientEdits = {} ) => {
156
159
  let action = {
157
- ...createEditActionPart( edits ),
160
+ kind: 'someKind',
161
+ name: 'someName',
162
+ recordId: 'someRecordId',
163
+ edits,
158
164
  transientEdits,
159
165
  };
160
166
  action = {
161
167
  type: 'EDIT_ENTITY_RECORD',
162
168
  ...action,
163
169
  meta: {
164
- undo: { ...action, edits: lastEdits },
170
+ undo: { edits: lastValues },
165
171
  },
166
172
  };
167
- lastEdits = { ...lastEdits, ...edits };
173
+ lastValues = { ...lastValues, ...edits };
168
174
  return action;
169
175
  };
170
176
  const createNextUndoState = ( ...args ) => {
@@ -172,17 +178,17 @@ describe( 'undo', () => {
172
178
  if ( args[ 0 ] === 'isUndo' || args[ 0 ] === 'isRedo' ) {
173
179
  // We need to "apply" the undo level here and build
174
180
  // the action to move the offset.
175
- lastEdits =
176
- undoState[
177
- undoState.length +
178
- undoState.offset -
179
- ( args[ 0 ] === 'isUndo' ? 2 : 0 )
180
- ].edits;
181
+ const lastEdits =
182
+ undoState.list[
183
+ undoState.list.length -
184
+ ( args[ 0 ] === 'isUndo' ? 1 : 0 ) +
185
+ undoState.offset
186
+ ];
187
+ lastEdits.forEach( ( { property, from, to } ) => {
188
+ lastValues[ property ] = args[ 0 ] === 'isUndo' ? from : to;
189
+ } );
181
190
  action = {
182
- type: 'EDIT_ENTITY_RECORD',
183
- meta: {
184
- [ args[ 0 ] ]: true,
185
- },
191
+ type: args[ 0 ] === 'isUndo' ? 'UNDO' : 'REDO',
186
192
  };
187
193
  } else if ( args[ 0 ] === 'isCreate' ) {
188
194
  action = { type: 'CREATE_UNDO_LEVEL' };
@@ -192,10 +198,9 @@ describe( 'undo', () => {
192
198
  return deepFreeze( undo( undoState, action ) );
193
199
  };
194
200
  beforeEach( () => {
195
- lastEdits = {};
201
+ lastValues = {};
196
202
  undoState = undefined;
197
- expectedUndoState = [];
198
- expectedUndoState.offset = 0;
203
+ expectedUndoState = { list: [], offset: 0 };
199
204
  } );
200
205
 
201
206
  it( 'initializes', () => {
@@ -208,19 +213,41 @@ describe( 'undo', () => {
208
213
  // Check that the first edit creates an undo level for the current state and
209
214
  // one for the new one.
210
215
  undoState = createNextUndoState( { value: 1 } );
211
- expectedUndoState.push(
212
- createEditActionPart( {} ),
213
- createEditActionPart( { value: 1 } )
214
- );
216
+ expectedUndoState.list.push( [
217
+ createExpectedDiff( 'value', { from: undefined, to: 1 } ),
218
+ ] );
215
219
  expect( undoState ).toEqual( expectedUndoState );
216
220
 
217
221
  // Check that the second and third edits just create an undo level for
218
222
  // themselves.
219
223
  undoState = createNextUndoState( { value: 2 } );
220
- expectedUndoState.push( createEditActionPart( { value: 2 } ) );
224
+ expectedUndoState.list.push( [
225
+ createExpectedDiff( 'value', { from: 1, to: 2 } ),
226
+ ] );
221
227
  expect( undoState ).toEqual( expectedUndoState );
222
228
  undoState = createNextUndoState( { value: 3 } );
223
- expectedUndoState.push( createEditActionPart( { value: 3 } ) );
229
+ expectedUndoState.list.push( [
230
+ createExpectedDiff( 'value', { from: 2, to: 3 } ),
231
+ ] );
232
+ expect( undoState ).toEqual( expectedUndoState );
233
+ } );
234
+
235
+ it( 'stacks multi-property undo levels', () => {
236
+ undoState = createNextUndoState();
237
+
238
+ undoState = createNextUndoState( { value: 1 } );
239
+ undoState = createNextUndoState( { value2: 2 } );
240
+ expectedUndoState.list.push(
241
+ [ createExpectedDiff( 'value', { from: undefined, to: 1 } ) ],
242
+ [ createExpectedDiff( 'value2', { from: undefined, to: 2 } ) ]
243
+ );
244
+ expect( undoState ).toEqual( expectedUndoState );
245
+
246
+ // Check that that creating another undo level merges the "edits"
247
+ undoState = createNextUndoState( { value: 2 } );
248
+ expectedUndoState.list.push( [
249
+ createExpectedDiff( 'value', { from: 1, to: 2 } ),
250
+ ] );
224
251
  expect( undoState ).toEqual( expectedUndoState );
225
252
  } );
226
253
 
@@ -229,11 +256,10 @@ describe( 'undo', () => {
229
256
  undoState = createNextUndoState( { value: 1 } );
230
257
  undoState = createNextUndoState( { value: 2 } );
231
258
  undoState = createNextUndoState( { value: 3 } );
232
- expectedUndoState.push(
233
- createEditActionPart( {} ),
234
- createEditActionPart( { value: 1 } ),
235
- createEditActionPart( { value: 2 } ),
236
- createEditActionPart( { value: 3 } )
259
+ expectedUndoState.list.push(
260
+ [ createExpectedDiff( 'value', { from: undefined, to: 1 } ) ],
261
+ [ createExpectedDiff( 'value', { from: 1, to: 2 } ) ],
262
+ [ createExpectedDiff( 'value', { from: 2, to: 3 } ) ]
237
263
  );
238
264
  expect( undoState ).toEqual( expectedUndoState );
239
265
 
@@ -255,17 +281,22 @@ describe( 'undo', () => {
255
281
  // Check that another edit will go on top when there
256
282
  // is no undo level offset.
257
283
  undoState = createNextUndoState( { value: 4 } );
258
- expectedUndoState.push( createEditActionPart( { value: 4 } ) );
284
+ expectedUndoState.list.push( [
285
+ createExpectedDiff( 'value', { from: 3, to: 4 } ),
286
+ ] );
259
287
  expect( undoState ).toEqual( expectedUndoState );
260
288
 
261
289
  // Check that undoing and editing will slice of
262
290
  // all the levels after the current one.
263
291
  undoState = createNextUndoState( 'isUndo' );
264
292
  undoState = createNextUndoState( 'isUndo' );
293
+
265
294
  undoState = createNextUndoState( { value: 5 } );
266
- expectedUndoState.pop();
267
- expectedUndoState.pop();
268
- expectedUndoState.push( createEditActionPart( { value: 5 } ) );
295
+ expectedUndoState.list.pop();
296
+ expectedUndoState.list.pop();
297
+ expectedUndoState.list.push( [
298
+ createExpectedDiff( 'value', { from: 2, to: 5 } ),
299
+ ] );
269
300
  expect( undoState ).toEqual( expectedUndoState );
270
301
  } );
271
302
 
@@ -277,10 +308,15 @@ describe( 'undo', () => {
277
308
  { transientValue: true }
278
309
  );
279
310
  undoState = createNextUndoState( { value: 3 } );
280
- expectedUndoState.push(
281
- createEditActionPart( {} ),
282
- createEditActionPart( { value: 1, transientValue: 2 } ),
283
- createEditActionPart( { value: 3 } )
311
+ expectedUndoState.list.push(
312
+ [
313
+ createExpectedDiff( 'value', { from: undefined, to: 1 } ),
314
+ createExpectedDiff( 'transientValue', {
315
+ from: undefined,
316
+ to: 2,
317
+ } ),
318
+ ],
319
+ [ createExpectedDiff( 'value', { from: 1, to: 3 } ) ]
284
320
  );
285
321
  expect( undoState ).toEqual( expectedUndoState );
286
322
  } );
@@ -292,10 +328,9 @@ describe( 'undo', () => {
292
328
  // transient edits.
293
329
  undoState = createNextUndoState( { value: 1 } );
294
330
  undoState = createNextUndoState( 'isCreate' );
295
- expectedUndoState.push(
296
- createEditActionPart( {} ),
297
- createEditActionPart( { value: 1 } )
298
- );
331
+ expectedUndoState.list.push( [
332
+ createExpectedDiff( 'value', { from: undefined, to: 1 } ),
333
+ ] );
299
334
  expect( undoState ).toEqual( expectedUndoState );
300
335
 
301
336
  // Check that transient edits are merged into the last
@@ -305,18 +340,19 @@ describe( 'undo', () => {
305
340
  { transientValue: true }
306
341
  );
307
342
  undoState = createNextUndoState( 'isCreate' );
308
- expectedUndoState[
309
- expectedUndoState.length - 1
310
- ].edits.transientValue = 2;
343
+ expectedUndoState.list[ expectedUndoState.list.length - 1 ].push(
344
+ createExpectedDiff( 'transientValue', { from: undefined, to: 2 } )
345
+ );
311
346
  expect( undoState ).toEqual( expectedUndoState );
312
347
 
313
- // Check that undo levels are created with the latest action,
314
- // even if undone.
348
+ // Check that create after undo does nothing.
315
349
  undoState = createNextUndoState( { value: 3 } );
316
350
  undoState = createNextUndoState( 'isUndo' );
317
351
  undoState = createNextUndoState( 'isCreate' );
318
- expectedUndoState.pop();
319
- expectedUndoState.push( createEditActionPart( { value: 3 } ) );
352
+ expectedUndoState.list.push( [
353
+ createExpectedDiff( 'value', { from: 1, to: 3 } ),
354
+ ] );
355
+ expectedUndoState.offset = -1;
320
356
  expect( undoState ).toEqual( expectedUndoState );
321
357
  } );
322
358
 
@@ -328,10 +364,10 @@ describe( 'undo', () => {
328
364
  { transientValue: true }
329
365
  );
330
366
  undoState = createNextUndoState( 'isUndo' );
331
- expectedUndoState.push(
332
- createEditActionPart( {} ),
333
- createEditActionPart( { value: 1, transientValue: 2 } )
334
- );
367
+ expectedUndoState.list.push( [
368
+ createExpectedDiff( 'value', { from: undefined, to: 1 } ),
369
+ createExpectedDiff( 'transientValue', { from: undefined, to: 2 } ),
370
+ ] );
335
371
  expectedUndoState.offset--;
336
372
  expect( undoState ).toEqual( expectedUndoState );
337
373
  } );
@@ -341,7 +377,6 @@ describe( 'undo', () => {
341
377
  undoState = createNextUndoState();
342
378
  undoState = createNextUndoState( { value } );
343
379
  undoState = createNextUndoState( { value: () => {} } );
344
- expectedUndoState.push( createEditActionPart( { value } ) );
345
380
  expect( undoState ).toEqual( expectedUndoState );
346
381
  } );
347
382
  } );