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
22 changes: 22 additions & 0 deletions app/controllers/pageflow/review/comment_threads_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,28 @@ def index

@comment_threads = entry.comment_threads.includes(comments: :creator)
end

def create
entry = DraftEntry.find(params[:entry_id])
authorize!(:read, entry.to_model)

@comment_thread = entry.comment_threads.build(thread_params)
@comment_thread.creator = current_user

@comment_thread.comments.build(
body: params[:comment_thread][:comment][:body],
creator: current_user
)

@comment_thread.save!
render :create, status: :created
end

private

def thread_params
params.require(:comment_thread).permit(:subject_type, :subject_id)
end
end
end
end
27 changes: 27 additions & 0 deletions app/controllers/pageflow/review/comments_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module Pageflow
module Review
# @api private
class CommentsController < Pageflow::ApplicationController
respond_to :json
before_action :authenticate_user!

def create
entry = DraftEntry.find(params[:entry_id])
authorize!(:read, entry.to_model)

thread = entry.comment_threads.find(params[:comment_thread_id])
@comment = thread.comments.build(comment_params)
@comment.creator = current_user
@comment.save!

render :create, status: :created
end

private

def comment_params
params.require(:comment).permit(:body)
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
json.partial!('pageflow/review/comment_threads/comment_thread',
comment_thread: @comment_thread)
1 change: 1 addition & 0 deletions app/views/pageflow/review/comments/create.json.jbuilder
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
json.partial!('pageflow/review/comments/comment', comment: @comment)
4 changes: 3 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@

namespace :review do
resources :entries, only: [] do
resources :comment_threads, only: [:index]
resources :comment_threads, only: [:index, :create] do
resources :comments, only: [:create]
end
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="robots" content="max-image-preview:large">
<% if seed_options[:load_commenting] %>
<%= csrf_meta_tags %>
<% end %>

<%= social_share_meta_tags_for(entry) %>
<%= meta_tags_for_entry(entry) %>
Expand Down
4 changes: 2 additions & 2 deletions entry_types/scrolled/bin/rspec-with-retry-on-timeout
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
#!/bin/bash

# See REDMINE-17430
# Running only the js specs should not take more than 20 seconds.
# Running only the js specs should not take more than 30 seconds.
# If Chrome Driver hangs, the timeout command will exit with 137.
# Retry or else exit with original exit status of rspec command.

for i in {1..10}; do
timeout --signal=KILL 30 bin/rspec $@
timeout --signal=KILL 45 bin/rspec $@
e=$?
[[ $e -gt 100 ]] && echo Timeout || exit $e;
done;
Expand Down
14 changes: 13 additions & 1 deletion entry_types/scrolled/config/locales/de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1912,4 +1912,16 @@ de:
content_element_margin_bottom: Unterer Außenabstand
expose_motif_area: Motiv freilegen
review:
toolbar_label: Kommentare
add_comment: Kommentar hinzufügen
cancel_add_comment: Abbrechen
select_content_element: Zum Kommentieren auswählen
new_topic: Neues Thema
add_comment_placeholder: Kommentar hinzufügen...
reply_placeholder: Antworten...
cancel: Abbrechen
send: Senden
enter_for_new_line: Enter für neue Zeile
toggle_replies: Antworten umschalten
reply_count:
one: 1 Antwort
other: '%{count} Antworten'
14 changes: 13 additions & 1 deletion entry_types/scrolled/config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1741,4 +1741,16 @@ en:
content_element_margin_bottom: Bottom margin
expose_motif_area: Expose motif
review:
toolbar_label: Comments
add_comment: Add comment
cancel_add_comment: Cancel
select_content_element: Select to comment
new_topic: New topic
add_comment_placeholder: Add a comment...
reply_placeholder: Reply...
cancel: Cancel
send: Send
enter_for_new_line: Enter for new line
toggle_replies: Toggle replies
reply_count:
one: 1 reply
other: '%{count} replies'
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import React from 'react';
import '@testing-library/jest-dom/extend-expect';
import {act} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {useFakeTranslations} from 'pageflow/testHelpers';

import {Entry} from 'frontend/Entry';
import {usePageObjects} from 'support/pageObjects';
import {renderInEntry} from 'support';
import {clearExtensions} from 'frontend/extensions';
import {loadCommentingComponents} from 'frontend/commenting';

