@wordpress/core-data 6.11.0 → 6.12.1

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 (131) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/README.md +5 -1
  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 +74 -23
  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 +11 -13
  18. package/build/hooks/use-entity-record.js.map +1 -1
  19. package/build/hooks/use-entity-records.js +4 -6
  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-apis.js +19 -0
  34. package/build/private-apis.js.map +1 -0
  35. package/build/private-selectors.js +37 -0
  36. package/build/private-selectors.js.map +1 -0
  37. package/build/queried-data/actions.js +2 -5
  38. package/build/queried-data/actions.js.map +1 -1
  39. package/build/queried-data/reducer.js +17 -47
  40. package/build/queried-data/reducer.js.map +1 -1
  41. package/build/queried-data/selectors.js +4 -11
  42. package/build/queried-data/selectors.js.map +1 -1
  43. package/build/reducer.js +167 -194
  44. package/build/reducer.js.map +1 -1
  45. package/build/resolvers.js +175 -220
  46. package/build/resolvers.js.map +1 -1
  47. package/build/selectors.js +53 -61
  48. package/build/selectors.js.map +1 -1
  49. package/build/utils/forward-resolver.js +4 -11
  50. package/build/utils/forward-resolver.js.map +1 -1
  51. package/build/utils/on-sub-key.js +1 -3
  52. package/build/utils/on-sub-key.js.map +1 -1
  53. package/build-module/actions.js +251 -276
  54. package/build-module/actions.js.map +1 -1
  55. package/build-module/batch/create-batch.js +8 -16
  56. package/build-module/batch/create-batch.js.map +1 -1
  57. package/build-module/batch/default-processor.js +1 -1
  58. package/build-module/batch/default-processor.js.map +1 -1
  59. package/build-module/entities.js +16 -25
  60. package/build-module/entities.js.map +1 -1
  61. package/build-module/entity-provider.js +71 -22
  62. package/build-module/entity-provider.js.map +1 -1
  63. package/build-module/fetch/__experimental-fetch-link-suggestions.js +2 -6
  64. package/build-module/fetch/__experimental-fetch-link-suggestions.js.map +1 -1
  65. package/build-module/fetch/__experimental-fetch-url-data.js +1 -2
  66. package/build-module/fetch/__experimental-fetch-url-data.js.map +1 -1
  67. package/build-module/hooks/use-entity-record.js +11 -13
  68. package/build-module/hooks/use-entity-record.js.map +1 -1
  69. package/build-module/hooks/use-entity-records.js +4 -6
  70. package/build-module/hooks/use-entity-records.js.map +1 -1
  71. package/build-module/hooks/use-query-select.js +1 -6
  72. package/build-module/hooks/use-query-select.js.map +1 -1
  73. package/build-module/index.js +1 -7
  74. package/build-module/index.js.map +1 -1
  75. package/build-module/locks/actions.js +3 -4
  76. package/build-module/locks/actions.js.map +1 -1
  77. package/build-module/locks/reducer.js +1 -4
  78. package/build-module/locks/reducer.js.map +1 -1
  79. package/build-module/locks/selectors.js +3 -4
  80. package/build-module/locks/selectors.js.map +1 -1
  81. package/build-module/locks/utils.js +3 -5
  82. package/build-module/locks/utils.js.map +1 -1
  83. package/build-module/private-apis.js +9 -0
  84. package/build-module/private-apis.js.map +1 -0
  85. package/build-module/private-selectors.js +28 -0
  86. package/build-module/private-selectors.js.map +1 -0
  87. package/build-module/queried-data/actions.js +2 -5
  88. package/build-module/queried-data/actions.js.map +1 -1
  89. package/build-module/queried-data/reducer.js +17 -47
  90. package/build-module/queried-data/reducer.js.map +1 -1
  91. package/build-module/queried-data/selectors.js +4 -11
  92. package/build-module/queried-data/selectors.js.map +1 -1
  93. package/build-module/reducer.js +168 -194
  94. package/build-module/reducer.js.map +1 -1
  95. package/build-module/resolvers.js +175 -220
  96. package/build-module/resolvers.js.map +1 -1
  97. package/build-module/selectors.js +55 -61
  98. package/build-module/selectors.js.map +1 -1
  99. package/build-module/utils/forward-resolver.js +4 -11
  100. package/build-module/utils/forward-resolver.js.map +1 -1
  101. package/build-module/utils/on-sub-key.js +1 -3
  102. package/build-module/utils/on-sub-key.js.map +1 -1
  103. package/build-types/actions.d.ts.map +1 -1
  104. package/build-types/entity-provider.d.ts.map +1 -1
  105. package/build-types/entity-types/wp-template.d.ts +4 -0
  106. package/build-types/entity-types/wp-template.d.ts.map +1 -1
  107. package/build-types/hooks/use-entity-record.d.ts.map +1 -1
  108. package/build-types/hooks/use-entity-records.d.ts +1 -1
  109. package/build-types/private-apis.d.ts +3 -0
  110. package/build-types/private-apis.d.ts.map +1 -0
  111. package/build-types/private-selectors.d.ts +25 -0
  112. package/build-types/private-selectors.d.ts.map +1 -0
  113. package/build-types/reducer.d.ts +6 -2
  114. package/build-types/reducer.d.ts.map +1 -1
  115. package/build-types/selectors.d.ts +16 -4
  116. package/build-types/selectors.d.ts.map +1 -1
  117. package/package.json +14 -12
  118. package/src/actions.js +9 -8
  119. package/src/entity-provider.js +72 -5
  120. package/src/entity-types/wp-template.ts +4 -0
  121. package/src/hooks/use-entity-record.ts +4 -2
  122. package/src/hooks/use-entity-records.ts +1 -1
  123. package/src/index.js +0 -1
  124. package/src/private-apis.js +10 -0
  125. package/src/private-selectors.ts +30 -0
  126. package/src/reducer.js +130 -104
  127. package/src/selectors.ts +33 -13
  128. package/src/test/reducer.js +89 -54
  129. package/src/test/selectors.js +8 -8
  130. package/tsconfig.json +2 -0
  131. package/tsconfig.tsbuildinfo +1 -1
