From 07526abe023012b867b344fbb6ab65d75819fb44 Mon Sep 17 00:00:00 2001 From: mendozabree Date: Mon, 19 Nov 2018 15:27:54 +0300 Subject: [PATCH] feat(articles): delete articles this feature will enable users to delete articles they have authored [Finishes #161776758] --- src/actions/actionCreators.js | 6 ++ src/actions/articleActions.js | 18 +++- src/actions/types.js | 1 + src/assets/App.css | 15 +++ src/components/Articles/DeleteModal.js | 80 ++++++++++++++ src/components/Articles/ReadArticle.js | 8 +- src/components/Articles/SingleArticle.js | 24 ++--- src/components/Articles/ViewArticle.js | 8 +- src/reducers/articlesReducer.js | 9 ++ src/tests/actions/articleActions.test.js | 59 ++++++---- src/tests/components/DeleteModal.test.js | 42 ++++++++ src/tests/reducers/articleReducer.test.js | 125 +++++----------------- src/tests/reducers/userReducer.test.js | 54 ++-------- 13 files changed, 262 insertions(+), 187 deletions(-) create mode 100644 src/components/Articles/DeleteModal.js create mode 100644 src/tests/components/DeleteModal.test.js diff --git a/src/actions/actionCreators.js b/src/actions/actionCreators.js index b2c3730..56d9139 100644 --- a/src/actions/actionCreators.js +++ b/src/actions/actionCreators.js @@ -14,6 +14,7 @@ import { GET_ALL_ARTICLES_INITIATED, LIKE_DISLIKE_SUCCESS, LIKE_DISLIKE_ERROR, + DELETE_ARTICLE_SUCCESS, } from './types'; export const socialLoginInitiated = () => ({ @@ -81,3 +82,8 @@ export const likeDislikeError = payload => ({ type: LIKE_DISLIKE_ERROR, payload, }); + +export const deleteArticleSuccess = payload => ({ + type: DELETE_ARTICLE_SUCCESS, + payload, +}); diff --git a/src/actions/articleActions.js b/src/actions/articleActions.js index ac5849b..54c328b 100644 --- a/src/actions/articleActions.js +++ b/src/actions/articleActions.js @@ -14,6 +14,7 @@ import { getArticlesInitiated, likeDislikeSuccess, likeDislikeError, + deleteArticleSuccess, } from './actionCreators'; export const postArticle = postData => dispatch => { @@ -70,9 +71,8 @@ export const fetchArticles = () => dispatch => { }; export const fetchSpecificArticle = slug => dispatch => { - dispatch(getArticlesInitiated(true)); - return axiosInstance - .get(`/api/articles/${slug}`) + axiosInstance + .get(`/api/articles/${slug}/`) .then((response) => { dispatch(getSpecificArticle(response.data)); }); @@ -113,3 +113,15 @@ export const likeDislike = (payload, slug) => dispatch => { return toast.error(error.response.data.detail, { autoClose: false, hideProgressBar: true }); }); }; + +export const deleteArticle = slug => dispatch => { + axiosInstance + .delete(`/api/articles/${slug}/`) + .then(() => { + dispatch(deleteArticleSuccess(true)); + toast.success( + 'The article was deleted!', + { autoClose: 3500, hideProgressBar: true }, + ); + }); +}; diff --git a/src/actions/types.js b/src/actions/types.js index f1b141c..fbec0a4 100644 --- a/src/actions/types.js +++ b/src/actions/types.js @@ -20,3 +20,4 @@ export const GET_USER_ARTICLES_SUCCESS = 'GET_USER_ARTICLES_SUCCESS'; export const GET_ALL_ARTICLES_INITIATED = 'GET_ALL_ARTICLES_INITIATED'; export const LIKE_DISLIKE_SUCCESS = 'LIKE_DISLIKE_SUCCESS'; export const LIKE_DISLIKE_ERROR = 'LIKE_DISLIKE_ERROR'; +export const DELETE_ARTICLE_SUCCESS = 'DELETE_ARTICLE_SUCCESS'; diff --git a/src/assets/App.css b/src/assets/App.css index ebc224b..23eafa6 100644 --- a/src/assets/App.css +++ b/src/assets/App.css @@ -427,6 +427,7 @@ header.uvp { font-size: 14px; border-radius: 6px; margin-right: 10px; + outline: #24292e; } .btn-edit:hover{ background-color: rgb(141, 189, 67); @@ -437,6 +438,7 @@ header.uvp { background-color: #eceff1; color: black; font-size: 14px; + outline: #24292e; border-radius: 6px; } .btn-delete:hover{ @@ -460,3 +462,16 @@ button:focus { outline: 0; color: #24292e; } + +.btn-cancel{ + padding: 5px 25px; + border: 2px solid rgb(107, 117, 126); + background-color: #eceff1; + color: black; + font-size: 14px; + border-radius: 6px; +} + +.btn-cancel:hover{ + background-color: rgb(107, 117, 126); +} diff --git a/src/components/Articles/DeleteModal.js b/src/components/Articles/DeleteModal.js new file mode 100644 index 0000000..ee6c502 --- /dev/null +++ b/src/components/Articles/DeleteModal.js @@ -0,0 +1,80 @@ +import React, { Component } from 'react'; +import { Redirect } from 'react-router-dom'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { bindActionCreators } from 'redux'; +import { deleteArticle } from '../../actions/articleActions'; + +export class DeleteModal extends Component { + constructor(props) { + super(props); + this.state = { + redirect: false, + }; + } + + componentWillReceiveProps(nextProps) { + if (nextProps.confirmDelete) { + this.setState({ + redirect: true, + }); + } + } + + handleClick = event => { + event.preventDefault(); + const { slug, deleteArticle } = this.props; + deleteArticle(slug); + }; + + render() { + const { redirect } = this.state; + if (redirect) { + const to = { pathname: '/dashboard' }; + return ( + + ); + } + return ( + + ); + } +} + +const mapStateToProps = state => ({ + confirmDelete: state.article.confirmDelete, +}); + +const matchDispatchToProps = dispatch => bindActionCreators({ + deleteArticle, +}, dispatch); + +DeleteModal.propTypes = { + confirmDelete: PropTypes.bool.isRequired, + deleteArticle: PropTypes.func.isRequired, + slug: PropTypes.string.isRequired, + +}; + +export default connect( + mapStateToProps, + matchDispatchToProps, +)(DeleteModal); diff --git a/src/components/Articles/ReadArticle.js b/src/components/Articles/ReadArticle.js index e479b75..db2c3f7 100644 --- a/src/components/Articles/ReadArticle.js +++ b/src/components/Articles/ReadArticle.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import user from '../../assets/images/user.png'; +import DeleteModal from './DeleteModal'; const articleCreated = articleDate => { const dateTime = new Date(articleDate); @@ -16,7 +17,7 @@ const getAuthor = author => { return 'btn-no-display'; }; -const ReadArticle = ({ article }) => ( +const ReadArticle = ({ article, slug }) => (
@@ -52,7 +53,10 @@ const ReadArticle = ({ article }) => (
- + +
+
+
diff --git a/src/components/Articles/SingleArticle.js b/src/components/Articles/SingleArticle.js index 27185a8..0831620 100644 --- a/src/components/Articles/SingleArticle.js +++ b/src/components/Articles/SingleArticle.js @@ -24,19 +24,17 @@ const SingleArticle = ({ article }) => {
-
-
-

- {articleCreated(article.createdAt)} - | - {article.reading_time} -

-
-
-
- -
-
+
+
+
+

+ {articleCreated(article.createdAt)} + | + {article.reading_time} +

+
+
+
diff --git a/src/components/Articles/ViewArticle.js b/src/components/Articles/ViewArticle.js index 56e5105..711ddf3 100644 --- a/src/components/Articles/ViewArticle.js +++ b/src/components/Articles/ViewArticle.js @@ -12,12 +12,16 @@ export class ViewArticle extends Component { } render() { - const { articlePayload, articlePayload: { article } } = this.props; + const { + articlePayload, + articlePayload: { article }, + match: { params: { slug } }, + } = this.props; return (
{Object.keys(articlePayload).length > 0 - && + && }
); diff --git a/src/reducers/articlesReducer.js b/src/reducers/articlesReducer.js index 4b12422..0a0f75a 100644 --- a/src/reducers/articlesReducer.js +++ b/src/reducers/articlesReducer.js @@ -11,6 +11,7 @@ import { GET_ALL_ARTICLES_INITIATED, LIKE_DISLIKE_ERROR, LIKE_DISLIKE_SUCCESS, + DELETE_ARTICLE_SUCCESS, } from '../actions/types'; const initialState = { @@ -24,6 +25,7 @@ const initialState = { articlePayload: {}, likeDislikeSuccess: false, likeDislikeError: {}, + confirmDelete: false, }; export const articlesReducer = (state = initialState, action) => { @@ -72,6 +74,7 @@ export const articlesReducer = (state = initialState, action) => { return { ...state, articlePayload: action.payload, + loading: false, }; case GET_USER_ARTICLES_SUCCESS: return { @@ -93,6 +96,12 @@ export const articlesReducer = (state = initialState, action) => { ...state, likeDislikeError: action.payload, }; + case DELETE_ARTICLE_SUCCESS: + return { + ...state, + loading: false, + confirmDelete: action.payload, + }; default: return state; } diff --git a/src/tests/actions/articleActions.test.js b/src/tests/actions/articleActions.test.js index 19d244b..87da3c8 100644 --- a/src/tests/actions/articleActions.test.js +++ b/src/tests/actions/articleActions.test.js @@ -10,6 +10,7 @@ import { fetchSpecificArticle, fetchUserArticles, likeDislike, + deleteArticle, } from '../../actions/articleActions'; import { CREATE_ARTICLE_SUCCESS, @@ -25,6 +26,7 @@ import { GET_ALL_ARTICLES_INITIATED, LIKE_DISLIKE_SUCCESS, LIKE_DISLIKE_ERROR, + DELETE_ARTICLE_SUCCESS, } from '../../actions/types'; let store; @@ -69,16 +71,13 @@ describe('articleActions', () => { }, }; const slug = 'an-article'; - mock.onGet(`api/articles/${slug}`) + mock.onGet(`/api/articles/${slug}/`) .reply(200, res_data); fetchSpecificArticle(slug)(store.dispatch); await flushAllPromises(); expect(store.getActions()).toEqual( - [ - { type: GET_ALL_ARTICLES_INITIATED, payload: true }, - { type: GET_SPECIFIC_ARTICLE_SUCCESS, payload: res_data }, - ], + [{ type: GET_SPECIFIC_ARTICLE_SUCCESS, payload: res_data }] ); }); @@ -87,11 +86,10 @@ describe('articleActions', () => { mock.onPost('/api/articles/').reply(403); postArticle()(store.dispatch); await flushAllPromises(); - expect(store.getActions()).toEqual( - [{ type: CREATE_ARTICLE_INITIATED, payload: true }, - { type: CREATE_ARTICLE_ERROR, payload: 'Re-login and try again' }, - ], - ); + expect(store.getActions()).toEqual([ + { type: CREATE_ARTICLE_INITIATED, payload: true }, + { type: CREATE_ARTICLE_ERROR, payload: 'Re-login and try again' }, + ]); }); it('should post an article', async () => { @@ -179,19 +177,6 @@ describe('articleActions', () => { ], ); }); -}); - -describe('likeDislikeAction', () => { - localStorage.setItem('auth_token', 'token'); - const payload = { reaction: 'Like' }; - const slug = 'testing-1-2-3'; - - beforeEach(() => { - mock = new MockAdapter(axiosInstance); - const middleware = [thunk]; - const mockStore = configureMockStore(middleware); - store = mockStore({}); - }); it('should return user artciles', async () => { const response_data = { @@ -213,6 +198,34 @@ describe('likeDislikeAction', () => { ], ); }); + + it('should delete an article', async () => { + const slug = 'react-redux'; + mock + .onDelete(`/api/articles/${slug}/`) + .reply(204); + deleteArticle(slug)(store.dispatch); + await flushAllPromises(); + expect(store.getActions()).toEqual( + [ + { type: DELETE_ARTICLE_SUCCESS, payload: true }, + ], + ); + }); +}); + +describe('likeDislikeAction', () => { + localStorage.setItem('auth_token', 'token'); + const payload = { reaction: 'Like' }; + const slug = 'testing-1-2-3'; + + beforeEach(() => { + mock = new MockAdapter(axiosInstance); + const middleware = [thunk]; + const mockStore = configureMockStore(middleware); + store = mockStore({}); + }); + it('should like an article', async () => { const response = { data: { diff --git a/src/tests/components/DeleteModal.test.js b/src/tests/components/DeleteModal.test.js new file mode 100644 index 0000000..d3a8b82 --- /dev/null +++ b/src/tests/components/DeleteModal.test.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { DeleteModal } from '../../components/Articles/DeleteModal'; + +describe('DeleteModal Component', () => { + let wrapper; + const confirmDelete = true; + const deleteArticle = jest.fn(); + const slug = 'react-redux'; + + const getEvent = (name = '', value = '') => ({ + preventDefault: jest.fn(), + target: { + name, + value, + }, + }); + + beforeEach(() => { + wrapper = shallow( + , + ); + }); + + it('should render the DeleteModal Component correctly', () => { + expect(wrapper).toMatchSnapshot(); + }); + + it('should call deleteArticle when handleClick is called', () => { + wrapper.instance().handleClick((getEvent())); + expect(deleteArticle).toBeCalled(); + }); + + it('should set sate redirect value to true on successfully deleting an article', () => { + wrapper.setProps({ confirmDelete: true }); + expect(wrapper.state().redirect).toBe(true); + }); +}); diff --git a/src/tests/reducers/articleReducer.test.js b/src/tests/reducers/articleReducer.test.js index 38f0874..915da20 100644 --- a/src/tests/reducers/articleReducer.test.js +++ b/src/tests/reducers/articleReducer.test.js @@ -11,6 +11,7 @@ import { GET_USER_ARTICLES_SUCCESS, LIKE_DISLIKE_SUCCESS, LIKE_DISLIKE_ERROR, + DELETE_ARTICLE_SUCCESS, } from '../../actions/types'; describe('articlesReducer', () => { @@ -28,6 +29,7 @@ describe('articlesReducer', () => { userArticlesPayload: {}, likeDislikeSuccess: false, likeDislikeError: {}, + confirmDelete: false, }; }); @@ -42,16 +44,9 @@ describe('articlesReducer', () => { }; const currentState = articlesReducer(initialState, action); expect(currentState).toEqual({ + ...initialState, articlesPayload: action.payload, - articlePayload: {}, - createArticleSuccess: false, - createArticleError: {}, - loading: false, - addCommentSuccess: false, - commentsPayload: {}, - userArticlesPayload: {}, - likeDislikeSuccess: false, - likeDislikeError: {}, + }); }); @@ -62,16 +57,8 @@ describe('articlesReducer', () => { }; const currentState = articlesReducer(initialState, action); expect(currentState).toEqual({ - articlesPayload: {}, + ...initialState, createArticleSuccess: true, - createArticleError: {}, - loading: false, - addCommentSuccess: false, - commentsPayload: {}, - userArticlesPayload: {}, - articlePayload: {}, - likeDislikeSuccess: false, - likeDislikeError: {}, }); }); @@ -83,16 +70,8 @@ describe('articlesReducer', () => { }; const currentState = articlesReducer(initialState, action); expect(currentState).toEqual({ - articlesPayload: {}, - createArticleSuccess: false, + ...initialState, createArticleError: postError, - loading: false, - addCommentSuccess: false, - commentsPayload: {}, - userArticlesPayload: {}, - articlePayload: {}, - likeDislikeSuccess: false, - likeDislikeError: {}, }); }); @@ -103,16 +82,8 @@ describe('articlesReducer', () => { }; const currentState = articlesReducer(initialState, action); expect(currentState).toEqual({ - articlesPayload: {}, - createArticleSuccess: false, - createArticleError: {}, + ...initialState, loading: true, - addCommentSuccess: false, - commentsPayload: {}, - userArticlesPayload: {}, - articlePayload: {}, - likeDislikeSuccess: false, - likeDislikeError: {}, }); }); @@ -123,16 +94,8 @@ describe('articlesReducer', () => { }; const currentState = articlesReducer(initialState, action); expect(currentState).toEqual({ - articlesPayload: {}, - createArticleSuccess: false, - createArticleError: {}, + ...initialState, loading: true, - addCommentSuccess: false, - commentsPayload: {}, - userArticlesPayload: {}, - articlePayload: {}, - likeDislikeSuccess: false, - likeDislikeError: {}, }); }); @@ -143,16 +106,8 @@ describe('articlesReducer', () => { }; const currentState = articlesReducer(initialState, action); expect(currentState).toEqual({ - articlesPayload: {}, - createArticleSuccess: false, - createArticleError: {}, - loading: false, + ...initialState, addCommentSuccess: true, - commentsPayload: {}, - userArticlesPayload: {}, - articlePayload: {}, - likeDislikeSuccess: false, - likeDislikeError: {}, }); }); @@ -163,16 +118,8 @@ describe('articlesReducer', () => { }; const currentState = articlesReducer(initialState, action); expect(currentState).toEqual({ - articlesPayload: {}, - createArticleSuccess: false, - createArticleError: {}, - loading: false, - addCommentSuccess: false, + ...initialState, commentsPayload: action.payload, - userArticlesPayload: {}, - articlePayload: {}, - likeDislikeSuccess: false, - likeDislikeError: {}, }); }); @@ -183,16 +130,8 @@ describe('articlesReducer', () => { }; const currentState = articlesReducer(initialState, action); expect(currentState).toEqual({ + ...initialState, articlePayload: action.payload, - articlesPayload: {}, - userArticlesPayload: {}, - createArticleSuccess: false, - loading: false, - addCommentSuccess: false, - commentsPayload: {}, - likeDislikeSuccess: false, - likeDislikeError: {}, - createArticleError: {}, }); }); @@ -203,16 +142,8 @@ describe('articlesReducer', () => { }; const currentState = articlesReducer(initialState, action); expect(currentState).toEqual({ - createArticleError: {}, - articlePayload: {}, - articlesPayload: {}, + ...initialState, userArticlesPayload: action.payload, - createArticleSuccess: false, - loading: false, - addCommentSuccess: false, - commentsPayload: {}, - likeDislikeSuccess: false, - likeDislikeError: {}, }); }); @@ -223,16 +154,8 @@ describe('articlesReducer', () => { }; const currentState = articlesReducer(initialState, action); expect(currentState).toEqual({ - articlePayload: {}, - userArticlesPayload: {}, - articlesPayload: {}, - createArticleSuccess: false, - createArticleError: {}, - loading: false, - addCommentSuccess: false, - commentsPayload: {}, + ...initialState, likeDislikeSuccess: true, - likeDislikeError: {}, }); }); @@ -244,16 +167,20 @@ describe('articlesReducer', () => { }; const currentState = articlesReducer(initialState, action); expect(currentState).toEqual({ - userArticlesPayload: {}, - articlePayload: {}, - articlesPayload: {}, - createArticleSuccess: false, - createArticleError: {}, - loading: false, - addCommentSuccess: false, - commentsPayload: {}, - likeDislikeSuccess: false, + ...initialState, likeDislikeError: connectionError, }); }); + + it('should set DELETE_ARTICLE_SUCCESS to true when article has been deleted', () => { + const action = { + type: DELETE_ARTICLE_SUCCESS, + payload: true, + }; + const currentState = articlesReducer(initialState, action); + expect(currentState).toEqual({ + ...initialState, + confirmDelete: true, + }); + }); }); diff --git a/src/tests/reducers/userReducer.test.js b/src/tests/reducers/userReducer.test.js index 57391ef..ce5160c 100644 --- a/src/tests/reducers/userReducer.test.js +++ b/src/tests/reducers/userReducer.test.js @@ -45,12 +45,8 @@ describe('userReducer', () => { }; const currentState = userReducer(initialState, action); expect(currentState).toEqual({ + ...initialState, registerUserSuccess: true, - registerUserError: {}, - isLoggedIn: false, - loading: false, - loginError: {}, - profilePayload: {}, }); }); @@ -61,12 +57,8 @@ describe('userReducer', () => { }; const currentState = userReducer(initialState, action); expect(currentState).toEqual({ - registerUserSuccess: false, + ...initialState, registerUserError: errorData, - isLoggedIn: false, - loginError: {}, - loading: false, - profilePayload: {}, }); }); @@ -77,12 +69,8 @@ describe('userReducer', () => { }; const currentState = userReducer(initialState, action); expect(currentState).toEqual({ - registerUserSuccess: false, - registerUserError: {}, + ...initialState, isLoggedIn: true, - loginError: {}, - loading: false, - profilePayload: {}, }); }); @@ -94,12 +82,8 @@ describe('userReducer', () => { }; const currentState = userReducer(initialState, action); expect(currentState).toEqual({ - registerUserSuccess: false, - registerUserError: {}, - isLoggedIn: false, + ...initialState, loginError: loginErrorResponse, - loading: false, - profilePayload: {}, }); }); @@ -113,12 +97,8 @@ describe('userReducer', () => { }; const currentState = userReducer(initialState, action); expect(currentState).toEqual({ - registerUserSuccess: false, + ...initialState, profilePayload: profileDetails, - registerUserError: {}, - isLoggedIn: false, - loading: false, - loginError: {}, }); }); @@ -129,12 +109,8 @@ describe('userReducer', () => { }; const currentState = userReducer(initialState, action); expect(currentState).toEqual({ - registerUserSuccess: false, - registerUserError: {}, - isLoggedIn: false, + ...initialState, loading: true, - loginError: {}, - profilePayload: {}, }); }); @@ -145,12 +121,8 @@ describe('userReducer', () => { }; const currentState = userReducer(initialState, action); expect(currentState).toEqual({ - registerUserSuccess: false, - registerUserError: {}, + ...initialState, isLoggedIn: false, - loading: false, - loginError: {}, - profilePayload: {}, }); }); it('should start loading on sociallogin', () => { @@ -161,12 +133,8 @@ describe('userReducer', () => { }; const currentState = userReducer(initialState, action); expect(currentState).toEqual({ - registerUserSuccess: false, - registerUserError: {}, - isLoggedIn: false, - loginError: {}, + ...initialState, loading: true, - profilePayload: {}, }); }); it('should have logged in true', () => { @@ -177,12 +145,8 @@ describe('userReducer', () => { }; const currentState = userReducer(initialState, action); expect(currentState).toEqual({ - registerUserSuccess: false, - registerUserError: {}, + ...initialState, isLoggedIn: true, - loginError: {}, - loading: false, - profilePayload: {}, }); }); });