Skip to content

Commit

Permalink
Merge pull request #28 from andela/ft-delete-article-169228078
Browse files Browse the repository at this point in the history
 Enable user delete their article
  • Loading branch information
bdushimi committed Nov 7, 2019
2 parents b765002 + 640cce7 commit e49d71b
Show file tree
Hide file tree
Showing 18 changed files with 489 additions and 29 deletions.
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"query-string": "^6.8.3",
"querystring": "^0.2.0",
"react": "^16.9.0",
"react-confirm-alert": "^2.4.1",
"react-dom": "^16.9.0",
"react-html-parser": "^2.0.2",
"react-js-pagination": "^3.0.2",
Expand Down
61 changes: 61 additions & 0 deletions src/app/common/articleMenu/ArticleDropdownMenu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable no-shadow */
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { confirmAlert } from 'react-confirm-alert';
import deleteArticle from '../../../feature/articles/deleteArticle/DeleteAction';
import ConfimAlertComponent from '../../confirmAlert/ConfirmAlertComponent';
import './ArticleDropdownMenu.scss';
import 'react-confirm-alert/src/react-confirm-alert.css';

export class ArticleDropdownMenu extends Component {
confirmDelete = () => {
const { slug, deleteArticle, author } = this.props;
const userName = localStorage.getItem('username');
confirmAlert({
customUI: ({ onClose }) => (
<ConfimAlertComponent
slug={slug}
deleteArticle={deleteArticle}
author={author}
userName={userName}
onClose={onClose}
/>
)
});
};

checkUser = () => {
const { isAuthenticated, history, pathname } = this.props;
isAuthenticated
? this.confirmDelete()
: history.push(`/login?redirectTo=${pathname}`);
};

render() {
return (
<div className="main-wrapper">
<ul className="dropdown__menu">
<li>Update</li>
<li onClick={this.checkUser}>Delete</li>
</ul>
</div>
);
}
}

const mapDispachtToProps = {
deleteArticle
};
const mapStateToProps = state => ({
isAuthenticated: state.login.isAuthenticated,
author: state.getSingleArticle.article.author
});

export default connect(
mapStateToProps,
mapDispachtToProps
)(withRouter(ArticleDropdownMenu));
57 changes: 57 additions & 0 deletions src/app/common/articleMenu/ArticleDropdownMenu.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
.main-wrapper {
text-align: center;
border: 1px solid black;
background-color: white;
width: 100px;
font-size: 14px;
position: absolute;
right: 12px;
.dropdown__menu {
list-style-type: none;
padding: 0;
margin: 0;

li {
text-transform: capitalize;
border: 1px solid #ddd;
width: 98px;
line-height: 2;
&:hover {
background: lightgray;
}
}
}
}
.modal-body {
background: #4887c2;
color: white;
width: 24em;
height: 10em;
text-align: center;
}
.modal-btn {
background: #4887c2;
color: white;
font-size: 16px;
line-height: 25px;
margin: 10px 10px 0 10px;
}
.btn__delete {
border: 1px red solid;
}

@media screen and (max-width: 400px) {
.main-wrapper {
width: 100px;
right: 9px !important;
}
.modal-body {
width: 19em;
}
}

@media screen and (max-width: 600px) {
.main-wrapper {
right: 25px;
}
}
63 changes: 63 additions & 0 deletions src/app/common/articleMenu/__tests__/ArticleDropdownMenu.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/* eslint-disable react/jsx-props-no-spreading */
import React from 'react';
import { shallow, mount } from 'enzyme';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import ArticleDropdownComponent, {
ArticleDropdownMenu
} from '../ArticleDropdownMenu';
import store from '../../../store/index';

const renderArticleDropdownMenu = args => {
const defaultProps = {
userName: 'jkadhuwa',
author: { userName: 'jkadhuwa' },
confirmDelete: jest.fn(),
checkUser: jest.fn(),
confirmAlert: jest.fn(() => {}),
deleteArticle: jest.fn(),
onClose: jest.fn(),
history: {
push: jest.fn()
}
};
const props = { ...defaultProps, ...args };
return shallow(<ArticleDropdownMenu {...props} />);
};

const wrapper = renderArticleDropdownMenu();

describe('Article dropdown menu tests', () => {
it('should test not call any handler if not authenticated', () => {
wrapper.setProps({ isAuthenticated: false });
const button = wrapper.find('li').at(1);
button.simulate('click');
wrapper.update();

expect(wrapper.instance().props.confirmDelete).toHaveBeenCalledTimes(0);
});
it('should test call confirmDelete if authenticated', () => {
wrapper.setProps({ isAuthenticated: true });
const button = wrapper.find('li').at(1);
const mockedFunction = jest.fn().mockImplementation(() => undefined);
const instance = wrapper.instance();
wrapper.instance().checkUser = mockedFunction;
button.simulate('click');
const updateState = instance.checkUser();
expect(updateState).toBe(undefined);
expect(mockedFunction).toHaveBeenCalledTimes(1);
});
});

describe('Testing connecting component to the store', () => {
const connectedWrapper = mount(
<Provider store={store}>
<MemoryRouter>
<ArticleDropdownComponent />
</MemoryRouter>
</Provider>
);
it('should render the component that is connected to the store', () => {
expect(connectedWrapper.exists()).toBe(true);
});
});
26 changes: 26 additions & 0 deletions src/app/confirmAlert/ConfirmAlertComponent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* eslint-disable no-unused-expressions */
import React from 'react';

