Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 24 additions & 11 deletions packages/core-data/src/entity-provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,39 @@ import { EntityContext } from './entity-context';
* Context provider component for providing
* an entity for a specific entity.
*
* @param {Object} props The component's props.
* @param {string} props.kind The entity kind.
* @param {string} props.type The entity name.
* @param {number} props.id The entity ID.
* @param {*} props.children The children to wrap.
* @param {Object} props The component's props.
* @param {string} props.kind The entity kind.
* @param {string} props.type The entity name.
* @param {number} props.id The entity ID.
* @param {number} [props.revisionId] Optional revision ID. When set,
* `useEntityProp` reads from the
* revision record instead of the
* current entity.
* @param {*} props.children The children to wrap.
*
* @return {Object} The provided children, wrapped with
* the entity's context provider.
*/
export default function EntityProvider( { kind, type: name, id, children } ) {
export default function EntityProvider( {
kind,
type: name,
id,
revisionId,
children,
} ) {
const parent = useContext( EntityContext );
const childContext = useMemo(
() => ( {
...parent,
[ kind ]: {
...parent?.[ kind ],
[ name ]: id,
},
...( kind && {
[ kind ]: {
...parent?.[ kind ],
[ name ]: id,
},
} ),
...( revisionId !== undefined && { revisionId } ),
} ),
[ parent, kind, name, id ]
[ parent, kind, name, id, revisionId ]
);
return (
<EntityContext.Provider value={ childContext }>
Expand Down
44 changes: 41 additions & 3 deletions packages/core-data/src/hooks/use-entity-prop.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
/**
* WordPress dependencies
*/
import { useCallback } from '@wordpress/element';
import { useCallback, useContext } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';

/**
* Internal dependencies
*/
import { STORE_NAME } from '../name';
import { DEFAULT_ENTITY_KEY } from '../entities';
import { EntityContext } from '../entity-context';
import useEntityId from './use-entity-id';

/**
Expand All @@ -30,9 +32,42 @@ import useEntityId from './use-entity-id';
export default function useEntityProp( kind, name, prop, _id ) {
const providerId = useEntityId( kind, name );
const id = _id ?? providerId;
const context = useContext( EntityContext );
const revisionId = context?.revisionId;

const { value, fullValue } = useSelect(
( select ) => {
if ( revisionId ) {
// Use getRevisions (not getRevision) to read from the
// already-cached collection. Using getRevision would
// trigger a redundant single-revision API fetch that
// can wipe the collection due to a race condition.
// See https://github.com/WordPress/gutenberg/pull/76043.
const revisions = select( STORE_NAME ).getRevisions(
kind,
name,
id,
{
per_page: -1,
context: 'edit',
}
);
const entityConfig = select( STORE_NAME ).getEntityConfig(
kind,
name
);
const revKey = entityConfig?.revisionKey || DEFAULT_ENTITY_KEY;
const revision = revisions?.find(
( r ) => r[ revKey ] === revisionId
);
return revision
? {
value: revision[ prop ],
fullValue: revision[ prop ],
}
: {};
}

const { getEntityRecord, getEditedEntityRecord } =
select( STORE_NAME );
const record = getEntityRecord( kind, name, id ); // Trigger resolver.
Expand All @@ -44,16 +79,19 @@ export default function useEntityProp( kind, name, prop, _id ) {
}
: {};
},
[ kind, name, id, prop ]
[ kind, name, id, prop, revisionId ]
);
const { editEntityRecord } = useDispatch( STORE_NAME );
const setValue = useCallback(
( newValue ) => {
if ( revisionId ) {
return;
}
editEntityRecord( kind, name, id, {
[ prop ]: newValue,
} );
},
[ editEntityRecord, kind, name, id, prop ]
[ editEntityRecord, kind, name, id, prop, revisionId ]
);

return [ value, setValue, fullValue ];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
store as blockEditorStore,
} from '@wordpress/block-editor';
import { createBlock, parse } from '@wordpress/blocks';
import { EntityProvider } from '@wordpress/core-data';
import { useSelect } from '@wordpress/data';
import { useEffect, useMemo, useRef } from '@wordpress/element';
import { addFilter } from '@wordpress/hooks';
Expand Down Expand Up @@ -234,12 +235,20 @@ export default function RevisionsCanvas( { showDiff } ) {
);

return revision ? (
<ExperimentalBlockEditorProvider value={ blocks } settings={ settings }>
<DiffStyleOverrides showDiff={ showDiff } />
<div className="editor-revisions-canvas__content">
<CanvasContent showDiff={ showDiff } />
</div>
</ExperimentalBlockEditorProvider>
// EntityProvider without kind/type/id inherits those from the
// parent context. Only revisionId is added so that useEntityProp
// reads from the revision record instead of the current entity.
<EntityProvider revisionId={ revision.id }>
<ExperimentalBlockEditorProvider
value={ blocks }
settings={ settings }
>
<DiffStyleOverrides showDiff={ showDiff } />
<div className="editor-revisions-canvas__content">
<CanvasContent showDiff={ showDiff } />
</div>
</ExperimentalBlockEditorProvider>
</EntityProvider>
) : (
<div className="editor-revisions-canvas__loading">
<Spinner />
Expand Down
71 changes: 71 additions & 0 deletions test/e2e/specs/editor/various/footnotes-revisions.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* WordPress dependencies
*/
const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );

test.describe( 'Footnotes in Revisions UI', () => {
test.beforeEach( async ( { admin } ) => {
await admin.createNewPost();
} );

test( 'should show the correct footnotes for each revision', async ( {
editor,
page,
} ) => {
// --- Revision 1: paragraph with footnote "alpha" ---
await editor.canvas
.getByRole( 'button', { name: 'Add default block' } )
.click();
await page.keyboard.type( 'Paragraph one' );

await editor.showBlockToolbar();
await editor.clickBlockToolbarButton( 'More' );
await page.getByRole( 'menuitem', { name: 'Footnote' } ).click();
await page.keyboard.type( 'alpha' );

await editor.saveDraft();

// --- Revision 2: change footnote to "beta" ---
await editor.canvas.locator( 'ol.wp-block-footnotes li span' ).click();
await page.keyboard.press( 'Meta+a' );
await page.keyboard.type( 'beta' );

await editor.saveDraft();

// --- Open the Revisions UI ---
await editor.openDocumentSettingsSidebar();
const settingsSidebar = page.getByRole( 'region', {
name: 'Editor settings',
} );
await settingsSidebar.getByRole( 'tab', { name: 'Post' } ).click();
await settingsSidebar
.getByRole( 'button', { name: '2', exact: true } )
.click();

// Wait for the revisions mode to be active.
await expect(
page.getByRole( 'button', { name: 'Restore' } )
).toBeVisible();

// Latest revision (revision 2) — footnote should say "beta".
await expect(
editor.canvas.locator( 'ol.wp-block-footnotes' )
).toBeVisible();
await expect(
editor.canvas.locator( 'ol.wp-block-footnotes li' ).first()
).toContainText( 'beta' );

// Navigate to oldest revision (revision 1).
const slider = page.getByRole( 'slider', { name: 'Revision' } );
await slider.focus();
await page.keyboard.press( 'Home' );

// Revision 1 — footnote should say "alpha", not "beta".
await expect(
editor.canvas.locator( 'ol.wp-block-footnotes li' ).first()
).toContainText( 'alpha' );
await expect(
editor.canvas.locator( 'ol.wp-block-footnotes li' ).first()
).not.toContainText( 'beta' );
} );
} );
Loading