Skip to content

Commit

Permalink
[MM-29630] Add error states to cloud guest invites when going over li…
Browse files Browse the repository at this point in the history
…mit (mattermost#6783)

* Don't show panel in subscription section if at user limit

* Show warning states when inviting more guests than user limit

* Working, PR commit

* Add snapshot
  • Loading branch information
nickmisasi committed Oct 14, 2020
1 parent aed2dc7 commit 9a27041
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ exports[`components/invitation_modal/InvitationModal should match the snapshot w
/>
}
/>
<InvitationModalGuestsStep
<Connect(InvitationModalGuestsStep)
currentTeamId="test"
defaultChannels={Array []}
defaultMessage=""
Expand Down
2 changes: 1 addition & 1 deletion components/invitation_modal/invitation_modal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {InviteTypes} from 'utils/constants';

import InvitationModalInitialStep from './invitation_modal_initial_step.jsx';
import InvitationModalMembersStep from './invitation_modal_members_step';
import InvitationModalGuestsStep from './invitation_modal_guests_step.jsx';
import InvitationModalGuestsStep from './invitation_modal_guest_step';
import InvitationModalConfirmStep from './invitation_modal_confirm_step.jsx';

import './invitation_modal.scss';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ exports[`components/invitation_modal/InvitationModalGuestsStep should match the
<UsersEmailsInput
ariaLabel="Invite People"
emailInvitationsEnabled={true}
errorMessageDefault="You have reached the user limit for your tier"
errorMessageId="invitation_modal.invite_members.hit_cloud_user_limit"
errorMessageValues={
Object {
"text": "0",
}
}
inputValue=""
loadingMessageDefault="Loading"
loadingMessageId="widgets.users_emails_input.loading"
Expand Down Expand Up @@ -166,15 +173,22 @@ exports[`components/invitation_modal/InvitationModalGuestsStep should match the
>
<UsersEmailsInput
ariaLabel="Invite People"
emailInvitationsEnabled={false}
emailInvitationsEnabled={true}
errorMessageDefault="You have reached the user limit for your tier"
errorMessageId="invitation_modal.invite_members.hit_cloud_user_limit"
errorMessageValues={
Object {
"text": "0",
}
}
inputValue=""
loadingMessageDefault="Loading"
loadingMessageId="widgets.users_emails_input.loading"
noMatchMessageDefault="No one found matching **{text}**"
noMatchMessageId="invitation_modal.guests.users_emails_input.no_user_found_matching-email-disabled"
noMatchMessageDefault="No one found matching **{text}**, type email to invite"
noMatchMessageId="invitation_modal.guests.users_emails_input.no_user_found_matching"
onChange={[Function]}
onInputChange={[Function]}
placeholder="Add guests"
placeholder="Add guests or email addresses"
showError={false}
usersLoader={[Function]}
validAddressMessageDefault="Invite **{email}** as a guest"
Expand All @@ -186,8 +200,8 @@ exports[`components/invitation_modal/InvitationModalGuestsStep should match the
className="help-text"
>
<FormattedMessage
defaultMessage="Add existing guests."
id="invitation_modal.guests.add_people.description-email-disabled"
defaultMessage="Add existing guests or send email invites to new guests."
id="invitation_modal.guests.add_people.description"
/>
</div>
</div>
Expand Down
43 changes: 43 additions & 0 deletions components/invitation_modal/invitation_modal_guest_step/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';

import {
getConfig,
getLicense,
} from 'mattermost-redux/selectors/entities/general';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
import {getStandardAnalytics} from 'mattermost-redux/actions/admin';
import {bindActionCreators, Dispatch} from 'redux';
import {GenericAction} from 'mattermost-redux/types/actions';
import {getCloudSubscription} from 'mattermost-redux/actions/cloud';

import {GlobalState} from 'types/store';

import {isAdmin} from 'utils/utils.jsx';

import InvitationModalGuestsStep from './invitation_modal_guests_step';

function mapStateToProps(state: GlobalState) {
return {
userLimit: getConfig(state).ExperimentalCloudUserLimit,
analytics: state.entities.admin.analytics,
userIsAdmin: isAdmin(getCurrentUser(state).roles),
isCloud: getLicense(state).Cloud === 'true',
subscription: state.entities.cloud.subscription,
};
}

function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
return {
actions: bindActionCreators(
{
getStandardAnalytics,
getCloudSubscription,
},
dispatch,
),
};
}

export default connect(mapStateToProps, mapDispatchToProps)(InvitationModalGuestsStep);
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ export default class InvitationModalGuestsStep extends React.PureComponent {
onEdit: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
emailInvitationsEnabled: PropTypes.bool.isRequired,
userLimit: PropTypes.string.isRequired,
currentUsers: PropTypes.number.isRequired,
userIsAdmin: PropTypes.bool.isRequired,
isCloud: PropTypes.bool.isRequired,
analytics: PropTypes.object.isRequired,
subscription: PropTypes.object.isRequired,
actions: PropTypes.shape({
getStandardAnalytics: PropTypes.func.isRequired,
getCloudSubscription: PropTypes.func.isRequired,
}).isRequired,
}

constructor(props) {
Expand Down Expand Up @@ -135,6 +145,48 @@ export default class InvitationModalGuestsStep extends React.PureComponent {
this.props.onSubmit(users, emails, this.state.channels, this.state.customMessageOpen ? this.state.customMessage : '', this.state.usersInputValue, this.state.channelsInputValue);
}

shouldShowPickerError = () => {
const {
userLimit,
analytics,
userIsAdmin,
isCloud,
subscription,
} = this.props;

if (subscription === null) {
return false;
}

if (subscription.is_paid_tier === 'true') {
return false;
}

if (userLimit === '0' || !userIsAdmin || !isCloud) {
return false;
}

// usersRemaining is calculated against the limit, the current users, and how many are being invited in the current flow
const usersRemaining =
userLimit -
(analytics.TOTAL_USERS + this.state.usersAndEmails.length);
if (usersRemaining === 0 && this.state.usersInputValue !== '') {
return true;
} else if (usersRemaining < 0) {
return true;
}
return false;
};

componentDidMount() {
if (!this.props.analytics) {
this.props.actions.getStandardAnalytics();
}
if (!this.props.subscription) {
this.props.actions.getCloudSubscription();
}
}

render() {
let inputPlaceholder = localizeMessage('invitation_modal.guests.search-and-add.placeholder', 'Add guests or email addresses');
let noMatchMessageId = t('invitation_modal.guests.users_emails_input.no_user_found_matching');
Expand All @@ -145,6 +197,7 @@ export default class InvitationModalGuestsStep extends React.PureComponent {
noMatchMessageId = t('invitation_modal.guests.users_emails_input.no_user_found_matching-email-disabled');
noMatchMessageDefault = 'No one found matching **{text}**';
}
const remainingUsers = this.props.userLimit - this.props.analytics.TOTAL_USERS;
return (
<div className='InvitationModalGuestsStep'>
<div className='modal-icon'>
Expand All @@ -171,32 +224,48 @@ export default class InvitationModalGuestsStep extends React.PureComponent {
<UsersEmailsInput
usersLoader={this.usersLoader}
placeholder={inputPlaceholder}
ariaLabel={localizeMessage('invitation_modal.guests.add_people.title', 'Invite People')}
ariaLabel={localizeMessage(
'invitation_modal.guests.add_people.title',
'Invite People',
)}
showError={this.shouldShowPickerError()}
errorMessageId={t(
'invitation_modal.invite_members.hit_cloud_user_limit',
)}
errorMessageDefault={
'You have reached the user limit for your tier'
}
errorMessageValues={{
text: remainingUsers < 0 ? '0' : remainingUsers,
}}
onChange={this.onUsersEmailsChange}
value={this.state.usersAndEmails}
onInputChange={this.onUsersInputChange}
inputValue={this.state.usersInputValue}
validAddressMessageId={t('invitation_modal.guests.users_emails_input.valid_email')}
validAddressMessageId={t(
'invitation_modal.guests.users_emails_input.valid_email',
)}
validAddressMessageDefault='Invite **{email}** as a guest'
noMatchMessageId={noMatchMessageId}
noMatchMessageDefault={noMatchMessageDefault}
emailInvitationsEnabled={this.props.emailInvitationsEnabled}
emailInvitationsEnabled={
this.props.emailInvitationsEnabled
}
/>
</div>
<div className='help-text'>

{this.props.emailInvitationsEnabled &&
<FormattedMessage
id='invitation_modal.guests.add_people.description'
defaultMessage='Add existing guests or send email invites to new guests.'
/>
}
{!this.props.emailInvitationsEnabled &&
<FormattedMessage
id='invitation_modal.guests.add_people.description-email-disabled'
defaultMessage='Add existing guests.'
/>
}
{this.props.emailInvitationsEnabled && (
<FormattedMessage
id='invitation_modal.guests.add_people.description'
defaultMessage='Add existing guests or send email invites to new guests.'
/>
)}
{!this.props.emailInvitationsEnabled && (
<FormattedMessage
id='invitation_modal.guests.add_people.description-email-disabled'
defaultMessage='Add existing guests.'
/>
)}
</div>
</div>
<div
Expand All @@ -217,7 +286,10 @@ export default class InvitationModalGuestsStep extends React.PureComponent {
{(placeholder) => (
<ChannelsInput
placeholder={placeholder}
ariaLabel={localizeMessage('invitation_modal.guests.add_channels.title', 'Search and Add Channels')}
ariaLabel={localizeMessage(
'invitation_modal.guests.add_channels.title',
'Search and Add Channels',
)}
channelsLoader={this.channelsLoader}
onChange={this.onChannelsChange}
onInputChange={this.onChannelsInputChange}
Expand All @@ -238,7 +310,7 @@ export default class InvitationModalGuestsStep extends React.PureComponent {
className='custom-message'
data-testid='customMessage'
>
{!this.state.customMessageOpen &&
{!this.state.customMessageOpen && (
<a
onClick={this.openCustomMessage}
href='#'
Expand All @@ -248,23 +320,25 @@ export default class InvitationModalGuestsStep extends React.PureComponent {
defaultMessage='Set a custom message'
/>
</a>
}
{this.state.customMessageOpen &&
)}
{this.state.customMessageOpen && (
<React.Fragment>
<div>
<FormattedMessage
id='invitation_modal.guests.custom-message.title'
defaultMessage='Custom message'
/>
<CloseCircleIcon onClick={this.closeCustomMessage}/>
<CloseCircleIcon
onClick={this.closeCustomMessage}
/>
</div>
<textarea
ref={this.textareaRef}
onChange={this.onMessageChange}
value={this.state.customMessage}
/>
</React.Fragment>
}
)}
<div className='help-text'>
<FormattedMessage
id='invitation_modal.guests.custom-message.description'
Expand All @@ -274,8 +348,17 @@ export default class InvitationModalGuestsStep extends React.PureComponent {
</div>
<div className='invite-guests'>
<button
className={'btn ' + (this.state.channels.length === 0 || this.state.usersAndEmails.length === 0 ? 'btn-inactive' : 'btn-primary')}
disabled={this.state.channels.length === 0 || this.state.usersAndEmails.length === 0}
className={
'btn ' +
(this.state.channels.length === 0 ||
this.state.usersAndEmails.length === 0 ?
'btn-inactive' :
'btn-primary')
}
disabled={
this.state.channels.length === 0 ||
this.state.usersAndEmails.length === 0
}
onClick={this.sendInvites}
id='inviteGuestButton'
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,35 @@ import {shallow} from 'enzyme';
import InvitationModalGuestsStep from './invitation_modal_guests_step.jsx';

describe('components/invitation_modal/InvitationModalGuestsStep', () => {
const props = {
teamName: 'Test Team',
currentTeamId: 'test-team-id',
inviteId: '123',
searchProfiles: jest.fn(),
emailInvitationsEnabled: true,
onSubmit: jest.fn(),
onEdit: jest.fn(),
userIsAdmin: true,
userLimit: '0',
currentUsers: 4,
isCloud: false,
subscription: {
is_paid_tier: 'false',
},
myInvitableChannels: [],
searchChannels: jest.fn(),
analytics: {
TOTAL_USERS: 10,
},
actions: {
getStandardAnalytics: () => {},
getCloudSubscription: () => {},
},
};
test('should match the snapshot', () => {
const wrapper = shallow(
<InvitationModalGuestsStep
teamName='Test Team'
currentTeamId='123'
myInvitableChannels={[]}
searchProfiles={jest.fn()}
searchChannels={jest.fn()}
emailInvitationsEnabled={true}
onSubmit={jest.fn()}
onEdit={jest.fn()}
{...props}
/>,
);
expect(wrapper).toMatchSnapshot();
Expand All @@ -26,14 +44,7 @@ describe('components/invitation_modal/InvitationModalGuestsStep', () => {
test('should match the snapshot when email invitations are disabled', () => {
const wrapper = shallow(
<InvitationModalGuestsStep
teamName='Test Team'
currentTeamId='123'
myInvitableChannels={[]}
searchProfiles={jest.fn()}
searchChannels={jest.fn()}
emailInvitationsEnabled={false}
onSubmit={jest.fn()}
onEdit={jest.fn()}
{...props}
/>,
);
expect(wrapper).toMatchSnapshot();
Expand Down

0 comments on commit 9a27041

Please sign in to comment.