diff --git a/entry_types/scrolled/app/helpers/pageflow_scrolled/packs_helper.rb b/entry_types/scrolled/app/helpers/pageflow_scrolled/packs_helper.rb index f16f3d6a58..67de4ac544 100644 --- a/entry_types/scrolled/app/helpers/pageflow_scrolled/packs_helper.rb +++ b/entry_types/scrolled/app/helpers/pageflow_scrolled/packs_helper.rb @@ -50,7 +50,8 @@ def scrolled_editor_packs(entry) end def scrolled_editor_stylesheet_packs(entry) - Pageflow.config_for(entry).additional_editor_packs.stylesheet_paths(entry) + + ['pageflow-scrolled-editor'] + + Pageflow.config_for(entry).additional_editor_packs.stylesheet_paths(entry) + Pageflow.config_for(entry).additional_frontend_packs.paths(entry) end diff --git a/entry_types/scrolled/package/.gitignore b/entry_types/scrolled/package/.gitignore index 1872c41511..47075eb0a6 100644 --- a/entry_types/scrolled/package/.gitignore +++ b/entry_types/scrolled/package/.gitignore @@ -1,6 +1,7 @@ /node_modules /contentElements /editor.js +/editor.css /frontend /frontend-server.js /contentElements-editor.js diff --git a/entry_types/scrolled/package/config/webpack.js b/entry_types/scrolled/package/config/webpack.js index a2b7e461c5..931aca8438 100644 --- a/entry_types/scrolled/package/config/webpack.js +++ b/entry_types/scrolled/package/config/webpack.js @@ -80,7 +80,10 @@ module.exports = { ] }, 'pageflow-scrolled-frontend-inlineEditing': { - import: ['pageflow-scrolled/frontend/inlineEditing.css'] + import: [ + 'pageflow-scrolled/frontend/inlineEditing.css', + 'pageflow-scrolled/review.css' + ] }, 'pageflow-scrolled-frontend-commenting': { import: [ diff --git a/entry_types/scrolled/package/jest.config.js b/entry_types/scrolled/package/jest.config.js index 57167f2ded..269d8a3383 100644 --- a/entry_types/scrolled/package/jest.config.js +++ b/entry_types/scrolled/package/jest.config.js @@ -22,6 +22,8 @@ module.exports = { moduleNameMapper: { '^pageflow-scrolled/contentElements-frontend$': '/src/contentElements/frontend', + "^pageflow-scrolled/editor\\.css$": "/spec/support/jest/editor-css-stub", + "^pageflow-scrolled/review\\.css$": "/spec/support/jest/review-css-stub", "^pageflow-scrolled/([^/]*)$": "/src/$1", // Make specs run even if ignored json file is not present diff --git a/entry_types/scrolled/package/spec/editor/controllers/PreviewMessageController-spec.js b/entry_types/scrolled/package/spec/editor/controllers/PreviewMessageController-spec.js index 5d203bc1a0..9d472225cc 100644 --- a/entry_types/scrolled/package/spec/editor/controllers/PreviewMessageController-spec.js +++ b/entry_types/scrolled/package/spec/editor/controllers/PreviewMessageController-spec.js @@ -1,5 +1,6 @@ import 'editor/config'; import {editor} from 'pageflow-scrolled/editor'; +import {features} from 'pageflow/frontend'; import {ScrolledEntry} from 'editor/models/ScrolledEntry'; import {PreviewMessageController} from 'editor/controllers/PreviewMessageController'; import {InsertContentElementDialogView} from 'editor/views/InsertContentElementDialogView'; @@ -47,6 +48,31 @@ describe('PreviewMessageController', () => { })).resolves.toMatchObject({type: 'ACK'}); }); + it('sends REVIEW_STATE_RESET to iframe after READY when commenting enabled', () => { + features.enable('frontend', ['commenting']); + jest.spyOn(window, 'fetch').mockResolvedValue({ + ok: true, + json: () => Promise.resolve({currentUser: {id: 1}, commentThreads: []}) + }); + + const entry = factories.entry(ScrolledEntry, {}, {entryTypeSeed: normalizeSeed()}); + const iframeWindow = createIframeWindow(); + + controller = new PreviewMessageController({entry, iframeWindow}); + + return expect(new Promise(resolve => { + iframeWindow.addEventListener('message', event => { + if (event.data.type === 'REVIEW_STATE_RESET') { + resolve(event.data); + } + }); + window.postMessage({type: 'READY'}, '*'); + })).resolves.toMatchObject({ + type: 'REVIEW_STATE_RESET', + payload: {currentUser: {id: 1}, commentThreads: []} + }); + }); + it('sets current section index in model on CHANGE_SECTION message', () => { const entry = factories.entry(ScrolledEntry, {}, {entryTypeSeed: normalizeSeed()}); const iframeWindow = createIframeWindow(); @@ -469,6 +495,22 @@ describe('PreviewMessageController', () => { })).resolves.toBe('/'); }); + it('navigates to comments route on SELECTED message for contentElementComments', () => { + const editor = factories.editorApi(); + const entry = factories.entry(ScrolledEntry, {}, { + entryTypeSeed: normalizeSeed({ + contentElements: [{id: 1}] + }) + }); + const iframeWindow = createIframeWindow(); + controller = new PreviewMessageController({entry, iframeWindow, editor}); + + return expect(new Promise(resolve => { + editor.on('navigate', resolve); + window.postMessage({type: 'SELECTED', payload: {id: 1, type: 'contentElementComments'}}, '*'); + })).resolves.toBe('/scrolled/content_elements/1/comments'); + }); + it('updates configuration on UPDATE_CONTENT_ELEMENT message', () => { const entry = factories.entry(ScrolledEntry, {}, { entryTypeSeed: normalizeSeed({ diff --git a/entry_types/scrolled/package/spec/editor/views/ContentElementCommentsView-spec.js b/entry_types/scrolled/package/spec/editor/views/ContentElementCommentsView-spec.js new file mode 100644 index 0000000000..435c1287f9 --- /dev/null +++ b/entry_types/scrolled/package/spec/editor/views/ContentElementCommentsView-spec.js @@ -0,0 +1,87 @@ +import '@testing-library/jest-dom/extend-expect'; +import BackboneEvents from 'backbone-events-standalone'; + +import {ContentElementCommentsView} from 'editor/views/ContentElementCommentsView'; + +import {useFakeTranslations, renderBackboneView} from 'pageflow/testHelpers'; +import {useEditorGlobals} from 'support'; +import {act, waitFor} from '@testing-library/react'; + +describe('ContentElementCommentsView', () => { + const {createEntry} = useEditorGlobals(); + + useFakeTranslations({ + 'pageflow_scrolled.review.add_comment_placeholder': 'Add a comment...', + 'pageflow_scrolled.review.new_topic': 'New topic', + 'pageflow_scrolled.review.send': 'Send' + }); + + it('displays threads from session state', () => { + const entry = createEntry({ + contentElements: [{id: 1, permaId: 10, typeName: 'textBlock'}] + }); + entry.reviewSession = fakeReviewSession({ + currentUser: {id: 1}, + commentThreads: [{ + id: 1, + subjectType: 'ContentElement', + subjectId: 10, + comments: [{id: 100, body: 'Looks good', creatorName: 'Alice'}] + }] + }); + + const view = new ContentElementCommentsView({ + entry, + model: entry.contentElements.get(1), + editor: {} + }); + + const {getByText} = renderBackboneView(view); + view.onShow(); + + expect(getByText('Looks good')).toBeInTheDocument(); + }); + + it('updates when session emits change:thread', async () => { + const entry = createEntry({ + contentElements: [{id: 1, permaId: 10, typeName: 'textBlock'}] + }); + entry.reviewSession = fakeReviewSession({ + currentUser: {id: 1}, + commentThreads: [] + }); + + const view = new ContentElementCommentsView({ + entry, + model: entry.contentElements.get(1), + editor: {} + }); + + const {getByText} = renderBackboneView(view); + act(() => view.onShow()); + + act(() => { + entry.reviewSession.trigger('change:thread', { + id: 1, + subjectType: 'ContentElement', + subjectId: 10, + comments: [{id: 100, body: 'New comment', creatorName: 'Bob'}] + }); + }); + + await waitFor(() => { + expect(getByText('New comment')).toBeInTheDocument(); + }); + }); +}); + +function fakeReviewSession(state = null) { + const session = { + state, + createThread: jest.fn().mockResolvedValue(), + createComment: jest.fn().mockResolvedValue() + }; + + Object.assign(session, BackboneEvents); + return session; +} diff --git a/entry_types/scrolled/package/spec/frontend/features/contentElementSelection-spec.js b/entry_types/scrolled/package/spec/frontend/features/contentElementSelection-spec.js index 2a0dbec8e2..335eec1cd7 100644 --- a/entry_types/scrolled/package/spec/frontend/features/contentElementSelection-spec.js +++ b/entry_types/scrolled/package/spec/frontend/features/contentElementSelection-spec.js @@ -1,11 +1,12 @@ import React from 'react'; import {frontend} from 'frontend'; +import {features} from 'pageflow/frontend'; import {useInlineEditingPageObjects, renderEntry} from 'support/pageObjects'; import {fakeParentWindow} from 'support'; import {changeLocationHash} from 'support/changeLocationHash'; import '@testing-library/jest-dom/extend-expect' -import {act} from '@testing-library/react'; +import {act, fireEvent, waitFor} from '@testing-library/react'; describe('content element selection', () => { useInlineEditingPageObjects(); @@ -99,4 +100,48 @@ describe('content element selection', () => { expect(mainContentElement.isSelected()).toBe(false); }); + + it('marks selection rect as selected when comment badge is clicked', async () => { + features.enable('frontend', ['commenting']); + + const {getByRole, getContentElementByTestId} = renderEntry({ + seed: { + contentElements: [{ + id: 1, + permaId: 10, + typeName: 'withTestId', + configuration: {testId: 5} + }] + } + }); + + act(() => { + window.dispatchEvent(new MessageEvent('message', { + data: { + type: 'REVIEW_STATE_RESET', + payload: { + currentUser: {id: 1}, + commentThreads: [{ + id: 1, + subjectType: 'ContentElement', + subjectId: 10, + comments: [{id: 100, body: 'Review this'}] + }] + } + }, + origin: window.location.origin + })); + }); + + await waitFor(() => { + expect(getByRole('status')).toBeInTheDocument(); + }); + + const contentElement = getContentElementByTestId(5); + expect(contentElement.isSelected()).toBe(false); + + fireEvent.click(getByRole('status')); + + expect(contentElement.isSelected()).toBe(true); + }); }); diff --git a/entry_types/scrolled/package/spec/frontend/features/editorCommentBadges-spec.js b/entry_types/scrolled/package/spec/frontend/features/editorCommentBadges-spec.js new file mode 100644 index 0000000000..914fbd536b --- /dev/null +++ b/entry_types/scrolled/package/spec/frontend/features/editorCommentBadges-spec.js @@ -0,0 +1,65 @@ +import '@testing-library/jest-dom/extend-expect'; +import {act, waitFor} from '@testing-library/react'; +import {features} from 'pageflow/frontend'; + +import {useInlineEditingPageObjects, renderEntry} from 'support/pageObjects'; +import {fakeParentWindow} from 'support'; + +describe('editor comment badges', () => { + useInlineEditingPageObjects(); + + beforeEach(() => { + fakeParentWindow(); + window.parent.postMessage = jest.fn(); + features.enable('frontend', ['commenting']); + }); + + it('does not display comment icon when element is not selected', () => { + const {queryByRole} = renderEntry({ + seed: { + contentElements: [{ + typeName: 'withTestId', + permaId: 10, + configuration: {testId: 5} + }] + } + }); + + expect(queryByRole('status')).not.toBeInTheDocument(); + }); + + it('displays dot badge when threads exist and element is not selected', async () => { + const {getByRole} = renderEntry({ + seed: { + contentElements: [{ + typeName: 'withTestId', + permaId: 10, + configuration: {testId: 5} + }] + } + }); + + act(() => { + window.dispatchEvent(new MessageEvent('message', { + data: { + type: 'REVIEW_STATE_RESET', + payload: { + currentUser: {id: 1}, + commentThreads: [{ + id: 1, + subjectType: 'ContentElement', + subjectId: 10, + comments: [{id: 100, body: 'Review this'}] + }] + } + }, + origin: window.location.origin + })); + }); + + await waitFor(() => { + expect(getByRole('status')).toBeInTheDocument(); + expect(getByRole('status')).not.toHaveTextContent(/\d/); + }); + }); +}); diff --git a/entry_types/scrolled/package/spec/frontend/features/selectedMessage-spec.js b/entry_types/scrolled/package/spec/frontend/features/selectedMessage-spec.js index b3510fc89d..67127c41f3 100644 --- a/entry_types/scrolled/package/spec/frontend/features/selectedMessage-spec.js +++ b/entry_types/scrolled/package/spec/frontend/features/selectedMessage-spec.js @@ -1,9 +1,10 @@ import React from 'react'; import {frontend, WidgetSelectionRect} from 'frontend'; +import {features} from 'pageflow/frontend'; import {useInlineEditingPageObjects, renderEntry} from 'support/pageObjects'; import {fakeParentWindow} from 'support'; -import {fireEvent} from '@testing-library/react'; +import {act, fireEvent, waitFor} from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect' describe('SELECTED message', () => { @@ -185,4 +186,43 @@ describe('SELECTED message', () => { payload: {id: 'header', type: 'widget'} }, expect.anything()); }); + + it('is posted with type contentElementComments when comment badge is clicked', async () => { + features.enable('frontend', ['commenting']); + + const {getByRole} = renderEntry({ + seed: { + contentElements: [{id: 1, permaId: 10, typeName: 'withTestId', configuration: {testId: 5}}] + } + }); + + act(() => { + window.dispatchEvent(new MessageEvent('message', { + data: { + type: 'REVIEW_STATE_RESET', + payload: { + currentUser: {id: 1}, + commentThreads: [{ + id: 1, + subjectType: 'ContentElement', + subjectId: 10, + comments: [{id: 100, body: 'Review this'}] + }] + } + }, + origin: window.location.origin + })); + }); + + await waitFor(() => { + expect(getByRole('status')).toBeInTheDocument(); + }); + + fireEvent.click(getByRole('status')); + + expect(window.parent.postMessage).toHaveBeenCalledWith({ + type: 'SELECTED', + payload: {id: 1, type: 'contentElementComments'} + }, expect.anything()); + }); }); diff --git a/entry_types/scrolled/package/spec/review/CommentBadge-spec.js b/entry_types/scrolled/package/spec/review/CommentBadge-spec.js index cfad9b4c09..187c17d058 100644 --- a/entry_types/scrolled/package/spec/review/CommentBadge-spec.js +++ b/entry_types/scrolled/package/spec/review/CommentBadge-spec.js @@ -5,6 +5,19 @@ import {CommentBadge} from 'review/CommentBadge'; import {renderWithReviewState} from 'testHelpers/renderWithReviewState'; describe('CommentBadge', () => { + it('does not display count for single thread', () => { + const {getByRole} = renderWithReviewState( + , + { + commentThreads: [ + {id: 1, subjectType: 'ContentElement', subjectId: 10, comments: []} + ] + } + ); + + expect(getByRole('status')).not.toHaveTextContent(/\d/); + }); + it('displays thread count for subject', () => { const {getByRole} = renderWithReviewState( , @@ -27,4 +40,63 @@ describe('CommentBadge', () => { expect(container).toBeEmptyDOMElement(); }); + + describe('mode icon', () => { + it('renders icon without count when no threads', () => { + const {getByRole} = renderWithReviewState( + + ); + + expect(getByRole('status')).toBeInTheDocument(); + expect(getByRole('status')).not.toHaveTextContent(/\d/); + }); + + it('renders full pill when threads exist', () => { + const {getByRole} = renderWithReviewState( + , + { + commentThreads: [ + {id: 1, subjectType: 'ContentElement', subjectId: 10, comments: []}, + {id: 2, subjectType: 'ContentElement', subjectId: 10, comments: []} + ] + } + ); + + expect(getByRole('status')).toHaveTextContent('2'); + }); + }); + + describe('mode dot', () => { + it('renders nothing when no threads', () => { + const {container} = renderWithReviewState( + + ); + + expect(container).toBeEmptyDOMElement(); + }); + + it('renders dot badge without count when threads exist', () => { + const {getByRole} = renderWithReviewState( + , + { + commentThreads: [ + {id: 1, subjectType: 'ContentElement', subjectId: 10, comments: []} + ] + } + ); + + expect(getByRole('status')).toBeInTheDocument(); + expect(getByRole('status')).not.toHaveTextContent(/\d/); + }); + }); + + describe('mode active', () => { + it('renders full pill even without threads', () => { + const {getByRole} = renderWithReviewState( + + ); + + expect(getByRole('status')).toBeInTheDocument(); + }); + }); }); diff --git a/entry_types/scrolled/package/spec/review/ReviewMessageHandler-spec.js b/entry_types/scrolled/package/spec/review/ReviewMessageHandler-spec.js index c7fbcf241f..98591fd1a2 100644 --- a/entry_types/scrolled/package/spec/review/ReviewMessageHandler-spec.js +++ b/entry_types/scrolled/package/spec/review/ReviewMessageHandler-spec.js @@ -13,18 +13,18 @@ function fakeReviewSession() { } describe('ReviewMessageHandler', () => { - it('calls session.createThread on CREATE_COMMENT_THREAD message', async () => { + it('calls session.createThread on CREATE_COMMENT_THREAD message from targetWindow', async () => { const session = fakeReviewSession(); - const targetWindow = {postMessage: jest.fn()}; - ReviewMessageHandler.create({session, targetWindow}); + ReviewMessageHandler.create({session, targetWindow: window}); window.dispatchEvent(new MessageEvent('message', { data: { type: 'CREATE_COMMENT_THREAD', payload: {subjectType: 'CE', subjectId: 10, body: 'Test'} }, - origin: window.location.origin + origin: window.location.origin, + source: window })); await new Promise(resolve => setTimeout(resolve, 0)); @@ -34,18 +34,18 @@ describe('ReviewMessageHandler', () => { }); }); - it('calls session.createComment on CREATE_COMMENT message', async () => { + it('calls session.createComment on CREATE_COMMENT message from targetWindow', async () => { const session = fakeReviewSession(); - const targetWindow = {postMessage: jest.fn()}; - ReviewMessageHandler.create({session, targetWindow}); + ReviewMessageHandler.create({session, targetWindow: window}); window.dispatchEvent(new MessageEvent('message', { data: { type: 'CREATE_COMMENT', payload: {threadId: 1, body: 'Reply'} }, - origin: window.location.origin + origin: window.location.origin, + source: window })); await new Promise(resolve => setTimeout(resolve, 0)); @@ -57,43 +57,72 @@ describe('ReviewMessageHandler', () => { it('posts REVIEW_STATE_RESET to target window on session reset', () => { const session = fakeReviewSession(); - const targetWindow = {postMessage: jest.fn()}; + const postMessage = jest.fn(); + jest.spyOn(window, 'postMessage').mockImplementation(postMessage); - ReviewMessageHandler.create({session, targetWindow}); + ReviewMessageHandler.create({session, targetWindow: window}); const state = {currentUser: {id: 42}, commentThreads: [{id: 1}]}; session.trigger('reset', state); - expect(targetWindow.postMessage).toHaveBeenCalledWith( + expect(postMessage).toHaveBeenCalledWith( {type: 'REVIEW_STATE_RESET', payload: state}, window.location.origin ); + + window.postMessage.mockRestore(); }); it('posts REVIEW_STATE_THREAD_CHANGE to target window on session change:thread', () => { const session = fakeReviewSession(); - const targetWindow = {postMessage: jest.fn()}; + const postMessage = jest.fn(); + jest.spyOn(window, 'postMessage').mockImplementation(postMessage); - ReviewMessageHandler.create({session, targetWindow}); + ReviewMessageHandler.create({session, targetWindow: window}); const thread = {id: 1, comments: [{body: 'Hello'}]}; session.trigger('change:thread', thread); - expect(targetWindow.postMessage).toHaveBeenCalledWith( + expect(postMessage).toHaveBeenCalledWith( {type: 'REVIEW_STATE_THREAD_CHANGE', payload: thread}, window.location.origin ); + + window.postMessage.mockRestore(); + }); + + it('ignores messages not from targetWindow', async () => { + const session = fakeReviewSession(); + const iframeWindow = {}; + + ReviewMessageHandler.create({session, targetWindow: iframeWindow}); + + window.dispatchEvent(new MessageEvent('message', { + data: { + type: 'CREATE_COMMENT_THREAD', + payload: {subjectType: 'CE', subjectId: 10, body: 'Test'} + }, + origin: window.location.origin, + source: window + })); + + await new Promise(resolve => setTimeout(resolve, 0)); + + expect(session.createThread).not.toHaveBeenCalled(); }); it('can be disposed', () => { const session = fakeReviewSession(); - const targetWindow = {postMessage: jest.fn()}; + const postMessage = jest.fn(); + jest.spyOn(window, 'postMessage').mockImplementation(postMessage); - const handler = ReviewMessageHandler.create({session, targetWindow}); + const handler = ReviewMessageHandler.create({session, targetWindow: window}); handler.dispose(); session.trigger('reset', {}); - expect(targetWindow.postMessage).not.toHaveBeenCalled(); + expect(postMessage).not.toHaveBeenCalled(); + + window.postMessage.mockRestore(); }); }); diff --git a/entry_types/scrolled/package/spec/support/jest/editor-css-stub.js b/entry_types/scrolled/package/spec/support/jest/editor-css-stub.js new file mode 100644 index 0000000000..f053ebf797 --- /dev/null +++ b/entry_types/scrolled/package/spec/support/jest/editor-css-stub.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/entry_types/scrolled/package/spec/support/jest/review-css-stub.js b/entry_types/scrolled/package/spec/support/jest/review-css-stub.js new file mode 100644 index 0000000000..f053ebf797 --- /dev/null +++ b/entry_types/scrolled/package/spec/support/jest/review-css-stub.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/entry_types/scrolled/package/src/editor/controllers/PreviewMessageController.js b/entry_types/scrolled/package/src/editor/controllers/PreviewMessageController.js index fab191cfb7..bf8266c0da 100644 --- a/entry_types/scrolled/package/src/editor/controllers/PreviewMessageController.js +++ b/entry_types/scrolled/package/src/editor/controllers/PreviewMessageController.js @@ -1,4 +1,5 @@ import {Object} from 'pageflow/ui'; +import {ReviewMessageHandler} from 'pageflow-scrolled/review'; import {watchCollections} from '../../entryState'; import {InsertContentElementDialogView} from '../views/InsertContentElementDialogView' import {SelectLinkDestinationDialogView} from '../views/SelectLinkDestinationDialogView' @@ -9,12 +10,20 @@ export const PreviewMessageController = Object.extend({ this.iframeWindow = iframeWindow; this.editor = editor; + if (entry.reviewSession) { + this.reviewMessageHandler = ReviewMessageHandler.create({ + session: entry.reviewSession, + targetWindow: iframeWindow + }); + } + this.listener = this.handleMessage.bind(this); window.addEventListener('message', this.listener); }, dispose() { window.removeEventListener('message', this.listener); + if (this.reviewMessageHandler) this.reviewMessageHandler.dispose(); }, handleMessage(message) { @@ -130,6 +139,10 @@ export const PreviewMessageController = Object.extend({ } postMessage({type: 'ACK'}) + + if (this.entry.reviewSession) { + this.entry.reviewSession.fetch(); + } } else if (message.data.type === 'CHANGE_SECTION') { this.entry.set({ @@ -142,7 +155,10 @@ export const PreviewMessageController = Object.extend({ this.preservedScrollTarget = null; - if (type === 'contentElement') { + if (type === 'contentElementComments') { + this.editor.navigate(`/scrolled/content_elements/${id}/comments`, {trigger: true}) + } + else if (type === 'contentElement') { const contentElement = this.entry.contentElements.get(id); this.editor.navigate(contentElement.getEditorPath(), {trigger: true}) } diff --git a/entry_types/scrolled/package/src/editor/controllers/SideBarController.js b/entry_types/scrolled/package/src/editor/controllers/SideBarController.js index acf491cce4..41fe8b9c19 100644 --- a/entry_types/scrolled/package/src/editor/controllers/SideBarController.js +++ b/entry_types/scrolled/package/src/editor/controllers/SideBarController.js @@ -1,12 +1,14 @@ import Marionette from 'backbone.marionette'; import {editor} from 'pageflow-scrolled/editor'; +import {BackButtonDecoratorView} from 'pageflow/editor'; import {EditChapterView} from '../views/EditChapterView'; import {EditSectionView} from '../views/EditSectionView'; import {EditSectionTransitionView} from '../views/EditSectionTransitionView'; import {EditSectionPaddingsView} from '../views/EditSectionPaddingsView'; import {EditContentElementView} from '../views/EditContentElementView'; +import {ContentElementCommentsView} from '../views/ContentElementCommentsView'; export const SideBarController = Marionette.Controller.extend({ initialize: function(options) { @@ -47,6 +49,16 @@ export const SideBarController = Marionette.Controller.extend({ })); }, + contentElementComments: function(id) { + this.region.show(new BackButtonDecoratorView({ + view: new ContentElementCommentsView({ + entry: this.entry, + model: this.entry.contentElements.get(id), + editor + }) + })); + }, + contentElement: function(id, tab) { this.region.show(new EditContentElementView({ entry: this.entry, diff --git a/entry_types/scrolled/package/src/editor/index.js b/entry_types/scrolled/package/src/editor/index.js index 015fe69113..fde7b1f00d 100644 --- a/entry_types/scrolled/package/src/editor/index.js +++ b/entry_types/scrolled/package/src/editor/index.js @@ -1,5 +1,8 @@ /*global pageflow*/ +import 'pageflow-scrolled/editor.css'; +import 'pageflow-scrolled/review.css'; + import './views/configurationEditors/groups/CommonContentElementAttributes'; import './views/configurationEditors/groups/LinkButtonVariant'; import './views/widgetTypes/roles'; diff --git a/entry_types/scrolled/package/src/editor/models/ScrolledEntry/index.js b/entry_types/scrolled/package/src/editor/models/ScrolledEntry/index.js index f608e996e9..e055437187 100644 --- a/entry_types/scrolled/package/src/editor/models/ScrolledEntry/index.js +++ b/entry_types/scrolled/package/src/editor/models/ScrolledEntry/index.js @@ -1,5 +1,7 @@ import {Entry, editor} from 'pageflow/editor'; import I18n from 'i18n-js'; +import {features} from 'pageflow/frontend'; +import {createReviewSession} from 'pageflow/review'; import {ConsentVendors} from '../ConsentVendors'; @@ -75,6 +77,10 @@ export const ScrolledEntry = Entry.extend({ editor.savingRecords.watch(this.chapters); this.scrolledSeed = seed; + + if (features.isEnabled('commenting')) { + this.reviewSession = createReviewSession({entryId: this.id}); + } }, insertContentElement(attributes, {id, at, splitPoint}) { diff --git a/entry_types/scrolled/package/src/editor/routers/SideBarRouter.js b/entry_types/scrolled/package/src/editor/routers/SideBarRouter.js index d89565d313..5e5b992c1b 100644 --- a/entry_types/scrolled/package/src/editor/routers/SideBarRouter.js +++ b/entry_types/scrolled/package/src/editor/routers/SideBarRouter.js @@ -7,6 +7,7 @@ export const SideBarRouter = Marionette.AppRouter.extend({ 'scrolled/sections/:id/paddings?position=:position': 'sectionPaddings', 'scrolled/sections/:id/paddings': 'sectionPaddings', 'scrolled/sections/:id': 'section', + 'scrolled/content_elements/:id/comments': 'contentElementComments', 'scrolled/content_elements/:id': 'contentElement' } }); diff --git a/entry_types/scrolled/package/src/editor/views/ContentElementCommentsView.js b/entry_types/scrolled/package/src/editor/views/ContentElementCommentsView.js new file mode 100644 index 0000000000..92c3d8d88a --- /dev/null +++ b/entry_types/scrolled/package/src/editor/views/ContentElementCommentsView.js @@ -0,0 +1,34 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import Marionette from 'backbone.marionette'; + +import {ReviewStateProvider, ReviewMessageHandler, ThreadList} from 'pageflow-scrolled/review'; + +import styles from './ContentElementCommentsView.module.css'; + +export const ContentElementCommentsView = Marionette.ItemView.extend({ + template: () => `
`, + + onShow() { + const session = this.options.entry.reviewSession; + + this.reviewMessageHandler = ReviewMessageHandler.create({ + session, + targetWindow: window + }); + + ReactDOM.render( + + + , + this.$el.find(`.${styles.container}`)[0] + ); + }, + + onClose() { + this.reviewMessageHandler.dispose(); + ReactDOM.unmountComponentAtNode(this.$el.find(`.${styles.container}`)[0]); + } +}); diff --git a/entry_types/scrolled/package/src/editor/views/ContentElementCommentsView.module.css b/entry_types/scrolled/package/src/editor/views/ContentElementCommentsView.module.css new file mode 100644 index 0000000000..72923e62fb --- /dev/null +++ b/entry_types/scrolled/package/src/editor/views/ContentElementCommentsView.module.css @@ -0,0 +1,14 @@ +.container { + position: relative; + padding-top: space(4); + --review-thread-box-shadow: none; + --review-thread-border: 1px solid var(--ui-on-surface-color-lightest); +} + +.newTopicButton { + composes: secondaryIconButton from './buttons.module.css'; + display: flex !important; + position: absolute; + right: 0; + bottom: 100%; +} diff --git a/entry_types/scrolled/package/src/frontend/commenting/Popover.js b/entry_types/scrolled/package/src/frontend/commenting/Popover.js index 09fdff57a9..b19f545514 100644 --- a/entry_types/scrolled/package/src/frontend/commenting/Popover.js +++ b/entry_types/scrolled/package/src/frontend/commenting/Popover.js @@ -53,8 +53,7 @@ export function Popover({subjectType, subjectId, placement}) { [styles.bottom]: onBottom})}>