From 7c92c4b1b2f3fd1e06a5d193d1ef1c44e2e1cea6 Mon Sep 17 00:00:00 2001 From: fxola Date: Sun, 1 Sep 2019 18:50:24 +0100 Subject: [PATCH] feature(edit-comment-history) Implement edit comment and view comment edit history functionality - write actions and reducer methods to edit comment - write actions and reducer methods to view comment edit history - write and style edit comment form component - write and style view edit history component - update single comment component to accomodate edit and edit history functionality - write tests - delivers[#166816082] --- src/actionTypes/index.js | 6 + src/components/CreateComment/index.jsx | 8 +- .../editCommentHistory.action.js | 27 +++ .../editCommentHistory.reducer.js | 25 +++ .../editCommentHistory.spec.js | 136 ++++++++++++++ src/components/EditCommentHistory/index.jsx | 48 +++++ src/components/EditCommentHistory/style.scss | 32 ++++ .../Editcomment/editComment.action.js | 22 +++ .../Editcomment/editComment.spec.js | 97 ++++++++++ .../Editcomment/editCommentReducer.js | 14 ++ src/components/Editcomment/index.jsx | 67 +++++++ src/components/Header/Header.scss | 13 +- src/components/ReportArticle/style.scss | 2 +- src/components/SingleComment/index.jsx | 170 +++++++++++++----- .../SingleComment/singleComment.scss | 3 + .../SingleComment/singleComment.spec.js | 2 +- src/store/rootReducer.js | 6 +- src/views/ReadArticlePage/index.jsx | 83 ++++++++- 18 files changed, 691 insertions(+), 70 deletions(-) create mode 100644 src/components/EditCommentHistory/editCommentHistory.action.js create mode 100644 src/components/EditCommentHistory/editCommentHistory.reducer.js create mode 100644 src/components/EditCommentHistory/editCommentHistory.spec.js create mode 100644 src/components/EditCommentHistory/index.jsx create mode 100644 src/components/EditCommentHistory/style.scss create mode 100644 src/components/Editcomment/editComment.action.js create mode 100644 src/components/Editcomment/editComment.spec.js create mode 100644 src/components/Editcomment/editCommentReducer.js create mode 100644 src/components/Editcomment/index.jsx diff --git a/src/actionTypes/index.js b/src/actionTypes/index.js index 103d01a..847a3ab 100644 --- a/src/actionTypes/index.js +++ b/src/actionTypes/index.js @@ -62,3 +62,9 @@ export const REQUEST_PASSWORD_RESET = 'REQUEST_PASSWORD_RESET'; export const GET_NOTIFICATION_SUCCESS = 'GET_NOTIFICATION_SUCCESS'; export const GET_NOTIFICATION_START = 'GET_NOTIFICATION_START'; +export const UPDATE_COMMENT_START = 'UPDATE_COMMENT_START'; +export const UPDATE_COMMENT = 'UPDATE_COMMENT'; +export const UPDATE_COMMENT_FAIL = 'UPDATE_COMMENT_FAIL'; +export const GET_COMMENT_EDIT_HISTORY_START = 'GET_COMMENT_EDIT_HISTORY_START'; +export const GET_COMMENT_EDIT_HISTORY = 'GET_COMMENT_EDIT_HISTORY'; +export const CLEAN_UP_COMMENT_EDIT_HISTORY = 'CLEAN_UP_COMMENT_EDIT_HISTORY'; diff --git a/src/components/CreateComment/index.jsx b/src/components/CreateComment/index.jsx index 13db93c..6e7808b 100644 --- a/src/components/CreateComment/index.jsx +++ b/src/components/CreateComment/index.jsx @@ -70,6 +70,10 @@ export class CreateComment extends Component { {...comment} datePublished={datePublished} handleLike={this.handleLike} + handleCommentModalOpen={this.props.handleCommentModalOpen} + handleCommentModalClose={this.props.handleCommentModalClose} + token={this.props.auth.user.token} + viewerEmail={this.props.auth.user.email} /> ); }); @@ -119,7 +123,9 @@ CreateComment.propTypes = { token: PropTypes.string, slugtoken: PropTypes.string, allComment: PropTypes.object, - auth: PropTypes.object + auth: PropTypes.object, + handleCommentModalOpen: PropTypes.func, + handleCommentModalClose: PropTypes.func }; export const mapStateToProps = state => { diff --git a/src/components/EditCommentHistory/editCommentHistory.action.js b/src/components/EditCommentHistory/editCommentHistory.action.js new file mode 100644 index 0000000..aa79554 --- /dev/null +++ b/src/components/EditCommentHistory/editCommentHistory.action.js @@ -0,0 +1,27 @@ +import axiosUtil from '../../utils/axiosConfig'; +import * as actionTypes from '../../actionTypes/index'; + +export const getCommentEditHistory = payload => { + return { type: actionTypes.GET_COMMENT_EDIT_HISTORY, payload }; +}; + +export const getCommentEditHistoryStart = () => { + return { type: actionTypes.GET_COMMENT_EDIT_HISTORY_START }; +}; + +export const cleanUpEditHistory = () => { + return { type: actionTypes.CLEAN_UP_COMMENT_EDIT_HISTORY }; +}; +export const getEditHistoryRequest = (slug, commentId, token) => { + return async dispatch => { + dispatch(cleanUpEditHistory()); + dispatch(getCommentEditHistoryStart()); + const response = await axiosUtil.get( + `articles/${slug}/comments/${commentId}/history`, + { + headers: { Authorization: `Bearer ${token}` } + } + ); + dispatch(getCommentEditHistory(response.data.data)); + }; +}; diff --git a/src/components/EditCommentHistory/editCommentHistory.reducer.js b/src/components/EditCommentHistory/editCommentHistory.reducer.js new file mode 100644 index 0000000..f83364b --- /dev/null +++ b/src/components/EditCommentHistory/editCommentHistory.reducer.js @@ -0,0 +1,25 @@ +import * as types from '../../actionTypes'; + +const commentEditHistoryReducer = (state = {}, action) => { + switch (action.type) { + case types.GET_COMMENT_EDIT_HISTORY_START: + return { + ...state, + isLoading: true + }; + case types.GET_COMMENT_EDIT_HISTORY: + return { + ...state, + history: action.payload, + isLoading: false + }; + case types.CLEAN_UP_COMMENT_EDIT_HISTORY: + return { + ...state, + isLoading: false + }; + default: + return state; + } +}; +export default commentEditHistoryReducer; diff --git a/src/components/EditCommentHistory/editCommentHistory.spec.js b/src/components/EditCommentHistory/editCommentHistory.spec.js new file mode 100644 index 0000000..78563c3 --- /dev/null +++ b/src/components/EditCommentHistory/editCommentHistory.spec.js @@ -0,0 +1,136 @@ +import React from 'react'; +import '@babel/polyfill'; +import mockAxios from 'axios'; +import { shallow } from 'enzyme'; +import editCommentHistoryReducer from './editCommentHistory.reducer'; +import configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { getEditHistoryRequest } from './editCommentHistory.action'; +import { EditCommentHistory } from './index.jsx'; +import { + GET_COMMENT_EDIT_HISTORY, + GET_COMMENT_EDIT_HISTORY_START, + CLEAN_UP_COMMENT_EDIT_HISTORY +} from '../../actionTypes/index'; +import Loader from '../LoadingIndicator/index.jsx'; + +const middlewares = [thunk]; +const mockStore = configureStore(middlewares); + +const slug = 'How-To-Become-a-10x-Engineer'; +const comment = 'Some new comment'; +const token = 'jndmfdfdfdfkldk'; +const response = { + '6:00pm, 12th August 2030': 'previous comment', + '6:01pm, 12th August 2030': 'another comment', + '6:03pm, 12th August 2030': 'another comment' +}; + +describe('Edit comment History actions tests', () => { + let store; + beforeEach(() => { + store = mockStore({}); + jest.resetAllMocks(); + }); + afterEach(() => { + store.clearActions(); + }); + + it('Should dispatch The UPDATE_COMMENT action', async () => { + const mockData = { + data: { + data: { + response + } + } + }; + + mockAxios.get.mockResolvedValue({ + data: mockData.data + }); + + const expectedActions = [ + { type: 'CLEAN_UP_COMMENT_EDIT_HISTORY' }, + { type: 'GET_COMMENT_EDIT_HISTORY_START' }, + { + type: 'GET_COMMENT_EDIT_HISTORY', + payload: { + response + } + } + ]; + + await store.dispatch(getEditHistoryRequest(slug, comment, token)); + expect(store.getActions()).toEqual(expectedActions); + }); +}); + +describe('Get Comment Edit History Reducer Tests', () => { + const initialState = {}; + it('Should return a new state if it recieves a GET_COMMENT_EDIT_HISTORY_START action type', () => { + const newState = editCommentHistoryReducer(initialState, { + type: GET_COMMENT_EDIT_HISTORY_START, + payload: {} + }); + expect(newState).toEqual({ + isLoading: true + }); + }); + + it('Should return a new state if it recieves a GET_COMMENT_EDIT_HISTORY action type', () => { + const newState = editCommentHistoryReducer(initialState, { + type: GET_COMMENT_EDIT_HISTORY, + payload: response + }); + expect(newState).toEqual({ + isLoading: false, + history: response + }); + }); + + it('Should return a new state if it recieves a CLEAN_UP_COMMENT_EDIT_HISTORY action type', () => { + const newState = editCommentHistoryReducer(initialState, { + type: CLEAN_UP_COMMENT_EDIT_HISTORY, + payload: {} + }); + expect(newState).toEqual({ + isLoading: false + }); + }); +}); + +describe('EditCommentHistory Template', () => { + it(`should render the EditCommentHistory Template when there is no edit history`, () => { + const props = { + commentHistory: { + history: { + commentEditHistory: {} + } + }, + isLoading: false + }; + const component = shallow(); + expect( + component + .find('.comment-edit-history') + .contains( +
This comment has no edit history
+ ) + ).toBeTruthy(); + }); + + it(`should render the EditCommentHistory Template when the request is loading`, () => { + const props = { + commentHistory: { + history: { + commentEditHistory: { response } + } + }, + isLoading: true + }; + const component = shallow(); + expect( + component.find('.comment-edit-history').contains() + ).toBeTruthy(); + }); +}); diff --git a/src/components/EditCommentHistory/index.jsx b/src/components/EditCommentHistory/index.jsx new file mode 100644 index 0000000..94499b8 --- /dev/null +++ b/src/components/EditCommentHistory/index.jsx @@ -0,0 +1,48 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import moment from 'moment'; +import './style.scss'; +import Loader from '../LoadingIndicator/index.jsx'; + +export class EditCommentHistory extends Component { + render() { + const { commentHistory, isLoading, lightTheme } = this.props; + const additionalClass = lightTheme ? 'comment-body-light' : ''; + const historyList = isLoading ? ( + + ) : commentHistory && + commentHistory.history && + Object.keys(commentHistory.history.commentEditHistory).length > 1 ? ( + Object.entries(commentHistory.history.commentEditHistory).map( + (comment, i) => { + return ( +
+ {comment[1]} + + {moment(comment[0]).format('MMMM Do, YYYY, h:mm:ss a')} + +
+ ); + } + ) + ) : ( +
This comment has no edit history
+ ); + return ( + <> +
{historyList}
+ + ); + } +} + +EditCommentHistory.propTypes = { + lightTheme: PropTypes.bool, + commentHistory: PropTypes.object, + slug: PropTypes.string, + token: PropTypes.string, + commentId: PropTypes.any, + isLoading: PropTypes.bool +}; + +export default EditCommentHistory; diff --git a/src/components/EditCommentHistory/style.scss b/src/components/EditCommentHistory/style.scss new file mode 100644 index 0000000..bd89907 --- /dev/null +++ b/src/components/EditCommentHistory/style.scss @@ -0,0 +1,32 @@ +.comment-edit-history { + height: 300px; + width: 50vw; + overflow: scroll; +} +.time-stamp { + background-color: rgba(0, 0, 0, 0.64); + border-top: 1px solid rgba(0, 0, 0, 0.125); + font-size: 0.5em; +} +.comment-body { + background: #161616; + border: 1px solid grey; + border-radius: 3px; + color: inherit; + min-height: 2em; + margin-bottom: 7px; +} +.edit-functionality:hover { + cursor: pointer; +} +.modal-main { + top: 35% !important; +} + +.comment-body-light { + background: white !important; + color: black !important; + .time-stamp { + background: rgba(0, 0, 0, 0.03) !important; + } +} diff --git a/src/components/Editcomment/editComment.action.js b/src/components/Editcomment/editComment.action.js new file mode 100644 index 0000000..4834ef7 --- /dev/null +++ b/src/components/Editcomment/editComment.action.js @@ -0,0 +1,22 @@ +import axiosUtil from '../../utils/axiosConfig'; +import * as actionTypes from '../../actionTypes/index'; + +export const updateComment = payload => { + return { + type: actionTypes.UPDATE_COMMENT, + payload: { payload, isEdited: true } + }; +}; + +export const updateCommentRequest = (slug, commentId, comment, token) => { + return async dispatch => { + const response = await axiosUtil.patch( + `articles/${slug}/comments/${commentId}/edit`, + { comment }, + { + headers: { Authorization: `Bearer ${token}` } + } + ); + dispatch(updateComment(response.data.data)); + }; +}; diff --git a/src/components/Editcomment/editComment.spec.js b/src/components/Editcomment/editComment.spec.js new file mode 100644 index 0000000..8500938 --- /dev/null +++ b/src/components/Editcomment/editComment.spec.js @@ -0,0 +1,97 @@ +import React from 'react'; +import '@babel/polyfill'; +import mockAxios from 'axios'; +import { shallow } from 'enzyme'; +import editCommentReducer from './editCommentReducer'; +import configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { updateCommentRequest } from './editComment.action'; +import { EditCommentForm } from './index.jsx'; +import { UPDATE_COMMENT } from '../../actionTypes/index'; + +const middlewares = [thunk]; +const mockStore = configureStore(middlewares); + +const slug = 'How-To-Become-a-10x-Engineer'; +const commentId = 44; +const comment = 'Some new comment'; +const token = 'jndmfdfdfdfkldk'; + +describe('Edit comment action tests', () => { + let store; + beforeEach(() => { + store = mockStore({}); + jest.resetAllMocks(); + }); + afterEach(() => { + store.clearActions(); + }); + + it('Should dispatch The UPDATE_COMMENT action', async () => { + const mockData = { + data: { + data: { + comment, + message: 'Comment Updated Successfully' + } + } + }; + + mockAxios.patch.mockResolvedValue({ + data: mockData.data + }); + + const expectedActions = [ + { + type: 'UPDATE_COMMENT', + payload: { + isEdited: true, + payload: { + comment, + message: 'Comment Updated Successfully' + } + } + } + ]; + + await store.dispatch(updateCommentRequest(slug, commentId, comment, token)); + expect(store.getActions()).toEqual(expectedActions); + }); +}); + +describe('Follow Reducer Tests', () => { + const initialState = {}; + it('Should return a new state if it recieves a FOLLOW_AUTHOR action type', () => { + const newState = editCommentReducer(initialState, { + type: UPDATE_COMMENT, + payload: { + comment, + message: 'Comment Updated Successfully' + } + }); + expect(newState).toEqual({ + updatedComment: { + comment: 'Some new comment', + message: 'Comment Updated Successfully' + } + }); + }); +}); + +describe('EditCommentForm template', () => { + const defaultProps = { + updateCommentRequest: jest.fn(), + commentId: 4, + slug, + token, + handleClose: jest.fn() + }; + + it(`should simulate a user's comment update`, () => { + const component = shallow(); + const event = { + preventDefault() {} + }; + component.find('form').simulate('submit', event); + }); +}); diff --git a/src/components/Editcomment/editCommentReducer.js b/src/components/Editcomment/editCommentReducer.js new file mode 100644 index 0000000..6340dc7 --- /dev/null +++ b/src/components/Editcomment/editCommentReducer.js @@ -0,0 +1,14 @@ +import * as types from '../../actionTypes'; + +const editCommentReducer = (state = {}, action) => { + switch (action.type) { + case types.UPDATE_COMMENT: + return { + ...state, + updatedComment: action.payload + }; + default: + return state; + } +}; +export default editCommentReducer; diff --git a/src/components/Editcomment/index.jsx b/src/components/Editcomment/index.jsx new file mode 100644 index 0000000..3c87255 --- /dev/null +++ b/src/components/Editcomment/index.jsx @@ -0,0 +1,67 @@ +import React, { Component } from 'react'; +import Textarea from '../TextArea/index.jsx'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; +import '../ReportArticle/style.scss'; + +export class EditCommentForm extends Component { + state = { + comment: '' + }; + + handleSubmit = event => { + event.preventDefault(); + const { + updateCommentRequest, + commentId, + slug, + token, + handleClose + } = this.props; + updateCommentRequest(slug, commentId, this.state.comment, token); + this.setState({ comment: '' }); + handleClose(); + }; + + handleInputChange = e => { + this.setState({ [e.target.name]: e.target.value }); + }; + + render() { + const { lightTheme, handleClose } = this.props; + return ( +
+
Update your comment
+ +