package/src/actions.js CHANGED
@@ -18,6 +18,7 @@ import { receiveItems, removeItems, receiveQueriedItems } from './queried-data';
18
18
  import { getOrLoadEntitiesConfig, DEFAULT_ENTITY_KEY } from './entities';
19
19
  import { createBatch } from './batch';
20
20
  import { STORE_NAME } from './name';
21
+ import { getUndoEdits, getRedoEdits } from './private-selectors';
21
22
 
22
23
  /**
23
24
  * Returns an action object used in signalling that authors have been received.
@@ -406,14 +407,14 @@ export const editEntityRecord =
406
407
  export const undo =
407
408
  () =>
408
409
  ( { select, dispatch } ) => {
409
- const undoEdit = select.getUndoEdit();
410
+ // Todo: we shouldn't have to pass "root" here.
411
+ const undoEdit = select( ( state ) => getUndoEdits( state.root ) );
410
412
  if ( ! undoEdit ) {
411
413
  return;
412
414
  }
413
415
  dispatch( {
414
- type: 'EDIT_ENTITY_RECORD',
415
- ...undoEdit,
416
- meta: { isUndo: true },
416
+ type: 'UNDO',
417
+ stackedEdits: undoEdit,
417
418
  } );
418
419
  };
419
420
 
@@ -424,14 +425,14 @@ export const undo =
424
425
  export const redo =
425
426
  () =>
426
427
  ( { select, dispatch } ) => {
427
- const redoEdit = select.getRedoEdit();
428
+ // Todo: we shouldn't have to pass "root" here.
429
+ const redoEdit = select( ( state ) => getRedoEdits( state.root ) );
428
430
  if ( ! redoEdit ) {
429
431
  return;
430
432
  }
431
433
  dispatch( {
432
- type: 'EDIT_ENTITY_RECORD',
433
- ...redoEdit,
434
- meta: { isRedo: true },
434
+ type: 'REDO',
435
+ stackedEdits: redoEdit,
435
436
  } );
436
437
  };
437
438
 
@@ -7,18 +7,22 @@ import {
7
7
  useCallback,
8
8
  useEffect,
9
9
  } from '@wordpress/element';
10
- import { useSelect, useDispatch } from '@wordpress/data';
10
+ import { useSelect, useDispatch, useRegistry } from '@wordpress/data';
11
11
  import { parse, __unstableSerializeAndClean } from '@wordpress/blocks';
12
+ import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
12
13
 
13
14
  /**
14
15
  * Internal dependencies
15
16
  */
