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
1 change: 1 addition & 0 deletions entry_types/scrolled/config/locales/de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ de:
drag_hint: Ziehen, um das Kapitel zu verschieben
save_error: Beim Speichern des Kapitels ist ein Fehler aufgetreten.
chapter: Kapitel
excursion: Exkurs
unnamed: Unbenannt
hidden_in_navigation: In der Navigationsleiste ausgeblendet
common_content_element_attributes:
Expand Down
1 change: 1 addition & 0 deletions entry_types/scrolled/config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ en:
drag_hint: Drag to move chapter
save_error: There was an error while saving this chapter.
chapter: Chapter
excursion: Excursion
unnamed: Untitled
hidden_in_navigation: Hidden in navigation bar
common_content_element_attributes:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,106 @@ describe('EntryCommentsView', () => {
'pageflow_scrolled.review.send': 'Send',
'pageflow_scrolled.editor.content_elements.textBlock.name': 'Text',
'pageflow_scrolled.editor.content_elements.image.name': 'Image',
'pageflow_scrolled.editor.comments_view.section': 'Section'
'pageflow_scrolled.editor.comments_view.section': 'Section',
'pageflow_scrolled.editor.chapter_item.chapter': 'Chapter',
'pageflow_scrolled.editor.chapter_item.excursion': 'Excursion'
});

it('renders a chapter heading with number and title above its groups', () => {
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: 'image'}]
});
entry.reviewSession = factories.reviewSession({
commentThreads: [{
id: 1, subjectType: 'ContentElement', subjectId: 1000,
comments: [{id: 100, body: 'A comment', creatorName: 'Alice'}]
}]
});

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

const heading = getByText('Intro');
const comment = getByText('A comment');

expect(getByText('Chapter 1')).toBeInTheDocument();
expect(heading.compareDocumentPosition(comment) &
Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
});

