Skip to content
Merged
1 change: 1 addition & 0 deletions packages/editor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Enhancements

- Notes: Remove "Add note" from the rich-text formatting toolbar's "More" (inline styles) dropdown. Adding a note is not an inline style, the item duplicated the block options entry, and the dropdown's chevron rendered as pressed whenever the caret sat inside a note ([#80531](https://github.com/WordPress/gutenberg/pull/80531)).
- Add a read-only code diff to the revisions screen ([#80314](https://github.com/WordPress/gutenberg/pull/80314)).

### New Features

Expand Down
14 changes: 12 additions & 2 deletions packages/editor/src/components/editor-interface/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ import TemplateValidationNotice from '../template-validation-notice';
import Header from '../header';
import InserterSidebar from '../inserter-sidebar';
import ListViewSidebar from '../list-view-sidebar';
import { RevisionsHeader, RevisionsCanvas } from '../post-revisions-preview';
import {
RevisionsHeader,
RevisionsCanvas,
RevisionsCodeDiff,
} from '../post-revisions-preview';
import { CollaboratorsOverlay } from '../collaborators-overlay';
import { useCollaboratorNotifications } from '../collaborators-presence/use-collaborator-notifications';
import SavePublishPanels from '../save-publish-panels';
Expand Down Expand Up @@ -174,7 +178,13 @@ export default function EditorInterface( {
onToggleDiff={ () => setShowRevisionDiff( ! showDiff ) }
/>
}
content={ <RevisionsCanvas /> }
content={
mode === 'text' ? (
<RevisionsCodeDiff />
) : (
<RevisionsCanvas />
)
}
sidebar={ <ComplementaryArea.Slot scope="core" /> }
/>
);
Expand Down
40 changes: 24 additions & 16 deletions packages/editor/src/components/more-menu/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import ToolsMoreMenuGroup from './tools-more-menu-group';
import ViewMoreMenuGroup from './view-more-menu-group';
import { store as editorStore } from '../../store';

export default function MoreMenu( { disabled = false } ) {
export default function MoreMenu( { isRevisionMode = false } ) {
const { openModal } = useDispatch( interfaceStore );
const { set: setPreference } = useDispatch( preferencesStore );
const { toggleDistractionFree } = useDispatch( editorStore );
Expand All @@ -35,24 +35,32 @@ export default function MoreMenu( { disabled = false } ) {
const turnOffDistractionFree = () => {
setPreference( 'core', 'distractionFree', false );
};
const dropdownProps = {
icon: moreVertical,
label: __( 'Options' ),
popoverProps: {
placement: 'bottom-end',
className: 'more-menu-dropdown__content',
},
toggleProps: {
showTooltip: ! showIconLabels,
...( showIconLabels && { variant: 'tertiary' } ),
tooltipPosition: 'bottom',
size: 'compact',
},
};

if ( isRevisionMode ) {
return (
<DropdownMenu { ...dropdownProps }>
{ () => <ModeSwitcher /> }
</DropdownMenu>
);
}

return (
<>
<DropdownMenu
icon={ moreVertical }
label={ __( 'Options' ) }
popoverProps={ {
placement: 'bottom-end',
className: 'more-menu-dropdown__content',
} }
toggleProps={ {
showTooltip: ! showIconLabels,
...( showIconLabels && { variant: 'tertiary' } ),
tooltipPosition: 'bottom',
size: 'compact',
disabled,
} }
>
<DropdownMenu { ...dropdownProps }>
{ ( { onClose } ) => (
<>
<MenuGroup label={ _x( 'View', 'noun' ) }>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default as RevisionsHeader } from './revisions-header';
export { default as RevisionsCanvas } from './revisions-canvas';
export { default as RevisionsCodeDiff } from './revisions-code-diff';
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
/**
* External dependencies
*/
import { diffLines } from 'diff';

/**
* WordPress dependencies
*/
import { Spinner } from '@wordpress/components';
import { store as coreStore } from '@wordpress/core-data';
import { useSelect } from '@wordpress/data';
import { useMemo } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { VisuallyHidden } from '@wordpress/ui';

/**
* Internal dependencies
*/
import { store as editorStore } from '../../store';
import { buildRevisionsPageQuery } from '../../store/private-selectors';
import { unlock } from '../../lock-unlock';

const MAX_DIFF_EDIT_LENGTH = 1000;
const DIFF_TIMEOUT = 100;

/**
* Diff parts often end in a newline. Remove the trailing empty item so it is
* not rendered as another source line.
*
* @param {string} value Diff part value.
* @return {string[]} Lines in the diff part.
*/
function splitLines( value ) {
const lines = value.split( '\n' );
if ( lines[ lines.length - 1 ] === '' ) {
lines.pop();
}
return lines;
}

/**
* Creates the rows shown in the code diff and adds line numbers for both
* revisions.
*
* @param {string} previousContent Previous revision content.
* @param {string} currentContent Selected revision content.
* @param {boolean} showDiff Whether changes should be highlighted.
* @return {Array<Object>} Code-diff rows.
*/
export function getCodeDiffRows( previousContent, currentContent, showDiff ) {
let parts = [ { value: currentContent } ];
if ( showDiff ) {
parts = diffLines( previousContent, currentContent, {
maxEditLength: MAX_DIFF_EDIT_LENGTH,
timeout: DIFF_TIMEOUT,
} );

// Line diffing can be quadratic for unrelated revisions. If it exceeds
// either limit, mark every old and new line instead.
if ( ! parts ) {
parts = [
{ value: previousContent, removed: true },
{ value: currentContent, added: true },
];
}
}
let previousLineNumber = 1;
let currentLineNumber = 1;

return parts.flatMap( ( part ) => {
let status = 'unchanged';
if ( part.added ) {
status = 'added';
} else if ( part.removed ) {
status = 'removed';
}

return splitLines( part.value ).map( ( value ) => {
const row = {
value,
status,
previousLineNumber: null,
currentLineNumber: null,
};

if ( status !== 'added' && showDiff ) {
row.previousLineNumber = previousLineNumber++;
}
if ( status !== 'removed' ) {
row.currentLineNumber = currentLineNumber++;
}

return row;
} );
} );
}

/**
* Determines whether the code diff should wait for an older revision or fall
* back to showing the selected revision without a diff.
*
* @param {Object} options Display options.
* @param {Object|null} options.previousRevision Previous revision.
* @param {boolean} options.showDiff Whether changes should be highlighted.
* @param {boolean} options.hasOlderRevisionPage Whether another revisions page exists.
* @param {boolean} options.hasFinishedPreviousRevision Whether the previous revision request finished.
* @return {Object} Code-diff display state.
*/
export function getCodeDiffDisplayState( {
previousRevision,
showDiff,
hasOlderRevisionPage,
hasFinishedPreviousRevision,
} ) {
const needsPreviousRevision =
showDiff && previousRevision === null && hasOlderRevisionPage;

return {
showDiff:
showDiff &&
! ( needsPreviousRevision && hasFinishedPreviousRevision ),
isPreviousRevisionLoading:
needsPreviousRevision && ! hasFinishedPreviousRevision,
};
}

export default function ConnectedRevisionsCodeDiff() {
const revisionDiff = useSelect( ( select ) => {
const editorSelectors = select( editorStore );
const coreSelectors = select( coreStore );
const {
getCurrentRevision,
getPreviousRevision,
getRevisionPage,
getRevisionsPerPage,
isShowingRevisionDiff,
} = unlock( editorSelectors );
const _previousRevision = getPreviousRevision();
const _showDiff = isShowingRevisionDiff();
const revisionPage = getRevisionPage();
const totalPages =
Math.ceil(
editorSelectors.getCurrentPostRevisionsCount() /
getRevisionsPerPage()
) || 1;
const hasOlderRevisionPage = revisionPage < totalPages;
let hasFinishedPreviousRevision = true;

if ( _showDiff && _previousRevision === null && hasOlderRevisionPage ) {
const postType = editorSelectors.getCurrentPostType();
const postId = editorSelectors.getCurrentPostId();
const entityConfig = coreSelectors.getEntityConfig(
'postType',
postType
);
const revisionKey = entityConfig?.revisionKey || 'id';

hasFinishedPreviousRevision = coreSelectors.hasFinishedResolution(
'getRevisions',
[
'postType',
postType,
postId,
buildRevisionsPageQuery( revisionKey, revisionPage + 1 ),
]
);
}

return {
revision: getCurrentRevision(),
previousRevision: _previousRevision,
...getCodeDiffDisplayState( {
previousRevision: _previousRevision,
showDiff: _showDiff,
hasOlderRevisionPage,
hasFinishedPreviousRevision,
} ),
};
}, [] );

return <RevisionsCodeDiff { ...revisionDiff } />;
}

/**
* Shows the selected revision's raw block markup as a read-only diff.
*
* @param {Object} props Component props.
* @param {Object} props.revision Selected revision.
* @param {Object|null} props.previousRevision Previous revision.
* @param {boolean} props.showDiff Whether to show changes.
* @param {boolean} props.isPreviousRevisionLoading Whether the previous revision is loading.
* @return {React.JSX.Element} The revision code diff.
*/
export function RevisionsCodeDiff( {
revision,
previousRevision,
showDiff,
isPreviousRevisionLoading,
} ) {
const rows = useMemo( () => {
if ( ! revision || isPreviousRevisionLoading ) {
return [];
}

return getCodeDiffRows(
previousRevision?.content?.raw ?? '',
revision.content?.raw ?? '',
showDiff
);
}, [ revision, previousRevision, showDiff, isPreviousRevisionLoading ] );

if ( ! revision || isPreviousRevisionLoading ) {
return (
<div className="editor-revisions-canvas__loading">
<Spinner />
</div>
);
}

const label = showDiff ? __( 'Code changes' ) : __( 'Revision code' );

return (
<div
className="editor-revisions-code-diff"
role="region"
aria-label={ label }
tabIndex={ 0 }
>
{ rows.length ? (
<table className="editor-revisions-code-diff__table">
<VisuallyHidden render={ <caption /> }>
{ label }
</VisuallyHidden>
<VisuallyHidden render={ <thead /> }>
<tr>
{ showDiff && <th>{ __( 'Previous line' ) }</th> }
<th>{ __( 'Current line' ) }</th>
{ showDiff && <th>{ __( 'Change' ) }</th> }
<th>{ __( 'Code' ) }</th>
</tr>
</VisuallyHidden>
<tbody>
{ rows.map( ( row, index ) => {
let marker = '';
let statusLabel = __( 'Unchanged' );
if ( row.status === 'added' ) {
marker = '+';
statusLabel = __( 'Added' );
} else if ( row.status === 'removed' ) {
marker = '−';
statusLabel = __( 'Removed' );
}

return (
<tr
key={ index }
className={ `editor-revisions-code-diff__line is-${ row.status }` }
>
{ showDiff && (
<td className="editor-revisions-code-diff__line-number is-previous">
{ row.previousLineNumber }
</td>
) }
<td className="editor-revisions-code-diff__line-number is-current">
{ row.currentLineNumber }
</td>
{ showDiff && (
<td className="editor-revisions-code-diff__marker">
<VisuallyHidden>
{ statusLabel }
</VisuallyHidden>
<span aria-hidden="true">
{ marker }
</span>
</td>
) }
<td className="editor-revisions-code-diff__code">
<code>{ row.value }</code>
</td>
</tr>
);
} ) }
</tbody>
</table>
) : (
<p className="editor-revisions-code-diff__empty">
{ __( 'This revision is empty.' ) }
</p>
) }
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ function RevisionsHeader( { showDiff, onToggleDiff } ) {
>
{ __( 'Restore' ) }
</Button>
<MoreMenu disabled />
<MoreMenu isRevisionMode />
</>
}
/>
Expand Down
Loading
Loading