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
4 changes: 3 additions & 1 deletion app/assets/stylesheets/pageflow/ui/properties.scss
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@
--ui-box-shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);

--ui-accent-color: #ffd24d;
--ui-accent-color-glow: #ffd24d3d;
--ui-accent-color-light: hsla(44, 100%, 65%, 0.6);
--ui-accent-color-lighter: hsla(44, 100%, 65%, 0.3);
--ui-accent-color-lightest: hsla(44, 100%, 65%, 0.1);
--ui-on-accent-color: #795f0f;

--ui-on-button-color: var(--ui-primary-color);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ def update
private

def thread_params
params.require(:comment_thread).permit(:subject_type, :subject_id)
permitted = params.require(:comment_thread).permit(:subject_type, :subject_id)
permitted[:subject_range] = params[:comment_thread][:subject_range]&.permit!
permitted
end
end
end
Expand Down
2 changes: 2 additions & 0 deletions app/models/pageflow/comment_thread.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ module Pageflow
class CommentThread < ApplicationRecord
include RevisionComponent

serialize :subject_range, coder: JSON

belongs_to :creator, class_name: 'User'
belongs_to :resolver, class_name: 'User', foreign_key: :resolved_by_id, optional: true
has_many :comments, dependent: :destroy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ json.call(comment_thread,
:perma_id,
:subject_type,
:subject_id,
:subject_range,
:creator_id,
:resolved_at,
:created_at,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddSubjectRangeToCommentThreads < ActiveRecord::Migration[7.1]
def change
add_column :pageflow_comment_threads, :subject_range, :text
end
end
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 @@ -1915,6 +1915,7 @@ de:
add_comment: Kommentar hinzufügen
cancel_add_comment: Abbrechen
select_content_element: Zum Kommentieren auswählen
select_text_to_comment: Text zum Kommentieren auswählen
new_topic: Neues Thema
add_comment_placeholder: Kommentar hinzufügen...
reply_placeholder: Antworten...
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 @@ -1744,6 +1744,7 @@ en:
add_comment: Add comment
cancel_add_comment: Cancel
select_content_element: Select to comment
select_text_to_comment: Select text to comment
new_topic: New topic
add_comment_placeholder: Add a comment...
reply_placeholder: Reply...
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import '@testing-library/jest-dom/extend-expect';
import {render} from '@testing-library/react';
import {useFakeTranslations} from 'pageflow/testHelpers';

import {AddCommentHint} from 'frontend/commenting/AddCommentHint';

describe('AddCommentHint', () => {
useFakeTranslations({
'pageflow_scrolled.review.select_text_to_comment': 'Select text to comment'
});

it('renders hint text', () => {
const {getByText} = render(<AddCommentHint />);

expect(getByText('Select text to comment')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import React from 'react';
import '@testing-library/jest-dom/extend-expect';
import userEvent from '@testing-library/user-event';
import {useFakeTranslations} from 'pageflow/testHelpers';

import {EditableText} from 'frontend/commenting/EditableText';
import {renderWithCommenting} from 'testHelpers/renderWithCommenting';
import {slateSelection} from 'frontend/commenting/slateSelection';

import {commentHighlightStyles as highlightStyles} from 'pageflow-scrolled/review';
import commentingStyles from 'frontend/commenting/EditableTextHighlight.module.css';

jest.mock('frontend/commenting/slateSelection');

describe('commenting EditableText', () => {
useFakeTranslations({
'pageflow_scrolled.review.select_text_to_comment': 'Select text 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'
});

const value = [{type: 'paragraph', children: [{text: 'Some text to comment on'}]}];

it('renders plain text without commenting UI when inlineComments is not set', () => {
const {getByText, queryByText, toggleAddCommentMode} = renderWithCommenting(
<EditableText value={value} />,
{inlineComments: false}
);

toggleAddCommentMode();

expect(getByText('Some text to comment on')).toBeInTheDocument();
expect(queryByText('Select text to comment')).not.toBeInTheDocument();
});

it('shows hint in add comment mode', () => {
const {getByText, toggleAddCommentMode} = renderWithCommenting(
<EditableText value={value} />
);

toggleAddCommentMode();

expect(getByText('Select text to comment')).toBeInTheDocument();
});

it('highlights selected range on mouseup in add comment mode', async () => {
const user = userEvent.setup();
const slateRange = {
anchor: {path: [0, 0], offset: 5},
focus: {path: [0, 0], offset: 9}
};

const {getByText, toggleAddCommentMode} = renderWithCommenting(
<EditableText value={value} />
);

toggleAddCommentMode();
await slateSelection.simulateChange(user, getByText('Some text to comment on'), slateRange);

const highlight = document.querySelector(`.${highlightStyles.highlight}`);
expect(highlight).toBeInTheDocument();
expect(highlight).toHaveTextContent('text');
});

it('hides hint after selection is made', async () => {
const user = userEvent.setup();
const slateRange = {
anchor: {path: [0, 0], offset: 5},
focus: {path: [0, 0], offset: 9}
};

const {getByText, queryByText, toggleAddCommentMode} = renderWithCommenting(
<EditableText value={value} />
);

toggleAddCommentMode();
await slateSelection.simulateChange(user, getByText('Some text to comment on'), slateRange);

expect(queryByText('Select text to comment')).not.toBeInTheDocument();
});

it('shows new thread form after selecting text in add comment mode', async () => {
const user = userEvent.setup();
const slateRange = {
anchor: {path: [0, 0], offset: 5},
focus: {path: [0, 0], offset: 9}
};

const {getByText, getByPlaceholderText, toggleAddCommentMode} = renderWithCommenting(
<EditableText value={value} />
);

toggleAddCommentMode();
await slateSelection.simulateChange(user, getByText('Some text to comment on'), slateRange);

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

it('skips hint and highlights immediately when text is already selected', async () => {
const user = userEvent.setup();
const slateRange = {
anchor: {path: [0, 0], offset: 5},
focus: {path: [0, 0], offset: 9}
};

const {getByText, queryByText, toggleAddCommentMode} = renderWithCommenting(
<EditableText value={value} />
);

await slateSelection.simulateChange(user, getByText('Some text to comment on'), slateRange);
toggleAddCommentMode();

expect(queryByText('Select text to comment')).not.toBeInTheDocument();

const highlight = document.querySelector(`.${highlightStyles.highlight}`);
expect(highlight).toBeInTheDocument();
expect(highlight).toHaveTextContent('text');
});

it('highlights thread ranges as clickable', () => {
const subjectRange = {
anchor: {path: [0, 0], offset: 5},
focus: {path: [0, 0], offset: 9}
};

const {container} = renderWithCommenting(
<EditableText value={value} />,
{
commentThreads: [{
id: 1,
subjectType: 'ContentElement',
subjectId: 10,
subjectRange,
comments: [{id: 1, body: 'A comment', creatorName: 'Alice', creatorId: 1}]
}]
}
);

const highlight = container.querySelector(`.${highlightStyles.highlight}`);
expect(highlight).toBeInTheDocument();
expect(highlight).toHaveTextContent('text');
expect(highlight).toHaveClass(commentingStyles.clickable);
});

it('shows badge for thread with subjectRange', () => {
const subjectRange = {
anchor: {path: [0, 0], offset: 5},
focus: {path: [0, 0], offset: 9}
};

const {getByRole} = renderWithCommenting(
<EditableText value={value} />,
{
commentThreads: [{
id: 1,
subjectType: 'ContentElement',
subjectId: 10,
subjectRange,
comments: [{id: 1, body: 'A comment', creatorName: 'Alice', creatorId: 1}]
}]
}
);

expect(getByRole('status')).toBeInTheDocument();
});

it('does not show new form when selecting range of existing thread', async () => {
const user = userEvent.setup();
const subjectRange = {
anchor: {path: [0, 0], offset: 5},
focus: {path: [0, 0], offset: 9}
};

const {container, queryByPlaceholderText, toggleAddCommentMode} = renderWithCommenting(
<EditableText value={value} />,
{
commentThreads: [{
id: 1,
subjectType: 'ContentElement',
subjectId: 10,
subjectRange,
comments: [{id: 1, body: 'A comment', creatorName: 'Alice', creatorId: 1}]
}]
}
);

toggleAddCommentMode();
await slateSelection.simulateChange(
user,
container.querySelector('[data-slate-editor]'),
subjectRange
);

expect(queryByPlaceholderText('Add a comment...')).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import userEvent from '@testing-library/user-event';
import {useFakeTranslations} from 'pageflow/testHelpers';

import {Entry} from 'frontend/Entry';
import {api} from 'frontend/api';
import {usePageObjects} from 'support/pageObjects';
import {renderInEntry} from 'support';
import {clearExtensions} from 'frontend/extensions';
Expand Down Expand Up @@ -170,6 +171,29 @@ describe('add comment mode', () => {
expect(queryByRole('button', {name: 'Select to comment'})).not.toBeInTheDocument();
});

it('does not show overlay on inlineComments elements', async () => {
api.contentElementTypes.register('withInlineComments', {
component: function WithInlineComments() {
return <div data-testid="inlineCommentsElement" />;
},
inlineComments: true
});

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

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

expect(queryAllByRole('button', {name: 'Select to comment'})).toHaveLength(1);
});

it('exits add comment mode when clicking outside', async () => {
const user = userEvent.setup();
const {getByRole, queryByRole} = renderInEntry(<Entry />, {
Expand Down
19 changes: 19 additions & 0 deletions entry_types/scrolled/package/spec/review/CommentBadge-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,25 @@ describe('CommentBadge', () => {
});
});

it('only counts threads matching subjectRange when provided', () => {
const subjectRange = {anchor: {path: [0, 0], offset: 5}, focus: {path: [0, 0], offset: 12}};

const {getByRole} = renderWithReviewState(
<CommentBadge subjectType="ContentElement" subjectId={10} subjectRange={subjectRange} />,
{
commentThreads: [
{id: 1, subjectType: 'ContentElement', subjectId: 10, subjectRange, comments: []},
{id: 2, subjectType: 'ContentElement', subjectId: 10, comments: []},
{id: 3, subjectType: 'ContentElement', subjectId: 10,
subjectRange: {anchor: {path: [1, 0], offset: 0}, focus: {path: [1, 0], offset: 5}},
comments: []}
]
}
);

expect(getByRole('status')).not.toHaveTextContent(/\d/);
});

describe('mode active', () => {
it('renders full pill even without threads', () => {
const {getByRole} = renderWithReviewState(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,28 @@ describe('ReviewMessageHandler', () => {
});
});

it('passes subjectRange through to session.createThread', async () => {
const session = fakeReviewSession();
const subjectRange = {anchor: {path: [0, 0], offset: 5}, focus: {path: [0, 0], offset: 12}};

ReviewMessageHandler.create({session, targetWindow: window});

window.dispatchEvent(new MessageEvent('message', {
data: {
type: 'CREATE_COMMENT_THREAD',
payload: {subjectType: 'CE', subjectId: 10, subjectRange, body: 'About this text'}
},
origin: window.location.origin,
source: window
}));

await new Promise(resolve => setTimeout(resolve, 0));

expect(session.createThread).toHaveBeenCalledWith(
expect.objectContaining({subjectRange})
);
});

it('calls session.createComment on CREATE_COMMENT message from targetWindow', async () => {
const session = fakeReviewSession();

Expand Down
Loading
Loading