Skip to content

Commit ac73245

Browse files
Notes: Keep anchor writes out of undo history (#84079)
Co-authored-by: Mamaduka <mamaduka@git.wordpress.org> Co-authored-by: adamsilverstein <adamsilverstein@git.wordpress.org>
1 parent 4942a9e commit ac73245

6 files changed

Lines changed: 94 additions & 74 deletions

File tree

‎packages/editor/CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
- Notes: Keep the clicked thread selected on a block with several notes, instead of switching to the block's primary note ([#83528](https://github.com/WordPress/gutenberg/pull/83528)).
1818
- Notes: Keep floating threads aligned with their blocks when an editor notice or the device preview moves the canvas ([#83485](https://github.com/WordPress/gutenberg/pull/83485)).
1919
- Notes: Add an Undo action to the "Note deleted." snackbar, and stop the editor's undo from bringing back the highlight of a deleted note ([#83988](https://github.com/WordPress/gutenberg/pull/83988)).
20+
- Notes: Stop the editor's undo from detaching a newly added note from its block, or bringing back the highlight of a resolved note ([#84079](https://github.com/WordPress/gutenberg/pull/84079)).
2021

2122
### Internal
2223

‎packages/editor/src/components/collab-sidebar/hooks.js‎

Lines changed: 36 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,14 @@ import { unlock } from '../../lock-unlock';
2222
import { createBoardStore } from './board-store';
2323
import {
2424
calculateNotePositions,
25-
clearInlineNoteMarker,
2625
findNoteInBlock,
2726
focusNoteThread,
2827
getInlineMarkerStart,
2928
getNoteIdsFromMetadata,
3029
addNoteIdToMetadata,
3130
pickPrimaryNote,
3231
readInlineSelection,
33-
removeNoteFormat,
32+
removeInlineNote,
3433
removeNoteIdFromMetadata,
3534
wrapInlineNote,
3635
} from './utils';
@@ -177,7 +176,6 @@ export function useNoteActions() {
177176
const { getCurrentPostId } = useSelect( editorStore );
178177
const {
179178
getBlockAttributes,
180-
getClientIdsWithDescendants,
181179
getSelectedBlockClientId,
182180
getSelectionStart,
183181
getSelectionEnd,
@@ -199,6 +197,17 @@ export function useNoteActions() {
199197
} );
200198
};
201199

200+
/*
201+
* Update a note's anchor without an undo step, since undo can't bring the
202+
* note back with it. The last call flags the post as changed, so "Save
203+
* draft" turns on.
204+
*/
205+
const updateNoteAnchor = ( clientId, attributes ) => {
206+
__unstableMarkNextChangeAsNotPersistent( { history: 'ignore' } );
207+
updateBlockAttributes( clientId, attributes );
208+
__unstableMarkLastChangeAsPersistent();
209+
};
210+
202211
const onCreate = async ( { content, parent } ) => {
203212
try {
204213
// Capture the target block and inline selection *before* the async
@@ -257,7 +266,7 @@ export function useNoteActions() {
257266
}
258267
}
259268

260-
updateBlockAttributes( clientId, newAttributes );
269+
updateNoteAnchor( clientId, newAttributes );
261270
}
262271

263272
createNotice(
@@ -274,7 +283,8 @@ export function useNoteActions() {
274283
}
275284
};
276285

277-
const onEdit = async ( { id, content, status } ) => {
286+
const onEdit = async ( note, { content, status } ) => {
287+
const { id } = note;
278288
try {
279289
// For resolution or reopen actions, create a new note with metadata.
280290
if ( status === 'approved' || status === 'hold' ) {
@@ -315,13 +325,17 @@ export function useNoteActions() {
315325

316326
// Resolving a note drops its inline highlight: strip the marker
317327
// so the note falls back to a block-level note in the content.
318-
if ( status === 'approved' ) {
319-
clearInlineNoteMarker(
320-
id,
321-
getClientIdsWithDescendants,
322-
getBlockAttributes,
323-
updateBlockAttributes
328+
const removed =
329+
status === 'approved' &&
330+
note.blockClientId &&
331+
removeInlineNote(
332+
getBlockAttributes( note.blockClientId ),
333+
id
324334
);
335+
if ( removed ) {
336+
updateNoteAnchor( note.blockClientId, {
337+
[ removed.attributeKey ]: removed.value,
338+
} );
325339
}
326340

327341
// The note visibly updates in place, so there is no snackbar,
@@ -361,17 +375,6 @@ export function useNoteActions() {
361375
}
362376
};
363377

364-
/*
365-
* Update a note's anchor without an undo step, since undo can't bring the
366-
* note back with it. The last call flags the post as changed, so "Save
367-
* draft" turns on.
368-
*/
369-
const updateNoteAnchor = ( clientId, attributes ) => {
370-
__unstableMarkNextChangeAsNotPersistent( { history: 'ignore' } );
371-
updateBlockAttributes( clientId, attributes );
372-
__unstableMarkLastChangeAsPersistent();
373-
};
374-
375378
const restoreNote = async ( noteId, anchor ) => {
376379
try {
377380
// Untrash first, so a failure doesn't leave an anchor without a note.
@@ -439,9 +442,7 @@ export function useNoteActions() {
439442
// Capture the target block *before* the async delete: selection may
440443
// shift during the round-trip, pointing the attribute cleanup at the
441444
// wrong block.
442-
const clientId = ! note.parent
443-
? note.blockClientId || getSelectedBlockClientId()
444-
: null;
445+
const clientId = ! note.parent ? note.blockClientId : null;
445446

446447
// Without `force`, this moves the note to the trash, so the
447448
// snackbar's Undo can bring it back.
@@ -465,19 +466,16 @@ export function useNoteActions() {
465466
};
466467
// Strip the inline marker too (if any) so the deleted note's
467468
// highlight doesn't linger in the content.
468-
const found = findNoteInBlock( attributes, note.id );
469-
if ( found ) {
470-
const next = removeNoteFormat(
471-
attributes[ found.attributeKey ],
472-
note.id
473-
);
474-
if ( next ) {
475-
newAttributes[ found.attributeKey ] = next;
476-
anchor.inline = {
477-
...found,
478-
text: next.text.slice( found.start, found.end ),
479-
};
480-
}
469+
const removed = removeInlineNote( attributes, note.id );
470+
if ( removed ) {
471+
const { attributeKey, start, end, value } = removed;
472+
newAttributes[ attributeKey ] = value;
473+
anchor.inline = {
474+
attributeKey,
475+
start,
476+
end,
477+
text: value.text.slice( start, end ),
478+
};
481479
}
482480
updateNoteAnchor( clientId, newAttributes );
483481
}

‎packages/editor/src/components/collab-sidebar/note-thread.jsx‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ export function NoteThread( {
148148
}
149149

150150
function handleResolve() {
151-
onEditNote( { id: note.id, status: 'approved' } );
151+
onEditNote( note, { status: 'approved' } );
152152
onDeselectNote();
153153
if ( isFloating ) {
154154
relatedBlockElement?.focus();
@@ -301,8 +301,7 @@ export function NoteThread( {
301301
onSubmit={ ( inputComment ) => {
302302
if ( 'approved' === note.status ) {
303303
// For reopening, include the content in the reopen action.
304-
return onEditNote( {
305-
id: note.id,
304+
return onEditNote( note, {
306305
status: 'hold',
307306
content: inputComment,
308307
} );

‎packages/editor/src/components/collab-sidebar/note.jsx‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export function Note( {
9999
id: 'reopen',
100100
title: _x( 'Reopen', 'Reopen note' ),
101101
isEligible: ( { status } ) => canResolve && hasResolved( status ),
102-
onClick: () => onEditNote( { id: note.id, status: 'hold' } ),
102+
onClick: () => onEditNote( note, { status: 'hold' } ),
103103
},
104104
{
105105
id: 'delete',
@@ -130,8 +130,7 @@ export function Note( {
130130
body = (
131131
<NoteForm
132132
onSubmit={ async ( value ) => {
133-
const saved = await onEditNote( {
134-
id: note.id,
133+
const saved = await onEditNote( note, {
135134
content: value,
136135
} );
137136
// Keep the form open on failure so the edit isn't lost.

‎packages/editor/src/components/collab-sidebar/utils.js‎

Lines changed: 9 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -520,38 +520,17 @@ export function removeNoteFormat( value, noteId ) {
520520
}
521521

522522
/**
523-
* Strip a note's inline `core/note` marker from whichever block holds it, if
524-
* any, so a deleted or resolved note's highlight does not linger in the content.
525-
* No-op for block-level notes (those carry no marker). Used by the resolve path,
526-
* which only knows the note id; the delete path strips the marker inline since
527-
* it already has the block.
523+
* Remove a note's inline `core/note` marker from block attributes.
528524
*
529-
* @param {number} noteId Note id whose marker to remove.
530-
* @param {() => string[]} getClientIdsWithDescendants Block-editor selector.
531-
* @param {(clientId: string) => Record<string, unknown>} getBlockAttributes Block-editor selector.
532-
* @param {(clientId: string, attributes: Object) => void} updateBlockAttributes Block-editor action.
525+
* @param {?Object} attributes Block attributes.
526+
* @param {number|string} noteId Note id whose marker to remove.
527+
* @return {?{attributeKey: string, start: number, end: number, value: RichTextData}} Marker range and the new attribute value, or null when no marker.
533528
*/
534-
export function clearInlineNoteMarker(
535-
noteId,
536-
getClientIdsWithDescendants,
537-
getBlockAttributes,
538-
updateBlockAttributes
539-
) {
540-
for ( const clientId of getClientIdsWithDescendants() ) {
541-
const attributes = getBlockAttributes( clientId );
542-
const found = findNoteInBlock( attributes, noteId );
543-
if ( ! found ) {
544-
continue;
545-
}
546-
const next = removeNoteFormat(
547-
attributes[ found.attributeKey ],
548-
noteId
549-
);
550-
if ( next ) {
551-
updateBlockAttributes( clientId, { [ found.attributeKey ]: next } );
552-
}
553-
return;
554-
}
529+
export function removeInlineNote( attributes, noteId ) {
530+
const found = findNoteInBlock( attributes, noteId );
531+
const value =
532+
found && removeNoteFormat( attributes[ found.attributeKey ], noteId );
533+
return value ? { ...found, value } : null;
555534
}
556535

557536
/**

‎test/e2e/specs/editor/various/block-notes.spec.js‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1732,6 +1732,45 @@ test.describe( 'Block Notes', () => {
17321732
} );
17331733
} );
17341734

1735+
test( 'keeps note anchors out of the undo history', async ( {
1736+
editor,
1737+
page,
1738+
pageUtils,
1739+
blockNoteUtils,
1740+
} ) => {
1741+
await editor.insertBlock( {
1742+
name: 'core/paragraph',
1743+
attributes: { content: 'Keep my anchor.' },
1744+
} );
1745+
// Start with an empty undo stack, so the shortcut can only reach the
1746+
// note's anchor.
1747+
await editor.saveDraft();
1748+
await page.reload();
1749+
await blockNoteUtils.showAllNotes();
1750+
1751+
const paragraph = editor.canvas.getByRole( 'document', {
1752+
name: 'Block: Paragraph',
1753+
} );
1754+
await paragraph.click();
1755+
await blockNoteUtils.selectBlockText();
1756+
await blockNoteUtils.addNote( 'Stay attached' );
1757+
const marker = editor.canvas.locator( 'mark.wp-note' );
1758+
1759+
// Undo doesn't detach the new note from its block.
1760+
await pageUtils.pressKeys( 'primary+z' );
1761+
await expect( marker ).toHaveText( 'Keep my anchor.' );
1762+
const [ block ] = await editor.getBlocks();
1763+
expect( block.attributes.metadata?.noteId ).toHaveLength( 1 );
1764+
1765+
// Undo doesn't bring back the marker of a resolved note.
1766+
await blockNoteUtils.getThread( 'Stay attached' ).click();
1767+
await page.getByRole( 'button', { name: 'Resolve' } ).click();
1768+
await expect( marker ).toHaveCount( 0 );
1769+
await pageUtils.pressKeys( 'primary+z' );
1770+
await expect( marker ).toHaveCount( 0 );
1771+
await expect( paragraph ).toHaveText( 'Keep my anchor.' );
1772+
} );
1773+
17351774
test.describe( 'Restoring a deleted note', () => {
17361775
async function addInlineNote( { editor, blockNoteUtils }, note ) {
17371776
const paragraph = editor.canvas.getByRole( 'document', {
@@ -1821,6 +1860,11 @@ test.describe( 'Block Notes', () => {
18211860
name: 'core/paragraph',
18221861
attributes: { content: 'Undo after delete.' },
18231862
} );
1863+
// Start with an empty undo stack, so the shortcut can't undo the
1864+
// block insertion instead.
1865+
await editor.saveDraft();
1866+
await page.reload();
1867+
await blockNoteUtils.showAllNotes();
18241868
await addInlineNote( { editor, blockNoteUtils }, 'Stay deleted' );
18251869

18261870
await blockNoteUtils.deleteNote();

0 commit comments

Comments
 (0)