Skip to content

Commit 0dd2585

Browse files
authored
Merge pull request #2395 from tf/preview-commenting
Preview commenting
2 parents 3fb5732 + e1640f7 commit 0dd2585

File tree

59 files changed

+2138
-73
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+2138
-73
lines changed

app/controllers/pageflow/review/comment_threads_controller.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,28 @@ def index
1111

1212
@comment_threads = entry.comment_threads.includes(comments: :creator)
1313
end
14+
15+
def create
16+
entry = DraftEntry.find(params[:entry_id])
17+
authorize!(:read, entry.to_model)
18+
19+
@comment_thread = entry.comment_threads.build(thread_params)
20+
@comment_thread.creator = current_user
21+
22+
@comment_thread.comments.build(
23+
body: params[:comment_thread][:comment][:body],
24+
creator: current_user
25+
)
26+
27+
@comment_thread.save!
28+
render :create, status: :created
29+
end
30+
31+
private
32+
33+
def thread_params
34+
params.require(:comment_thread).permit(:subject_type, :subject_id)
35+
end
1436
end
1537
end
1638
end
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
module Pageflow
2+
module Review
3+
# @api private
4+
class CommentsController < Pageflow::ApplicationController
5+
respond_to :json
6+
before_action :authenticate_user!
7+
8+
def create
9+
entry = DraftEntry.find(params[:entry_id])
10+
authorize!(:read, entry.to_model)
11+
12+
thread = entry.comment_threads.find(params[:comment_thread_id])
13+
@comment = thread.comments.build(comment_params)
14+
@comment.creator = current_user
15+
@comment.save!
16+
17+
render :create, status: :created
18+
end
19+
20+
private
21+
22+
def comment_params
23+
params.require(:comment).permit(:body)
24+
end
25+
end
26+
end
27+
end
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
json.partial!('pageflow/review/comment_threads/comment_thread',
2+
comment_thread: @comment_thread)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
json.partial!('pageflow/review/comments/comment', comment: @comment)

config/routes.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@
7979

8080
namespace :review do
8181
resources :entries, only: [] do
82-
resources :comment_threads, only: [:index]
82+
resources :comment_threads, only: [:index, :create] do
83+
resources :comments, only: [:create]
84+
end
8385
end
8486
end
8587

entry_types/scrolled/app/views/pageflow_scrolled/entries/show.html.erb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
<meta charset="utf-8" />
88
<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">
99
<meta name="robots" content="max-image-preview:large">
10+
<% if seed_options[:load_commenting] %>
11+
<%= csrf_meta_tags %>
12+
<% end %>
1013

1114
<%= social_share_meta_tags_for(entry) %>
1215
<%= meta_tags_for_entry(entry) %>

entry_types/scrolled/bin/rspec-with-retry-on-timeout

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
#!/bin/bash
22

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

88
for i in {1..10}; do
9-
timeout --signal=KILL 30 bin/rspec $@
9+
timeout --signal=KILL 45 bin/rspec $@
1010
e=$?
1111
[[ $e -gt 100 ]] && echo Timeout || exit $e;
1212
done;

entry_types/scrolled/config/locales/de.yml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1912,4 +1912,16 @@ de:
19121912
content_element_margin_bottom: Unterer Außenabstand
19131913
expose_motif_area: Motiv freilegen
19141914
review:
1915-
toolbar_label: Kommentare
1915+
add_comment: Kommentar hinzufügen
1916+
cancel_add_comment: Abbrechen
1917+
select_content_element: Zum Kommentieren auswählen
1918+
new_topic: Neues Thema
1919+
add_comment_placeholder: Kommentar hinzufügen...
1920+
reply_placeholder: Antworten...
1921+
cancel: Abbrechen
1922+
send: Senden
1923+
enter_for_new_line: Enter für neue Zeile
1924+
toggle_replies: Antworten umschalten
1925+
reply_count:
1926+
one: 1 Antwort
1927+
other: '%{count} Antworten'

