Commit bc48cd4b authored by Jasper van Esveld's avatar Jasper van Esveld Committed by Joram Wilander

Migrate delete_post_modal.jsx to be pure and use Redux (#989)

* Migrate DeletePostModal to be pure and use Redux

Migrate DeletePostModal to be pure and use Redux

* Fix styling problem

* Changes based on reviews

* Removed old post action

Added some functionality from the old action to the component

* Remove unused imports from post_actions

* Add browserhistory test

Clean up test baseProps
Pass teamname instead of team object

* Fix styling problem
parent 5c2d6361
......@@ -188,16 +188,6 @@ export function toggleShortcutsModal() {
});
}
export function showDeletePostModal(post, commentCount = 0, isRHS) {
AppDispatcher.handleViewAction({
type: ActionTypes.TOGGLE_DELETE_POST_MODAL,
value: true,
isRHS,
post,
commentCount,
});
}
export function showChannelHeaderUpdateModal(channel) {
AppDispatcher.handleViewAction({
type: ActionTypes.TOGGLE_CHANNEL_HEADER_UPDATE_MODAL,
......
......@@ -8,7 +8,6 @@ import * as PostActions from 'mattermost-redux/actions/posts';
import * as Selectors from 'mattermost-redux/selectors/entities/posts';
import {comparePosts} from 'mattermost-redux/utils/post_utils';
import {browserHistory} from 'utils/browser_history';
import {sendDesktopNotification} from 'actions/notification_actions.jsx';
import {loadNewDMIfNeeded, loadNewGMIfNeeded} from 'actions/user_actions.jsx';
import * as RhsActions from 'actions/views/rhs';
......@@ -16,8 +15,7 @@ import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import PostStore from 'stores/post_store.jsx';
import store from 'stores/redux_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import {getSelectedPostId, getRhsState} from 'selectors/rhs';
import {getRhsState} from 'selectors/rhs';
import {ActionTypes, Constants, RHSStates} from 'utils/constants.jsx';
import {EMOJI_PATTERN} from 'utils/emoticons.jsx';
import * as UserAgent from 'utils/user_agent';
......@@ -202,46 +200,6 @@ export function emitEmojiPosted(emoji) {
});
}
export async function deletePost(channelId, post, success) {
const {currentUserId} = getState().entities.users;
let hardDelete = false;
if (post.user_id === currentUserId) {
hardDelete = true;
}
await PostActions.deletePost(post, hardDelete)(dispatch, getState);
if (post.id === getSelectedPostId(getState())) {
dispatch({
type: ActionTypes.SELECT_POST,
postId: '',
channelId: '',
});
}
dispatch({
type: PostTypes.REMOVE_POST,
data: post,
});
// Needed for search store
AppDispatcher.handleViewAction({
type: Constants.ActionTypes.REMOVE_POST,
post,
});
const {focusedPostId} = getState().views.channel;
const channel = getState().entities.channels.channels[post.channel_id];
if (post.id === focusedPostId && channel) {
browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + channel.name);
}
if (success) {
success();
}
}
const POST_INCREASE_AMOUNT = Constants.POST_CHUNK_SIZE / 2;
// Returns true if there are more posts to load
......
......@@ -7,7 +7,6 @@ import {Route} from 'react-router-dom';
import Pluggable from 'plugins/pluggable';
import AnnouncementBar from 'components/announcement_bar';
import DeletePostModal from 'components/delete_post_modal.jsx';
import EditPostModal from 'components/edit_post_modal';
import GetPostLinkModal from 'components/get_post_link_modal';
import GetTeamInviteLinkModal from 'components/get_team_invite_link_modal';
......@@ -64,7 +63,6 @@ export default class ChannelController extends React.Component {
<ImportThemeModal/>
<TeamSettingsModal/>
<EditPostModal/>
<DeletePostModal/>
<RemovedFromChannelModal/>
<ResetStatusModal/>
<LeavePrivateChannelModal/>
......
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import {Modal} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
import {deletePost} from 'actions/post_actions.jsx';
import ModalStore from 'stores/modal_store.jsx';
import Constants from 'utils/constants.jsx';
import {browserHistory} from 'utils/browser_history';
import * as UserAgent from 'utils/user_agent.jsx';
var ActionTypes = Constants.ActionTypes;
export default class DeletePostModal extends React.PureComponent {
static propTypes = {
channelName: PropTypes.string,
focusedPostId: PropTypes.string,
teamName: PropTypes.string,
post: PropTypes.object.isRequired,
commentCount: PropTypes.number.isRequired,
/**
* Does the post come from RHS mode
*/
isRHS: PropTypes.bool.isRequired,
/**
* Function called when modal is dismissed
*/
onHide: PropTypes.func.isRequired,
actions: PropTypes.shape({
/**
* Function called for deleting post
*/
deletePost: PropTypes.func.isRequired,
}),
}
export default class DeletePostModal extends React.Component {
constructor(props) {
super(props);
this.handleDelete = this.handleDelete.bind(this);
this.onHide = this.onHide.bind(this);
this.state = {
show: false,
post: null,
commentCount: 0,
error: '',
show: true,
};
}
componentDidMount() {
ModalStore.addModalListener(ActionTypes.TOGGLE_DELETE_POST_MODAL, this.handleToggle);
}
handleDelete = async () => {
const {
actions,
channelName,
focusedPostId,
post,
teamName,
} = this.props;
componentWillUnmount() {
ModalStore.removeModalListener(ActionTypes.TOGGLE_DELETE_POST_MODAL, this.handleToggle);
}
const {data} = await actions.deletePost(post);
componentDidUpdate(prevProps, prevState) {
if (this.state.show && !prevState.show) {
setTimeout(() => {
if (this.deletePostBtn) {
this.deletePostBtn.focus();
}
}, 200);
if (post.id === focusedPostId && channelName) {
browserHistory.push('/' + teamName + '/channels/' + channelName);
}
}
handleDelete = () => {
deletePost(
this.state.post.channel_id,
this.state.post,
() => {
this.handleHide();
},
(err) => {
this.setState({error: err.message});
}
);
}
handleToggle = (value, args) => {
this.setState({
show: value,
post: args.post,
isRHS: args.isRHS,
commentCount: args.commentCount,
error: '',
});
if (data) {
this.onHide();
}
}
handleHide = () => {
onHide() {
this.setState({show: false});
if (!UserAgent.isMobile()) {
if (this.state.isRHS) {
document.getElementById('reply_textbox').focus();
var element;
if (this.props.isRHS) {
element = document.getElementById('reply_textbox');
} else {
document.getElementById('post_textbox').focus();
element = document.getElementById('post_textbox');
}
if (element) {
element.focus();
}
}
}
render() {
if (!this.state.post) {
return null;
}
var error = null;
if (this.state.error) {
error = <div className='form-group has-error'><label className='control-label'>{this.state.error}</label></div>;
}
var commentWarning = '';
if (this.state.commentCount > 0) {
if (this.props.commentCount > 0) {
commentWarning = (
<FormattedMessage
id='delete_post.warning'
defaultMessage='This post has {count, number} {count, plural, one {comment} other {comments}} on it.'
values={{
count: this.state.commentCount,
count: this.props.commentCount,
}}
/>
);
}
const postTerm = this.state.post.root_id ? (
const postTerm = this.props.post.root_id ? (
<FormattedMessage
id='delete_post.comment'
defaultMessage='Comment'
......@@ -115,7 +111,8 @@ export default class DeletePostModal extends React.Component {
return (
<Modal
show={this.state.show}
onHide={this.handleHide}
onHide={this.onHide}
onExited={this.props.onHide}
enforceFocus={false}
>
<Modal.Header closeButton={true}>
......@@ -140,7 +137,6 @@ export default class DeletePostModal extends React.Component {
<br/>
<br/>
{commentWarning}
{error}
</Modal.Body>
<Modal.Footer>
<button
......
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {deletePost} from 'mattermost-redux/actions/posts';
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
import DeletePostModal from './delete_post_modal.jsx';
function mapStateToProps(state, ownProps) {
const channel = getChannel(state, ownProps.post.channel_id);
let channelName = '';
if (channel) {
channelName = channel.name;
}
const {focusedPostId} = state.views.channel;
return {
channelName,
focusedPostId,
teamName: getCurrentTeam(state).name,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
deletePost,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(DeletePostModal);
......@@ -27,30 +27,35 @@ export default class DotMenu extends Component {
actions: PropTypes.shape({
/*
/**
* Function flag the post
*/
flagPost: PropTypes.func.isRequired,
/*
/**
* Function to unflag the post
*/
unflagPost: PropTypes.func.isRequired,
/*
* Function to set the edting post
/**
* Function to set the editing post
*/
setEditingPost: PropTypes.func.isRequired,
/*
/**
* Function to pin the post
*/
pinPost: PropTypes.func.isRequired,
/*
/**
* Function to unpin the post
*/
unpinPost: PropTypes.func.isRequired,
/**
* Function to open a modal
*/
openModal: PropTypes.func.isRequired,
}).isRequired,
}
......@@ -185,6 +190,9 @@ export default class DotMenu extends Component {
idCount={this.props.idCount}
post={this.props.post}
commentCount={type === 'Post' ? this.props.commentCount : 0}
actions={{
openModal: this.props.actions.openModal,
}}
/>
);
}
......
......@@ -5,8 +5,9 @@ import PropTypes from 'prop-types';
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {showDeletePostModal, showGetPostLinkModal} from 'actions/global_actions.jsx';
import Constants from 'utils/constants.jsx';
import {showGetPostLinkModal} from 'actions/global_actions.jsx';
import DeletePostModal from 'components/delete_post_modal';
import {Constants, ModalIdentifiers} from 'utils/constants.jsx';
import * as Utils from 'utils/utils.jsx';
export default function DotMenuItem(props) {
......@@ -27,7 +28,18 @@ export default function DotMenuItem(props) {
function handleDeletePost(e) {
e.preventDefault();
showDeletePostModal(props.post, props.commentCount, props.isRHS);
const deletePostModalData = {
ModalId: ModalIdentifiers.DELETE_POST,
dialogType: DeletePostModal,
dialogProps: {
post: props.post,
commentCount: props.commentCount,
isRHS: props.isRHS,
},
};
props.actions.openModal(deletePostModalData);
}
const attrib = {};
......@@ -101,15 +113,20 @@ DotMenuItem.propTypes = {
actions: PropTypes.shape({
/*
/**
* Function to pin the post
*/
pinPost: PropTypes.func,
/*
/**
* Function to unpin the post
*/
unpinPost: PropTypes.func,
/**
* Function to open a modal
*/
openModal: PropTypes.func,
}),
};
......
......@@ -5,6 +5,7 @@ import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {flagPost, unflagPost} from 'mattermost-redux/actions/posts';
import {openModal} from 'actions/views/modals';
import {pinPost, unpinPost, setEditingPost} from 'actions/post_actions.jsx';
import DotMenu from './dot_menu.jsx';
......@@ -21,6 +22,7 @@ function mapDispatchToProps(dispatch) {
setEditingPost,
pinPost,
unpinPost,
openModal,
}, dispatch),
};
}
......
......@@ -7,11 +7,11 @@ import {FormattedMessage} from 'react-intl';
import PropTypes from 'prop-types';
import * as Selectors from 'mattermost-redux/selectors/entities/posts';
import * as GlobalActions from 'actions/global_actions.jsx';
import store from 'stores/redux_store.jsx';
import Constants from 'utils/constants.jsx';
import {Constants, ModalIdentifiers} from 'utils/constants.jsx';
import * as UserAgent from 'utils/user_agent.jsx';
import * as Utils from 'utils/utils.jsx';
import DeletePostModal from 'components/delete_post_modal';
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay.jsx';
import EmojiIcon from 'components/svg/emoji_icon';
import Textbox from 'components/textbox.jsx';
......@@ -165,7 +165,18 @@ export default class EditPostModal extends React.PureComponent {
const hasAttachment = editingPost.post.file_ids && editingPost.post.file_ids.length > 0;
if (updatedPost.message.trim().length === 0 && !hasAttachment) {
this.handleHide(false);
GlobalActions.showDeletePostModal(Selectors.getPost(getState(), editingPost.postId), editingPost.commentsCount, editingPost.isRHS);
const deletePostModalData = {
ModalId: ModalIdentifiers.DELETE_POST,
dialogType: DeletePostModal,
dialogProps: {
post: Selectors.getPost(getState(), editingPost.postId),
commentCount: editingPost.commentsCount,
isRHS: editingPost.isRHS,
},
};
this.props.actions.openModal(deletePostModalData);
return;
}
......
......@@ -8,6 +8,7 @@ import {Preferences} from 'mattermost-redux/constants';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
import {openModal} from 'actions/views/modals';
import {hideEditPostModal} from 'actions/post_actions';
import {editPost} from 'actions/views/edit_post_modal';
import {getEditingPost} from 'selectors/posts';
......@@ -28,6 +29,7 @@ function mapDispatchToProps(dispatch) {
addMessageIntoHistory,
editPost,
hideEditPostModal,
openModal,
}, dispatch),
};
}
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/delete_post_modal should match snapshot for delete_post_modal with 0 comments 1`] = `
<Modal
animation={true}
autoFocus={true}
backdrop={true}
bsClass="modal"
dialogComponentClass={[Function]}
enforceFocus={false}
keyboard={true}
manager={
ModalManager {
"add": [Function],
"containers": Array [],
"data": Array [],
"handleContainerOverflow": true,
"hideSiblingNodes": true,
"isTopModal": [Function],
"modals": Array [],
"remove": [Function],
}
}
onExited={[Function]}
onHide={[Function]}
renderBackdrop={[Function]}
restoreFocus={true}
show={true}
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeLabel="Close"
>
<ModalTitle
bsClass="modal-title"
componentClass="h4"
>
<FormattedMessage
defaultMessage="Confirm {term} Delete"
id="delete_post.confirm"
values={
Object {
"term": <FormattedMessage
defaultMessage="Post"
id="delete_post.post"
values={Object {}}
/>,
}
}
/>
</ModalTitle>
</ModalHeader>
<ModalBody
bsClass="modal-body"
componentClass="div"
>
<FormattedMessage
defaultMessage="Are you sure you want to delete this {term}?"
id="delete_post.question"
values={
Object {
"term": <FormattedMessage
defaultMessage="Post"
id="delete_post.post"
values={Object {}}
/>,
}
}
/>
<br />
<br />
</ModalBody>
<ModalFooter
bsClass="modal-footer"
componentClass="div"
>
<button
className="btn btn-default"
type="button"
>
<FormattedMessage
defaultMessage="Cancel"
id="delete_post.cancel"
values={Object {}}
/>
</button>
<button
autoFocus={true}
className="btn btn-danger"
onClick={[Function]}
type="button"
>
<FormattedMessage
defaultMessage="Delete"
id="delete_post.del"
values={Object {}}
/>
</button>
</ModalFooter>
</Modal>
`;
exports[`components/delete_post_modal should match snapshot for delete_post_modal with 1 comment 1`] = `
<Modal
animation={true}
autoFocus={true}
backdrop={true}
bsClass="modal"
dialogComponentClass={[Function]}
enforceFocus={false}
keyboard={true}
manager={
ModalManager {
"add": [Function],
"containers": Array [],
"data": Array [],
"handleContainerOverflow": true,
"hideSiblingNodes": true,
"isTopModal": [Function],
"modals": Array [],
"remove": [Function],
}
}
onExited={[Function]}
onHide={[Function]}
renderBackdrop={[Function]}
restoreFocus={true}
show={true}
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeLabel="Close"
>
<ModalTitle
bsClass="modal-title"
componentClass="h4"
>
<FormattedMessage
defaultMessage="Confirm {term} Delete"
id="delete_post.confirm"
values={
Object {
"term": <FormattedMessage
defaultMessage="Post"
id="delete_post.post"
values={Object {}}
/>,
}
}
/>
</ModalTitle>
</ModalHeader>
<ModalBody
bsClass="modal-body"
componentClass="div"
>
<FormattedMessage
defaultMessage="Are you sure you want to delete this {term}?"
id="delete_post.question"
values={
Object {
"term": <FormattedMessage
defaultMessage="Post"
id="delete_post.post"
values={Object {}}
/>,
}
}
/>
<br />
<br />
<FormattedMessage
defaultMessage="This post has {count, number} {count, plural, one {comment} other {comments}} on it."
id="delete_post.warning"
values={
Object {
"count": 1,
}
}
/>
</ModalBody>
<ModalFooter
bsClass="modal-footer"
componentClass="div"
>
<button
className="btn btn-default"
type="button"
>
<FormattedMessage
defaultMessage="Cancel"
id="delete_post.cancel"
values={Object {}}
/>
</button>
<button
autoFocus={true}
className="btn btn-danger"
onClick={[Function]}
type="button"
>
<FormattedMessage
defaultMessage="Delete"
id="delete_post.del"
values={Object {}}
/>
</button>
</ModalFooter>
</Modal>
`;
\ No newline at end of file
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import {Modal} from 'react-bootstrap';
import {browserHistory} from 'utils/browser_history';
import DeletePostModal from 'components/delete_post_modal/delete_post_modal.jsx';
describe('components/delete_post_modal', () => {
function emptyFunction() {} //eslint-disable-line no-empty-function
const post = {
id: '123',
message: 'test',
channel_id: '5',
};
const baseProps = {
post,
commentCount: 0,