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: 4 additions & 0 deletions app/assets/stylesheets/pageflow/ui/properties.scss
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@
--ui-box-shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
--ui-box-shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);

--ui-accent-color: #ffd24d;
--ui-accent-color-glow: #ffd24d3d;
--ui-on-accent-color: #795f0f;

--ui-on-button-color: var(--ui-primary-color);
--ui-button-border-color: var(--ui-primary-color-light);
--ui-button-hover-border-color: var(--ui-primary-color);
Expand Down
16 changes: 16 additions & 0 deletions app/controllers/pageflow/review/comment_threads_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module Pageflow
module Review
# @api private
class CommentThreadsController < Pageflow::ApplicationController
respond_to :json
before_action :authenticate_user!

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

@comment_threads = entry.comment_threads.includes(comments: :creator)
end
end
end
end
15 changes: 15 additions & 0 deletions app/models/pageflow/comment.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module Pageflow
# @api private
class Comment < ApplicationRecord
include NestedRevisionComponent

belongs_to :comment_thread
belongs_to :creator, class_name: 'User'

validates :body, presence: true

def entry_for_auto_generated_perma_id
comment_thread.revision.entry
end
end
end
13 changes: 13 additions & 0 deletions app/models/pageflow/comment_thread.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module Pageflow
# @api private
class CommentThread < ApplicationRecord
include RevisionComponent

belongs_to :creator, class_name: 'User'
has_many :comments, dependent: :destroy

nested_revision_components :comments

validates :subject_type, :subject_id, presence: true
end
end
1 change: 1 addition & 0 deletions app/models/pageflow/entry_at_revision.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def initialize(entry, revision, theme: nil)
:author, :publisher, :keywords,
:published_at,
:noindex?,
:comment_threads,
:configuration,
:structured_data_type_name,
to: :revision)
Expand Down
2 changes: 2 additions & 0 deletions app/models/pageflow/revision.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ class Revision < ApplicationRecord # rubocop:todo Style/Documentation
has_many :chapters, -> { order(CHAPTER_ORDER) }, through: :storylines
has_many :pages, -> { reorder(PAGE_ORDER) }, through: :storylines

has_many :comment_threads, dependent: :destroy

has_many :file_usages, dependent: :destroy

has_many :image_files, -> { extending WithFileUsageExtension },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
json.key_format!(camelize: :lower)

json.call(comment_thread,
:id,
:perma_id,
:subject_type,
:subject_id,
:creator_id,
:resolved_at,
:created_at,
:updated_at)

json.comments(comment_thread.comments) do |comment|
json.partial!('pageflow/review/comments/comment', comment:)
end
10 changes: 10 additions & 0 deletions app/views/pageflow/review/comment_threads/index.json.jbuilder
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
json.key_format!(camelize: :lower)

json.current_user do
json.id current_user.id
json.name current_user.full_name
end

json.comment_threads(@comment_threads) do |comment_thread|
json.partial!('pageflow/review/comment_threads/comment_thread', comment_thread:)
end
11 changes: 11 additions & 0 deletions app/views/pageflow/review/comments/_comment.json.jbuilder
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
json.key_format!(camelize: :lower)

json.call(comment,
:id,
:perma_id,
:creator_id,
:body,
:created_at,
:updated_at)

json.creator_name comment.creator.full_name
6 changes: 6 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@
resource :oembed, only: [:show]
end

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

root to: redirect('/admin')
end

Expand Down
30 changes: 30 additions & 0 deletions db/migrate/20260323000000_create_comment_threads_and_comments.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
class CreateCommentThreadsAndComments < ActiveRecord::Migration[6.0]
def change
create_table :pageflow_comment_threads do |t|
t.integer :revision_id, null: false
t.integer :perma_id
t.string :subject_type, null: false
t.integer :subject_id, null: false
t.integer :creator_id, null: false
t.datetime :resolved_at
t.integer :resolved_by_id
t.timestamps
end

add_index :pageflow_comment_threads, :revision_id
add_index :pageflow_comment_threads, :creator_id
add_index :pageflow_comment_threads, [:revision_id, :subject_type, :subject_id],
name: 'index_comment_threads_on_revision_and_subject'

create_table :pageflow_comments do |t|
t.integer :comment_thread_id, null: false
t.integer :perma_id
t.integer :creator_id, null: false
t.text :body, null: false
t.timestamps
end