entry_types/scrolled/config/locales/en.yml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1741,4 +1741,16 @@ en:
17411741
content_element_margin_bottom: Bottom margin
17421742
expose_motif_area: Expose motif
17431743
review:
1744-
toolbar_label: Comments
1744+
add_comment: Add comment
1745+
cancel_add_comment: Cancel
1746+
select_content_element: Select to comment
1747+
new_topic: New topic
1748+
add_comment_placeholder: Add a comment...
1749+
reply_placeholder: Reply...
1750+
cancel: Cancel
1751+
send: Send
1752+
enter_for_new_line: Enter for new line
1753+
toggle_replies: Toggle replies
1754+
reply_count:
1755+
one: 1 reply
1756+
other: '%{count} replies'
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import React from 'react';
2+
import '@testing-library/jest-dom/extend-expect';
3+
import {act} from '@testing-library/react';
4+
import userEvent from '@testing-library/user-event';
5+
import {useFakeTranslations} from 'pageflow/testHelpers';
6+
7+
import {Entry} from 'frontend/Entry';
8+
import {usePageObjects} from 'support/pageObjects';
9+
import {renderInEntry} from 'support';
10+
import {clearExtensions} from 'frontend/extensions';
11+
import {loadCommentingComponents} from 'frontend/commenting';
12+
13+
describe('add comment mode', () => {
14+
usePageObjects();
15+
16+
useFakeTranslations({
17+
'pageflow_scrolled.review.add_comment': 'Add comment',
18+
'pageflow_scrolled.review.select_content_element': 'Select to comment',
19+
'pageflow_scrolled.review.add_comment_placeholder': 'Add a comment...',
20+
'pageflow_scrolled.review.new_topic': 'New topic',
21+
'pageflow_scrolled.review.send': 'Send',
22+
'pageflow_scrolled.review.cancel': 'Cancel',
23+
'pageflow_scrolled.review.cancel_add_comment': 'Cancel add comment'
24+
});
25+
26+
beforeEach(() => {
27+
jest.spyOn(window, 'fetch').mockResolvedValue({
28+
ok: true,
29+
json: () => Promise.resolve({currentUser: null, commentThreads: []})
30+
});
31+
32+
loadCommentingComponents();
33+
});
34+
35+
afterEach(() => {
36+
act(() => clearExtensions());
37+
window.fetch.mockRestore();
38+
});
39+
40+
it('renders add comment button', () => {
41+
const {getByRole} = renderInEntry(<Entry />, {
42+
seed: {
43+
contentElements: [{
44+
typeName: 'withTestId',
45+
configuration: {testId: 5}
46+
}]
47+
}
48+
});
49+
50+
expect(getByRole('button', {name: 'Add comment'})).toBeInTheDocument();
51+
});
52+
53+
it('shows highlight overlays on content elements when activated', async () => {
54+
const user = userEvent.setup();
55+
const {getByRole} = renderInEntry(<Entry />, {
56+
seed: {
57+
contentElements: [{
58+
typeName: 'withTestId',
59+
configuration: {testId: 5}
60+
}]
61+
}
62+
});
63+
64+
await user.click(getByRole('button', {name: 'Add comment'}));
65+
66+
expect(getByRole('button', {name: 'Select to comment'})).toBeInTheDocument();
67+
});
68+
69+
it('opens new thread form when selecting element', async () => {
70+
const user = userEvent.setup();
71+
const {getByRole, getByPlaceholderText} = renderInEntry(<Entry />, {
72+
seed: {
73+
contentElements: [{
74+
typeName: 'withTestId',
75+
configuration: {testId: 5}
76+
}]
77+
}
78+
});
79+
80+
await user.click(getByRole('button', {name: 'Add comment'}));
81+
await user.click(getByRole('button', {name: 'Select to comment'}));
82+
83+
expect(getByPlaceholderText('Add a comment...')).toBeInTheDocument();
84+
});
85+
86+
it('auto expands new thread form when selecting element with existing threads', async () => {
87+
window.fetch.mockResolvedValue({
88+
ok: true,
89+
json: () => Promise.resolve({
90+
currentUser: null,
91+
commentThreads: [{
92+
id: 1,
93+
subjectType: 'ContentElement',
94+
subjectId: 10,
95+
comments: [{id: 1, body: 'Existing comment', createdAt: '2026-01-01T00:00:00Z'}]
96+
}]
97+
})
98+
});
99+
100+
const user = userEvent.setup();
101+
const {getByRole, getByPlaceholderText} = renderInEntry(<Entry />, {
102+
seed: {
103+
contentElements: [{
104+
typeName: 'withTestId',
105+
permaId: 10,
106+
configuration: {testId: 5}
107+
}]
108+
}
109+
});
110+
111+
await user.click(getByRole('button', {name: 'Add comment'}));
112+
await user.click(getByRole('button', {name: 'Select to comment'}));
113+
114+
expect(getByPlaceholderText('Add a comment...')).toBeInTheDocument();
115+
});
116+
117+
it('closes popover when cancelling new thread form on element without threads', async () => {
118+
const user = userEvent.setup();
119+
const {getByRole, queryByRole} = renderInEntry(<Entry />, {
120+
seed: {
121+
contentElements: [{
122+
typeName: 'withTestId',
123+
configuration: {testId: 5}
124+
}]
125+
}
126+
});
127+
128+
await user.click(getByRole('button', {name: 'Add comment'}));
129+
await user.click(getByRole('button', {name: 'Select to comment'}));
130+
await user.click(getByRole('button', {name: 'Cancel'}));
131+
132+
expect(queryByRole('status')).not.toBeInTheDocument();
133+
});
134+
135+
it('exits add comment mode when overlay is clicked', async () => {
136+
const user = userEvent.setup();
137+
const {getByRole, queryByRole} = renderInEntry(<Entry />, {
138+
seed: {
139+
contentElements: [{
140+
typeName: 'withTestId',
141+
configuration: {testId: 5}
142+
}]
143+
}
144+
});
145+
146+
await user.click(getByRole('button', {name: 'Add comment'}));
147+
expect(getByRole('button', {name: 'Select to comment'})).toBeInTheDocument();
148+
149+
await user.click(getByRole('button', {name: 'Select to comment'}));
150+
151+
expect(queryByRole('button', {name: 'Select to comment'})).not.toBeInTheDocument();
152+
});
153+
154+
it('deactivates when clicking toggle button again', async () => {
155+
const user = userEvent.setup();
156+
const {getByRole, queryByRole} = renderInEntry(<Entry />, {
157+
seed: {
158+
contentElements: [{
159+
typeName: 'withTestId',
160+
configuration: {testId: 5}
161+
}]
162+
}
163+
});
164+
165+
await user.click(getByRole('button', {name: 'Add comment'}));
166+
expect(getByRole('button', {name: 'Select to comment'})).toBeInTheDocument();
167+
168+
await user.click(getByRole('button', {name: 'Cancel add comment'}));
169+
170+
expect(queryByRole('button', {name: 'Select to comment'})).not.toBeInTheDocument();
171+
});
172+
173+
it('exits add comment mode when clicking outside', async () => {
174+
const user = userEvent.setup();
175+
const {getByRole, queryByRole} = renderInEntry(<Entry />, {
176+
seed: {
177+
contentElements: [{
178+
typeName: 'withTestId',
179+
configuration: {testId: 5}
180+
}]
181+
}
182+
});
183+
184+
await user.click(getByRole('button', {name: 'Add comment'}));
185+
expect(getByRole('button', {name: 'Select to comment'})).toBeInTheDocument();
186+
187+
await user.click(document.body);
188+
189+
expect(queryByRole('button', {name: 'Select to comment'})).not.toBeInTheDocument();
190+
});
191+
});

0 commit comments

Comments
 (0)