describe('add comment mode', () => {
usePageObjects();

useFakeTranslations({
'pageflow_scrolled.review.add_comment': 'Add comment',
'pageflow_scrolled.review.select_content_element': 'Select to comment',
'pageflow_scrolled.review.add_comment_placeholder': 'Add a comment...',
'pageflow_scrolled.review.new_topic': 'New topic',
'pageflow_scrolled.review.send': 'Send',
'pageflow_scrolled.review.cancel': 'Cancel',
'pageflow_scrolled.review.cancel_add_comment': 'Cancel add comment'
});

beforeEach(() => {
jest.spyOn(window, 'fetch').mockResolvedValue({
ok: true,
json: () => Promise.resolve({currentUser: null, commentThreads: []})
});

loadCommentingComponents();
});

afterEach(() => {
act(() => clearExtensions());
window.fetch.mockRestore();
});

it('renders add comment button', () => {
const {getByRole} = renderInEntry(<Entry />, {
seed: {
contentElements: [{
typeName: 'withTestId',
configuration: {testId: 5}
}]
}
});

expect(getByRole('button', {name: 'Add comment'})).toBeInTheDocument();
});

it('shows highlight overlays on content elements when activated', async () => {
const user = userEvent.setup();
const {getByRole} = renderInEntry(<Entry />, {
seed: {
contentElements: [{
typeName: 'withTestId',
configuration: {testId: 5}
}]
}
});

await user.click(getByRole('button', {name: 'Add comment'}));

expect(getByRole('button', {name: 'Select to comment'})).toBeInTheDocument();
});

it('opens new thread form when selecting element', async () => {
const user = userEvent.setup();
const {getByRole, getByPlaceholderText} = renderInEntry(<Entry />, {
seed: {
contentElements: [{
typeName: 'withTestId',
configuration: {testId: 5}
}]
}
});

await user.click(getByRole('button', {name: 'Add comment'}));
await user.click(getByRole('button', {name: 'Select to comment'}));

expect(getByPlaceholderText('Add a comment...')).toBeInTheDocument();
});

it('auto expands new thread form when selecting element with existing threads', async () => {
window.fetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({
currentUser: null,
commentThreads: [{
id: 1,
subjectType: 'ContentElement',
subjectId: 10,
comments: [{id: 1, body: 'Existing comment', createdAt: '2026-01-01T00:00:00Z'}]
}]
})
});

const user = userEvent.setup();
const {getByRole, getByPlaceholderText} = renderInEntry(<Entry />, {
seed: {
contentElements: [{
typeName: 'withTestId',
permaId: 10,
configuration: {testId: 5}
}]
}
});

await user.click(getByRole('button', {name: 'Add comment'}));
await user.click(getByRole('button', {name: 'Select to comment'}));

expect(getByPlaceholderText('Add a comment...')).toBeInTheDocument();
});

it('closes popover when cancelling new thread form on element without threads', async () => {
const user = userEvent.setup();
const {getByRole, queryByRole} = renderInEntry(<Entry />, {
seed: {
contentElements: [{
typeName: 'withTestId',
configuration: {testId: 5}
}]
}
});

await user.click(getByRole('button', {name: 'Add comment'}));
await user.click(getByRole('button', {name: 'Select to comment'}));
await user.click(getByRole('button', {name: 'Cancel'}));

expect(queryByRole('status')).not.toBeInTheDocument();
});

it('exits add comment mode when overlay is clicked', async () => {
const user = userEvent.setup();
const {getByRole, queryByRole} = renderInEntry(<Entry />, {
seed: {
contentElements: [{
typeName: 'withTestId',
configuration: {testId: 5}
}]
}
});

await user.click(getByRole('button', {name: 'Add comment'}));
expect(getByRole('button', {name: 'Select to comment'})).toBeInTheDocument();

await user.click(getByRole('button', {name: 'Select to comment'}));

expect(queryByRole('button', {name: 'Select to comment'})).not.toBeInTheDocument();
});

it('deactivates when clicking toggle button again', async () => {
const user = userEvent.setup();
const {getByRole, queryByRole} = renderInEntry(<Entry />, {
seed: {
contentElements: [{
typeName: 'withTestId',
configuration: {testId: 5}
}]
}
});

await user.click(getByRole('button', {name: 'Add comment'}));
expect(getByRole('button', {name: 'Select to comment'})).toBeInTheDocument();

await user.click(getByRole('button', {name: 'Cancel add comment'}));

expect(queryByRole('button', {name: 'Select to comment'})).not.toBeInTheDocument();
});

it('exits add comment mode when clicking outside', async () => {
const user = userEvent.setup();
const {getByRole, queryByRole} = renderInEntry(<Entry />, {
seed: {
contentElements: [{
typeName: 'withTestId',
configuration: {testId: 5}
}]
}
});

await user.click(getByRole('button', {name: 'Add comment'}));
expect(getByRole('button', {name: 'Select to comment'})).toBeInTheDocument();

await user.click(document.body);

expect(queryByRole('button', {name: 'Select to comment'})).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import React from 'react';
import '@testing-library/jest-dom/extend-expect';
import {act, waitFor} from '@testing-library/react';
import {act, fireEvent, waitFor} from '@testing-library/react';

import {Entry} from 'frontend/Entry';
import {usePageObjects} from 'support/pageObjects';
import {renderInEntry} from 'support';
import {clearExtensions} from 'frontend/extensions';
import {loadCommentingComponents} from 'frontend/commenting';

describe('commenting mode', () => {
describe('commenting badges', () => {
usePageObjects();

beforeEach(() => {
Expand Down Expand Up @@ -49,4 +49,36 @@ describe('commenting mode', () => {
expect(getByRole('status')).toBeInTheDocument();
});
});

it('shows thread list when clicking badge', async () => {
window.fetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({
currentUser: {id: 42, name: 'Alice'},
commentThreads: [
{id: 1, subjectType: 'ContentElement', subjectId: 1, comments: [
{id: 10, body: 'Nice work', creatorName: 'Bob', creatorId: 2}
]}
]
})
});

const {getByRole, getByText} = renderInEntry(<Entry />, {
seed: {
contentElements: [{
typeName: 'withTestId',
configuration: {testId: 5}
}]
}
});

await waitFor(() => {
expect(getByRole('status')).toBeInTheDocument();
});

fireEvent.click(getByRole('status'));

expect(getByText('Nice work')).toBeInTheDocument();
expect(getByText('Bob')).toBeInTheDocument();
});
});
Loading
Loading