add_index :pageflow_comments, :comment_thread_id
add_index :pageflow_comments, :creator_id
end
end
1 change: 1 addition & 0 deletions entry_types/scrolled/package/.eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
/contentElements-editor.js
/contentElements-frontend.js
/contentElements-frontend.css
/review.js
/testHelpers.js
/widgets
/.storybook/out/
Expand Down
2 changes: 2 additions & 0 deletions entry_types/scrolled/package/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
/contentElements-editor.js
/contentElements-frontend.js
/contentElements-frontend.css
/review.css
/review.js
/testHelpers.js
/widgets
/.storybook/out/
Expand Down
2 changes: 2 additions & 0 deletions entry_types/scrolled/package/.storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ module.exports = {
alias: {
...config.resolve.alias,
'pageflow/frontend': path.resolve(__dirname, '../../../../package/src/frontend'),
'pageflow/review': path.resolve(__dirname, '../../../../package/src/review'),
'pageflow-scrolled/frontend': path.resolve(__dirname, '../src/frontend'),
'pageflow-scrolled/review': path.resolve(__dirname, '../src/review'),
'pageflow-scrolled/testHelpers': path.resolve(__dirname, '../src/testHelpers')
}
}
Expand Down
5 changes: 4 additions & 1 deletion entry_types/scrolled/package/config/webpack.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ module.exports = {
import: ['pageflow-scrolled/frontend/inlineEditing.css']
},
'pageflow-scrolled-frontend-commenting': {
import: ['pageflow-scrolled/frontend/commenting.css']
import: [
'pageflow-scrolled/frontend/commenting.css',
'pageflow-scrolled/review.css'
]
}
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react';
import '@testing-library/jest-dom/extend-expect';
import {act, 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', () => {
usePageObjects();

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

loadCommentingComponents();
});

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

it('fetches threads from API and displays badge', async () => {
window.fetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({
currentUser: {id: 42, name: 'Alice'},
commentThreads: [
{id: 1, subjectType: 'ContentElement', subjectId: 1, comments: []}
]
})
});

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

await waitFor(() => {
expect(getByRole('status')).toBeInTheDocument();
});
});
});
45 changes: 45 additions & 0 deletions entry_types/scrolled/package/spec/review/CommentBadge-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react';
import '@testing-library/jest-dom/extend-expect';
import {render, act} from '@testing-library/react';

import {ReviewStateProvider} from 'review/ReviewStateProvider';
import {CommentBadge} from 'review/CommentBadge';

describe('CommentBadge', () => {
it('displays thread count for subject', () => {
const {getByRole} = render(
<ReviewStateProvider>
<CommentBadge subjectType="ContentElement" subjectId={10} />
</ReviewStateProvider>
);

act(() => {
window.dispatchEvent(new MessageEvent('message', {
data: {
type: 'REVIEW_STATE_RESET',
payload: {
currentUser: {id: 1},
commentThreads: [
{id: 1, subjectType: 'ContentElement', subjectId: 10, comments: []},
{id: 2, subjectType: 'ContentElement', subjectId: 10, comments: []},
{id: 3, subjectType: 'ContentElement', subjectId: 20, comments: []}
]
}
},
origin: window.location.origin
}));
});

expect(getByRole('status')).toHaveTextContent('2');
});

it('renders nothing when no threads exist for subject', () => {
const {container} = render(
<ReviewStateProvider>
<CommentBadge subjectType="ContentElement" subjectId={10} />
</ReviewStateProvider>
);

expect(container).toBeEmptyDOMElement();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import BackboneEvents from 'backbone-events-standalone';

import {ReviewMessageHandler} from 'review/ReviewMessageHandler';

function fakeReviewSession() {
const session = {};
Object.assign(session, BackboneEvents);
return session;
}

describe('ReviewMessageHandler', () => {
it('posts REVIEW_STATE_RESET to target window on session reset', () => {
const session = fakeReviewSession();
const targetWindow = {postMessage: jest.fn()};

ReviewMessageHandler.create({session, targetWindow});

const state = {currentUser: {id: 42}, commentThreads: [{id: 1}]};
session.trigger('reset', state);

expect(targetWindow.postMessage).toHaveBeenCalledWith(
{type: 'REVIEW_STATE_RESET', payload: state},
window.location.origin
);
});

it('posts REVIEW_STATE_THREAD_CHANGE to target window on session change:thread', () => {
const session = fakeReviewSession();
const targetWindow = {postMessage: jest.fn()};

ReviewMessageHandler.create({session, targetWindow});

const thread = {id: 1, comments: [{body: 'Hello'}]};
session.trigger('change:thread', thread);

expect(targetWindow.postMessage).toHaveBeenCalledWith(
{type: 'REVIEW_STATE_THREAD_CHANGE', payload: thread},
window.location.origin
);
});

it('can be disposed', () => {
const session = fakeReviewSession();
const targetWindow = {postMessage: jest.fn()};

const handler = ReviewMessageHandler.create({session, targetWindow});
handler.dispose();

session.trigger('reset', {});

expect(targetWindow.postMessage).not.toHaveBeenCalled();
});
});
Loading
Loading