Skip to content

Commit 4c48c6c

Browse files
ellatrixMamaduka
authored andcommitted
Revisions: Add Meta fields diff panel to document sidebar (#76341)
Co-authored-by: ellatrix <ellatrix@git.wordpress.org> Co-authored-by: Mamaduka <mamaduka@git.wordpress.org>
1 parent 59ca84f commit 4c48c6c

7 files changed

Lines changed: 251 additions & 44 deletions

File tree

Lines changed: 8 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
/**
22
* WordPress dependencies
33
*/
4-
import { PanelBody } from '@wordpress/components';
54
import { store as blockEditorStore } from '@wordpress/block-editor';
65
import { useSelect } from '@wordpress/data';
76
import { __ } from '@wordpress/i18n';
87

98
/**
109
* Internal dependencies
1110
*/
12-
import PostPanelRow from '../post-panel-row';
11+
import RevisionDiffPanel from '../revision-diff-panel';
1312

1413
/**
1514
* Panel that shows changed block attributes for the selected block
@@ -27,48 +26,14 @@ export default function RevisionBlockDiffPanel() {
2726
return null;
2827
}
2928

30-
const diffInfo = block.attributes?.__revisionDiffStatus;
31-
const changedAttributes = diffInfo?.changedAttributes;
32-
33-
if ( ! changedAttributes ) {
34-
return null;
35-
}
36-
37-
const fields = Object.entries( changedAttributes ).map(
38-
( [ key, parts ] ) => (
39-
<PostPanelRow key={ key } label={ key }>
40-
<span className="editor-revision-fields-diff__value">
41-
{ parts.map( ( part, index ) => {
42-
if ( part.added ) {
43-
return (
44-
<ins
45-
key={ index }
46-
className="editor-revision-fields-diff__added"
47-
>
48-
{ part.value }
49-
</ins>
50-
);
51-
}
52-
if ( part.removed ) {
53-
return (
54-
<del
55-
key={ index }
56-
className="editor-revision-fields-diff__removed"
57-
>
58-
{ part.value }
59-
</del>
60-
);
61-
}
62-
return <span key={ index }>{ part.value }</span>;
63-
} ) }
64-
</span>
65-
</PostPanelRow>
66-
)
67-
);
29+
const changedAttributes =
30+
block.attributes?.__revisionDiffStatus?.changedAttributes;
6831

6932
return (
70-
<PanelBody title={ __( 'Changed attributes' ) } initialOpen>
71-
{ fields }
72-
</PanelBody>
33+
<RevisionDiffPanel
34+
title={ __( 'Changed attributes' ) }
35+
entries={ changedAttributes }
36+
initialOpen
37+
/>
7338
);
7439
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* WordPress dependencies
3+
*/
4+
import { PanelBody } from '@wordpress/components';
5+
6+
/**
7+
* Internal dependencies
8+
*/
9+
import PostPanelRow from '../post-panel-row';
10+
11+
/**
12+
* Renders a panel of word-level diffs.
13+
*
14+
* @param {Object} props
15+
* @param {string} props.title Panel title.
16+
* @param {Object} props.entries Map of key → diffWords parts arrays.
17+
* @param {boolean} props.initialOpen Whether the panel starts open.
18+
*/
19+
export default function RevisionDiffPanel( { title, entries, initialOpen } ) {
20+
if ( ! entries ) {
21+
return null;
22+
}
23+
24+
const fields = Object.entries( entries ).map( ( [ key, parts ] ) => (
25+
<PostPanelRow key={ key } label={ key }>
26+
<span className="editor-revision-fields-diff__value">
27+
{ parts.map( ( part, index ) => {
28+
if ( part.added ) {
29+
return (
30+
<ins
31+
key={ index }
32+
className="editor-revision-fields-diff__added"
33+
>
34+
{ part.value }
35+
</ins>
36+
);
37+
}
38+
if ( part.removed ) {
39+
return (
40+
<del
41+
key={ index }
42+
className="editor-revision-fields-diff__removed"
43+
>
44+
{ part.value }
45+
</del>
46+
);
47+
}
48+
return <span key={ index }>{ part.value }</span>;
49+
} ) }
50+
</span>
51+
</PostPanelRow>
52+
) );
53+
54+
return (
55+
<PanelBody title={ title } initialOpen={ initialOpen }>
56+
{ fields }
57+
</PanelBody>
58+
);
59+
}

packages/editor/src/components/revision-block-diff/style.scss renamed to packages/editor/src/components/revision-diff-panel/style.scss

File renamed without changes.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import { diffWords } from 'diff/lib/diff/word';
5+
6+
/**
7+
* WordPress dependencies
8+
*/
9+
import { useSelect } from '@wordpress/data';
10+
import { useMemo } from '@wordpress/element';
11+
import { __ } from '@wordpress/i18n';
12+
13+
/**
14+
* Internal dependencies
15+
*/
16+
import RevisionDiffPanel from '../revision-diff-panel';
17+
import { store as editorStore } from '../../store';
18+
import { unlock } from '../../lock-unlock';
19+
20+
/**
21+
* Safely stringifies a value for display and comparison.
22+
*
23+
* @param {*} value The value to stringify.
24+
* @return {string} The stringified value.
25+
*/
26+
function stringifyValue( value ) {
27+
if ( value === null || value === undefined ) {
28+
return '';
29+
}
30+
if ( typeof value === 'object' ) {
31+
return JSON.stringify( value, null, 2 );
32+
}
33+
return String( value );
34+
}
35+
36+
/**
37+
* Panel that shows meta field diffs between the current revision and
38+
* the previous revision in the document sidebar during revision mode.
39+
*/
40+
export default function RevisionFieldsDiffPanel() {
41+
const { revision, previousRevision } = useSelect( ( select ) => {
42+
const { getCurrentRevision, getPreviousRevision } = unlock(
43+
select( editorStore )
44+
);
45+
46+
return {
47+
revision: getCurrentRevision(),
48+
previousRevision: getPreviousRevision(),
49+
};
50+
}, [] );
51+
52+
const entries = useMemo( () => {
53+
if ( ! revision ) {
54+
return null;
55+
}
56+
57+
const revisionMeta = revision.meta ?? {};
58+
const previousMeta = previousRevision?.meta ?? {};
59+
const allMetaKeys = new Set( [
60+
...Object.keys( revisionMeta ),
61+
...Object.keys( previousMeta ),
62+
] );
63+
64+
const result = {};
65+
66+
for ( const key of allMetaKeys ) {
67+
const revStr = stringifyValue( revisionMeta[ key ] );
68+
const prevStr = stringifyValue( previousMeta[ key ] );
69+
70+
if ( ! revStr && ! prevStr ) {
71+
continue;
72+
}
73+
74+
result[ key ] = diffWords( prevStr, revStr );
75+
}
76+
77+
if ( Object.keys( result ).length === 0 ) {
78+
return null;
79+
}
80+
81+
return result;
82+
}, [ revision, previousRevision ] );
83+
84+
return (
85+
<RevisionDiffPanel
86+
title={ __( 'Meta' ) }
87+
entries={ entries }
88+
initialOpen={ false }
89+
/>
90+
);
91+
}

packages/editor/src/components/sidebar/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import PluginDocumentSettingPanel from '../plugin-document-setting-panel';
2727
import PluginSidebar from '../plugin-sidebar';
2828
import PostSummary from './post-summary';
2929
import PostTaxonomiesPanel from '../post-taxonomies/panel';
30+
import RevisionFieldsDiffPanel from '../revision-fields-diff';
3031
import PostTransformPanel from '../post-transform-panel';
3132
import SidebarHeader from './header';
3233
import TemplateContentPanel from '../template-content-panel';
@@ -128,6 +129,7 @@ const SidebarContent = ( {
128129
<PostSummary
129130
onActionPerformed={ onActionPerformed }
130131
/>
132+
{ isRevisionsMode && <RevisionFieldsDiffPanel /> }
131133
{ ! isRevisionsMode && (
132134
<>
133135
<PluginDocumentSettingPanel.Slot />

packages/editor/src/style.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
@use "./components/post-panel-section/style.scss" as *;
3939
@use "./components/post-publish-panel/style.scss" as *;
4040
@use "./components/post-revisions-preview/style.scss" as *;
41-
@use "./components/revision-block-diff/style.scss" as *;
41+
@use "./components/revision-diff-panel/style.scss" as *;
4242
@use "./components/post-saved-state/style.scss" as *;
4343
@use "./components/post-schedule/style.scss" as *;
4444
@use "./components/post-status/style.scss" as *;
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/**
2+
* WordPress dependencies
3+
*/
4+
const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
5+
6+
test.describe( 'Revision Fields Diff Panel', () => {
7+
test.beforeEach( async ( { admin } ) => {
8+
await admin.createNewPost();
9+
} );
10+
11+
test( 'should show only the revision footnotes, not the current post footnotes', async ( {
12+
editor,
13+
page,
14+
} ) => {
15+
// Revision 1: paragraph with footnote "alpha".
16+
await editor.canvas
17+
.getByRole( 'button', { name: 'Add default block' } )
18+
.click();
19+
await page.keyboard.type( 'Paragraph one' );
20+
await editor.showBlockToolbar();
21+
await editor.clickBlockToolbarButton( 'More' );
22+
await page.getByRole( 'menuitem', { name: 'Footnote' } ).click();
23+
await page.keyboard.type( 'alpha' );
24+
await editor.saveDraft();
25+
26+
// Revision 2: add a second paragraph with footnote "beta".
27+
// Insert programmatically to avoid focus issues with footnotes block.
28+
await page.evaluate( () => {
29+
const block = window.wp.blocks.createBlock( 'core/paragraph', {
30+
content: 'Paragraph two',
31+
} );
32+
window.wp.data
33+
.dispatch( 'core/block-editor' )
34+
.insertBlock( block, 1 );
35+
} );
36+
await editor.canvas
37+
.getByRole( 'document', { name: 'Block: Paragraph' } )
38+
.nth( 1 )
39+
.click();
40+
await page.keyboard.press( 'End' );
41+
await editor.showBlockToolbar();
42+
await editor.clickBlockToolbarButton( 'More' );
43+
await page.getByRole( 'menuitem', { name: 'Footnote' } ).click();
44+
await page.keyboard.type( 'beta' );
45+
await editor.saveDraft();
46+
47+
// Open revisions UI.
48+
await editor.openDocumentSettingsSidebar();
49+
const settingsSidebar = page.getByRole( 'region', {
50+
name: 'Editor settings',
51+
} );
52+
await settingsSidebar.getByRole( 'tab', { name: 'Post' } ).click();
53+
await settingsSidebar
54+
.getByRole( 'button', { name: '2', exact: true } )
55+
.click();
56+
await expect(
57+
page.getByRole( 'button', { name: 'Restore' } )
58+
).toBeVisible();
59+
60+
// Expand the Meta panel.
61+
await settingsSidebar.getByRole( 'button', { name: 'Meta' } ).click();
62+
63+
const footnotesRow = settingsSidebar.locator(
64+
'.editor-post-panel__row',
65+
{
66+
has: page.locator(
67+
'.editor-post-panel__row-label:text("footnotes")'
68+
),
69+
}
70+
);
71+
const footnotesControl = footnotesRow.locator(
72+
'.editor-post-panel__row-control'
73+
);
74+
75+
// Latest revision (2 footnotes) diffs against rev 1 (1 footnote).
76+
// Should contain both "alpha" and "beta".
77+
await expect( footnotesControl ).toContainText( 'alpha' );
78+
await expect( footnotesControl ).toContainText( 'beta' );
79+
80+
// Navigate to oldest revision.
81+
const slider = page.getByRole( 'slider', { name: 'Revision' } );
82+
await slider.focus();
83+
await page.keyboard.press( 'Home' );
84+
85+
// Oldest revision (1 footnote) has no previous revision.
86+
// Should only contain "alpha", NOT "beta".
87+
await expect( footnotesControl ).toContainText( 'alpha' );
88+
await expect( footnotesControl ).not.toContainText( 'beta' );
89+
} );
90+
} );

0 commit comments

Comments
 (0)