Skip to content
Draft
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
58 changes: 39 additions & 19 deletions packages/editor/src/components/collab-sidebar/comments.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ import {
import { unlock } from '../../lock-unlock';
import CommentAuthorInfo from './comment-author-info';
import CommentForm from './comment-form';
import { focusCommentThread, getCommentExcerpt } from './utils';
import {
focusCommentThread,
getCommentExcerpt,
getNoteIdsFromMetadata,
} from './utils';
import { useFloatingThread } from './hooks';
import { AddComment } from './add-comment';
import { store as editorStore } from '../../store';
Expand Down Expand Up @@ -70,22 +74,25 @@ export function Comments( {
useDispatch( blockEditorStore )
);

const { blockCommentId, selectedBlockClientId, orderedBlockIds } =
useSelect( ( select ) => {
const { blockNoteIds, selectedBlockClientId, orderedBlockIds } = useSelect(
( select ) => {
const {
getBlockAttributes,
getSelectedBlockClientId,
getClientIdsWithDescendants,
} = select( blockEditorStore );
const clientId = getSelectedBlockClientId();
const metadata = clientId
? getBlockAttributes( clientId )?.metadata
: null;
return {
blockCommentId: clientId
? getBlockAttributes( clientId )?.metadata?.noteId
: null,
blockNoteIds: getNoteIdsFromMetadata( metadata ),
selectedBlockClientId: clientId,
orderedBlockIds: getClientIdsWithDescendants(),
};
}, [] );
},
[]
);

const relatedBlockElement = useBlockElement( selectedBlockClientId );

Expand All @@ -106,15 +113,14 @@ export function Comments( {
};
// Insert the new comment block at the right order within the threads.
orderedBlockIds.forEach( ( blockId ) => {
// Get ALL threads for this block (not just the first one)
const threadsForBlock = t.filter(
( thread ) => thread.blockClientId === blockId
);
orderedThreads.push( ...threadsForBlock );
if ( blockId === selectedBlockClientId ) {
// For selected block, add new note thread after existing threads
orderedThreads.push( newNoteThread );
} else {
const threadForBlock = t.find(
( thread ) => thread.blockClientId === blockId
);
if ( threadForBlock ) {
orderedThreads.push( threadForBlock );
}
}
} );
return orderedThreads;
Expand Down Expand Up @@ -158,11 +164,25 @@ export function Comments( {

// Auto-select the related comment thread when a block is selected.
useEffect( () => {
// Fallback to 'new-note-thread' when showing the comment board for a new note.
setSelectedThread(
newNoteFormState === 'open' ? 'new-note-thread' : blockCommentId
);
}, [ blockCommentId, newNoteFormState ] );
if ( newNoteFormState === 'open' ) {
setSelectedThread( 'new-note-thread' );
} else if ( blockNoteIds.length > 0 ) {
// Find first unresolved thread for this block, or use first thread
const unresolvedThread = noteThreads.find(
( thread ) =>
blockNoteIds.includes( thread.id ) &&
thread.status === 'hold'
);
const firstThread = noteThreads.find( ( thread ) =>
blockNoteIds.includes( thread.id )
);
setSelectedThread(
unresolvedThread?.id ?? firstThread?.id ?? null
);
} else {
setSelectedThread( null );
}
}, [ blockNoteIds, newNoteFormState, noteThreads ] );

const setBlockRef = useCallback( ( id, blockRef ) => {
setBlockRefs( ( prev ) => ( { ...prev, [ id ]: blockRef } ) );
Expand Down
53 changes: 38 additions & 15 deletions packages/editor/src/components/collab-sidebar/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ import { store as interfaceStore } from '@wordpress/interface';
import { store as editorStore } from '../../store';
import { collabSidebarName } from './constants';
import { unlock } from '../../lock-unlock';
import { noop } from './utils';
import {
noop,
getNoteIdsFromMetadata,
addNoteIdToMetadata,
removeNoteIdFromMetadata,
} from './utils';

const { useBlockElement, cleanEmptyObject } = unlock( blockEditorPrivateApis );

Expand Down Expand Up @@ -72,9 +77,10 @@ export function useBlockComments( postId ) {
}

const blocksWithComments = clientIds.reduce( ( results, clientId ) => {
const commentId = getBlockAttributes( clientId )?.metadata?.noteId;
if ( commentId ) {
results[ clientId ] = commentId;
const metadata = getBlockAttributes( clientId )?.metadata;
const noteIds = getNoteIdsFromMetadata( metadata );
if ( noteIds.length > 0 ) {
results[ clientId ] = noteIds;
}
return results;
}, {} );
Expand All @@ -87,7 +93,10 @@ export function useBlockComments( postId ) {
const commentIdToBlockClientId = Object.keys(
blocksWithComments
).reduce( ( mapping, clientId ) => {
mapping[ blocksWithComments[ clientId ] ] = clientId;
const noteIds = blocksWithComments[ clientId ];
noteIds.forEach( ( noteId ) => {
mapping[ noteId ] = clientId;
} );
return mapping;
}, {} );

Expand Down Expand Up @@ -128,17 +137,21 @@ export function useBlockComments( postId ) {

// Prepare sets to determine which threads are linked to existing blocks.
const mappedIds = new Set(
Object.values( blocksWithComments ).map( ( id ) => String( id ) )
Object.values( blocksWithComments )
.flat()
.map( ( id ) => String( id ) )
);

// Get comments by block order, first unresolved, then resolved.
const unresolvedSortedComments = Object.values( blocksWithComments )
.flat()
.map( ( commentId ) => threadIdMap.get( String( commentId ) ) )
.filter(
( thread ) => thread !== undefined && thread.status === 'hold'
);

const resolvedSortedComments = Object.values( blocksWithComments )
.flat()
.map( ( commentId ) => threadIdMap.get( String( commentId ) ) )
.filter(
( thread ) =>
Expand Down Expand Up @@ -207,12 +220,16 @@ export function useBlockCommentsActions( reflowComments = noop ) {
// If it's a main comment, update the block attributes with the comment id.
if ( ! parent && savedRecord?.id ) {
const clientId = getSelectedBlockClientId();
if ( ! clientId ) {
return savedRecord;
}
const metadata = getBlockAttributes( clientId )?.metadata;
const updatedMetadata = addNoteIdToMetadata(
metadata,
savedRecord.id
);
updateBlockAttributes( clientId, {
metadata: {
...metadata,
noteId: savedRecord.id,
},
metadata: cleanEmptyObject( updatedMetadata ),
} );
}

Expand Down Expand Up @@ -312,13 +329,19 @@ export function useBlockCommentsActions( reflowComments = noop ) {
);

if ( ! comment.parent ) {
const clientId = getSelectedBlockClientId();
// Use blockClientId if available, otherwise fall back to selected block
const clientId =
comment.blockClientId || getSelectedBlockClientId();
if ( ! clientId ) {
return;
}
const metadata = getBlockAttributes( clientId )?.metadata;
const updatedMetadata = removeNoteIdFromMetadata(
metadata,
comment.id
);
updateBlockAttributes( clientId, {
metadata: cleanEmptyObject( {
...metadata,
noteId: undefined,
} ),
metadata: cleanEmptyObject( updatedMetadata ),
} );
}

Expand Down
27 changes: 18 additions & 9 deletions packages/editor/src/components/collab-sidebar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
useBlockCommentsActions,
useEnableFloatingSidebar,
} from './hooks';
import { focusCommentThread } from './utils';
import { focusCommentThread, getNoteIdsFromMetadata } from './utils';
import PostTypeSupportCheck from '../post-type-support-check';
import { unlock } from '../../lock-unlock';

Expand Down Expand Up @@ -92,15 +92,16 @@ function NotesSidebar( { postId } ) {

const showFloatingSidebar = isLargeViewport;

const { clientId, blockCommentId } = useSelect( ( select ) => {
const { clientId, blockNoteIds } = useSelect( ( select ) => {
const { getBlockAttributes, getSelectedBlockClientId } =
select( blockEditorStore );
const _clientId = getSelectedBlockClientId();
const metadata = _clientId
? getBlockAttributes( _clientId )?.metadata
: null;
return {
clientId: _clientId,
blockCommentId: _clientId
? getBlockAttributes( _clientId )?.metadata?.noteId
: null,
blockNoteIds: getNoteIdsFromMetadata( metadata ),
};
}, [] );
const { isDistractionFree } = useSelect( ( select ) => {
Expand All @@ -126,10 +127,18 @@ function NotesSidebar( { postId } ) {
const { merged: GlobalStyles } = useGlobalStylesContext();
const backgroundColor = GlobalStyles?.styles?.color?.background;

// Find the current thread for the selected block.
const currentThread = blockCommentId
? resultComments.find( ( thread ) => thread.id === blockCommentId )
: null;
// Find threads for the selected block.
const currentThreads =
blockNoteIds.length > 0
? resultComments.filter( ( thread ) =>
blockNoteIds.includes( thread.id )
)
: [];
// Use first unresolved thread, or first thread overall, for UI interactions
const currentThread =
currentThreads.find( ( thread ) => thread.status === 'hold' ) ??
currentThreads[ 0 ] ??
null;
const showAllNotesSidebar = resultComments.length > 0;

async function openTheSidebar() {
Expand Down
111 changes: 111 additions & 0 deletions packages/editor/src/components/collab-sidebar/test/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* Internal dependencies
*/
import {
getNoteIdsFromMetadata,
addNoteIdToMetadata,
removeNoteIdFromMetadata,
} from '../utils';

describe( 'getNoteIdsFromMetadata', () => {
it( 'returns empty array for null metadata', () => {
expect( getNoteIdsFromMetadata( null ) ).toEqual( [] );
} );

it( 'returns empty array for undefined metadata', () => {
expect( getNoteIdsFromMetadata( undefined ) ).toEqual( [] );
} );

it( 'returns empty array for metadata without noteId', () => {
expect( getNoteIdsFromMetadata( {} ) ).toEqual( [] );
expect( getNoteIdsFromMetadata( { name: 'test' } ) ).toEqual( [] );
} );

it( 'returns array from scalar noteId (legacy format)', () => {
expect( getNoteIdsFromMetadata( { noteId: 42 } ) ).toEqual( [ 42 ] );
} );

it( 'returns array from array noteId', () => {
expect( getNoteIdsFromMetadata( { noteId: [ 1, 2, 3 ] } ) ).toEqual( [
1, 2, 3,
] );
} );

it( 'filters out falsy values from array', () => {
expect(
getNoteIdsFromMetadata( { noteId: [ 1, null, 2, undefined, 3 ] } )
).toEqual( [ 1, 2, 3 ] );
} );
} );

describe( 'addNoteIdToMetadata', () => {
it( 'creates array for first note on empty metadata', () => {
const result = addNoteIdToMetadata( {}, 42 );
expect( result.noteId ).toEqual( [ 42 ] );
} );

it( 'creates array for first note on null metadata', () => {
const result = addNoteIdToMetadata( null, 42 );
expect( result.noteId ).toEqual( [ 42 ] );
} );

it( 'creates array for first note on undefined metadata', () => {
const result = addNoteIdToMetadata( undefined, 42 );
expect( result.noteId ).toEqual( [ 42 ] );
} );

it( 'converts scalar noteId to array and appends new id', () => {
const result = addNoteIdToMetadata( { noteId: 1 }, 2 );
expect( result.noteId ).toEqual( [ 1, 2 ] );
} );

it( 'appends to existing array', () => {
const result = addNoteIdToMetadata( { noteId: [ 1, 2 ] }, 3 );
expect( result.noteId ).toEqual( [ 1, 2, 3 ] );
} );

it( 'prevents duplicates', () => {
const result = addNoteIdToMetadata( { noteId: [ 1, 2 ] }, 1 );
expect( result ).toEqual( { noteId: [ 1, 2 ] } );
} );

it( 'preserves other metadata properties', () => {
const result = addNoteIdToMetadata( { noteId: 1, name: 'test' }, 2 );
expect( result ).toEqual( { noteId: [ 1, 2 ], name: 'test' } );
} );
} );

describe( 'removeNoteIdFromMetadata', () => {
it( 'removes noteId from array', () => {
const result = removeNoteIdFromMetadata( { noteId: [ 1, 2, 3 ] }, 2 );
expect( result.noteId ).toEqual( [ 1, 3 ] );
} );

it( 'returns undefined noteId when array becomes empty', () => {
const result = removeNoteIdFromMetadata( { noteId: [ 1 ] }, 1 );
expect( result.noteId ).toBeUndefined();
} );

it( 'handles removing from scalar noteId (legacy format)', () => {
const result = removeNoteIdFromMetadata( { noteId: 42 }, 42 );
expect( result.noteId ).toBeUndefined();
} );

it( 'handles removing non-existent id', () => {
const result = removeNoteIdFromMetadata( { noteId: [ 1, 2 ] }, 99 );
expect( result.noteId ).toEqual( [ 1, 2 ] );
} );

it( 'handles empty metadata', () => {
const result = removeNoteIdFromMetadata( {}, 1 );
expect( result.noteId ).toBeUndefined();
} );

it( 'preserves other metadata properties', () => {
const result = removeNoteIdFromMetadata(
{ noteId: [ 1, 2 ], name: 'test' },
1
);
expect( result ).toEqual( { noteId: [ 2 ], name: 'test' } );
} );
} );
Loading
Loading