Mono repo -> Master (#22553)
Combines the following repositories into one: https://github.com/mattermost/mattermost-server https://github.com/mattermost/mattermost-webapp https://github.com/mattermost/focalboard https://github.com/mattermost/mattermost-plugin-playbooks
Этот коммит содержится в:
@@ -0,0 +1,41 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/cloud_start_trial_btn/cloud_start_trial_btn should match snapshot 1`] = `
|
||||
<ContextProvider
|
||||
value={
|
||||
Object {
|
||||
"store": Object {
|
||||
"clearActions": [Function],
|
||||
"dispatch": [Function],
|
||||
"getActions": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
},
|
||||
"subscription": Subscription {
|
||||
"handleChangeWrapper": [Function],
|
||||
"listeners": Object {
|
||||
"notify": [Function],
|
||||
},
|
||||
"onStateChange": [Function],
|
||||
"parentSub": undefined,
|
||||
"store": Object {
|
||||
"clearActions": [Function],
|
||||
"dispatch": [Function],
|
||||
"getActions": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
},
|
||||
"unsubscribe": null,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<CloudStartTrialButton
|
||||
message="Cloud Start trial"
|
||||
onClick={[MockFunction]}
|
||||
telemetryId="test_telemetry_id"
|
||||
/>
|
||||
</ContextProvider>
|
||||
`;
|
||||
@@ -0,0 +1,18 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`/components/cloud_start_trial/input_business_email should match snapshot 1`] = `
|
||||
<ForwardRef
|
||||
autoComplete="off"
|
||||
autoFocus={true}
|
||||
containerClassName="request-business-email-container"
|
||||
customMessage={null}
|
||||
inputClassName="request-business-email-input"
|
||||
label="Enter business email"
|
||||
name="request-business-email"
|
||||
onChange={[MockFunction]}
|
||||
placeholder="name@companyname.com"
|
||||
required={true}
|
||||
type="email"
|
||||
value="foo@example.com"
|
||||
/>
|
||||
`;
|
||||
@@ -0,0 +1,39 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/request_business_email_modal/request_business_email_modal should match snapshot 1`] = `
|
||||
<ContextProvider
|
||||
value={
|
||||
Object {
|
||||
"store": Object {
|
||||
"clearActions": [Function],
|
||||
"dispatch": [Function],
|
||||
"getActions": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
},
|
||||
"subscription": Subscription {
|
||||
"handleChangeWrapper": [Function],
|
||||
"listeners": Object {
|
||||
"notify": [Function],
|
||||
},
|
||||
"onStateChange": [Function],
|
||||
"parentSub": undefined,
|
||||
"store": Object {
|
||||
"clearActions": [Function],
|
||||
"dispatch": [Function],
|
||||
"getActions": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
},
|
||||
"unsubscribe": null,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<RequestBusinessEmailModal
|
||||
onExited={[MockFunction]}
|
||||
/>
|
||||
</ContextProvider>
|
||||
`;
|
||||
@@ -0,0 +1,14 @@
|
||||
.CloudStartTrialButton {
|
||||
&:not(.style-link) {
|
||||
width: fit-content;
|
||||
padding: 13px 20px;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
&.style-link {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {ReactWrapper, shallow} from 'enzyme';
|
||||
|
||||
import {Provider} from 'react-redux';
|
||||
|
||||
import {act} from 'react-dom/test-utils';
|
||||
|
||||
import * as cloudActions from 'actions/cloud';
|
||||
|
||||
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
import mockStore from 'tests/test_store';
|
||||
|
||||
import {trackEvent} from 'actions/telemetry_actions.jsx';
|
||||
|
||||
import {TELEMETRY_CATEGORIES} from 'utils/constants';
|
||||
|
||||
import CloudStartTrialButton from './cloud_start_trial_btn';
|
||||
|
||||
jest.mock('actions/telemetry_actions.jsx', () => {
|
||||
const original = jest.requireActual('actions/telemetry_actions.jsx');
|
||||
return {
|
||||
...original,
|
||||
trackEvent: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('mattermost-redux/actions/general', () => ({
|
||||
...jest.requireActual('mattermost-redux/actions/general'),
|
||||
getLicenseConfig: () => ({type: 'adsf'}),
|
||||
getClientConfig: () => ({type: 'adsf'}),
|
||||
}));
|
||||
|
||||
jest.mock('mattermost-redux/actions/cloud', () => ({
|
||||
...jest.requireActual('mattermost-redux/actions/cloud'),
|
||||
getCloudSubscription: () => ({type: 'adsf'}),
|
||||
getCloudProducts: () => ({type: 'adsf'}),
|
||||
getCloudLimits: () => ({}),
|
||||
}));
|
||||
|
||||
describe('components/cloud_start_trial_btn/cloud_start_trial_btn', () => {
|
||||
const state = {
|
||||
entities: {
|
||||
admin: {},
|
||||
general: {
|
||||
license: {
|
||||
IsLicensed: 'true',
|
||||
Cloud: 'true',
|
||||
},
|
||||
},
|
||||
cloud: {
|
||||
subscription: {
|
||||
is_free_trial: 'false',
|
||||
trial_end_at: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
views: {
|
||||
modals: {
|
||||
modalState: {
|
||||
learn_more_trial_modal: {
|
||||
open: 'true',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const store = mockStore(state);
|
||||
|
||||
const props = {
|
||||
onClick: jest.fn(),
|
||||
message: 'Cloud Start trial',
|
||||
telemetryId: 'test_telemetry_id',
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<Provider store={store}>
|
||||
<CloudStartTrialButton {...props}/>
|
||||
</Provider>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should handle on click and change button text on SUCCESSFUL trial request', async () => {
|
||||
const mockOnClick = jest.fn();
|
||||
const requestTrialFn: () => () => Promise<any> = () => () => Promise.resolve(true);
|
||||
jest.spyOn(cloudActions, 'requestCloudTrial').mockImplementation(requestTrialFn);
|
||||
|
||||
let wrapper: ReactWrapper<any>;
|
||||
|
||||
// Mount the component
|
||||
await act(async () => {
|
||||
wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<CloudStartTrialButton
|
||||
{...props}
|
||||
onClick={mockOnClick}
|
||||
email='fakeemail@topreventbusinessemailvalidation'
|
||||
/>
|
||||
</Provider>,
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
expect(wrapper.find('.CloudStartTrialButton').text().includes('Cloud Start trial')).toBe(true);
|
||||
wrapper.find('.CloudStartTrialButton').simulate('click');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
expect(wrapper.find('.CloudStartTrialButton').text().includes('Loaded!')).toBe(true);
|
||||
});
|
||||
|
||||
expect(mockOnClick).toHaveBeenCalled();
|
||||
|
||||
expect(trackEvent).toHaveBeenCalledWith(TELEMETRY_CATEGORIES.CLOUD_START_TRIAL_BUTTON, 'test_telemetry_id');
|
||||
});
|
||||
|
||||
test('should handle on click and change button text on FAILED trial request', async () => {
|
||||
const mockOnClick = jest.fn();
|
||||
const requestTrialFn: () => () => Promise<any> = () => () => Promise.resolve(true);
|
||||
jest.spyOn(cloudActions, 'requestCloudTrial').mockImplementation(requestTrialFn);
|
||||
|
||||
let wrapper: ReactWrapper<any>;
|
||||
|
||||
// Mount the component
|
||||
await act(async () => {
|
||||
wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<CloudStartTrialButton
|
||||
{...props}
|
||||
onClick={mockOnClick}
|
||||
/>
|
||||
</Provider>,
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
expect(wrapper.find('.CloudStartTrialButton').text().includes('Cloud Start trial')).toBe(true);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('.CloudStartTrialButton').simulate('click');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
expect(wrapper.find('.CloudStartTrialButton').text().includes('Failed')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,183 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import {DispatchFunc} from 'mattermost-redux/types/actions';
|
||||
|
||||
import useGetSubscription from 'components/common/hooks/useGetSubscription';
|
||||
|
||||
import {requestCloudTrial, validateWorkspaceBusinessEmail, getCloudLimits} from 'actions/cloud';
|
||||
import {trackEvent} from 'actions/telemetry_actions';
|
||||
import {openModal, closeModal} from 'actions/views/modals';
|
||||
|
||||
import TrialBenefitsModal from 'components/trial_benefits_modal/trial_benefits_modal';
|
||||
|
||||
import {ModalIdentifiers, TELEMETRY_CATEGORIES} from 'utils/constants';
|
||||
|
||||
import RequestBusinessEmailModal from './request_business_email_modal';
|
||||
import './cloud_start_trial_btn.scss';
|
||||
|
||||
export type CloudStartTrialBtnProps = {
|
||||
message: string;
|
||||
telemetryId: string;
|
||||
onClick?: () => void;
|
||||
extraClass?: string;
|
||||
afterTrialRequest?: () => void;
|
||||
email?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
enum TrialLoadStatus {
|
||||
NotStarted = 'NOT_STARTED',
|
||||
Started = 'STARTED',
|
||||
Success = 'SUCCESS',
|
||||
Failed = 'FAILED',
|
||||
Embargoed = 'EMBARGOED',
|
||||
}
|
||||
|
||||
const TIME_UNTIL_CACHE_PURGE_GUESS = 5000;
|
||||
|
||||
const CloudStartTrialButton = ({
|
||||
message,
|
||||
telemetryId,
|
||||
extraClass,
|
||||
onClick,
|
||||
afterTrialRequest,
|
||||
email,
|
||||
disabled = false,
|
||||
}: CloudStartTrialBtnProps) => {
|
||||
const {formatMessage} = useIntl();
|
||||
const dispatch = useDispatch<DispatchFunc>();
|
||||
const subscription = useGetSubscription();
|
||||
const [openBusinessEmailModal, setOpenBusinessEmailModal] = useState(false);
|
||||
const [status, setLoadStatus] = useState(TrialLoadStatus.NotStarted);
|
||||
|
||||
const validateBusinessEmailOnLoad = async () => {
|
||||
const isValidBusinessEmail = await validateWorkspaceBusinessEmail()();
|
||||
if (!isValidBusinessEmail) {
|
||||
setOpenBusinessEmailModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
validateBusinessEmailOnLoad();
|
||||
}, []);
|
||||
|
||||
const requestStartTrial = async (): Promise<TrialLoadStatus> => {
|
||||
setLoadStatus(TrialLoadStatus.Started);
|
||||
|
||||
// email is set ONLY from the instance of this component created in the requestBusinessEmail modal.
|
||||
// So the flow is the following: If the email of the admin and the
|
||||
// email of the CWS customer are not valid, the requestBusinessModal is shown and that component will
|
||||
// create this StartCloudTrialBtn passing the email as Truthy, so the requetTrial flow continues normally
|
||||
if (openBusinessEmailModal && !email) {
|
||||
trackEvent(
|
||||
TELEMETRY_CATEGORIES.CLOUD_START_TRIAL_BUTTON,
|
||||
'trial_request_attempt_with_no_valid_business_email',
|
||||
);
|
||||
await dispatch(closeModal(ModalIdentifiers.LEARN_MORE_TRIAL_MODAL));
|
||||
openRequestBusinessEmailModal();
|
||||
setLoadStatus(TrialLoadStatus.Failed);
|
||||
return TrialLoadStatus.Failed;
|
||||
}
|
||||
|
||||
const subscriptionUpdated = await dispatch(requestCloudTrial('start_cloud_trial_btn', subscription?.id as string, (email || '')));
|
||||
if (!subscriptionUpdated) {
|
||||
setLoadStatus(TrialLoadStatus.Failed);
|
||||
return TrialLoadStatus.Failed;
|
||||
}
|
||||
|
||||
function ensureUpdatedData() {
|
||||
// Depending on timing of pods rolling, the webhook may still not get sent.
|
||||
// Re-request limits as a just-in-case, but only well after any
|
||||
// pods still alive should have either purged cache,
|
||||
// updated limits, or be brand new pods that won't be holding onto stale limits
|
||||
// We don't need to re-request subscription: the updated value is sent in the
|
||||
// request cloud trial response.
|
||||
// We don't need to request license: its update process is independent
|
||||
// from subscription/limit changes and always happens after pods roll.
|
||||
dispatch(getCloudLimits());
|
||||
}
|
||||
|
||||
setTimeout(ensureUpdatedData, TIME_UNTIL_CACHE_PURGE_GUESS);
|
||||
if (afterTrialRequest) {
|
||||
afterTrialRequest();
|
||||
}
|
||||
setLoadStatus(TrialLoadStatus.Success);
|
||||
return TrialLoadStatus.Success;
|
||||
};
|
||||
|
||||
const openTrialBenefitsModal = async (status: TrialLoadStatus) => {
|
||||
// Only open the benefits modal if the trial request succeeded
|
||||
if (status !== TrialLoadStatus.Success) {
|
||||
return;
|
||||
}
|
||||
await dispatch(openModal({
|
||||
modalId: ModalIdentifiers.TRIAL_BENEFITS_MODAL,
|
||||
dialogType: TrialBenefitsModal,
|
||||
dialogProps: {trialJustStarted: true},
|
||||
}));
|
||||
};
|
||||
|
||||
const openRequestBusinessEmailModal = () => {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.REQUEST_BUSINESS_EMAIL_MODAL,
|
||||
dialogType: RequestBusinessEmailModal,
|
||||
}));
|
||||
};
|
||||
|
||||
const btnText = (status: TrialLoadStatus): string => {
|
||||
switch (status) {
|
||||
case TrialLoadStatus.Started:
|
||||
return formatMessage({id: 'start_cloud_trial.modal.gettingTrial', defaultMessage: 'Getting Trial...'});
|
||||
case TrialLoadStatus.Success:
|
||||
return formatMessage({id: 'start_cloud_trial.modal.loaded', defaultMessage: 'Loaded!'});
|
||||
case TrialLoadStatus.Failed:
|
||||
return formatMessage({id: 'start_cloud_trial.modal.failed', defaultMessage: 'Failed'});
|
||||
case TrialLoadStatus.Embargoed:
|
||||
return formatMessage({id: 'admin.license.trial-request.embargoed'});
|
||||
default:
|
||||
return message;
|
||||
}
|
||||
};
|
||||
const startCloudTrial = async () => {
|
||||
if (status !== TrialLoadStatus.NotStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedStatus = await requestStartTrial();
|
||||
|
||||
if (updatedStatus !== TrialLoadStatus.Success) {
|
||||
return;
|
||||
}
|
||||
|
||||
trackEvent(
|
||||
TELEMETRY_CATEGORIES.CLOUD_START_TRIAL_BUTTON,
|
||||
telemetryId,
|
||||
);
|
||||
|
||||
// on click will execute whatever action is sent from the invoking place, if nothing is sent, open the trial benefits modal
|
||||
if (onClick) {
|
||||
onClick();
|
||||
return;
|
||||
}
|
||||
|
||||
await openTrialBenefitsModal(updatedStatus);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
id='start_cloud_trial_btn'
|
||||
className={`CloudStartTrialButton ${extraClass}`}
|
||||
onClick={startCloudTrial}
|
||||
disabled={disabled || status === TrialLoadStatus.Failed}
|
||||
>
|
||||
{btnText(status)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default CloudStartTrialButton;
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {mount, shallow} from 'enzyme';
|
||||
|
||||
import {ItemStatus} from 'utils/constants';
|
||||
|
||||
import InputBusinessEmail from './input_business_email';
|
||||
|
||||
describe('/components/cloud_start_trial/input_business_email', () => {
|
||||
const handleEmailValuesMockFn = jest.fn();
|
||||
const baseProps = {
|
||||
handleEmailValues: handleEmailValuesMockFn,
|
||||
email: 'foo@example.com',
|
||||
customInputLabel: null,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(<InputBusinessEmail {...baseProps}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('test input business email displays the input element correctly', () => {
|
||||
const wrapper = mount(
|
||||
<InputBusinessEmail {...baseProps}/>,
|
||||
);
|
||||
const inputElement = wrapper.find('.Input');
|
||||
expect(inputElement.length).toBe(1);
|
||||
});
|
||||
|
||||
test('test input business email displays the SUCCESS custom message correctly', () => {
|
||||
const wrapper = mount(
|
||||
<InputBusinessEmail {...{...baseProps, customInputLabel: {type: ItemStatus.SUCCESS, value: 'success value'}}}/>,
|
||||
);
|
||||
const customMessageElement = wrapper.find('.Input___customMessage.Input___success');
|
||||
expect(customMessageElement.length).toBe(1);
|
||||
});
|
||||
|
||||
test('test input business email displays the WARNING custom message correctly', () => {
|
||||
const wrapper = mount(
|
||||
<InputBusinessEmail {...{...baseProps, customInputLabel: {type: ItemStatus.WARNING, value: 'warning value'}}}/>,
|
||||
);
|
||||
const customMessageElement = wrapper.find('.Input___customMessage.Input___warning');
|
||||
expect(customMessageElement.length).toBe(1);
|
||||
});
|
||||
|
||||
test('test input business email displays the ERROR custom message correctly', () => {
|
||||
const wrapper = mount(
|
||||
<InputBusinessEmail {...{...baseProps, customInputLabel: {type: ItemStatus.ERROR, value: 'error value'}}}/>,
|
||||
);
|
||||
const customMessageElement = wrapper.find('.Input___customMessage.Input___error');
|
||||
expect(customMessageElement.length).toBe(1);
|
||||
});
|
||||
|
||||
test('test input business email displays the INFO custom message correctly', () => {
|
||||
const wrapper = mount(
|
||||
<InputBusinessEmail {...{...baseProps, customInputLabel: {type: ItemStatus.INFO, value: 'info value'}}}/>,
|
||||
);
|
||||
const customMessageElement = wrapper.find('.Input___customMessage.Input___info');
|
||||
expect(customMessageElement.length).toBe(1);
|
||||
});
|
||||
|
||||
test('test the input element handles the onChange event correctly', () => {
|
||||
const event = {
|
||||
target: {value: 'email@domain.com'},
|
||||
};
|
||||
const wrapper = mount(
|
||||
<InputBusinessEmail {...baseProps}/>,
|
||||
);
|
||||
const inputElement = wrapper.find('.Input');
|
||||
|
||||
inputElement.find('input').simulate('change', event);
|
||||
expect(handleEmailValuesMockFn).toBeCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import Input, {CustomMessageInputType} from 'components/widgets/inputs/input/input';
|
||||
interface InputBusinessEmailProps {
|
||||
email: string;
|
||||
handleEmailValues: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
customInputLabel: CustomMessageInputType;
|
||||
}
|
||||
|
||||
const InputBusinessEmail = ({
|
||||
email,
|
||||
handleEmailValues,
|
||||
customInputLabel,
|
||||
}: InputBusinessEmailProps): JSX.Element => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
return (
|
||||
<Input
|
||||
type='email'
|
||||
autoComplete='off'
|
||||
autoFocus={true}
|
||||
required={true}
|
||||
value={email}
|
||||
name='request-business-email'
|
||||
containerClassName='request-business-email-container'
|
||||
inputClassName='request-business-email-input'
|
||||
label={formatMessage({id: 'start_cloud_trial.modal.enter_trial_email.input.label', defaultMessage: 'Enter business email'})}
|
||||
placeholder={formatMessage({id: 'start_cloud_trial.modal.enter_trial_email.input.placeholder', defaultMessage: 'name@companyname.com'})}
|
||||
onChange={handleEmailValues}
|
||||
customMessage={customInputLabel}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputBusinessEmail;
|
||||
@@ -0,0 +1,115 @@
|
||||
@import 'utils/variables';
|
||||
@import 'utils/mixins';
|
||||
|
||||
.RequestBusinessEmailModal {
|
||||
height: 320px;
|
||||
|
||||
&.modal-dialog {
|
||||
margin-top: calc(50vh - 350px) !important;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 0 !important;
|
||||
border-color: rgba(var(--center-channel-color-rgb), 0.16);
|
||||
background: var(--center-channel-bg);
|
||||
border-radius: 8px;
|
||||
color: var(--center-channel-color);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
.close {
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus,
|
||||
&:active:focus {
|
||||
background-color: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
color: rgba(var(--center-channel-color-rgb), 0.72);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
top: 6px;
|
||||
right: 4px;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
border-radius: 4px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.56) !important;
|
||||
font-family:
|
||||
'Open Sans',
|
||||
sans-serif;
|
||||
font-size: 32px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
height: 38px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: var(--center-channel-bg) !important;
|
||||
border-radius: 8px;
|
||||
color: var(--center-channel-color);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: calc(100% - 38px);
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
|
||||
.GenericModal__body {
|
||||
height: 100%;
|
||||
padding: 0 24px 24px 24px;
|
||||
|
||||
.container-footer {
|
||||
bottom: 0;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.request-business-email-input {
|
||||
height: 34px !important;
|
||||
border: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.start-trial-email-title {
|
||||
margin-bottom: 22px;
|
||||
color: var(--center-channel-color);
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.start-trial-email-description {
|
||||
margin-bottom: 16px;
|
||||
color: var(--center-channel-color);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.start-trial-email-disclaimer {
|
||||
margin-top: 56px;
|
||||
}
|
||||
|
||||
.start-trial-button {
|
||||
display: flex;
|
||||
|
||||
button {
|
||||
@include primary-button;
|
||||
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-centered {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 12px 24px 24px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {Provider} from 'react-redux';
|
||||
import {act} from 'react-dom/test-utils';
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import * as cloudActions from 'actions/cloud';
|
||||
|
||||
import GenericModal from 'components/generic_modal';
|
||||
|
||||
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
import mockStore from 'tests/test_store';
|
||||
|
||||
import RequestBusinessEmailModal from './request_business_email_modal';
|
||||
|
||||
jest.useFakeTimers();
|
||||
jest.mock('lodash/debounce', () => jest.fn((fn) => fn));
|
||||
|
||||
describe('components/request_business_email_modal/request_business_email_modal', () => {
|
||||
const state = {
|
||||
entities: {
|
||||
users: {
|
||||
currentUserId: 'current_user_id',
|
||||
},
|
||||
admin: {},
|
||||
general: {
|
||||
license: {
|
||||
IsLicensed: 'true',
|
||||
Cloud: 'true',
|
||||
},
|
||||
config: {},
|
||||
},
|
||||
cloud: {
|
||||
subscription: {id: 'subscriptionID'},
|
||||
},
|
||||
},
|
||||
views: {
|
||||
modals: {
|
||||
modalState: {
|
||||
request_business_email_modal: {
|
||||
open: 'true',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const props = {
|
||||
onExited: jest.fn(),
|
||||
};
|
||||
|
||||
const store = mockStore(state);
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<Provider store={store}>
|
||||
<RequestBusinessEmailModal {...props}/>
|
||||
</Provider>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should show the Start Cloud Trial Button', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<RequestBusinessEmailModal {...props}/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
const startTrialBtn = wrapper.find('CloudStartTrialButton');
|
||||
expect(startTrialBtn).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('should call on close', async () => {
|
||||
const mockOnClose = jest.fn();
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<RequestBusinessEmailModal
|
||||
{...props}
|
||||
onClose={mockOnClose}
|
||||
/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find(GenericModal).props().onExited();
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
test('should call on exited', async () => {
|
||||
const mockOnExited = jest.fn();
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<RequestBusinessEmailModal
|
||||
{...props}
|
||||
onExited={mockOnExited}
|
||||
/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find(GenericModal).props().onExited();
|
||||
expect(mockOnExited).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
test('should show the Input to enter the valid Business Email', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<RequestBusinessEmailModal {...props}/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
expect(wrapper.find('InputBusinessEmail')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('should start with Start Cloud Trial Button disabled', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<RequestBusinessEmailModal {...props}/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
const startTrialBtn = wrapper.find('CloudStartTrialButton');
|
||||
expect(startTrialBtn.props().disabled).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('should ENABLE the trial button if email is VALID', async () => {
|
||||
// mock validation response to TRUE meaning the email is a valid email
|
||||
const validateBusinessEmail = () => () => Promise.resolve(true);
|
||||
jest.spyOn(cloudActions, 'validateBusinessEmail').mockImplementation(validateBusinessEmail);
|
||||
|
||||
const event = {
|
||||
target: {value: 'valid-email@domain.com'},
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<RequestBusinessEmailModal {...props}/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
const inputBusinessEmail = wrapper.find('InputBusinessEmail');
|
||||
const input = inputBusinessEmail.find('input');
|
||||
input.find('input').at(0).simulate('change', event);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
wrapper.update();
|
||||
const startTrialBtn = wrapper.find('CloudStartTrialButton');
|
||||
expect(startTrialBtn.props().disabled).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('should show the success custom message if the email is valid', async () => {
|
||||
// mock validation response to TRUE meaning the email is a valid email
|
||||
const validateBusinessEmail = () => () => Promise.resolve(true);
|
||||
|
||||
jest.spyOn(cloudActions, 'validateBusinessEmail').mockImplementation(validateBusinessEmail);
|
||||
|
||||
const event = {
|
||||
target: {value: 'valid-email@domain.com'},
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<RequestBusinessEmailModal {...props}/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
const inputBusinessEmail = wrapper.find('InputBusinessEmail');
|
||||
const input = inputBusinessEmail.find('input');
|
||||
input.find('input').at(0).simulate('change', event);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
wrapper.update();
|
||||
const customMessageElement = wrapper.find('.Input___customMessage.Input___success');
|
||||
expect(customMessageElement.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('should DISABLE the trial button if email is INVALID', async () => {
|
||||
// mock validation response to FALSE meaning the email is an invalid email
|
||||
const validateBusinessEmail = () => () => Promise.resolve(false);
|
||||
jest.spyOn(cloudActions, 'validateBusinessEmail').mockImplementation(validateBusinessEmail);
|
||||
|
||||
const event = {
|
||||
target: {value: 'INvalid-email@domain.com'},
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<RequestBusinessEmailModal {...props}/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
const inputBusinessEmail = wrapper.find('InputBusinessEmail');
|
||||
const input = inputBusinessEmail.find('input');
|
||||
input.find('input').at(0).simulate('change', event);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
wrapper.update();
|
||||
const startTrialBtn = wrapper.find('CloudStartTrialButton');
|
||||
expect(startTrialBtn.props().disabled).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('should show the error custom message if the email is invalid', async () => {
|
||||
// mock validation response to FALSE meaning the email is an invalid email
|
||||
const validateBusinessEmail = () => () => Promise.resolve(false);
|
||||
jest.spyOn(cloudActions, 'validateBusinessEmail').mockImplementation(validateBusinessEmail);
|
||||
|
||||
const event = {
|
||||
target: {value: 'INvalid-email@domain.com'},
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<RequestBusinessEmailModal {...props}/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
const inputBusinessEmail = wrapper.find('InputBusinessEmail');
|
||||
const input = inputBusinessEmail.find('input');
|
||||
input.find('input').at(0).simulate('change', event);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
wrapper.update();
|
||||
const customMessageElement = wrapper.find('.Input___customMessage.Input___error');
|
||||
expect(customMessageElement.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,171 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import {debounce} from 'lodash';
|
||||
|
||||
import {DispatchFunc} from 'mattermost-redux/types/actions';
|
||||
|
||||
import {trackEvent} from 'actions/telemetry_actions';
|
||||
import {closeModal} from 'actions/views/modals';
|
||||
import {validateBusinessEmail} from 'actions/cloud';
|
||||
|
||||
import {ItemStatus, TELEMETRY_CATEGORIES, ModalIdentifiers, LicenseLinks, AboutLinks} from 'utils/constants';
|
||||
|
||||
import GenericModal from 'components/generic_modal';
|
||||
import {CustomMessageInputType} from 'components/widgets/inputs/input/input';
|
||||
import ExternalLink from 'components/external_link';
|
||||
|
||||
import {isEmail} from 'mattermost-redux/utils/helpers';
|
||||
|
||||
import StartCloudTrialBtn from './cloud_start_trial_btn';
|
||||
import InputBusinessEmail from './input_business_email';
|
||||
|
||||
import './request_business_email_modal.scss';
|
||||
|
||||
type Props = {
|
||||
onClose?: () => void;
|
||||
onExited: () => void;
|
||||
}
|
||||
|
||||
const RequestBusinessEmailModal = (
|
||||
{
|
||||
onClose,
|
||||
onExited,
|
||||
}: Props): JSX.Element | null => {
|
||||
const {formatMessage} = useIntl();
|
||||
const dispatch = useDispatch<DispatchFunc>();
|
||||
const [email, setEmail] = useState<string>('');
|
||||
const [customInputLabel, setCustomInputLabel] = useState<CustomMessageInputType>(null);
|
||||
const [trialBtnDisabled, setTrialBtnDisabled] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
trackEvent(
|
||||
TELEMETRY_CATEGORIES.REQUEST_BUSINESS_EMAIL,
|
||||
'request_business_email',
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleOnClose = useCallback(() => {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
|
||||
onExited();
|
||||
}, [onClose, onExited]);
|
||||
|
||||
const handleEmailValues = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const email = e.target.value;
|
||||
setEmail(email.trim().toLowerCase());
|
||||
|
||||
validateEmail(email);
|
||||
}, []);
|
||||
|
||||
const validateEmail = useCallback(debounce(async (email: string) => {
|
||||
// no value set, no validation and clean the custom input label
|
||||
if (!email) {
|
||||
setTrialBtnDisabled(true);
|
||||
setCustomInputLabel(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// function isEmail aready handle empty / null value
|
||||
if (!isEmail(email)) {
|
||||
const errMsg = formatMessage({id: 'request_business_email_modal.invalidEmail', defaultMessage: 'This doesn\'t look like a valid email'});
|
||||
setCustomInputLabel({type: ItemStatus.WARNING, value: errMsg});
|
||||
setTrialBtnDisabled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// go and validate the email against the validateBusinessEmail endpoint
|
||||
const isValidBusinessEmail = await validateBusinessEmail(email)();
|
||||
if (!isValidBusinessEmail) {
|
||||
const errMsg = formatMessage({id: 'request_business_email_modal.not_business_email', defaultMessage: 'This doesn\'t look like a business email'});
|
||||
setCustomInputLabel({type: ItemStatus.ERROR, value: errMsg});
|
||||
setTrialBtnDisabled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// if it is a valid business email, proceed, enable the start trial button and notify the user about the email is valid
|
||||
const okMsg = formatMessage({id: 'request_business_email_modal.valid_business_email', defaultMessage: 'This is a valid email'});
|
||||
setCustomInputLabel({type: ItemStatus.SUCCESS, value: okMsg});
|
||||
setTrialBtnDisabled(false);
|
||||
}, 250), []);
|
||||
|
||||
// this function will be executed after successfull trial request, closing this request business email modal
|
||||
const closeMeAfterSuccessTrialReq = async () => {
|
||||
await dispatch(closeModal(ModalIdentifiers.REQUEST_BUSINESS_EMAIL_MODAL));
|
||||
};
|
||||
|
||||
return (
|
||||
<GenericModal
|
||||
className='RequestBusinessEmailModal'
|
||||
id='RequestBusinessEmailModal'
|
||||
onExited={handleOnClose}
|
||||
>
|
||||
<div className='start-trial-email-title'>
|
||||
<FormattedMessage
|
||||
id='start_cloud_trial.modal.enter_trial_email.title'
|
||||
defaultMessage='Enter an email to start your trial'
|
||||
/>
|
||||
</div>
|
||||
<div className='start-trial-email-description'>
|
||||
<FormattedMessage
|
||||
id='start_cloud_trial.modal.enter_trial_email.description'
|
||||
defaultMessage='Start a trial and enter a business email to get started. '
|
||||
/>
|
||||
</div>
|
||||
<div className='start-trial-email-input'>
|
||||
<InputBusinessEmail
|
||||
email={email}
|
||||
handleEmailValues={handleEmailValues}
|
||||
customInputLabel={customInputLabel}
|
||||
/>
|
||||
</div>
|
||||
<div className='start-trial-email-disclaimer'>
|
||||
<FormattedMessage
|
||||
id='request_business_email.start_trial.modal.disclaimer'
|
||||
defaultMessage='By selecting <highlight>“Start trial”</highlight>, I agree to the <linkEvaluation>Mattermost Software and Services License Agreement</linkEvaluation>, <linkPrivacy>privacy policy</linkPrivacy> and receiving product emails.'
|
||||
values={{
|
||||
highlight: (msg: React.ReactNode) => (
|
||||
<strong>
|
||||
{msg}
|
||||
</strong>
|
||||
),
|
||||
linkEvaluation: (msg: React.ReactNode) => (
|
||||
<ExternalLink
|
||||
href={LicenseLinks.SOFTWARE_SERVICES_LICENSE_AGREEMENT}
|
||||
location='request_business_email_modal'
|
||||
>
|
||||
{msg}
|
||||
</ExternalLink>
|
||||
),
|
||||
linkPrivacy: (msg: React.ReactNode) => (
|
||||
<ExternalLink
|
||||
href={AboutLinks.PRIVACY_POLICY}
|
||||
location='request_business_email_modal'
|
||||
>
|
||||
{msg}
|
||||
</ExternalLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='start-trial-button'>
|
||||
<StartCloudTrialBtn
|
||||
message={formatMessage({id: 'cloud.startTrial.modal.btn', defaultMessage: 'Start trial'})}
|
||||
telemetryId='request_business_email_modal'
|
||||
disabled={trialBtnDisabled}
|
||||
email={email}
|
||||
afterTrialRequest={closeMeAfterSuccessTrialReq}
|
||||
/>
|
||||
</div>
|
||||
</GenericModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RequestBusinessEmailModal;
|
||||
Ссылка в новой задаче
Block a user