diff --git a/webapp/actions/user_actions.jsx b/webapp/actions/user_actions.jsx index 606dccdb81..63c716d5db 100644 --- a/webapp/actions/user_actions.jsx +++ b/webapp/actions/user_actions.jsx @@ -26,6 +26,7 @@ const dispatch = store.dispatch; const getState = store.getState; import * as Selectors from 'mattermost-redux/selectors/entities/users'; +import {getBool} from 'mattermost-redux/selectors/entities/preferences'; import { getProfiles, @@ -43,12 +44,15 @@ import { createUser, login, loadMe as loadMeRedux, - updateUserRoles as updateUserRolesRedux + updateUserRoles as updateUserRolesRedux, + getStatus, + setStatus } from 'mattermost-redux/actions/users'; import {getClientConfig, getLicenseConfig} from 'mattermost-redux/actions/general'; import {getTeamMembersByIds, getMyTeamMembers} from 'mattermost-redux/actions/teams'; import {getChannelAndMyMember} from 'mattermost-redux/actions/channels'; +import {Preferences as PreferencesRedux} from 'mattermost-redux/constants'; export function loadMe(callback) { loadMeRedux()(dispatch, getState).then( @@ -831,3 +835,23 @@ export function loadMyTeamMembers() { } ); } + +export function autoResetStatus() { + return async (doDispatch, doGetState) => { + const {currentUserId} = getState().entities.users; + const userStatus = await getStatus(currentUserId)(doDispatch, doGetState); + + if (!userStatus.manual) { + return userStatus; + } + + const autoReset = getBool(getState(), PreferencesRedux.CATEGORY_AUTO_RESET_MANUAL_STATUS, currentUserId, false); + + if (autoReset) { + setStatus({user_id: currentUserId, status: 'online'})(doDispatch, doGetState); + return userStatus; + } + + return userStatus; + }; +} diff --git a/webapp/components/admin_console/system_users/system_users_dropdown.jsx b/webapp/components/admin_console/system_users/system_users_dropdown.jsx index 159e133e59..fe53ade443 100644 --- a/webapp/components/admin_console/system_users/system_users_dropdown.jsx +++ b/webapp/components/admin_console/system_users/system_users_dropdown.jsx @@ -202,7 +202,7 @@ export default class SystemUsersDropdown extends React.Component { title={title} message={message} confirmButtonClass={confirmButtonClass} - confirmButton={deactivateMemberButton} + confirmButtonText={deactivateMemberButton} onConfirm={this.handleDeactivateMember} onCancel={this.handleDeactivateCancel} /> @@ -467,7 +467,7 @@ export default class SystemUsersDropdown extends React.Component { show={this.state.showDemoteModal} title={title} message={message} - confirmButton={confirmButton} + confirmButtonText={confirmButton} onConfirm={this.handleDemoteSubmit} onCancel={this.handleDemoteCancel} /> diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx index b817cb57d5..7116b435c0 100644 --- a/webapp/components/channel_header.jsx +++ b/webapp/components/channel_header.jsx @@ -272,7 +272,7 @@ export default class ChannelHeader extends React.Component { title={title} message={message} confirmButtonClass={buttonClass} - confirmButton={button} + confirmButtonText={button} onConfirm={() => ChannelActions.leaveChannel(this.state.channel.id)} onCancel={this.hideLeaveChannelModal} /> diff --git a/webapp/components/confirm_modal.jsx b/webapp/components/confirm_modal.jsx index 89656a776e..72f341efbd 100644 --- a/webapp/components/confirm_modal.jsx +++ b/webapp/components/confirm_modal.jsx @@ -1,18 +1,70 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import {FormattedMessage} from 'react-intl'; -import {Modal} from 'react-bootstrap'; - -import PropTypes from 'prop-types'; - import React from 'react'; +import {Modal} from 'react-bootstrap'; +import PropTypes from 'prop-types'; +import {FormattedMessage} from 'react-intl'; export default class ConfirmModal extends React.Component { - constructor(props) { - super(props); + static propTypes = { - this.handleKeypress = this.handleKeypress.bind(this); + /* + * Set to show modal + */ + show: PropTypes.bool.isRequired, + + /* + * Title to use for the modal + */ + title: PropTypes.node, + + /* + * Message to display in the body of the modal + */ + message: PropTypes.node, + + /* + * The CSS class to apply to the confirm button + */ + confirmButtonClass: PropTypes.string, + + /* + * Text/jsx element on the confirm button + */ + confirmButtonText: PropTypes.node, + + /* + * Text/jsx element on the cancel button + */ + cancelButtonText: PropTypes.node, + + /* + * Set to show checkbox + */ + showCheckbox: PropTypes.bool, + + /* + * Text/jsx element to display with the checkbox + */ + checkboxText: PropTypes.node, + + /* + * Function called when the confirm button or ENTER is pressed. Passes `true` if the checkbox is checked + */ + onConfirm: PropTypes.func.isRequired, + + /* + * Function called when the cancel button is pressed or the modal is hidden. Passes `true` if the checkbox is checked + */ + onCancel: PropTypes.func.isRequired + } + + static defaultProps = { + title: '', + message: '', + confirmButtonClass: 'btn btn-primary', + confirmButtonText: '' } componentDidMount() { @@ -33,13 +85,50 @@ export default class ConfirmModal extends React.Component { } } - handleKeypress(e) { + handleKeypress = (e) => { if (e.key === 'Enter' && this.props.show) { - this.props.onConfirm(e); + this.handleConfirm(); } } + handleConfirm = () => { + const checked = this.refs.checkbox ? this.refs.checkbox.checked : false; + this.props.onConfirm(checked); + } + + handleCancel = () => { + const checked = this.refs.checkbox ? this.refs.checkbox.checked : false; + this.props.onCancel(checked); + } + render() { + let checkbox; + if (this.props.showCheckbox) { + checkbox = ( +
+ +
+ ); + } + + let cancelText; + if (this.props.cancelButtonText) { + cancelText = this.props.cancelButtonText; + } else { + cancelText = ( + + ); + } + return ( {this.props.message} + {checkbox} ); } } - -ConfirmModal.defaultProps = { - title: '', - message: '', - confirmButtonClass: 'btn btn-primary', - confirmButton: '' -}; -ConfirmModal.propTypes = { - show: PropTypes.bool.isRequired, - title: PropTypes.node, - message: PropTypes.node, - confirmButtonClass: PropTypes.string, - confirmButton: PropTypes.node, - onConfirm: PropTypes.func.isRequired, - onCancel: PropTypes.func.isRequired -}; diff --git a/webapp/components/create_post.jsx b/webapp/components/create_post.jsx index 2735718e1c..31c19bd5c2 100644 --- a/webapp/components/create_post.jsx +++ b/webapp/components/create_post.jsx @@ -120,7 +120,9 @@ export default class CreatePost extends React.Component { } doSubmit(e) { - e.preventDefault(); + if (e) { + e.preventDefault(); + } const post = {}; post.file_ids = []; diff --git a/webapp/components/delete_modal_trigger.jsx b/webapp/components/delete_modal_trigger.jsx index 9a7222ec7a..54a1d6604f 100644 --- a/webapp/components/delete_modal_trigger.jsx +++ b/webapp/components/delete_modal_trigger.jsx @@ -28,8 +28,8 @@ export default class DeleteModalTrigger extends React.Component { }); } - handleConfirm(e) { - this.props.onDelete(e); + handleConfirm() { + this.props.onDelete(); } handleCancel() { @@ -57,7 +57,7 @@ export default class DeleteModalTrigger extends React.Component { show={this.state.showDeleteModal} title={this.modalTitle} message={this.modalMessage} - confirmButton={this.modalConfirmButton} + confirmButtonText={this.modalConfirmButton} onConfirm={this.handleConfirm} onCancel={this.handleCancel} onKeyDown={this.handleKeyDown} diff --git a/webapp/components/integrations/components/edit_command.jsx b/webapp/components/integrations/components/edit_command.jsx index f4c05e4eb4..ae10404638 100644 --- a/webapp/components/integrations/components/edit_command.jsx +++ b/webapp/components/integrations/components/edit_command.jsx @@ -719,7 +719,7 @@ export default class EditCommand extends React.Component { this.setState({showConfirmModal: false})} diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx index 6f8df3709f..e488145e1d 100644 --- a/webapp/components/navbar.jsx +++ b/webapp/components/navbar.jsx @@ -753,7 +753,7 @@ export default class Navbar extends React.Component { title={title} message={message} confirmButtonClass={buttonClass} - confirmButton={button} + confirmButtonText={button} onConfirm={() => ChannelActions.leaveChannel(this.state.channel.id)} onCancel={this.hideLeaveChannelModal} /> diff --git a/webapp/components/needs_team/needs_team.jsx b/webapp/components/needs_team/needs_team.jsx index 95556fb316..4f5188a47e 100644 --- a/webapp/components/needs_team/needs_team.jsx +++ b/webapp/components/needs_team/needs_team.jsx @@ -1,9 +1,8 @@ -import PropTypes from 'prop-types'; - // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. import React from 'react'; +import PropTypes from 'prop-types'; import $ from 'jquery'; @@ -42,6 +41,7 @@ import RemovedFromChannelModal from 'components/removed_from_channel_modal.jsx'; import ImportThemeModal from 'components/user_settings/import_theme_modal.jsx'; import InviteMemberModal from 'components/invite_member_modal.jsx'; import LeaveTeamModal from 'components/leave_team_modal.jsx'; +import ResetStatusModal from 'components/reset_status_modal'; import iNoBounce from 'inobounce'; import * as UserAgent from 'utils/user_agent.jsx'; @@ -229,6 +229,7 @@ export default class NeedsTeam extends React.Component { + ); diff --git a/webapp/components/reset_status_modal/index.js b/webapp/components/reset_status_modal/index.js new file mode 100644 index 0000000000..34f08c7a56 --- /dev/null +++ b/webapp/components/reset_status_modal/index.js @@ -0,0 +1,34 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; + +import {Preferences} from 'mattermost-redux/constants'; +import {get} from 'mattermost-redux/selectors/entities/preferences'; + +import {savePreferences} from 'mattermost-redux/actions/preferences'; +import {setStatus} from 'mattermost-redux/actions/users'; +import {autoResetStatus} from 'actions/user_actions.jsx'; + +import ResetStatusModal from './reset_status_modal.jsx'; + +function mapStateToProps(state, ownProps) { + const {currentUserId} = state.entities.users; + return { + ...ownProps, + autoResetPref: get(state, Preferences.CATEGORY_AUTO_RESET_MANUAL_STATUS, currentUserId, '') + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + autoResetStatus, + setStatus, + savePreferences + }, dispatch) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(ResetStatusModal); diff --git a/webapp/components/reset_status_modal/reset_status_modal.jsx b/webapp/components/reset_status_modal/reset_status_modal.jsx new file mode 100644 index 0000000000..4a04d75615 --- /dev/null +++ b/webapp/components/reset_status_modal/reset_status_modal.jsx @@ -0,0 +1,142 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import ConfirmModal from 'components/confirm_modal.jsx'; + +import {toTitleCase} from 'utils/utils.jsx'; + +import React from 'react'; +import PropTypes from 'prop-types'; +import {FormattedMessage} from 'react-intl'; +import {Preferences} from 'mattermost-redux/constants'; + +export default class ResetStatusModal extends React.PureComponent { + static propTypes = { + + /* + * The user's preference for whether their status is automatically reset + */ + autoResetPref: PropTypes.string, + actions: PropTypes.shape({ + + /* + * Function to get and then reset the user's status if needed + */ + autoResetStatus: PropTypes.func.isRequired, + + /* + * Function to set the status for a user + */ + setStatus: PropTypes.func.isRequired, + + /* + * Function to save user preferences + */ + savePreferences: PropTypes.func.isRequired + }).isRequired + } + + constructor(props) { + super(props); + + this.state = { + show: false, + currentUserStatus: {} + }; + } + + componentDidMount() { + this.props.actions.autoResetStatus().then( + (status) => { + const statusIsManual = status.manual; + const autoResetPrefNotSet = this.props.autoResetPref === ''; + + this.setState({ + currentUserStatus: status, // Set in state until status refactor where we store 'manual' field in redux + show: Boolean(statusIsManual && autoResetPrefNotSet) + }); + } + ); + } + + onConfirm = (checked) => { + this.setState({show: false}); + + const newStatus = {...this.state.currentUserStatus}; + newStatus.status = 'online'; + this.props.actions.setStatus(newStatus); + + if (checked) { + const pref = {category: Preferences.CATEGORY_AUTO_RESET_MANUAL_STATUS, user_id: newStatus.user_id, name: newStatus.user_id, value: 'true'}; + this.props.actions.savePreferences(pref.user_id, [pref]); + } + } + + onCancel = (checked) => { + this.setState({show: false}); + + if (checked) { + const status = {...this.state.currentUserStatus}; + const pref = {category: Preferences.CATEGORY_AUTO_RESET_MANUAL_STATUS, user_id: status.user_id, name: status.user_id, value: 'false'}; + this.props.actions.savePreferences(pref.user_id, [pref]); + } + } + + render() { + const userStatus = toTitleCase(this.state.currentUserStatus.status || ''); + const manualStatusTitle = ( + + ); + + const manualStatusMessage = ( + + ); + + const manualStatusButton = ( + + ); + + const manualStatusCancel = ( + + ); + + const manualStatusCheckbox = ( + + ); + + return ( + + ); + } +} diff --git a/webapp/components/team_members_dropdown/team_members_dropdown.jsx b/webapp/components/team_members_dropdown/team_members_dropdown.jsx index 0837fabd08..041c4a8597 100644 --- a/webapp/components/team_members_dropdown/team_members_dropdown.jsx +++ b/webapp/components/team_members_dropdown/team_members_dropdown.jsx @@ -357,7 +357,7 @@ export default class TeamMembersDropdown extends React.Component { show={this.state.showDemoteModal} title={title} message={message} - confirmButton={confirmButton} + confirmButtonText={confirmButton} onConfirm={this.handleDemoteSubmit} onCancel={this.handleDemoteCancel} /> diff --git a/webapp/components/user_settings/user_settings_modal.jsx b/webapp/components/user_settings/user_settings_modal.jsx index 0074ffce8b..eee41ac102 100644 --- a/webapp/components/user_settings/user_settings_modal.jsx +++ b/webapp/components/user_settings/user_settings_modal.jsx @@ -269,7 +269,7 @@ class UserSettingsModal extends React.Component { + } + checkboxText={ + + } + confirmButtonClass="btn btn-primary" + confirmButtonText={ + + } + message={ + + } + onCancel={[Function]} + onConfirm={[Function]} + show={false} + showCheckbox={true} + title={ + + } +/> +`; diff --git a/webapp/tests/components/reset_status_modal.test.jsx b/webapp/tests/components/reset_status_modal.test.jsx new file mode 100644 index 0000000000..420bb2bfe0 --- /dev/null +++ b/webapp/tests/components/reset_status_modal.test.jsx @@ -0,0 +1,29 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; + +import ResetStatusModal from 'components/reset_status_modal/reset_status_modal.jsx'; + +describe('components/ResetStatusModal', () => { + test('should match snapshot', () => { + function emptyFunction() {} //eslint-disable-line no-empty-function + + async function fakeAutoReset() { //eslint-disable-line require-await + return {status: 'away', manual: true, user_id: 'fake'}; + } + + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 343f1582f7..01cba4d16e 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -4884,7 +4884,7 @@ math-expression-evaluator@^1.2.14: mattermost-redux@mattermost/mattermost-redux#webapp-master: version "0.0.1" - resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/eaf3d811a8f9b9814f4d07c49bfc6d91e73a38be" + resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/30af9bdf41aeac4ca5a37817773bd8a9b8372a4a" dependencies: deep-equal "1.0.1" harmony-reflect "1.5.1"