diff options
author | Joram Wilander <jwawilander@gmail.com> | 2017-06-06 17:24:08 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-06-06 17:24:08 -0400 |
commit | 6e7b912ec61a6a791e0e8405ff6f6bd7e622a187 (patch) | |
tree | c2fa1d2bae0b344859339d9077af5d822420e7bb | |
parent | 02f09b8af90f1df38762b5257291b31597575dbb (diff) | |
download | chat-6e7b912ec61a6a791e0e8405ff6f6bd7e622a187.tar.gz chat-6e7b912ec61a6a791e0e8405ff6f6bd7e622a187.tar.bz2 chat-6e7b912ec61a6a791e0e8405ff6f6bd7e622a187.zip |
PLT-4257 Add pop-up asking if user wants to reset status (#6526)
* Add pop-up asking if user wants to reset status
* Update test snapshot
* Update prop name for old uses of confirm modal
* Updating checkbox (#6586)
* Updating style for checkbox (#6596)
20 files changed, 418 insertions, 49 deletions
diff --git a/webapp/actions/user_actions.jsx b/webapp/actions/user_actions.jsx index 606dccdb8..63c716d5d 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 159e133e5..fe53ade44 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 b817cb57d..7116b435c 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 89656a776..72f341efb 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 React from 'react'; import {Modal} from 'react-bootstrap'; - import PropTypes from 'prop-types'; - -import React from 'react'; +import {FormattedMessage} from 'react-intl'; export default class ConfirmModal extends React.Component { - constructor(props) { - super(props); + static propTypes = { + + /* + * 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, - this.handleKeypress = this.handleKeypress.bind(this); + /* + * 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 = ( + <div className='checkbox text-right margin-bottom--none'> + <label> + <input + ref='checkbox' + type='checkbox' + /> + {this.props.checkboxText} + </label> + </div> + ); + } + + let cancelText; + if (this.props.cancelButtonText) { + cancelText = this.props.cancelButtonText; + } else { + cancelText = ( + <FormattedMessage + id='confirm_modal.cancel' + defaultMessage='Cancel' + /> + ); + } + return ( <Modal className='modal-confirm' @@ -51,43 +140,25 @@ export default class ConfirmModal extends React.Component { </Modal.Header> <Modal.Body> {this.props.message} + {checkbox} </Modal.Body> <Modal.Footer> <button type='button' className='btn btn-default' - onClick={this.props.onCancel} + onClick={this.handleCancel} > - <FormattedMessage - id='confirm_modal.cancel' - defaultMessage='Cancel' - /> + {cancelText} </button> <button type='button' className={this.props.confirmButtonClass} - onClick={this.props.onConfirm} + onClick={this.handleConfirm} > - {this.props.confirmButton} + {this.props.confirmButtonText} </button> </Modal.Footer> </Modal> ); } } - -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 2735718e1..31c19bd5c 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 9a7222ec7..54a1d6604 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 f4c05e4eb..ae1040463 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 { <ConfirmModal title={confirmTitle} message={confirmMessage} - confirmButton={confirmButton} + confirmButtonText={confirmButton} show={this.state.showConfirmModal} onConfirm={this.handleUpdate} onCancel={this.confirmModalDismissed} diff --git a/webapp/components/integrations/components/edit_outgoing_webhook.jsx b/webapp/components/integrations/components/edit_outgoing_webhook.jsx index bd270e870..785023d14 100644 --- a/webapp/components/integrations/components/edit_outgoing_webhook.jsx +++ b/webapp/components/integrations/components/edit_outgoing_webhook.jsx @@ -180,7 +180,7 @@ export default class EditOutgoingWebhook extends AbstractOutgoingWebhook { <ConfirmModal title={confirmTitle} message={confirmMessage} - confirmButton={confirmButton} + confirmButtonText={confirmButton} show={this.state.showConfirmModal} onConfirm={this.handleUpdate} onCancel={this.confirmModalDismissed} diff --git a/webapp/components/invite_member_modal.jsx b/webapp/components/invite_member_modal.jsx index f4b2d0555..1e500115d 100644 --- a/webapp/components/invite_member_modal.jsx +++ b/webapp/components/invite_member_modal.jsx @@ -517,7 +517,7 @@ class InviteMemberModal extends React.Component { <ConfirmModal title={formatMessage(holders.modalTitle)} message={formatMessage(holders.modalMessage)} - confirmButton={formatMessage(holders.modalButton)} + confirmButtonText={formatMessage(holders.modalButton)} show={this.state.showConfirmModal} onConfirm={this.handleHide.bind(this, false)} onCancel={() => this.setState({showConfirmModal: false})} diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx index 6f8df3709..e488145e1 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 95556fb31..4f5188a47 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 { <EditPostModal/> <DeletePostModal/> <RemovedFromChannelModal/> + <ResetStatusModal/> </div> </div> ); diff --git a/webapp/components/reset_status_modal/index.js b/webapp/components/reset_status_modal/index.js new file mode 100644 index 000000000..34f08c7a5 --- /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 000000000..4a04d7561 --- /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 = ( + <FormattedMessage + id='modal.manual_status.title' + defaultMessage='Your status is set to "{status}"' + values={{ + status: userStatus + }} + /> + ); + + const manualStatusMessage = ( + <FormattedMessage + id='modal.manual_status.message' + defaultMessage='Would you like to switch your status to "Online"?' + /> + ); + + const manualStatusButton = ( + <FormattedMessage + id='modal.manual_status.button' + defaultMessage='Yes, set my status to "Online"' + /> + ); + + const manualStatusCancel = ( + <FormattedMessage + id='modal.manual_status.cancel' + defaultMessage='No, keep it as "{status}"' + values={{ + status: userStatus + }} + /> + ); + + const manualStatusCheckbox = ( + <FormattedMessage + id='modal.manual_status.ask' + defaultMessage='Do not ask me again' + /> + ); + + return ( + <ConfirmModal + show={this.state.show} + title={manualStatusTitle} + message={manualStatusMessage} + confirmButtonText={manualStatusButton} + onConfirm={this.onConfirm} + cancelButtonText={manualStatusCancel} + onCancel={this.onCancel} + showCheckbox={true} + checkboxText={manualStatusCheckbox} + /> + ); + } +} diff --git a/webapp/components/team_members_dropdown/team_members_dropdown.jsx b/webapp/components/team_members_dropdown/team_members_dropdown.jsx index 0837fabd0..041c4a859 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 0074ffce8..eee41ac10 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 { <ConfirmModal title={formatMessage(holders.confirmTitle)} message={formatMessage(holders.confirmMsg)} - confirmButton={formatMessage(holders.confirmBtns)} + confirmButtonText={formatMessage(holders.confirmBtns)} show={this.state.showConfirmModal} onConfirm={this.handleConfirm} onCancel={this.handleCancelConfirmation} diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index ca7101154..a250e5e6c 100755 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -1791,6 +1791,11 @@ "more_direct_channels.new_convo_note": "This will start a new conversation. If you’re adding a lot of people, consider creating a private channel instead.", "more_direct_channels.new_convo_note.full": "You’ve reached the maximum number of people for this conversation. Consider creating a private channel instead.", "more_direct_channels.title": "Direct Messages", + "modal.manaul_status.title": "Your status is set to \"{status}\"", + "modal.manaul_status.message": "Would you like to switch your status to \"Online\"?", + "modal.manaul_status.button": "Yes, set my status to \"Online\"", + "modal.manaul_status.cancel": "No, keep it as \"{status}\"", + "modal.manaul_status.ask": "Do not ask me again", "msg_typing.areTyping": "{users} and {last} are typing...", "msg_typing.isTyping": "{user} is typing...", "msg_typing.someone": "Someone", diff --git a/webapp/sass/layout/_forms.scss b/webapp/sass/layout/_forms.scss index fe847d1d6..143879e2c 100644 --- a/webapp/sass/layout/_forms.scss +++ b/webapp/sass/layout/_forms.scss @@ -1,5 +1,12 @@ @charset 'UTF-8'; +.radio, +.checkbox { + &.margin-bottom--none { + margin-bottom: 0; + } +} + .form-horizontal { .modal-intro { margin: -10px 0 30px; diff --git a/webapp/tests/components/__snapshots__/reset_status_modal.test.jsx.snap b/webapp/tests/components/__snapshots__/reset_status_modal.test.jsx.snap new file mode 100644 index 000000000..ec992a25e --- /dev/null +++ b/webapp/tests/components/__snapshots__/reset_status_modal.test.jsx.snap @@ -0,0 +1,54 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/ResetStatusModal should match snapshot 1`] = ` +<ConfirmModal + cancelButtonText={ + <FormattedMessage + defaultMessage="No, keep it as \\"{status}\\"" + id="modal.manual_status.cancel" + values={ + Object { + "status": "", + } + } + /> + } + checkboxText={ + <FormattedMessage + defaultMessage="Do not ask me again" + id="modal.manual_status.ask" + values={Object {}} + /> + } + confirmButtonClass="btn btn-primary" + confirmButtonText={ + <FormattedMessage + defaultMessage="Yes, set my status to \\"Online\\"" + id="modal.manual_status.button" + values={Object {}} + /> + } + message={ + <FormattedMessage + defaultMessage="Would you like to switch your status to \\"Online\\"?" + id="modal.manual_status.message" + values={Object {}} + /> + } + onCancel={[Function]} + onConfirm={[Function]} + show={false} + showCheckbox={true} + title={ + <FormattedMessage + defaultMessage="Your status is set to \\"{status}\\"" + id="modal.manual_status.title" + values={ + Object { + "status": "", + } + } + /> + } +/> +`; 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 000000000..420bb2bfe --- /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( + <ResetStatusModal + autoResetPref='' + actions={{ + autoResetStatus: fakeAutoReset, + setStatus: emptyFunction, + savePreferences: emptyFunction + }} + /> + ); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 343f1582f..01cba4d16 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" |