it('groups excursion chapters below main chapters', () => {
const entry = createEntry({
storylines: [
{id: 1000, permaId: 100, position: 0, configuration: {main: true}},
{id: 2000, permaId: 200, position: 1, configuration: {}}
],
chapters: [
{id: 1, permaId: 10, storylineId: 1000, position: 0, configuration: {title: 'Main chapter'}},
{id: 2, permaId: 20, storylineId: 2000, position: 0, configuration: {title: 'My excursion'}}
],
sections: [
{id: 1, permaId: 100, chapterId: 1, position: 0},
{id: 2, permaId: 200, chapterId: 2, position: 0}
],
contentElements: [
{id: 1, permaId: 1000, sectionId: 1, typeName: 'image'},
{id: 2, permaId: 2000, sectionId: 2, typeName: 'image'}
]
});
entry.reviewSession = factories.reviewSession({
commentThreads: [
{id: 1, subjectType: 'ContentElement', subjectId: 1000,
comments: [{id: 1, body: 'in main', creatorName: 'A'}]},
{id: 2, subjectType: 'ContentElement', subjectId: 2000,
comments: [{id: 2, body: 'in excursion', creatorName: 'B'}]}
]
});

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

const mainComment = getByText('in main');
const excursionHeading = getByText('My excursion');
const excursionComment = getByText('in excursion');

expect(getByText('Excursion')).toBeInTheDocument();
expect(mainComment.compareDocumentPosition(excursionHeading) &
Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(mainComment.compareDocumentPosition(excursionComment) &
Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
});

it('does not render a heading for a chapter without threads', () => {
const entry = createEntry({
chapters: [
{id: 1, permaId: 10, storylineId: 1000, position: 0, configuration: {title: 'Has comments'}},
{id: 2, permaId: 20, storylineId: 1000, position: 1, configuration: {title: 'Empty chapter'}}
],
sections: [
{id: 1, permaId: 100, chapterId: 1, position: 0},
{id: 2, permaId: 200, chapterId: 2, position: 0}
],
contentElements: [
{id: 1, permaId: 1000, sectionId: 1, typeName: 'image'},
{id: 2, permaId: 2000, sectionId: 2, typeName: 'image'}
]
});
entry.reviewSession = factories.reviewSession({
commentThreads: [{
id: 1, subjectType: 'ContentElement', subjectId: 1000,
comments: [{id: 1, body: 'a comment', creatorName: 'A'}]
}]
});

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

expect(getByText('Has comments')).toBeInTheDocument();
expect(queryByText('Empty chapter')).not.toBeInTheDocument();
});

it('renders a thread group only for content elements that have threads', () => {
Expand Down
88 changes: 88 additions & 0 deletions entry_types/scrolled/package/spec/entryState/structure-spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
useEntryStructure,
useEntryStructureWithContentElements,
useSectionsWithChapter,
useChapter,
useChapters,
Expand Down Expand Up @@ -274,6 +275,93 @@ describe('useEntryStructure', () => {
});
});

describe('useEntryStructureWithContentElements', () => {
it('nests ordered content elements under each section', () => {
const {result} = renderHookInEntry(
() => useEntryStructureWithContentElements(),
{
seed: {
storylines: storylinesSeed,
chapters: chaptersSeed,
sections: sectionsSeed,
contentElements: contentElementsSeed
}
}
);

expect(result.current.main[0].sections[0].contentElements).toMatchObject([
{id: 1, permaId: 1001, sectionId: 1, type: 'heading'},
{id: 2, permaId: 1002, sectionId: 1, type: 'textBlock'}
]);
});

it('nests content elements under excursion sections', () => {
const {result} = renderHookInEntry(
() => useEntryStructureWithContentElements(),
{
seed: {
storylines: storylinesSeed,
chapters: chaptersSeed,
sections: sectionsSeed,
contentElements: [
...contentElementsSeed,
{id: 6, permaId: 1006, sectionId: 3, typeName: 'textBlock', configuration: {}}
]
}
}
);

expect(result.current.excursions[0].sections[0].contentElements).toMatchObject([
{permaId: 1006, type: 'textBlock'}
]);
});

it('includes content elements with backdrop position', () => {
const {result} = renderHookInEntry(
() => useEntryStructureWithContentElements(),
{
seed: {
sections: [{id: 1, permaId: 101}],
contentElements: [
{id: 1, permaId: 1001, sectionId: 1, typeName: 'image',
configuration: {position: 'backdrop'}}
]
}
}
);

expect(result.current.main[0].sections[0].contentElements).toMatchObject([
{permaId: 1001, type: 'image'}
]);
});

it('keeps the fields useEntryStructure returns on sections and chapters', () => {
const {result} = renderHookInEntry(
() => useEntryStructureWithContentElements(),
{
seed: {
storylines: storylinesSeed,
chapters: chaptersSeed,
sections: sectionsSeed,
contentElements: contentElementsSeed
}
}
);

expect(result.current.main[0]).toMatchObject({
permaId: 10,
title: 'Chapter 1',
isExcursion: false
});
expect(result.current.main[0].sections[0]).toMatchObject({
permaId: 101,
sectionIndex: 0,
transition: 'scroll'
});
expect(result.current.mainSectionsCount).toBe(2);
});
});

describe('useSectionsWithChapter', () => {
it('returns sections with nested chapter object', () => {
const {result} = renderHookInEntry(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,52 @@ describe('ReviewStateProvider', () => {
expect(result.current[0].id).toBe(1);
});

it('filters by resolved option', () => {
it('returns all threads when no subject is given', () => {
const {result} = renderHook(
() => useCommentThreads({subjectType: 'CE', subjectId: 10}, {resolved: false}),
() => useCommentThreads(),
{
wrapper: ({children}) => (
<ReviewStateProvider initialState={{
currentUser: null,
commentThreads: [
{id: 1, subjectType: 'CE', subjectId: 10, comments: []},
{id: 2, subjectType: 'Section', subjectId: 20, comments: []}
]
}}>
{children}
</ReviewStateProvider>
)
}
);

expect(result.current.map(t => t.id)).toEqual([1, 2]);
});

it('filters all threads by resolution when no subject is given', () => {
const {result} = renderHook(
() => useCommentThreads({resolution: 'unresolved'}),
{
wrapper: ({children}) => (
<ReviewStateProvider initialState={{
currentUser: null,
commentThreads: [
{id: 1, subjectType: 'CE', subjectId: 10, resolvedAt: null, comments: []},
{id: 2, subjectType: 'Section', subjectId: 20, resolvedAt: '2026-04-09', comments: []},
{id: 3, subjectType: 'CE', subjectId: 30, resolvedAt: null, comments: []}
]
}}>
{children}
</ReviewStateProvider>
)
}
);

expect(result.current.map(t => t.id)).toEqual([1, 3]);
});

it('filters by resolution unresolved', () => {
const {result} = renderHook(
() => useCommentThreads({subjectType: 'CE', subjectId: 10, resolution: 'unresolved'}),
{
wrapper: ({children}) => (
<ReviewStateProvider initialState={{
Expand All @@ -82,6 +125,28 @@ describe('ReviewStateProvider', () => {
expect(result.current.map(t => t.id)).toEqual([1, 3]);
});

it('filters by resolution resolved', () => {
const {result} = renderHook(
() => useCommentThreads({subjectType: 'CE', subjectId: 10, resolution: 'resolved'}),
{
wrapper: ({children}) => (
<ReviewStateProvider initialState={{
currentUser: null,
commentThreads: [
{id: 1, subjectType: 'CE', subjectId: 10, resolvedAt: null, comments: []},
{id: 2, subjectType: 'CE', subjectId: 10, resolvedAt: '2026-04-09', comments: []},
{id: 3, subjectType: 'CE', subjectId: 10, resolvedAt: '2026-04-10', comments: []}
]
}}>
{children}
</ReviewStateProvider>
)
}
);

expect(result.current.map(t => t.id)).toEqual([2, 3]);
});

it('updates single thread on thread change message', async () => {
const {result, waitForNextUpdate} = renderHook(
() => useCommentThreads({subjectType: 'CE', subjectId: 10}),
Expand Down
Loading
Loading