Skip to content
Merged
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
5 changes: 5 additions & 0 deletions entry_types/scrolled/config/locales/de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1643,6 +1643,11 @@ de:
comments: Kommentare
comments_view:
activity: Letzte Aktivität
always_show_comments: Kommentare immer anzeigen
display_options: Kommentaranzeige
filter:
all: Alle Themen
unresolved: Ungelöste Themen
new_thread: Neues Thema
section: Abschnitt
tabs:
Expand Down
5 changes: 5 additions & 0 deletions entry_types/scrolled/config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1625,6 +1625,11 @@ en:
comments: Comments
comments_view:
activity: Latest activity
always_show_comments: Always show comments
display_options: Comment display
filter:
all: All topics
unresolved: Unresolved topics
new_thread: New topic
section: Section
tabs:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
postSelectLinkDestinationMessage
} from 'frontend/inlineEditing/postMessage';
import {setupGlobals} from 'pageflow/testHelpers';
import {normalizeSeed, factories, createIframeWindow, useFakeXhr} from 'support';
import {normalizeSeed, factories, createIframeWindow, tick, useFakeXhr} from 'support';
import {enableFetchMocks} from 'jest-fetch-mock';

enableFetchMocks();
Expand Down Expand Up @@ -1176,6 +1176,73 @@ describe('PreviewMessageController', () => {
})).resolves.toEqual(1);
});

describe('comment display filter', () => {
beforeEach(() => {
features.enable('frontend', ['commenting']);
fetch.mockResponse(JSON.stringify({currentUser: {id: 1}, commentThreads: []}));
window.localStorage.clear();
});

function createEntry() {
return factories.entry(ScrolledEntry, {}, {entryTypeSeed: normalizeSeed()});
}

function recordPayloads(iframeWindow) {
const payloads = [];

iframeWindow.addEventListener('message', event => {
if (event.data.type === 'CHANGE_COMMENT_DISPLAY_FILTER') {
payloads.push(event.data.payload);
}
});

return payloads;
}

// The iframe starts out displaying unresolved threads everywhere, so
// anything else has to be handed over again on every reload.
it('sends what the editor displays after READY', async () => {
const entry = createEntry();
entry.commentDisplayFilter.set({resolution: 'all', alwaysShowComments: false});
const iframeWindow = createIframeWindow();
controller = new PreviewMessageController({entry, iframeWindow});

const payloads = recordPayloads(iframeWindow);
await postReadyMessageAndWaitForAcknowledgement(iframeWindow);
await tick();

expect(payloads).toEqual([{resolution: 'all', alwaysShowComments: false}]);
});

it('sends the resolution when the reviewer changes the filter', async () => {
const entry = createEntry();
const iframeWindow = createIframeWindow();
controller = new PreviewMessageController({entry, iframeWindow});

const payloads = recordPayloads(iframeWindow);
await postReadyMessageAndWaitForAcknowledgement(iframeWindow);
entry.commentDisplayFilter.set('resolution', 'all');
await tick();

expect(payloads.map(payload => payload.resolution))
.toEqual(['unresolved', 'all']);
});

it('sends along that comments only show for the selection', async () => {
const entry = createEntry();
const iframeWindow = createIframeWindow();
controller = new PreviewMessageController({entry, iframeWindow});

const payloads = recordPayloads(iframeWindow);
await postReadyMessageAndWaitForAcknowledgement(iframeWindow);
entry.commentDisplayFilter.set('alwaysShowComments', false);
await tick();

expect(payloads.map(payload => payload.alwaysShowComments))
.toEqual([true, false]);
});
});

