Skip to content
Open
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
38 changes: 38 additions & 0 deletions apps/comments/comments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,44 @@ describe( 'conversation', () => {
);
} );

it( 'a spam comment shows Not Spam and Trash — no Approve, Reply or Edit', () => {
const { root } = mount(
{ selected: 1 },
data( { thread: thread( [ row( { status: 'spam', openstation_can_edit: true } ) ] ) } ),
);
const actions = Array.from( root.querySelectorAll( '.os-comments__msg-actions os-button' ) ).map( ( b ) => b.textContent?.trim() );
// Trash is intentionally kept for spam rows: the Recycle Bin is the
// permanent-deletion path; trashing a spam comment routes it there.
expect( actions ).toEqual( [ 'Not Spam', 'Trash' ] );
expect( actions ).not.toContain( 'Approve' );
expect( actions ).not.toContain( 'Reply' );
expect( actions ).not.toContain( 'Edit' );
} );

it( 'a trashed comment shows Restore and Spam — no Approve, Reply, Edit or Trash', () => {
const { root } = mount(
{ selected: 1 },
data( { thread: thread( [ row( { status: 'trash', openstation_can_edit: true } ) ] ) } ),
);
const actions = Array.from( root.querySelectorAll( '.os-comments__msg-actions os-button' ) ).map( ( b ) => b.textContent?.trim() );
expect( actions ).toEqual( [ 'Restore', 'Spam' ] );
expect( actions ).not.toContain( 'Approve' );
expect( actions ).not.toContain( 'Reply' );
expect( actions ).not.toContain( 'Edit' );
} );

it( 'Not Spam dispatches unspam; Restore dispatches untrash — no confirmation needed', () => {
const spamRow = row( { status: 'spam' } );
const { root: spamRoot, ctx: spamCtx } = mount( { selected: 1 }, data( { thread: thread( [ spamRow ] ) } ) );
( spamRoot.querySelector( '.os-comments__msg-actions os-button' ) as HTMLElement ).click();
expect( spamCtx.dispatch ).toHaveBeenCalledWith( 'moderate', { ids: [ 1 ], action: 'unspam' }, { confirm: null } );

const trashRow = row( { status: 'trash' } );
const { root: trashRoot, ctx: trashCtx } = mount( { selected: 1 }, data( { thread: thread( [ trashRow ] ) } ) );
( trashRoot.querySelector( '.os-comments__msg-actions os-button' ) as HTMLElement ).click();
expect( trashCtx.dispatch ).toHaveBeenCalledWith( 'moderate', { ids: [ 1 ], action: 'untrash' }, { confirm: null } );
} );

it( 'Reply retargets the composer; Send dispatches the draft and Enter sends too', async () => {
const rows = [ row( { id: 1 } ), row( { id: 2, parent: 1, author_name: 'Bob' } ) ];
const { root, ctx, ui } = mount( { selected: 1 }, data( { thread: thread( rows ) } ) );
Expand Down
48 changes: 37 additions & 11 deletions apps/comments/parts/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,23 +175,49 @@ function messageActions( ctx: Ctx, ui: UiState, row: CommentRow ): TemplateResul
// first, then the authoring verbs, then the two destructive ones.
const items: TemplateResult[] = [];
if ( canModerate ) {
const approveAction: BulkAction = status === 'approved' ? 'unapprove' : 'approve';
items.push(
actionButton(
status === 'approved' ? __( 'Unapprove' ) : __( 'Approve' ),
'default',
busyOn( approveAction ),
() => void moderate( ctx, ui, row.id, approveAction ),
),
);
// Spam/trash replace Approve with their own restorative verb —
// wp_set_comment_status( 'approve' ) destroys _wp_trash_meta_status, bypassing moderation.
if ( 'spam' === status ) {
items.push(
actionButton(
__( 'Not Spam' ),
'default',
busyOn( 'unspam' ),
() => void moderate( ctx, ui, row.id, 'unspam' ),
),
);
} else if ( 'trash' === status ) {
items.push(
actionButton(
__( 'Restore' ),
'default',
busyOn( 'untrash' ),
() => void moderate( ctx, ui, row.id, 'untrash' ),
),
);
} else {
const approveAction: BulkAction = 'approved' === status ? 'unapprove' : 'approve';
items.push(
actionButton(
'unapprove' === approveAction ? __( 'Unapprove' ) : __( 'Approve' ),
'default',
busyOn( approveAction ),
() => void moderate( ctx, ui, row.id, approveAction ),
),
);
}
}
// Replying posts a comment — gated on `edit_posts`, the cap the
// reply action and route enforce (the parent's post is re-checked
// server-side), so the action isn't offered to someone it will 403.
if ( canReply ) {
// Core also withholds Reply and Edit for spam/trash: a reply under a
// quarantined parent is semantically incoherent, and editing spam/trash
// content offers no value until it is restored first.
const isQuarantined = 'spam' === status || 'trash' === status;
if ( canReply && ! isQuarantined ) {
items.push( actionButton( __( 'Reply' ), 'default', false, () => openComposerFor( ctx, ui, row ) ) );
}
if ( row.openstation_can_edit ) {
if ( row.openstation_can_edit && ! isQuarantined ) {
items.push( actionButton( __( 'Edit' ), 'default', false, () => openInlineEdit( ctx, ui, row ) ) );
}
if ( canModerate ) {
Expand Down
Loading