16
17
  import { STORE_NAME } from './name';
18
+ import { unlock } from './private-apis';
17
19
 
18
20
  /** @typedef {import('@wordpress/blocks').WPBlock} WPBlock */
19
21
 
20
22
  const EMPTY_ARRAY = [];
21
23
 
24
+ let oldFootnotes = {};
25
+
22
26
  /**
23
27
  * Internal dependencies
24
28
  */
@@ -150,6 +154,8 @@ export function useEntityProp( kind, name, prop, _id ) {
150
154
  * @return {[WPBlock[], Function, Function]} The block array and setters.
151
155
  */
152
156
  export function useEntityBlockEditor( kind, name, { id: _id } = {} ) {
157
+ const [ meta, updateMeta ] = useEntityProp( kind, name, 'meta', _id );
158
+ const registry = useRegistry();
153
159
  const providerId = useEntityId( kind, name );
154
160
  const id = _id ?? providerId;
155
161
  const { content, blocks } = useSelect(
@@ -184,6 +190,61 @@ export function useEntityBlockEditor( kind, name, { id: _id } = {} ) {
184
190
  }
185
191
  }, [ content ] );
186
192
 
193
+ const updateFootnotes = useCallback(
194
+ ( _blocks ) => {
195
+ if ( ! meta ) return;
196
+ // If meta.footnotes is empty, it means the meta is not registered.
197
+ if ( meta.footnotes === undefined ) return;
198
+
199
+ const { getRichTextValues } = unlock( blockEditorPrivateApis );
200
+ const _content = getRichTextValues( _blocks ).join( '' ) || '';
201
+ const newOrder = [];
202
+
203
+ // This can be avoided when
204
+ // https://github.com/WordPress/gutenberg/pull/43204 lands. We can then
205
+ // get the order directly from the rich text values.
206
+ if ( _content.indexOf( 'data-fn' ) !== -1 ) {
207
+ const regex = /data-fn="([^"]+)"/g;
208
+ let match;
209
+ while ( ( match = regex.exec( _content ) ) !== null ) {
210
+ newOrder.push( match[ 1 ] );
211
+ }
212
+ }
213
+
214
+ const footnotes = meta.footnotes
215
+ ? JSON.parse( meta.footnotes )
216
+ : [];
217
+ const currentOrder = footnotes.map( ( fn ) => fn.id );
218
+
219
+ if ( currentOrder.join( '' ) === newOrder.join( '' ) ) return;
220
+
221
+ const newFootnotes = newOrder.map(
222
+ ( fnId ) =>
223
+ footnotes.find( ( fn ) => fn.id === fnId ) ||
224
+ oldFootnotes[ fnId ] || {
225
+ id: fnId,
226
+ content: '',
227
+ }
228
+ );
229
+
230
+ oldFootnotes = {
231
+ ...oldFootnotes,
232
+ ...footnotes.reduce( ( acc, fn ) => {
233
+ if ( ! newOrder.includes( fn.id ) ) {
234
+ acc[ fn.id ] = fn;
235
+ }
236
+ return acc;
237
+ }, {} ),
238
+ };
239
+
240
+ updateMeta( {
241
+ ...meta,
242
+ footnotes: JSON.stringify( newFootnotes ),
243
+ } );
244
+ },
245
+ [ meta, updateMeta ]
246
+ );
247
+
187
248
  const onChange = useCallback(
188
249
  ( newBlocks, options ) => {
189
250
  const { selection } = options;
@@ -200,18 +261,24 @@ export function useEntityBlockEditor( kind, name, { id: _id } = {} ) {
200
261
  edits.content = ( { blocks: blocksForSerialization = [] } ) =>
201
262
  __unstableSerializeAndClean( blocksForSerialization );
202
263
 
203
- editEntityRecord( kind, name, id, edits );
264
+ registry.batch( () => {
265
+ updateFootnotes( edits.blocks );
266
+ editEntityRecord( kind, name, id, edits );
267
+ } );
204
268
  },
205
- [ kind, name, id, blocks ]
269
+ [ kind, name, id, blocks, updateFootnotes ]
206
270
  );
207
271
 
208
272
  const onInput = useCallback(
209
273
  ( newBlocks, options ) => {
210
274
  const { selection } = options;
211
275
  const edits = { blocks: newBlocks, selection };
212
- editEntityRecord( kind, name, id, edits );
276
+ registry.batch( () => {
277
+ updateFootnotes( edits.blocks );
278
+ editEntityRecord( kind, name, id, edits );
279
+ } );
213
280
  },
214
- [ kind, name, id ]
281
+ [ kind, name, id, updateFootnotes ]
215
282
  );
216
283
 
217
284
  return [ blocks ?? EMPTY_ARRAY, onInput, onChange ];
@@ -85,6 +85,10 @@ declare module './base-entity-records' {
85
85
  * Whether a template is a custom template.
86
86
  */
87
87
  is_custom: Record< string, string >;
88
+ /**
89
+ * The date the template was last modified, in the site's timezone.
90
+ */
91
+ modified: ContextualField< string, 'view' | 'edit', C >;
88
92
  }
89
93
  }