it('sends CHANGE_EMULATION_MODE message to iframe on change:emulation_mode event on model', async () => {
const entry = factories.entry(ScrolledEntry, {}, {
entryTypeSeed: normalizeSeed({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {CommentDisplayFilter} from 'editor/models/CommentDisplayFilter';

describe('CommentDisplayFilter', () => {
beforeEach(() => {
window.localStorage.clear();
});

it('shows unresolved threads by default', () => {
const filter = new CommentDisplayFilter();

expect(filter.get('resolution')).toEqual('unresolved');
expect(filter.showsResolved()).toBe(false);
});

it('remembers the resolution across editor sessions', () => {
new CommentDisplayFilter().set('resolution', 'all');

const filter = new CommentDisplayFilter();

expect(filter.get('resolution')).toEqual('all');
expect(filter.showsResolved()).toBe(true);
});

it('remembers going back to unresolved threads', () => {
new CommentDisplayFilter().set('resolution', 'all');
new CommentDisplayFilter().set('resolution', 'unresolved');

expect(new CommentDisplayFilter().get('resolution')).toEqual('unresolved');
});

it('displays comments everywhere by default', () => {
expect(new CommentDisplayFilter().get('alwaysShowComments')).toBe(true);
});

it('remembers displaying comments only for the selection', () => {
new CommentDisplayFilter().set('alwaysShowComments', false);

expect(new CommentDisplayFilter().get('alwaysShowComments')).toBe(false);
});

it('remembers going back to displaying comments everywhere', () => {
new CommentDisplayFilter().set('alwaysShowComments', false);
new CommentDisplayFilter().set('alwaysShowComments', true);

expect(new CommentDisplayFilter().get('alwaysShowComments')).toBe(true);
});

it('does not inherit the resolution of the published entry preview', () => {
window.localStorage['pageflow.scrolled.commentsResolution'] = 'all';

expect(new CommentDisplayFilter().get('resolution')).toEqual('unresolved');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import styles from 'editor/views/CommentsView.module.css';

import {factories, useFakeTranslations, renderBackboneView} from 'pageflow/testHelpers';
import {useEditorGlobals} from 'support';
import {fireEvent} from '@testing-library/dom';
import {fireEvent, within} from '@testing-library/dom';
import {act} from '@testing-library/react';

function unselectedEntry(createEntry) {
Expand All @@ -30,6 +30,10 @@ describe('CommentsView', () => {
'pageflow_scrolled.editor.comments_view.tabs.selection': 'For selection',
'pageflow_scrolled.editor.comments_view.new_thread': 'New topic',
'pageflow_scrolled.editor.comments_view.activity': 'Latest activity',
'pageflow_scrolled.editor.comments_view.display_options': 'Comment display',
'pageflow_scrolled.editor.comments_view.always_show_comments': 'Always show comments',
'pageflow_scrolled.editor.comments_view.filter.unresolved': 'Unresolved topics',
'pageflow_scrolled.editor.comments_view.filter.all': 'All topics',
'pageflow.editor.templates.back_button_decorator.outline': 'Outline'
});

Expand Down Expand Up @@ -195,6 +199,79 @@ describe('CommentsView', () => {
});
});

describe('display options menu', () => {
let menuContainer;

beforeEach(() => {
menuContainer = document.createElement('div');
menuContainer.id = 'editor_menu_container';
document.body.appendChild(menuContainer);
});

afterEach(() => {
menuContainer.remove();
});

function renderMenu(entry) {
renderBackboneView(new CommentsView({entry, editor}));

return within(menuContainer);
}

it('checks the menu item of the resolution the filter is set to', () => {
const entry = setupEntry();
entry.commentDisplayFilter.set('resolution', 'all');

const {getByRole} = renderMenu(entry);

expect(getByRole('link', {name: 'All topics'}).closest('li'))
.toHaveClass('is_checked');
expect(getByRole('link', {name: 'Unresolved topics'}).closest('li'))
.not.toHaveClass('is_checked');
});

it('sets the resolution when a menu item is clicked', () => {
const entry = setupEntry();

const {getByRole} = renderMenu(entry);
fireEvent.click(getByRole('link', {name: 'All topics'}));

expect(entry.commentDisplayFilter.get('resolution')).toEqual('all');
expect(getByRole('link', {name: 'All topics'}).closest('li'))
.toHaveClass('is_checked');
});

it('checks displaying comments always while the entry does', () => {
const entry = setupEntry();

const {getByRole} = renderMenu(entry);

expect(getByRole('link', {name: 'Always show comments'}).closest('li'))
.toHaveClass('is_checked');
});

it('displays comments only for the selection when unchecked', () => {
const entry = setupEntry();

const {getByRole} = renderMenu(entry);
fireEvent.click(getByRole('link', {name: 'Always show comments'}));

expect(entry.commentDisplayFilter.get('alwaysShowComments')).toBe(false);
expect(getByRole('link', {name: 'Always show comments'}).closest('li'))
.not.toHaveClass('is_checked');
});

it('displays comments everywhere again when checked back', () => {
const entry = setupEntry();
entry.commentDisplayFilter.set('alwaysShowComments', false);

const {getByRole} = renderMenu(entry);
fireEvent.click(getByRole('link', {name: 'Always show comments'}));

expect(entry.commentDisplayFilter.get('alwaysShowComments')).toBe(true);
});
});

describe('activity link', () => {
it('navigates to the activity route when clicked', () => {
const entry = setupEntry();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ describe('EntryCommentsView', () => {
'pageflow_scrolled.editor.chapter_item.chapter': 'Chapter',
'pageflow_scrolled.editor.chapter_item.excursion': 'Excursion',
'pageflow_scrolled.review.refers_to_deleted_element': 'Refers to a deleted element',
'pageflow_scrolled.review.resolved_count.one': '1 resolved',
'pageflow_scrolled.review.resolved_count.other': '%{count} resolved',
'pageflow_scrolled.review.reply_count.one': '1 reply',
'pageflow_scrolled.review.reply_count.other': '%{count} replies'
});
Expand Down Expand Up @@ -394,7 +396,7 @@ describe('EntryCommentsView', () => {
expect(getByText('on other').closest('[aria-current="true"]')).toBeNull();
});

it('keeps resolved threads folded away when an element is selected', () => {
it('keeps resolved threads out of the list when an element is selected', () => {
const entry = createEntry({
contentElements: [{id: 1, permaId: 10, typeName: 'image'}]
});
Expand Down Expand Up @@ -681,6 +683,100 @@ describe('EntryCommentsView', () => {
Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
});

describe('resolution filter', () => {
function createEntryWithResolvedThread() {
const entry = createEntry({
chapters: [
{id: 1, permaId: 10, storylineId: 1000, position: 0,
configuration: {title: 'Intro'}}
],
sections: [{id: 1, permaId: 100, chapterId: 1, position: 0}],
contentElements: [
{id: 1, permaId: 1000, sectionId: 1, typeName: 'textBlock', position: 0},
{id: 2, permaId: 2000, sectionId: 1, typeName: 'image', position: 1}
]
});
entry.reviewSession = factories.reviewSession({
commentThreads: [
{id: 1, subjectType: 'ContentElement', subjectId: 1000,
comments: [{id: 10, body: 'still open', creatorName: 'Alice'}]},
{id: 2, subjectType: 'ContentElement', subjectId: 2000,
resolvedAt: '2026-08-17T10:00:00.000Z',
comments: [{id: 20, body: 'already resolved', creatorName: 'Bob'}]}
]
});

return entry;
}

it('omits resolved threads and their pill while showing unresolved ones', () => {
const entry = createEntryWithResolvedThread();

const {getByText, queryByText} = renderBackboneView(
new EntryCommentsView({entry, editor})
);

expect(getByText('still open')).toBeInTheDocument();
expect(queryByText('already resolved')).not.toBeInTheDocument();
expect(queryByText('1 resolved')).not.toBeInTheDocument();
});

it('leaves out the group of an element whose threads are all resolved', () => {
const entry = createEntryWithResolvedThread();

const {getByText, queryByText} = renderBackboneView(
new EntryCommentsView({entry, editor})
);

expect(getByText('Text')).toBeInTheDocument();
expect(queryByText('Image')).not.toBeInTheDocument();
});

it('leaves out the heading of a chapter whose threads are all resolved', () => {
const entry = createEntryWithResolvedThread();
entry.reviewSession = factories.reviewSession({
commentThreads: [
{id: 2, subjectType: 'ContentElement', subjectId: 2000,
resolvedAt: '2026-08-17T10:00:00.000Z',
comments: [{id: 20, body: 'already resolved', creatorName: 'Bob'}]}
]
});

const {queryByText} = renderBackboneView(
new EntryCommentsView({entry, editor})
);

expect(queryByText('Intro')).not.toBeInTheDocument();
});

it('lists resolved threads while showing all', () => {
const entry = createEntryWithResolvedThread();
entry.commentDisplayFilter.set('resolution', 'all');

const {getByText} = renderBackboneView(
new EntryCommentsView({entry, editor})
);

expect(getByText('still open')).toBeInTheDocument();
expect(getByText('already resolved')).toBeInTheDocument();
expect(getByText('Image')).toBeInTheDocument();
});

it('follows the filter while the reviewer changes it', () => {
const entry = createEntryWithResolvedThread();

const {getByText, queryByText} = renderBackboneView(
new EntryCommentsView({entry, editor})
);

expect(queryByText('already resolved')).not.toBeInTheDocument();

act(() => { entry.commentDisplayFilter.set('resolution', 'all'); });

expect(getByText('already resolved')).toBeInTheDocument();
});
});

it('highlights all threads of the selected section', () => {
const entry = createEntry({
sections: [{id: 1, permaId: 10}, {id: 2, permaId: 20}]
Expand Down
Loading
Loading