export default function ConfirmAlertComponent(props) {
const {
slug, deleteArticle, onClose, userName, author
} = props;
return (
<div className="custom-ui modal-body">
<h1>Are you sure</h1>
<p>You want to delete this article?</p>
<button className="modal-btn btn__cancel" type="button" onClick={onClose}>
Cancel
</button>
<button
className="modal-btn btn__delete"
type="button"
onClick={() => {
userName === author.userName ? (deleteArticle(slug), onClose()) : '';
}}
>
Yes, Delete it!
</button>
</div>
);
}
47 changes: 47 additions & 0 deletions src/app/confirmAlert/__tests__/ConfirmAlertComponent.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/* eslint-disable react/jsx-props-no-spreading */
import React from 'react';
import { mount } from 'enzyme';
import ConfirmAlertComponent from '../ConfirmAlertComponent';

const renderConfirmAlertComponent = args => {
const defaultProps = {
userName: 'jkadhuwa',
author: { userName: 'jkadhuwa' },
deleteArticle: jest.fn(),
onClose: jest.fn(),
};
const props = { ...defaultProps, ...args };
return mount(<ConfirmAlertComponent {...props} />);
};

const renderConfirmAlert = args => {
const defaultProps = {
userName: 'jkadhuwa',
author: { userName: 'jkadhu' },
deleteArticle: jest.fn(),
};
const props = { ...defaultProps, ...args };
return mount(<ConfirmAlertComponent {...props} />);
};

const wrapper = renderConfirmAlertComponent();
const wrongWrapper = renderConfirmAlert();

describe('Article dropdown menu tests', () => {
it('should test not call deleteArticle action if author', () => {
wrapper.setProps({ isAuthenticated: true });
const button = wrapper.find('.btn__delete');
button.simulate('click');
wrapper.update();
expect(wrapper.props().deleteArticle).toHaveBeenCalledTimes(1);
});
});
describe('Article dropdown menu tests when not owner', () => {
it('should test not call any handler if not author', () => {
wrongWrapper.setProps({ isAuthenticated: true });
const button = wrongWrapper.find('.btn__delete');
button.simulate('click');
wrongWrapper.update();
expect(wrongWrapper.props().deleteArticle).toHaveBeenCalledTimes(0);
});
});
2 changes: 1 addition & 1 deletion src/app/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ export default combineReducers({
getAllArticles,
followAuthor: followReducer,
getSingleArticle,
social: socialReducer,
social: socialReducer
});
2 changes: 2 additions & 0 deletions src/feature/articles/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export const DISLIKE_ARTICLE_FAIL = 'DISLIKE_ARTICLE_FAIL';
export const LIKE_ARTICLE_FAIL = 'LIKE_ARTICLE_FAIL';
export const DISLIKE_ARTICLE_SUCCESS = 'DISLIKE_ARTICLE_SUCCESS';
export const LOADING = 'LOADING';
export const DELETE_ARTICLE_SUCCESS = 'DELETE_ARTICLE_SUCCESS';
export const DELETE_ARTICLE_FAIL = 'DELETE_ARTICLE_FAIL';
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable no-shadow */
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { toast } from 'react-toastify';
Expand Down
19 changes: 19 additions & 0 deletions src/feature/articles/deleteArticle/DeleteAction.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import axios from 'axios';
import { DELETE_ARTICLE_SUCCESS, DELETE_ARTICLE_FAIL } from '../constants';
import setAxiosConfig from '../../../app/common/config/axiosConfig';
import { BACKEND_URL } from '../../../app/common/config/appConfig';

const deleteArticle = slug => async dispatch => {
try {
await axios.delete(`${BACKEND_URL}/articles/${slug}`, setAxiosConfig());
dispatch({
type: DELETE_ARTICLE_SUCCESS
});
} catch (error) {
dispatch({
type: DELETE_ARTICLE_FAIL
});
}
};

export default deleteArticle;
50 changes: 50 additions & 0 deletions src/feature/articles/deleteArticle/__tests__/DeleteAction.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import moxios from 'moxios';
import { makeMockStore } from '../../../../app/common/config/mockStore';
import deleteArticle from '../DeleteAction';

const store = makeMockStore({ article: {} });
describe('Delete Article tests', () => {
beforeEach(() => moxios.install());
afterEach(() => {
moxios.uninstall();
store.clearActions();
});
test('Should dispatch DELETE_ARTICLE_FAIL action', () => {
const error = 'No articles the moment';
moxios.wait(() => {
const request = moxios.requests.mostRecent();
request.respondWith({
status: 403,
response: {
data: {
error: 'You can not delete someones article!!!'
}
}
});
});
const expected = ['DELETE_ARTICLE_FAIL'];
return store.dispatch(deleteArticle(error)).then(() => {
const dispatchedActions = store.getActions();
const dispatchedTypes = dispatchedActions.map(action => action.type);
expect(dispatchedTypes).toEqual(expected);
});
});
test('Should dispatch DELETE_ARTICLE_SUCCESS action', () => {
const slug = 'No articles the moment';
moxios.wait(() => {
const request = moxios.requests.mostRecent();
request.respondWith({
status: 200,
response: {
message: 'Successfully deleted'
}
});
});
const expected = ['DELETE_ARTICLE_SUCCESS'];
return store.dispatch(deleteArticle(slug)).then(() => {
const dispatchedActions = store.getActions();
const dispatchedTypes = dispatchedActions.map(action => action.type);
expect(dispatchedTypes).toEqual(expected);
});
});
});
Loading

0 comments on commit e49d71b

Please sign in to comment.