90
94
  }
@@ -160,7 +160,7 @@ export default function useEntityRecord< RecordType >(
160
160
  ...saveOptions,
161
161
  } ),
162
162
  } ),
163
- [ recordId ]
163
+ [ editEntityRecord, kind, name, recordId, saveEditedEntityRecord ]
164
164
  );
165
165
 
166
166
  const { editedRecord, hasEdits } = useSelect(
@@ -182,7 +182,9 @@ export default function useEntityRecord< RecordType >(
182
182
  const { data: record, ...querySelectRest } = useQuerySelect(
183
183
  ( query ) => {
184
184
  if ( ! options.enabled ) {
185
- return null;
185
+ return {
186
+ data: null,
187
+ };
186
188
  }
187
189
  return query( coreStore ).getEntityRecord( kind, name, recordId );
188
190
  },
@@ -43,7 +43,7 @@ const EMPTY_ARRAY = [];
43
43
  * @param options Optional hook options.
44
44
  * @example
45
45
  * ```js
46
- * import { useEntityRecord } from '@wordpress/core-data';
46
+ * import { useEntityRecords } from '@wordpress/core-data';
47
47
  *
48
48
  * function PageTitlesList() {
49
49
  * const { records, isResolving } = useEntityRecords( 'postType', 'page' );
package/src/index.js CHANGED
@@ -62,7 +62,6 @@ const storeConfig = () => ( {
62
62
  * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/data/README.md#createReduxStore
63
63
  */
64
64
  export const store = createReduxStore( STORE_NAME, storeConfig() );
65
-
66
65
  register( store );
67
66
 
68
67
  export { default as EntityProvider } from './entity-provider';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis';
5
+
6
+ export const { lock, unlock } =
7
+ __dangerousOptInToUnstableAPIsOnlyForCoreModules(
8
+ 'I know using unstable features means my plugin or theme will inevitably break on the next WordPress release.',
9
+ '@wordpress/core-data'
10
+ );
@@ -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
  /**