Merge branch 'master' into MM-51858-fix-workspace-deletion-telemetry
Этот коммит содержится в:
@@ -12,8 +12,8 @@ import {
|
||||
import {logout, loadMe, loadMeREST} from 'mattermost-redux/actions/users';
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
import {getConfig, isPerformanceDebuggingEnabled} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getCurrentTeamId, getMyTeams, getTeam, getMyTeamMember, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getBool, isCollapsedThreadsEnabled, isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentTeamId, getMyTeams, getTeam, getMyTeamMember, getTeamMemberships, getActiveTeamsList} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getBool, getIsOnboardingFlowEnabled, isCollapsedThreadsEnabled, isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUser, getCurrentUserId, isFirstAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getCurrentChannelStats, getCurrentChannelId, getMyChannelMember, getRedirectChannelNameForTeam, getChannelsNameMapInTeam, getAllDirectChannels, getChannelMessageCount} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {appsEnabled} from 'mattermost-redux/selectors/entities/apps';
|
||||
@@ -352,7 +352,7 @@ export async function redirectUserToDefaultTeam() {
|
||||
// Assume we need to load the user if they don't have any team memberships loaded or the user loaded
|
||||
let user = getCurrentUser(state);
|
||||
const shouldLoadUser = Utils.isEmptyObject(getTeamMemberships(state)) || !user;
|
||||
|
||||
const onboardingFlowEnabled = getIsOnboardingFlowEnabled(state);
|
||||
if (shouldLoadUser) {
|
||||
if (isGraphQLEnabled(state)) {
|
||||
await dispatch(loadMe());
|
||||
@@ -374,8 +374,9 @@ export async function redirectUserToDefaultTeam() {
|
||||
const teamId = LocalStorageStore.getPreviousTeamId(user.id);
|
||||
|
||||
let myTeams = getMyTeams(state);
|
||||
if (myTeams.length === 0) {
|
||||
if (isUserFirstAdmin) {
|
||||
const teams = getActiveTeamsList(state);
|
||||
if (teams.length === 0) {
|
||||
if (isUserFirstAdmin && onboardingFlowEnabled) {
|
||||
getHistory().push('/preparing-workspace');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import {selectLhsItem} from 'actions/views/lhs';
|
||||
import {GlobalState} from 'types/store';
|
||||
import {LhsItemType, LhsPage} from 'types/store/lhs';
|
||||
|
||||
import {CardSizes, InsightsWidgetTypes, TimeFrame, TimeFrames} from '@mattermost/types/insights';
|
||||
@@ -41,17 +40,20 @@ type SelectOption = {
|
||||
|
||||
const Insights = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// check if either of focalboard plugin or boards product is enabled
|
||||
const focalboardPluginEnabled = useSelector((state: GlobalState) => state.plugins.plugins?.focalboard);
|
||||
let focalboardProductEnabled = false;
|
||||
const products = useProducts();
|
||||
if (products) {
|
||||
focalboardProductEnabled = products.some((product) => product.pluginId === suitePluginIds.focalboard || product.pluginId === suitePluginIds.boards);
|
||||
}
|
||||
const focalboardEnabled = focalboardPluginEnabled || focalboardProductEnabled;
|
||||
|
||||
const playbooksEnabled = useSelector((state: GlobalState) => state.plugins.plugins?.playbooks);
|
||||
let focalboardEnabled = false;
|
||||
let playbooksEnabled = false;
|
||||
if (products) {
|
||||
products.forEach((product) => {
|
||||
if (product.pluginId === suitePluginIds.boards) {
|
||||
focalboardEnabled = true;
|
||||
} else if (product.pluginId === suitePluginIds.playbooks) {
|
||||
playbooksEnabled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const currentUserId = useSelector(getCurrentUserId);
|
||||
const currentTeamId = useSelector(getCurrentTeamId);
|
||||
|
||||
|
||||
@@ -59,13 +59,13 @@ const ContactSalesCard = (props: Props) => {
|
||||
title = (
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.privateCloudCard.cloudEnterprise.title'
|
||||
defaultMessage='Looking for an annual discount? '
|
||||
defaultMessage='Looking to rollout Mattermost for your entire organization? '
|
||||
/>
|
||||
);
|
||||
description = (
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.privateCloudCard.cloudEnterprise.description'
|
||||
defaultMessage='At Mattermost, we work with you and your team to meet your needs throughout the product. If you are looking for an annual discount, please reach out to our sales team.'
|
||||
defaultMessage='At Mattermost, we work with you and your organization to meet your needs throughout the product. If you’re considering a wider rollout, talk to us.'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
@@ -103,13 +103,13 @@ const ContactSalesCard = (props: Props) => {
|
||||
title = (
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.privateCloudCard.cloudEnterprise.title'
|
||||
defaultMessage='Looking for an annual discount? '
|
||||
defaultMessage='Looking to rollout Mattermost for your entire organization? '
|
||||
/>
|
||||
);
|
||||
description = (
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.privateCloudCard.cloudEnterprise.description'
|
||||
defaultMessage='At Mattermost, we work with you and your team to meet your needs throughout the product. If you are looking for an annual discount, please reach out to our sales team.'
|
||||
defaultMessage='At Mattermost, we work with you and your organization to meet your needs throughout the product. If you’re considering a wider rollout, talk to us.'
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -163,7 +163,7 @@ const ToYearlyNudgeBannerDismissable = () => {
|
||||
type={announcementType}
|
||||
showCloseButton={daysToProMonthlyEnd > 10}
|
||||
onButtonClick={() => openPurchaseModal({trackingLocation: 'to_yearly_nudge_annoucement_bar'})}
|
||||
modalButtonText={t('cloud_billing.nudge_to_yearly.learn_more')}
|
||||
modalButtonText={t('cloud_billing.nudge_to_yearly.update_billing')}
|
||||
modalButtonDefaultText='Update billing'
|
||||
message={message}
|
||||
showLinkAsButton={true}
|
||||
|
||||
@@ -55,7 +55,7 @@ export const noBillingHistory = (
|
||||
</div>
|
||||
);
|
||||
|
||||
export const freeTrial = (onUpgradeMattermostCloud: (callerInfo: string) => void, daysLeftOnTrial: number) => (
|
||||
export const freeTrial = (onUpgradeMattermostCloud: (callerInfo: string) => void, daysLeftOnTrial: number, reverseTrial: boolean) => (
|
||||
<div className='UpgradeMattermostCloud'>
|
||||
<div className='UpgradeMattermostCloud__image'>
|
||||
<UpgradeSvg
|
||||
@@ -104,10 +104,21 @@ export const freeTrial = (onUpgradeMattermostCloud: (callerInfo: string) => void
|
||||
onClick={() => onUpgradeMattermostCloud('billing_summary_free_trial_upgrade_button')}
|
||||
className='UpgradeMattermostCloud__upgradeButton'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.cloudTrial.subscribeButton'
|
||||
defaultMessage='Upgrade Now'
|
||||
/>
|
||||
{
|
||||
reverseTrial ? (
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.cloudTrial.purchaseButton'
|
||||
defaultMessage='Purchase Now'
|
||||
/>
|
||||
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.cloudTrial.subscribeButton'
|
||||
defaultMessage='Upgrade Now'
|
||||
/>
|
||||
)
|
||||
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import React from 'react';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {getSubscriptionProduct, checkHadPriorTrial, getCloudSubscription} from 'mattermost-redux/selectors/entities/cloud';
|
||||
import {cloudReverseTrial} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {CloudProducts} from 'utils/constants';
|
||||
|
||||
@@ -27,20 +28,23 @@ type BillingSummaryProps = {
|
||||
const BillingSummary = ({isFreeTrial, daysLeftOnTrial, onUpgradeMattermostCloud}: BillingSummaryProps) => {
|
||||
const subscription = useSelector(getCloudSubscription);
|
||||
const product = useSelector(getSubscriptionProduct);
|
||||
const reverseTrial = useSelector(cloudReverseTrial);
|
||||
|
||||
let body = noBillingHistory;
|
||||
|
||||
const isPreTrial = subscription?.is_free_trial === 'false' && subscription?.trial_end_at === 0;
|
||||
const hasPriorTrial = useSelector(checkHadPriorTrial);
|
||||
const showTryEnterprise = product?.sku === CloudProducts.STARTER && isPreTrial;
|
||||
const showUpgradeProfessional = product?.sku === CloudProducts.STARTER && hasPriorTrial;
|
||||
const isStarterPreTrial = product?.sku === CloudProducts.STARTER && isPreTrial;
|
||||
const isStarterPostTrial = product?.sku === CloudProducts.STARTER && hasPriorTrial;
|
||||
|
||||
if (showTryEnterprise) {
|
||||
if (isStarterPreTrial && reverseTrial) {
|
||||
body = <UpgradeToProfessionalCard/>;
|
||||
} else if (isStarterPreTrial) {
|
||||
body = tryEnterpriseCard;
|
||||
} else if (showUpgradeProfessional) {
|
||||
} else if (isStarterPostTrial) {
|
||||
body = <UpgradeToProfessionalCard/>;
|
||||
} else if (isFreeTrial) {
|
||||
body = freeTrial(onUpgradeMattermostCloud, daysLeftOnTrial);
|
||||
body = freeTrial(onUpgradeMattermostCloud, daysLeftOnTrial, reverseTrial);
|
||||
} else if (subscription?.last_invoice && !subscription?.upcoming_invoice) {
|
||||
const invoice = subscription.last_invoice;
|
||||
const fullCharges = invoice.line_items.filter((item) => item.type === 'full');
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__Icon {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
&__Title {
|
||||
color: var(--sys-denim-center-channel-text);
|
||||
font-family: Metropolis;
|
||||
@@ -21,15 +25,17 @@
|
||||
}
|
||||
|
||||
&__Usage {
|
||||
color: var(--center-channel-color);
|
||||
text-align: left;
|
||||
|
||||
&-Highlighted {
|
||||
color: black;
|
||||
font-weight: 700;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
&__Warning {
|
||||
color: var(--center-channel-color);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
@@ -184,8 +184,8 @@ export default function DeleteWorkspaceModal(props: Props) {
|
||||
className='DeleteWorkspaceModal'
|
||||
onExited={handleClickCancel}
|
||||
>
|
||||
<div>
|
||||
<LaptopAlertSVG/>
|
||||
<div className='DeleteWorkspaceModal__Icon'>
|
||||
<LaptopAlertSVG height={156}/>
|
||||
</div>
|
||||
<div className='DeleteWorkspaceModal__Title'>
|
||||
<FormattedMessage
|
||||
@@ -196,7 +196,7 @@ export default function DeleteWorkspaceModal(props: Props) {
|
||||
<div className='DeleteWorkspaceModal__Usage'>
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.deleteWorkspaceModal.usage'
|
||||
defaultMessage='As part of your paid subscription to Mattermost {product_name} you have created '
|
||||
defaultMessage='As part of your subscription to Mattermost {sku} you have created '
|
||||
values={{
|
||||
sku: product?.name,
|
||||
}}
|
||||
|
||||
@@ -31,6 +31,7 @@ describe('components/feature_discovery', () => {
|
||||
hadPrevCloudTrial={false}
|
||||
isSubscriptionLoaded={true}
|
||||
isPaidSubscription={false}
|
||||
cloudFreeDeprecated={false}
|
||||
actions={{
|
||||
getPrevTrialLicense: jest.fn(),
|
||||
getCloudSubscription: jest.fn(),
|
||||
@@ -58,6 +59,7 @@ describe('components/feature_discovery', () => {
|
||||
isCloudTrial={false}
|
||||
hadPrevCloudTrial={false}
|
||||
isPaidSubscription={false}
|
||||
cloudFreeDeprecated={false}
|
||||
isSubscriptionLoaded={true}
|
||||
actions={{
|
||||
getPrevTrialLicense: jest.fn(),
|
||||
@@ -87,6 +89,7 @@ describe('components/feature_discovery', () => {
|
||||
isCloudTrial={false}
|
||||
hadPrevCloudTrial={false}
|
||||
isSubscriptionLoaded={false}
|
||||
cloudFreeDeprecated={false}
|
||||
isPaidSubscription={false}
|
||||
actions={{
|
||||
getPrevTrialLicense: jest.fn(),
|
||||
|
||||
@@ -59,6 +59,7 @@ type Props = {
|
||||
isSubscriptionLoaded: boolean;
|
||||
isPaidSubscription: boolean;
|
||||
customer?: CloudCustomer;
|
||||
cloudFreeDeprecated: boolean;
|
||||
}
|
||||
|
||||
type State = {
|
||||
@@ -205,6 +206,23 @@ export default class FeatureDiscovery extends React.PureComponent<Props, State>
|
||||
extraClass='btn btn-primary'
|
||||
/>
|
||||
);
|
||||
if (this.props.cloudFreeDeprecated) {
|
||||
ctaPrimaryButton = (
|
||||
<button
|
||||
className='btn btn-primary'
|
||||
data-testid='featureDiscovery_primaryCallToAction'
|
||||
onClick={() => {
|
||||
trackEvent(TELEMETRY_CATEGORIES.SELF_HOSTED_ADMIN, 'click_enterprise_contact_sales_feature_discovery');
|
||||
this.contactSalesFunc();
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.ldap_feature_discovery_cloud.call_to_action.primary_sales'
|
||||
defaultMessage='Contact sales'
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
} else if (hadPrevCloudTrial) {
|
||||
// if it is cloud, but this account already had a free trial, then the cta button must be Upgrade now
|
||||
ctaPrimaryButton = (
|
||||
@@ -259,7 +277,7 @@ export default class FeatureDiscovery extends React.PureComponent<Props, State>
|
||||
/>
|
||||
</ExternalLink>
|
||||
{gettingTrialError}
|
||||
{(!this.props.isCloud || canRequestCloudFreeTrial) && <p className='trial-legal-terms'>
|
||||
{((!this.props.isCloud || canRequestCloudFreeTrial) && !this.props.cloudFreeDeprecated) && <p className='trial-legal-terms'>
|
||||
{canRequestCloudFreeTrial ? (
|
||||
<FormattedMessage
|
||||
id='admin.feature_discovery.trial-request.accept-terms.cloudFree'
|
||||
|
||||
@@ -9,6 +9,7 @@ import {getCloudSubscription} from 'mattermost-redux/actions/cloud';
|
||||
import {Action, GenericAction} from 'mattermost-redux/types/actions';
|
||||
import {checkHadPriorTrial, getCloudCustomer} from 'mattermost-redux/selectors/entities/cloud';
|
||||
import {getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {deprecateCloudFree} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {ModalData} from 'types/actions';
|
||||
import {GlobalState} from 'types/store';
|
||||
@@ -29,6 +30,7 @@ function mapStateToProps(state: GlobalState) {
|
||||
const hasPriorTrial = checkHadPriorTrial(state);
|
||||
const isCloudTrial = subscription?.is_free_trial === 'true';
|
||||
const customer = getCloudCustomer(state);
|
||||
const cloudFreeDeprecated = deprecateCloudFree(state);
|
||||
return {
|
||||
stats: state.entities.admin.analytics,
|
||||
prevTrialLicense: state.entities.admin.prevTrialLicense,
|
||||
@@ -38,6 +40,7 @@ function mapStateToProps(state: GlobalState) {
|
||||
hadPrevCloudTrial: hasPriorTrial,
|
||||
isPaidSubscription: isCloud && license?.SkuShortName !== LicenseSkus.Starter && !isCloudTrial,
|
||||
customer,
|
||||
cloudFreeDeprecated,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {screen} from '@testing-library/react';
|
||||
import {renderWithIntlAndStore} from 'tests/react_testing_utils';
|
||||
import * as cloudActions from 'mattermost-redux/actions/cloud';
|
||||
|
||||
import {CloudProducts} from 'utils/constants';
|
||||
|
||||
import PaymentAnnouncementBar from './';
|
||||
|
||||
jest.mock('mattermost-redux/actions/cloud', () => {
|
||||
const original = jest.requireActual('mattermost-redux/actions/cloud');
|
||||
return {
|
||||
...original,
|
||||
__esModule: true,
|
||||
|
||||
// just testing that it fired, not that the result updated or anything like that
|
||||
getCloudCustomer: jest.fn(() => ({type: 'bogus'})),
|
||||
};
|
||||
});
|
||||
|
||||
describe('PaymentAnnouncementBar', () => {
|
||||
const happyPathStore = {
|
||||
entities: {
|
||||
users: {
|
||||
currentUserId: 'me',
|
||||
profiles: {
|
||||
me: {
|
||||
roles: 'system_admin',
|
||||
},
|
||||
},
|
||||
},
|
||||
general: {
|
||||
license: {
|
||||
Cloud: 'true',
|
||||
},
|
||||
},
|
||||
cloud: {
|
||||
subscription: {
|
||||
product_id: 'prod_something',
|
||||
last_invoice: {
|
||||
status: 'failed',
|
||||
},
|
||||
},
|
||||
customer: {
|
||||
payment_method: {
|
||||
exp_month: 12,
|
||||
exp_year: (new Date()).getFullYear() + 1,
|
||||
},
|
||||
},
|
||||
products: {
|
||||
prod_something: {
|
||||
id: 'prod_something',
|
||||
sku: CloudProducts.PROFESSIONAL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
views: {
|
||||
announcementBar: {
|
||||
announcementBarState: {
|
||||
announcementBarCount: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('when most recent payment failed, shows that', () => {
|
||||
renderWithIntlAndStore(<PaymentAnnouncementBar/>, happyPathStore);
|
||||
screen.getByText('Your most recent payment failed');
|
||||
});
|
||||
|
||||
it('when card is expired, shows that', () => {
|
||||
const store = JSON.parse(JSON.stringify(happyPathStore));
|
||||
store.entities.cloud.customer.payment_method.exp_year = (new Date()).getFullYear() - 1;
|
||||
store.entities.cloud.subscription.last_invoice.status = 'success';
|
||||
renderWithIntlAndStore(<PaymentAnnouncementBar/>, store);
|
||||
screen.getByText('Your credit card has expired', {exact: false});
|
||||
});
|
||||
|
||||
it('when needed, fetches, customer', () => {
|
||||
const store = JSON.parse(JSON.stringify(happyPathStore));
|
||||
store.entities.cloud.customer = null;
|
||||
store.entities.cloud.subscription.last_invoice.status = 'success';
|
||||
renderWithIntlAndStore(<PaymentAnnouncementBar/>, store);
|
||||
expect(cloudActions.getCloudCustomer).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('when not an admin, does not fetch customer', () => {
|
||||
const store = JSON.parse(JSON.stringify(happyPathStore));
|
||||
store.entities.users.profiles.me.roles = '';
|
||||
renderWithIntlAndStore(<PaymentAnnouncementBar/>, store);
|
||||
expect(cloudActions.getCloudCustomer).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,53 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators, Dispatch} from 'redux';
|
||||
|
||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {GenericAction} from 'mattermost-redux/types/actions';
|
||||
import {getCloudSubscription, getCloudCustomer} from 'mattermost-redux/actions/cloud';
|
||||
|
||||
import {isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
import {
|
||||
getCloudSubscription as selectCloudSubscription,
|
||||
getCloudCustomer as selectCloudCustomer,
|
||||
getSubscriptionProduct,
|
||||
} from 'mattermost-redux/selectors/entities/cloud';
|
||||
import {CloudProducts} from 'utils/constants';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
import PaymentAnnouncementBar from './payment_announcement_bar';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const subscription = selectCloudSubscription(state);
|
||||
const customer = selectCloudCustomer(state);
|
||||
const subscriptionProduct = getSubscriptionProduct(state);
|
||||
return {
|
||||
userIsAdmin: isCurrentUserSystemAdmin(state),
|
||||
isCloud: getLicense(state).Cloud === 'true',
|
||||
subscription,
|
||||
customer,
|
||||
isStarterFree: subscriptionProduct?.sku === CloudProducts.STARTER,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
|
||||
return {
|
||||
actions: bindActionCreators(
|
||||
{
|
||||
savePreferences,
|
||||
openModal,
|
||||
getCloudSubscription,
|
||||
getCloudCustomer,
|
||||
},
|
||||
dispatch,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PaymentAnnouncementBar);
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useSelector, useDispatch} from 'react-redux';
|
||||
import {isEmpty} from 'lodash';
|
||||
|
||||
import {DispatchFunc} from 'mattermost-redux/types/actions';
|
||||
import {getCloudCustomer} from 'mattermost-redux/actions/cloud';
|
||||
import {getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {
|
||||
getCloudSubscription as selectCloudSubscription,
|
||||
getCloudCustomer as selectCloudCustomer,
|
||||
getSubscriptionProduct,
|
||||
} from 'mattermost-redux/selectors/entities/cloud';
|
||||
import {isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {getHistory} from 'utils/browser_history';
|
||||
import {isCustomerCardExpired} from 'utils/cloud_utils';
|
||||
import {AnnouncementBarTypes, CloudProducts, ConsolePages} from 'utils/constants';
|
||||
import {t} from 'utils/i18n';
|
||||
|
||||
import AnnouncementBar from '../default_announcement_bar';
|
||||
|
||||
export default function PaymentAnnouncementBar() {
|
||||
const [requestedCustomer, setRequestedCustomer] = useState(false);
|
||||
const dispatch = useDispatch<DispatchFunc>();
|
||||
const subscription = useSelector(selectCloudSubscription);
|
||||
const customer = useSelector(selectCloudCustomer);
|
||||
const isStarterFree = useSelector(getSubscriptionProduct)?.sku === CloudProducts.STARTER;
|
||||
const userIsAdmin = useSelector(isCurrentUserSystemAdmin);
|
||||
const isCloud = useSelector(getLicense).Cloud === 'true';
|
||||
|
||||
useEffect(() => {
|
||||
if (isCloud && !isStarterFree && isEmpty(customer) && userIsAdmin && !requestedCustomer) {
|
||||
setRequestedCustomer(true);
|
||||
dispatch(getCloudCustomer());
|
||||
}
|
||||
},
|
||||
[isCloud, isStarterFree, customer, userIsAdmin, requestedCustomer]);
|
||||
|
||||
const mostRecentPaymentFailed = subscription?.last_invoice?.status === 'failed';
|
||||
|
||||
if (
|
||||
// Prevents banner flashes if the subscription hasn't been loaded yet
|
||||
isEmpty(subscription) ||
|
||||
isStarterFree ||
|
||||
!isCloud ||
|
||||
!userIsAdmin ||
|
||||
isEmpty(customer) ||
|
||||
(!isCustomerCardExpired(customer) && !mostRecentPaymentFailed)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const updatePaymentInfo = () => {
|
||||
getHistory().push(ConsolePages.PAYMENT_INFO);
|
||||
};
|
||||
|
||||
let message = (
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.creditCardExpired'
|
||||
defaultMessage='Your credit card has expired. Update your payment information to avoid disruption.'
|
||||
/>
|
||||
);
|
||||
|
||||
if (mostRecentPaymentFailed) {
|
||||
message = (
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.mostRecentPaymentFailed'
|
||||
defaultMessage='Your most recent payment failed'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AnnouncementBar
|
||||
type={AnnouncementBarTypes.CRITICAL}
|
||||
showCloseButton={false}
|
||||
onButtonClick={updatePaymentInfo}
|
||||
modalButtonText={t('admin.billing.subscription.updatePaymentInfo')}
|
||||
modalButtonDefaultText={'Update payment info'}
|
||||
message={message}
|
||||
showLinkAsButton={true}
|
||||
isTallBanner={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {isEmpty} from 'lodash';
|
||||
|
||||
import {CloudCustomer, Subscription} from '@mattermost/types/cloud';
|
||||
|
||||
import {getHistory} from 'utils/browser_history';
|
||||
import {isCustomerCardExpired} from 'utils/cloud_utils';
|
||||
import {AnnouncementBarTypes} from 'utils/constants';
|
||||
import {t} from 'utils/i18n';
|
||||
|
||||
import AnnouncementBar from '../default_announcement_bar';
|
||||
|
||||
type Props = {
|
||||
userIsAdmin: boolean;
|
||||
isCloud: boolean;
|
||||
subscription?: Subscription;
|
||||
customer?: CloudCustomer;
|
||||
isStarterFree: boolean;
|
||||
actions: {
|
||||
getCloudSubscription: () => void;
|
||||
getCloudCustomer: () => void;
|
||||
};
|
||||
};
|
||||
|
||||
class PaymentAnnouncementBar extends React.PureComponent<Props> {
|
||||
async componentDidMount() {
|
||||
if (isEmpty(this.props.customer)) {
|
||||
await this.props.actions.getCloudCustomer();
|
||||
}
|
||||
}
|
||||
|
||||
isMostRecentPaymentFailed = () => {
|
||||
return this.props.subscription?.last_invoice?.status === 'failed';
|
||||
};
|
||||
|
||||
shouldShowBanner = () => {
|
||||
const {userIsAdmin, isCloud, subscription} = this.props;
|
||||
|
||||
// Prevents banner flashes if the subscription hasn't been loaded yet
|
||||
if (subscription === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.props.isStarterFree) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isCloud) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!userIsAdmin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isCustomerCardExpired(this.props.customer) && !this.isMostRecentPaymentFailed()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
updatePaymentInfo = () => {
|
||||
getHistory().push('/admin_console/billing/payment_info');
|
||||
};
|
||||
|
||||
render() {
|
||||
if (isEmpty(this.props.customer) || isEmpty(this.props.subscription)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.shouldShowBanner()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AnnouncementBar
|
||||
type={AnnouncementBarTypes.CRITICAL}
|
||||
showCloseButton={false}
|
||||
onButtonClick={this.updatePaymentInfo}
|
||||
modalButtonText={t('admin.billing.subscription.updatePaymentInfo')}
|
||||
modalButtonDefaultText={'Update payment info'}
|
||||
message={this.isMostRecentPaymentFailed() ? t('admin.billing.subscription.mostRecentPaymentFailed') : t('admin.billing.subscription.creditCardExpired')}
|
||||
showLinkAsButton={true}
|
||||
isTallBanner={true}
|
||||
/>
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PaymentAnnouncementBar;
|
||||
@@ -6,6 +6,7 @@ import {useIntl} from 'react-intl';
|
||||
import {useSelector, useDispatch} from 'react-redux';
|
||||
import {useLocation, useHistory} from 'react-router-dom';
|
||||
|
||||
import {redirectUserToDefaultTeam} from 'actions/global_actions';
|
||||
import {trackEvent} from 'actions/telemetry_actions.jsx';
|
||||
|
||||
import LaptopAlertSVG from 'components/common/svg_images_components/laptop_alert_svg';
|
||||
@@ -14,6 +15,7 @@ import LoadingScreen from 'components/loading_screen';
|
||||
|
||||
import {clearErrors, logError} from 'mattermost-redux/actions/errors';
|
||||
import {verifyUserEmail, getMe} from 'mattermost-redux/actions/users';
|
||||
import {getIsOnboardingFlowEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {DispatchFunc} from 'mattermost-redux/types/actions';
|
||||
|
||||
@@ -38,6 +40,7 @@ const DoVerifyEmail = () => {
|
||||
const token = params.get('token') ?? '';
|
||||
|
||||
const loggedIn = Boolean(useSelector(getCurrentUserId));
|
||||
const onboardingFlowEnabled = useSelector(getIsOnboardingFlowEnabled);
|
||||
|
||||
const [verifyStatus, setVerifyStatus] = useState(VerifyStatus.PENDING);
|
||||
const [serverError, setServerError] = useState('');
|
||||
@@ -49,11 +52,15 @@ const DoVerifyEmail = () => {
|
||||
|
||||
const handleRedirect = () => {
|
||||
if (loggedIn) {
|
||||
// need info about whether admin or not,
|
||||
// and whether admin has already completed
|
||||
// first time onboarding. Instead of fetching and orchestrating that here,
|
||||
// let the default root component handle it.
|
||||
history.push('/');
|
||||
if (onboardingFlowEnabled) {
|
||||
// need info about whether admin or not,
|
||||
// and whether admin has already completed
|
||||
// first time onboarding. Instead of fetching and orchestrating that here,
|
||||
// let the default root component handle it.
|
||||
history.push('/');
|
||||
return;
|
||||
}
|
||||
redirectUserToDefaultTeam();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ Object {
|
||||
aria-controls="CENTER_dropdown_post_id_1"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-label="Actions"
|
||||
aria-label="more"
|
||||
class="post-menu__item"
|
||||
data-testid="PostDotMenu-Button-post_id_1"
|
||||
id="CENTER_button_post_id_1"
|
||||
@@ -34,7 +34,7 @@ Object {
|
||||
aria-controls="CENTER_dropdown_post_id_1"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-label="Actions"
|
||||
aria-label="more"
|
||||
class="post-menu__item"
|
||||
data-testid="PostDotMenu-Button-post_id_1"
|
||||
id="CENTER_button_post_id_1"
|
||||
@@ -121,7 +121,7 @@ exports[`components/dot_menu/DotMenu should match snapshot, on Center 1`] = `
|
||||
}
|
||||
menuButton={
|
||||
Object {
|
||||
"aria-label": "Actions",
|
||||
"aria-label": "more",
|
||||
"children": <DotsHorizontalIcon
|
||||
size={16}
|
||||
/>,
|
||||
|
||||
@@ -497,7 +497,7 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
|
||||
class: classNames('post-menu__item', {
|
||||
'post-menu__item--active': this.props.isMenuOpen,
|
||||
}),
|
||||
'aria-label': formatMessage({id: 'post_info.dot_menu.tooltip.actions', defaultMessage: 'Actions'}),
|
||||
'aria-label': formatMessage({id: 'post_info.dot_menu.tooltip.more', defaultMessage: 'More'}).toLowerCase(),
|
||||
children: <DotsHorizontalIcon size={16}/>,
|
||||
}}
|
||||
menu={{
|
||||
|
||||
@@ -13,6 +13,7 @@ import {checkHadPriorTrial} from 'mattermost-redux/selectors/entities/cloud';
|
||||
import {isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getPrevTrialLicense} from 'mattermost-redux/actions/admin';
|
||||
import {deprecateCloudFree} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import CloudStartTrialButton from 'components/cloud_start_trial/cloud_start_trial_btn';
|
||||
import StartTrialBtn from 'components/learn_more_trial_modal/start_trial_btn';
|
||||
@@ -59,6 +60,7 @@ const FeatureRestrictedModal = ({
|
||||
dispatch(getPrevTrialLicense());
|
||||
}, []);
|
||||
|
||||
const cloudFreeDeprecated = useSelector(deprecateCloudFree);
|
||||
const hasCloudPriorTrial = useSelector(checkHadPriorTrial);
|
||||
const prevTrialLicense = useSelector((state: GlobalState) => state.entities.admin.prevTrialLicense);
|
||||
const hasSelfHostedPriorTrial = prevTrialLicense.IsLicensed === 'true';
|
||||
@@ -100,7 +102,7 @@ const FeatureRestrictedModal = ({
|
||||
|
||||
const getTitle = () => {
|
||||
if (isSystemAdmin) {
|
||||
return hasPriorTrial ? titleAdminPostTrial : titleAdminPreTrial;
|
||||
return (hasPriorTrial || cloudFreeDeprecated) ? titleAdminPostTrial : titleAdminPreTrial;
|
||||
}
|
||||
|
||||
return titleEndUser;
|
||||
@@ -108,13 +110,13 @@ const FeatureRestrictedModal = ({
|
||||
|
||||
const getMessage = () => {
|
||||
if (isSystemAdmin) {
|
||||
return hasPriorTrial ? messageAdminPostTrial : messageAdminPreTrial;
|
||||
return (hasPriorTrial || cloudFreeDeprecated) ? messageAdminPostTrial : messageAdminPreTrial;
|
||||
}
|
||||
|
||||
return messageEndUser;
|
||||
};
|
||||
|
||||
const showStartTrial = isSystemAdmin && !hasPriorTrial;
|
||||
const showStartTrial = isSystemAdmin && !hasPriorTrial && !cloudFreeDeprecated;
|
||||
|
||||
// define what is the secondary button text and action, by default will be the View Plan button
|
||||
let secondaryBtnMsg = formatMessage({id: 'feature_restricted_modal.button.plans', defaultMessage: 'View plans'});
|
||||
|
||||
@@ -8,6 +8,7 @@ import {withRouter} from 'react-router-dom';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {GenericAction} from 'mattermost-redux/types/actions';
|
||||
import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getIsOnboardingFlowEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {isFirstAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {getUserGuideDropdownPluginMenuItems} from 'selectors/plugins';
|
||||
@@ -31,6 +32,7 @@ function mapStateToProps(state: GlobalState) {
|
||||
teamUrl: getCurrentRelativeTeamUrl(state),
|
||||
pluginMenuItems: getUserGuideDropdownPluginMenuItems(state),
|
||||
isFirstAdmin: isFirstAdmin(state),
|
||||
onboardingFlowEnabled: getIsOnboardingFlowEnabled(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ describe('components/channel_header/components/UserGuideDropdown', () => {
|
||||
},
|
||||
pluginMenuItems: [],
|
||||
isFirstAdmin: false,
|
||||
onboardingFlowEnabled: false,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/user
|
||||
import {getSubscriptionProduct, checkHadPriorTrial} from 'mattermost-redux/selectors/entities/cloud';
|
||||
import {DispatchFunc} from 'mattermost-redux/types/actions';
|
||||
import {getPrevTrialLicense} from 'mattermost-redux/actions/admin';
|
||||
import {deprecateCloudFree} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {closeModal, openModal} from 'actions/views/modals';
|
||||
|
||||
@@ -43,6 +44,7 @@ export type Props = {
|
||||
export default function InviteAs(props: Props) {
|
||||
const {formatMessage} = useIntl();
|
||||
const license = useSelector(getLicense);
|
||||
const cloudFreeDeprecated = useSelector(deprecateCloudFree);
|
||||
const dispatch = useDispatch<DispatchFunc>();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -85,7 +87,7 @@ export default function InviteAs(props: Props) {
|
||||
if (isFreeTrial) {
|
||||
ctaExtraContentMsg = formatMessage({id: 'free.professional_feature.professional', defaultMessage: 'Professional feature'});
|
||||
} else {
|
||||
ctaExtraContentMsg = hasPriorTrial ? formatMessage({id: 'free.professional_feature.upgrade', defaultMessage: 'Upgrade'}) : formatMessage({id: 'free.professional_feature.try_free', defaultMessage: 'Professional feature- try it out free'});
|
||||
ctaExtraContentMsg = (hasPriorTrial || cloudFreeDeprecated) ? formatMessage({id: 'free.professional_feature.upgrade', defaultMessage: 'Upgrade'}) : formatMessage({id: 'free.professional_feature.try_free', defaultMessage: 'Professional feature- try it out free'});
|
||||
}
|
||||
|
||||
const restrictedIndicator = (
|
||||
|
||||
@@ -33,6 +33,12 @@ describe('components/learn_more_trial_modal/learn_more_trial_modal', () => {
|
||||
entities: {
|
||||
users: {
|
||||
currentUserId: 'current_user_id',
|
||||
profiles: {
|
||||
current_user_id: {
|
||||
id: 'current_user_id',
|
||||
roles: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
admin: {
|
||||
analytics: {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import {useSelector, useDispatch} from 'react-redux';
|
||||
|
||||
import {trackEvent} from 'actions/telemetry_actions';
|
||||
@@ -16,10 +16,13 @@ import MonitorImacLikeSVG from 'components/common/svg_images_components/monitor_
|
||||
import SystemRolesSVG from 'components/admin_console/feature_discovery/features/images/system_roles_svg';
|
||||
import CloudStartTrialButton from 'components/cloud_start_trial/cloud_start_trial_btn';
|
||||
import {BtnStyle} from 'components/common/carousel/carousel_button';
|
||||
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
|
||||
import ExternalLink from 'components/external_link';
|
||||
|
||||
import {closeModal} from 'actions/views/modals';
|
||||
import {DispatchFunc} from 'mattermost-redux/types/actions';
|
||||
import {getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {deprecateCloudFree} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import StartTrialBtn from './start_trial_btn';
|
||||
|
||||
@@ -43,8 +46,11 @@ const LearnMoreTrialModal = (
|
||||
const [embargoed, setEmbargoed] = useState(false);
|
||||
const dispatch = useDispatch<DispatchFunc>();
|
||||
|
||||
const [, salesLink] = useOpenSalesLink();
|
||||
|
||||
// Cloud conditions
|
||||
const license = useSelector(getLicense);
|
||||
const cloudFreeDeprecated = useSelector(deprecateCloudFree);
|
||||
const isCloud = license?.Cloud === 'true';
|
||||
|
||||
const handleEmbargoError = useCallback(() => {
|
||||
@@ -78,6 +84,20 @@ const LearnMoreTrialModal = (
|
||||
extraClass={'btn btn-primary start-cloud-trial-btn'}
|
||||
/>
|
||||
);
|
||||
if (cloudFreeDeprecated) {
|
||||
startTrialBtn = (
|
||||
<ExternalLink
|
||||
location='learn_more_trial_modal'
|
||||
href={salesLink}
|
||||
className='btn btn-primary start-cloud-trial-btn'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='learn_more_trial_modal.contact_sales'
|
||||
defaultMessage='Contact sales'
|
||||
/>
|
||||
</ExternalLink>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const handleOnClose = useCallback(() => {
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {useSelector} from 'react-redux';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import TrialBenefitsModalStepMore from 'components/trial_benefits_modal/trial_benefits_modal_step_more';
|
||||
import {deprecateCloudFree} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import './learn_more_trial_modal_step.scss';
|
||||
import {AboutLinks, LicenseLinks} from 'utils/constants';
|
||||
@@ -35,6 +36,7 @@ const LearnMoreTrialModalStep = (
|
||||
buttonLabel,
|
||||
handleOnClose,
|
||||
}: LearnMoreTrialModalStepProps) => {
|
||||
const cloudFreeDeprecated = useSelector(deprecateCloudFree);
|
||||
return (
|
||||
<div
|
||||
id={`learnMoreTrialModalStep-${id}`}
|
||||
@@ -59,32 +61,36 @@ const LearnMoreTrialModalStep = (
|
||||
telemetryId={'learn_more_trial_modal'}
|
||||
/>
|
||||
)}
|
||||
<div className='disclaimer'>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='start_trial.modal.disclaimer'
|
||||
defaultMessage='By clicking “Start trial”, I agree to the <linkEvaluation>Mattermost Software and Services License Agreement</linkEvaluation>, <linkPrivacy>privacy policy</linkPrivacy> and receiving product emails.'
|
||||
values={{
|
||||
linkEvaluation: (msg: React.ReactNode) => (
|
||||
<ExternalLink
|
||||
href={LicenseLinks.SOFTWARE_SERVICES_LICENSE_AGREEMENT}
|
||||
location='learn_more_trial_modal_step'
|
||||
>
|
||||
{msg}
|
||||
</ExternalLink>
|
||||
),
|
||||
linkPrivacy: (msg: React.ReactNode) => (
|
||||
<ExternalLink
|
||||
href={AboutLinks.PRIVACY_POLICY}
|
||||
location='learn_more_trial_modal_step'
|
||||
>
|
||||
{msg}
|
||||
</ExternalLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{
|
||||
cloudFreeDeprecated ? '' : (
|
||||
<div className='disclaimer'>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='start_trial.modal.disclaimer'
|
||||
defaultMessage='By clicking “Start trial”, I agree to the <linkEvaluation>Mattermost Software and Services License Agreement</linkEvaluation>, <linkPrivacy>privacy policy</linkPrivacy> and receiving product emails.'
|
||||
values={{
|
||||
linkEvaluation: (msg: React.ReactNode) => (
|
||||
<ExternalLink
|
||||
href={LicenseLinks.SOFTWARE_SERVICES_LICENSE_AGREEMENT}
|
||||
location='learn_more_trial_modal_step'
|
||||
>
|
||||
{msg}
|
||||
</ExternalLink>
|
||||
),
|
||||
linkPrivacy: (msg: React.ReactNode) => (
|
||||
<ExternalLink
|
||||
href={AboutLinks.PRIVACY_POLICY}
|
||||
location='learn_more_trial_modal_step'
|
||||
>
|
||||
{msg}
|
||||
</ExternalLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{bottomLeftMessage && (
|
||||
<div className='bottom-text-left-message'>
|
||||
{bottomLeftMessage}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getIsOnboardingFlowEnabled, isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getTeamByName, getMyTeamMember} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||
import {isSystemAdmin} from 'mattermost-redux/utils/user_utils';
|
||||
@@ -104,6 +104,7 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
||||
const currentUser = useSelector(getCurrentUser);
|
||||
const experimentalPrimaryTeam = useSelector((state: GlobalState) => (ExperimentalPrimaryTeam ? getTeamByName(state, ExperimentalPrimaryTeam) : undefined));
|
||||
const experimentalPrimaryTeamMember = useSelector((state: GlobalState) => getMyTeamMember(state, experimentalPrimaryTeam?.id ?? ''));
|
||||
const onboardingFlowEnabled = useSelector(getIsOnboardingFlowEnabled);
|
||||
const isCloud = useSelector(isCurrentLicenseCloud);
|
||||
const graphQLEnabled = useSelector(isGraphQLEnabled);
|
||||
|
||||
@@ -634,12 +635,14 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
||||
} else if (experimentalPrimaryTeamMember.team_id) {
|
||||
// Only set experimental team if user is on that team
|
||||
history.push(`/${ExperimentalPrimaryTeam}`);
|
||||
} else {
|
||||
} else if (onboardingFlowEnabled) {
|
||||
// need info about whether admin or not,
|
||||
// and whether admin has already completed
|
||||
// first time onboarding. Instead of fetching and orchestrating that here,
|
||||
// let the default root component handle it.
|
||||
history.push('/');
|
||||
} else {
|
||||
redirectUserToDefaultTeam();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -75,7 +75,33 @@ export type MarketplaceItemProps = {
|
||||
versionLabel: JSX.Element| null;
|
||||
};
|
||||
|
||||
export default class MarketplaceItem extends React.PureComponent <MarketplaceItemProps> {
|
||||
type MarketplaceItemState = {
|
||||
showTooltip: boolean;
|
||||
};
|
||||
|
||||
export default class MarketplaceItem extends React.PureComponent <MarketplaceItemProps, MarketplaceItemState> {
|
||||
descriptionRef: React.RefObject<HTMLParagraphElement>;
|
||||
|
||||
constructor(props: MarketplaceItemProps) {
|
||||
super(props);
|
||||
|
||||
this.descriptionRef = React.createRef();
|
||||
|
||||
this.state = {
|
||||
showTooltip: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.enableToolTipIfNeeded();
|
||||
}
|
||||
|
||||
enableToolTipIfNeeded = (): void => {
|
||||
const element = this.descriptionRef.current;
|
||||
const showTooltip = element && element.offsetWidth < element.scrollWidth;
|
||||
this.setState({showTooltip: Boolean(showTooltip)});
|
||||
};
|
||||
|
||||
render(): JSX.Element {
|
||||
const {labels = null} = this.props;
|
||||
let icon;
|
||||
@@ -105,12 +131,37 @@ export default class MarketplaceItem extends React.PureComponent <MarketplaceIte
|
||||
</>
|
||||
);
|
||||
|
||||
const description = (
|
||||
<p className={classNames('more-modal__description', {error_text: this.props.error})}>
|
||||
{this.props.error || this.props.description}
|
||||
const descriptionText = this.props.error || this.props.description;
|
||||
let description = (
|
||||
<p
|
||||
className={classNames('more-modal__description', {error_text: this.props.error})}
|
||||
ref={this.descriptionRef}
|
||||
>
|
||||
{descriptionText}
|
||||
</p>
|
||||
);
|
||||
|
||||
if (this.state.showTooltip) {
|
||||
const displayNameToolTip = (
|
||||
<Tooltip
|
||||
id='marketplace-item-description__tooltip'
|
||||
className='more-modal__description-tooltip'
|
||||
>
|
||||
{descriptionText}
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
description = (
|
||||
<OverlayTrigger
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
placement='top'
|
||||
overlay={displayNameToolTip}
|
||||
>
|
||||
{description}
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
let pluginDetails;
|
||||
if (this.props.homepageUrl) {
|
||||
pluginDetails = (
|
||||
|
||||
@@ -90,6 +90,7 @@
|
||||
overflow-y: scroll;
|
||||
|
||||
.more-modal__row {
|
||||
overflow: hidden;
|
||||
min-height: 80px;
|
||||
padding: 16px 20px;
|
||||
border-bottom: none;
|
||||
@@ -99,10 +100,11 @@
|
||||
}
|
||||
|
||||
.update {
|
||||
padding: 10px 10px 0 0;
|
||||
border-top: 1px solid rgba(black, 0.1);
|
||||
margin: 10px 10px 0 0;
|
||||
font-size: 0.9em;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.more-modal__details {
|
||||
@@ -117,7 +119,7 @@
|
||||
|
||||
.more-modal__description {
|
||||
margin: 2px 0 0;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||
color: var(--center-channel-color-rgb);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
@@ -275,3 +277,9 @@
|
||||
height: 390px;
|
||||
}
|
||||
}
|
||||
|
||||
.more-modal__description-tooltip {
|
||||
.tooltip-inner {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
getBool,
|
||||
isCollapsedThreadsEnabled,
|
||||
} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentTeam, getCurrentTeamId, getTeam, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentTeam, getTeam, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {Emoji} from '@mattermost/types/emojis';
|
||||
@@ -48,7 +48,6 @@ interface OwnProps {
|
||||
post?: Post | UserActivityPost;
|
||||
previousPostId?: string;
|
||||
postId?: string;
|
||||
teamId?: string;
|
||||
shouldHighlight?: boolean;
|
||||
location: keyof typeof Locations;
|
||||
}
|
||||
@@ -120,7 +119,6 @@ function makeMapStateToProps() {
|
||||
const config = getConfig(state);
|
||||
const enableEmojiPicker = config.EnableEmojiPicker === 'true';
|
||||
const enablePostUsernameOverride = config.EnablePostUsernameOverride === 'true';
|
||||
const teamId = ownProps.teamId || getCurrentTeamId(state);
|
||||
const channel = state.entities.channels.channels[post.channel_id];
|
||||
const shortcutReactToLastPostEmittedFrom = getShortcutReactToLastPostEmittedFrom(state);
|
||||
|
||||
@@ -148,6 +146,7 @@ function makeMapStateToProps() {
|
||||
}
|
||||
|
||||
const currentTeam = getCurrentTeam(state);
|
||||
const team = getTeam(state, channel.team_id);
|
||||
let teamName = currentTeam.name;
|
||||
let teamDisplayName = '';
|
||||
|
||||
@@ -159,7 +158,6 @@ function makeMapStateToProps() {
|
||||
!isDMorGM && // Not show for DM or GMs since they don't belong to a team
|
||||
memberships && Object.values(memberships).length > 1 // Not show if the user only belongs to one team
|
||||
) {
|
||||
const team = getTeam(state, channel.team_id);
|
||||
teamDisplayName = team?.display_name;
|
||||
teamName = team?.name || currentTeam.name;
|
||||
}
|
||||
@@ -186,7 +184,6 @@ function makeMapStateToProps() {
|
||||
enablePostUsernameOverride,
|
||||
isEmbedVisible: isEmbedVisible(state, post.id),
|
||||
isReadOnly: false,
|
||||
teamId,
|
||||
currentUserId: getCurrentUserId(state),
|
||||
isFirstReply: previousPost ? isFirstReply(post, previousPost) : false,
|
||||
hasReplies: getReplyCount(state, post) > 0,
|
||||
@@ -200,7 +197,8 @@ function makeMapStateToProps() {
|
||||
compactDisplay: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT,
|
||||
colorizeUsernames: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLORIZE_USERNAMES, Preferences.COLORIZE_USERNAMES_DEFAULT) === 'true',
|
||||
shouldShowActionsMenu: shouldShowActionsMenu(state, post),
|
||||
|
||||
currentTeam,
|
||||
team,
|
||||
shortcutReactToLastPostEmittedFrom,
|
||||
isBot,
|
||||
collapsedThreadsEnabled: isCollapsedThreadsEnabled(state),
|
||||
|
||||
@@ -50,10 +50,12 @@ import {Emoji} from '@mattermost/types/emojis';
|
||||
|
||||
import PostUserProfile from './user_profile';
|
||||
import PostOptions from './post_options';
|
||||
import {Team} from '@mattermost/types/teams';
|
||||
|
||||
export type Props = {
|
||||
post: Post;
|
||||
teamId: string;
|
||||
currentTeam: Team;
|
||||
team?: Team;
|
||||
currentUserId: string;
|
||||
compactDisplay?: boolean;
|
||||
colorizeUsernames?: boolean;
|
||||
@@ -123,6 +125,7 @@ const PostComponent = (props: Props): JSX.Element => {
|
||||
const isRHS = props.location === Locations.RHS_ROOT || props.location === Locations.RHS_COMMENT || props.location === Locations.SEARCH;
|
||||
const postRef = useRef<HTMLDivElement>(null);
|
||||
const postHeaderRef = useRef<HTMLDivElement>(null);
|
||||
const teamId = props.team?.id || props.currentTeam.id;
|
||||
|
||||
const [hover, setHover] = useState(false);
|
||||
const [a11yActive, setA11y] = useState(false);
|
||||
@@ -355,7 +358,15 @@ const PostComponent = (props: Props): JSX.Element => {
|
||||
return;
|
||||
}
|
||||
props.actions.selectPostFromRightHandSideSearch(post);
|
||||
}, [post, props.actions]);
|
||||
}, [post, props.actions, props.actions.selectPostFromRightHandSideSearch]);
|
||||
|
||||
const handleThreadClick = useCallback((e: React.MouseEvent) => {
|
||||
if (props.currentTeam.id === props.team?.id) {
|
||||
handleCommentClick(e);
|
||||
} else {
|
||||
handleJumpClick(e);
|
||||
}
|
||||
}, [handleCommentClick, handleJumpClick]);
|
||||
|
||||
const postClass = classNames('post__body', {'post--edited': PostUtils.isEdited(post), 'search-item-snippet': isSearchResultItem});
|
||||
|
||||
@@ -435,7 +446,7 @@ const PostComponent = (props: Props): JSX.Element => {
|
||||
const threadFooter = props.location !== Locations.RHS_ROOT && props.isCollapsedThreadsEnabled && !post.root_id && (props.hasReplies || post.is_following) ? (
|
||||
<ThreadFooter
|
||||
threadId={post.id}
|
||||
replyClick={handleCommentClick}
|
||||
replyClick={handleThreadClick}
|
||||
/>
|
||||
) : null;
|
||||
const currentPostDay = getDateForUnixTicks(post.create_at);
|
||||
@@ -538,6 +549,7 @@ const PostComponent = (props: Props): JSX.Element => {
|
||||
{((!hideProfilePicture && props.location === Locations.CENTER) || hover || props.location !== Locations.CENTER) &&
|
||||
<PostTime
|
||||
isPermalink={!(Posts.POST_DELETED === post.state || isPostPendingOrFailed(post))}
|
||||
teamName={props.team?.name}
|
||||
eventTime={post.create_at}
|
||||
postId={post.id}
|
||||
location={props.location}
|
||||
@@ -577,6 +589,7 @@ const PostComponent = (props: Props): JSX.Element => {
|
||||
{!props.isPostBeingEdited &&
|
||||
<PostOptions
|
||||
{...props}
|
||||
teamId={teamId}
|
||||
setActionsMenuInitialisationState={props.actions.setActionsMenuInitialisationState}
|
||||
handleDropdownOpened={handleDropdownOpened}
|
||||
handleCommentClick={handleCommentClick}
|
||||
|
||||
@@ -95,7 +95,11 @@ const PostUserProfile = (props: Props): JSX.Element | null => {
|
||||
/>
|
||||
);
|
||||
|
||||
botIndicator = (<BotTag/>);
|
||||
// user profile component checks and add bot tag in case webhook is from bot account, but if webhook is from user account we need this.
|
||||
|
||||
if (!isBot) {
|
||||
botIndicator = (<BotTag/>);
|
||||
}
|
||||
} else if (isFromAutoResponder) {
|
||||
userProfile = (
|
||||
<span className='auto-responder'>
|
||||
|
||||
@@ -71,7 +71,145 @@ exports[`InviteMembers component should match snapshot 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="PageLine PageLine--no-left"
|
||||
style="margin-top: 50px; margin-left: 50px; height: calc(30vh);"
|
||||
style="margin-top: 50px; margin-left: 50px; height: calc(35vh);"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`InviteMembers component should match snapshot when it is cloud 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="InviteMembers-body test-class"
|
||||
>
|
||||
<div
|
||||
class="SingleColumnLayout"
|
||||
style="width: 547px;"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="PageLine PageLine--no-left"
|
||||
style="margin-bottom: 50px; margin-left: 50px; height: calc(25vh);"
|
||||
/>
|
||||
<div>
|
||||
Previous step
|
||||
</div>
|
||||
<h1
|
||||
class="PreparingWorkspaceTitle"
|
||||
>
|
||||
<span>
|
||||
Who works with you?
|
||||
</span>
|
||||
</h1>
|
||||
<p
|
||||
class="PreparingWorkspaceDescription"
|
||||
>
|
||||
<span>
|
||||
Collaboration is tough by yourself. Invite a few team members. Separate each email address with a space or comma.
|
||||
</span>
|
||||
</p>
|
||||
<div
|
||||
class="PreparingWorkspacePageBody"
|
||||
>
|
||||
<div
|
||||
class="UsersEmailsInput empty no-selections css-2b097c-container"
|
||||
>
|
||||
<span
|
||||
aria-live="assertive"
|
||||
class="css-1laao21-a11yText"
|
||||
>
|
||||
<p
|
||||
id="aria-selection-event"
|
||||
>
|
||||
|
||||
|
||||
</p>
|
||||
<p
|
||||
id="aria-context"
|
||||
>
|
||||
|
||||
0 results available. Use Up and Down to choose options, press Enter to select the currently focused option, press Escape to exit the menu, press Tab to select the option and exit the menu.
|
||||
</p>
|
||||
</span>
|
||||
<div
|
||||
class="users-emails-input__control users-emails-input__control--is-focused users-emails-input__control--menu-is-open css-1pahdxg-control"
|
||||
>
|
||||
<div
|
||||
class="users-emails-input__value-container users-emails-input__value-container--is-multi css-1hwfws3"
|
||||
>
|
||||
<div
|
||||
class="users-emails-input__placeholder css-1lxtzh0-placeholder"
|
||||
>
|
||||
Enter email addresses
|
||||
</div>
|
||||
<div
|
||||
class="css-gftnqw-Input"
|
||||
>
|
||||
<div
|
||||
class="users-emails-input__input"
|
||||
style="display: inline-block;"
|
||||
>
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
aria-label="Invite People"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
id="react-select-2-input"
|
||||
spellcheck="false"
|
||||
style="box-sizing: content-box; width: 2px; border: 0px; opacity: 1; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
style="position: absolute; top: 0px; left: 0px; visibility: hidden; height: 0px; overflow: scroll; white-space: pre; font-family: -webkit-small-control; letter-spacing: normal; text-transform: none;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="users-emails-input__menu css-26l3qy-menu"
|
||||
>
|
||||
<div
|
||||
class="users-emails-input__menu-list users-emails-input__menu-list--is-multi css-4ljt47-MenuList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="InviteMembers__submit"
|
||||
>
|
||||
<button
|
||||
class="primary-button"
|
||||
disabled=""
|
||||
>
|
||||
<span>
|
||||
Send invites
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
class="InviteMembersLink"
|
||||
>
|
||||
<button
|
||||
class="InviteMembersLink__button--single"
|
||||
data-testid="shareLinkInputButton"
|
||||
>
|
||||
<i
|
||||
class="icon icon-content-copy"
|
||||
/>
|
||||
<span>
|
||||
Copy Link
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="PageLine PageLine--no-left"
|
||||
style="margin-top: 50px; margin-left: 50px; height: calc(35vh);"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/preparing-workspace/invite_members_link should match snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="InviteMembersLink"
|
||||
>
|
||||
<button
|
||||
class="InviteMembersLink__button--single"
|
||||
data-testid="shareLinkInputButton"
|
||||
>
|
||||
<i
|
||||
class="icon icon-content-copy"
|
||||
/>
|
||||
<span>
|
||||
Copy Link
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/preparing-workspace/invite_members_link should match snapshot when displayed including the input field 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="InviteMembersLink"
|
||||
|
||||
@@ -1,23 +1,70 @@
|
||||
@import 'utils/mixins';
|
||||
|
||||
// UX decision to show no more than about 5 1/4 lines of users/emails at a time.
|
||||
$less-than-6-user-lines-height: 227px;
|
||||
|
||||
.InviteMembers-body {
|
||||
display: flex;
|
||||
// page width - channels preview width - progress dots width - people overlap width
|
||||
max-width: calc(100vw - 600px - 120px - 30px);
|
||||
|
||||
.PreparingWorkspaceDescription span {
|
||||
display: inline-block;
|
||||
max-width: 530px;
|
||||
}
|
||||
|
||||
.UsersEmailsInput {
|
||||
max-width: 420px;
|
||||
max-width: 540px;
|
||||
|
||||
&.no-selections {
|
||||
// placeholder and input position is difficult to change.
|
||||
// This overrides the default positioning of the input & placeholders
|
||||
// to make the taller than normal input look ok when nothing has yet been selected
|
||||
.users-emails-input__value-container {
|
||||
margin-top: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.users-emails-input__control {
|
||||
overflow: auto;
|
||||
min-height: 108px;
|
||||
max-height: $less-than-6-user-lines-height;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.InviteMembers {
|
||||
&__submit {
|
||||
display: flex;
|
||||
width: 400px;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.InviteMembersLink__button--single {
|
||||
width: fit-content;
|
||||
min-width: 148px;
|
||||
margin-right: 8px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.fade-in-skip-button {
|
||||
animation: fade-in 2s forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@include simple-in-and-out-before("InviteMembers");
|
||||
|
||||
.ChannelsPreview--enter-from-after {
|
||||
|
||||
@@ -9,6 +9,7 @@ import InviteMembers from './invite_members';
|
||||
|
||||
describe('InviteMembers component', () => {
|
||||
let defaultProps: ComponentProps<any>;
|
||||
const setEmailsFn = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
defaultProps = {
|
||||
@@ -21,8 +22,12 @@ describe('InviteMembers component', () => {
|
||||
onPageView: jest.fn(),
|
||||
previous: <div>{'Previous step'}</div>,
|
||||
next: jest.fn(),
|
||||
setEmails: setEmailsFn,
|
||||
show: true,
|
||||
transitionDirection: 'forward',
|
||||
inferredProtocol: null,
|
||||
isSelfHosted: true,
|
||||
emails: [],
|
||||
};
|
||||
});
|
||||
|
||||
@@ -32,6 +37,17 @@ describe('InviteMembers component', () => {
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match snapshot when it is cloud', () => {
|
||||
const component = withIntl(
|
||||
<InviteMembers
|
||||
{...defaultProps}
|
||||
isSelfHosted={false}
|
||||
/>,
|
||||
);
|
||||
const {container} = render(component);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders invite URL', () => {
|
||||
const component = withIntl(<InviteMembers {...defaultProps}/>);
|
||||
render(component);
|
||||
@@ -68,4 +84,16 @@ describe('InviteMembers component', () => {
|
||||
fireEvent.click(button);
|
||||
expect(defaultProps.next).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows send invites button when in cloud', () => {
|
||||
const component = withIntl(
|
||||
<InviteMembers
|
||||
{...defaultProps}
|
||||
isSelfHosted={false}
|
||||
/>,
|
||||
);
|
||||
render(component);
|
||||
const button = screen.getByRole('button', {name: 'Send invites'});
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo, useEffect} from 'react';
|
||||
import React, {useState, useMemo, useEffect} from 'react';
|
||||
import {CSSTransition} from 'react-transition-group';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {t} from 'utils/i18n';
|
||||
import {Constants} from 'utils/constants';
|
||||
|
||||
import UsersEmailsInput from 'components/widgets/inputs/users_emails_input';
|
||||
|
||||
import {Animations, mapAnimationReasonToClass, Form, PreparingWorkspacePageProps} from './steps';
|
||||
|
||||
@@ -12,20 +19,30 @@ import Description from './description';
|
||||
import PageBody from './page_body';
|
||||
import SingleColumnLayout from './single_column_layout';
|
||||
|
||||
import InviteMembersLink from './invite_members_link';
|
||||
import PageLine from './page_line';
|
||||
import InviteMembersLink from './invite_members_link';
|
||||
|
||||
import './invite_members.scss';
|
||||
|
||||
type Props = PreparingWorkspacePageProps & {
|
||||
disableEdits: boolean;
|
||||
className?: string;
|
||||
teamInviteId?: string;
|
||||
emails: Form['teamMembers']['invites'];
|
||||
setEmails: (emails: Form['teamMembers']['invites']) => void;
|
||||
teamInviteId: string;
|
||||
formUrl: Form['url'];
|
||||
configSiteUrl?: string;
|
||||
browserSiteUrl: string;
|
||||
inferredProtocol: 'http' | 'https' | null;
|
||||
isSelfHosted: boolean;
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
const InviteMembers = (props: Props) => {
|
||||
const [email, setEmail] = useState('');
|
||||
const [showSkipButton, setShowSkipButton] = useState(false);
|
||||
|
||||
const {formatMessage} = useIntl();
|
||||
let className = 'InviteMembers-body';
|
||||
if (props.className) {
|
||||
className += ' ' + props.className;
|
||||
@@ -33,6 +50,30 @@ const InviteMembers = (props: Props) => {
|
||||
|
||||
useEffect(props.onPageView, []);
|
||||
|
||||
useEffect(() => {
|
||||
setShowSkipButton(false);
|
||||
const timer = setTimeout(() => {
|
||||
setShowSkipButton(true);
|
||||
}, 3000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [props.show]);
|
||||
|
||||
const placeholder = formatMessage({
|
||||
id: 'onboarding_wizard.invite_members.placeholder',
|
||||
defaultMessage: 'Enter email addresses',
|
||||
});
|
||||
const errorProperties = {
|
||||
showError: false,
|
||||
errorMessageId: t(
|
||||
'invitation_modal.invite_members.exceeded_max_add_members_batch',
|
||||
),
|
||||
errorMessageDefault: 'No more than **{text}** people can be invited at once',
|
||||
errorMessageValues: {
|
||||
text: Constants.MAX_ADD_MEMBERS_BATCH.toString(),
|
||||
},
|
||||
};
|
||||
|
||||
const inviteURL = useMemo(() => {
|
||||
let urlBase = '';
|
||||
if (props.configSiteUrl && !props.configSiteUrl.includes('localhost')) {
|
||||
@@ -45,14 +86,128 @@ const InviteMembers = (props: Props) => {
|
||||
return `${urlBase}/signup_user_complete/?id=${props.teamInviteId}`;
|
||||
}, [props.teamInviteId, props.configSiteUrl, props.browserSiteUrl, props.formUrl]);
|
||||
|
||||
const description = (
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.invite_members.description_link'}
|
||||
defaultMessage='Collaboration is tough by yourself. Invite a few team members using the invitation link below.'
|
||||
let suppressNoOptionsMessage = true;
|
||||
if (props.emails?.length > Constants.MAX_ADD_MEMBERS_BATCH) {
|
||||
errorProperties.showError = true;
|
||||
|
||||
// We want to suppress the no options message, unless the message that is going to be displayed
|
||||
// is the max users warning
|
||||
suppressNoOptionsMessage = false;
|
||||
}
|
||||
|
||||
const cloudInviteMembersInput = (
|
||||
<UsersEmailsInput
|
||||
{...errorProperties}
|
||||
usersLoader={() => Promise.resolve([])}
|
||||
placeholder={placeholder}
|
||||
ariaLabel={formatMessage({
|
||||
id: 'invitation_modal.members.search_and_add.title',
|
||||
defaultMessage: 'Invite People',
|
||||
})}
|
||||
onChange={(emails: Array<UserProfile | string>) => {
|
||||
// There should not be any users found or passed,
|
||||
// because the usersLoader should never return any.
|
||||
// Filtering them out in case there are any
|
||||
// and to resolve Typescript errors
|
||||
props.setEmails(emails.filter((x) => typeof x === 'string') as string[]);
|
||||
}}
|
||||
value={props.emails}
|
||||
onInputChange={setEmail}
|
||||
inputValue={email}
|
||||
emailInvitationsEnabled={true}
|
||||
autoFocus={true}
|
||||
validAddressMessageId={t('invitation_modal.members.users_emails_input.valid_email')}
|
||||
validAddressMessageDefault={'Invite **{email}** as a team member'}
|
||||
suppressNoOptionsMessage={suppressNoOptionsMessage}
|
||||
/>
|
||||
);
|
||||
|
||||
const inviteInteraction = <InviteMembersLink inviteURL={inviteURL}/>;
|
||||
const inviteLink = (
|
||||
<InviteMembersLink
|
||||
inviteURL={inviteURL}
|
||||
inputAndButtonStyle={props.isSelfHosted}
|
||||
/>
|
||||
);
|
||||
|
||||
const inviteMemberBodyContent = () => {
|
||||
if (props.isSelfHosted) {
|
||||
return (
|
||||
<>
|
||||
<Title>
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.invite_members.title'}
|
||||
defaultMessage='Invite your team members'
|
||||
/>
|
||||
</Title>
|
||||
<Description>
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.invite_members.description_link'}
|
||||
defaultMessage='Collaboration is tough by yourself. Invite a few team members using the invitation link below.'
|
||||
/>
|
||||
</Description>
|
||||
<PageBody>
|
||||
{inviteLink}
|
||||
</PageBody>
|
||||
<div className='InviteMembers__submit'>
|
||||
<button
|
||||
className='primary-button'
|
||||
disabled={props.disableEdits}
|
||||
onClick={props.next}
|
||||
>
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.invite_members.next_link'}
|
||||
defaultMessage='Finish setup'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Title>
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.invite_members_cloud.title'}
|
||||
defaultMessage='Who works with you?'
|
||||
/>
|
||||
</Title>
|
||||
<Description>
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.invite_members.description'}
|
||||
defaultMessage='Collaboration is tough by yourself. Invite a few team members. Separate each email address with a space or comma.'
|
||||
/>
|
||||
</Description>
|
||||
<PageBody>
|
||||
{cloudInviteMembersInput}
|
||||
</PageBody>
|
||||
<div className='InviteMembers__submit'>
|
||||
<button
|
||||
className='primary-button'
|
||||
disabled={props.disableEdits || props.emails.length === 0}
|
||||
onClick={props.next}
|
||||
>
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.invite_members.next'}
|
||||
defaultMessage='Send invites'
|
||||
/>
|
||||
|
||||
</button>
|
||||
{inviteLink}
|
||||
{showSkipButton &&
|
||||
<button
|
||||
className='link-style fade-in-skip-button'
|
||||
onClick={props.skip}
|
||||
>
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.invite_members.skip'}
|
||||
defaultMessage='Skip'
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<CSSTransition
|
||||
@@ -73,35 +228,12 @@ const InviteMembers = (props: Props) => {
|
||||
noLeft={true}
|
||||
/>
|
||||
{props.previous}
|
||||
<Title>
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.invite_members.title'}
|
||||
defaultMessage='Invite your team members'
|
||||
/>
|
||||
</Title>
|
||||
<Description>
|
||||
{description}
|
||||
</Description>
|
||||
<PageBody>
|
||||
{inviteInteraction}
|
||||
</PageBody>
|
||||
<div className='InviteMembers__submit'>
|
||||
<button
|
||||
className='primary-button'
|
||||
disabled={props.disableEdits}
|
||||
onClick={props.next}
|
||||
>
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.invite_members.next_link'}
|
||||
defaultMessage='Finish setup'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{inviteMemberBodyContent()}
|
||||
<PageLine
|
||||
style={{
|
||||
marginTop: '50px',
|
||||
marginLeft: '50px',
|
||||
height: 'calc(30vh)',
|
||||
height: 'calc(35vh)',
|
||||
}}
|
||||
noLeft={true}
|
||||
/>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@import 'utils/mixins';
|
||||
|
||||
.InviteMembersLink {
|
||||
display: flex;
|
||||
|
||||
@@ -9,35 +11,21 @@
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.2);
|
||||
border-left: 1px solid rgba(var(--center-channel-color-rgb), 0.2);
|
||||
background: rgba(var(--center-channel-color-rgb), 0.04);
|
||||
background: var(--center-channel-color-rgb);
|
||||
border-radius: 4px 0 0 4px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.56);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&__button {
|
||||
button {
|
||||
display: flex;
|
||||
width: 180px;
|
||||
max-width: 382px;
|
||||
height: 48px;
|
||||
flex-grow: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--button-bg);
|
||||
background: var(--center-channel-bg);
|
||||
border-radius: 0 4px 4px 0;
|
||||
color: var(--button-bg);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
|
||||
&:hover {
|
||||
background: rgba(var(--button-bg-rgb), 0.08);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba(var(--button-bg-rgb), 0.08);
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
height: 24px;
|
||||
@@ -48,4 +36,28 @@
|
||||
fill: var(--button-bg);
|
||||
}
|
||||
}
|
||||
|
||||
&__button {
|
||||
width: 180px;
|
||||
height: 48px;
|
||||
border: 1px solid var(--button-bg);
|
||||
background: var(--center-channel-bg);
|
||||
border-radius: 0 4px 4px 0;
|
||||
color: var(--button-bg);
|
||||
|
||||
&:hover {
|
||||
background: rgba(var(--button-bg-rgb), 0.08);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba(var(--button-bg-rgb), 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
&__button--single {
|
||||
@include tertiary-button;
|
||||
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,39 @@ describe('components/preparing-workspace/invite_members_link', () => {
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match snapshot when displayed including the input field', () => {
|
||||
const component = withIntl(
|
||||
<InviteMembersLink
|
||||
inviteURL={inviteURL}
|
||||
inputAndButtonStyle={true}
|
||||
/>,
|
||||
);
|
||||
|
||||
const {container} = render(component);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders only with the button if the inputAndButton option is false', () => {
|
||||
const component = withIntl(
|
||||
<InviteMembersLink
|
||||
inviteURL={inviteURL}
|
||||
inputAndButtonStyle={false}
|
||||
/>,
|
||||
);
|
||||
render(component);
|
||||
const input = screen.queryByText(inviteURL);
|
||||
expect(input).not.toBeInTheDocument();
|
||||
const button = screen.getByRole('button', {name: /copy link/i});
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders an input field with the invite URL', () => {
|
||||
const component = withIntl(<InviteMembersLink inviteURL={inviteURL}/>);
|
||||
const component = withIntl(
|
||||
<InviteMembersLink
|
||||
inviteURL={inviteURL}
|
||||
inputAndButtonStyle={true}
|
||||
/>,
|
||||
);
|
||||
render(component);
|
||||
const input = screen.getByDisplayValue(inviteURL);
|
||||
expect(input).toBeInTheDocument();
|
||||
|
||||
@@ -11,30 +11,36 @@ import './invite_members_link.scss';
|
||||
|
||||
type Props = {
|
||||
inviteURL: string;
|
||||
inputAndButtonStyle?: boolean;
|
||||
}
|
||||
|
||||
const InviteMembersLink = (props: Props) => {
|
||||
const InviteMembersLink = ({
|
||||
inviteURL,
|
||||
inputAndButtonStyle = false,
|
||||
}: Props) => {
|
||||
const copyText = useCopyText({
|
||||
trackCallback: () => trackEvent('first_admin_setup', 'admin_setup_click_copy_invite_link'),
|
||||
text: props.inviteURL,
|
||||
text: inviteURL,
|
||||
});
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<div className='InviteMembersLink'>
|
||||
<input
|
||||
className='InviteMembersLink__input'
|
||||
type='text'
|
||||
readOnly={true}
|
||||
value={props.inviteURL}
|
||||
aria-label={intl.formatMessage({
|
||||
id: 'onboarding_wizard.invite_members.copy_link_input',
|
||||
defaultMessage: 'team invite link',
|
||||
})}
|
||||
data-testid='shareLinkInput'
|
||||
/>
|
||||
{inputAndButtonStyle &&
|
||||
<input
|
||||
className='InviteMembersLink__input'
|
||||
type='text'
|
||||
readOnly={true}
|
||||
value={inviteURL}
|
||||
aria-label={intl.formatMessage({
|
||||
id: 'onboarding_wizard.invite_members.copy_link_input',
|
||||
defaultMessage: 'team invite link',
|
||||
})}
|
||||
data-testid='shareLinkInput'
|
||||
/>
|
||||
}
|
||||
<button
|
||||
className='InviteMembersLink__button'
|
||||
className={`InviteMembersLink__button${inputAndButtonStyle ? '' : '--single'}`}
|
||||
onClick={copyText.onClick}
|
||||
data-testid='shareLinkInputButton'
|
||||
>
|
||||
@@ -48,7 +54,7 @@ const InviteMembersLink = (props: Props) => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className='icon icon-link-variant'/>
|
||||
{inputAndButtonStyle ? <i className='icon icon-link-variant'/> : <i className='icon icon-content-copy'/>}
|
||||
<FormattedMessage
|
||||
id='onboarding_wizard.invite_members.copy_link'
|
||||
defaultMessage='Copy Link'
|
||||
|
||||
@@ -38,8 +38,8 @@ type Props = PreparingWorkspacePageProps & {
|
||||
setInviteId: (inviteId: string) => void;
|
||||
}
|
||||
|
||||
const reportValidationError = debounce(() => {
|
||||
trackEvent('first_admin_setup', 'validate_organization_error');
|
||||
const reportValidationError = debounce((error: string) => {
|
||||
trackEvent('first_admin_setup', 'admin_onboarding_organization_submit_fail', {error});
|
||||
}, 700, {leading: false});
|
||||
|
||||
const Organization = (props: Props) => {
|
||||
@@ -123,7 +123,7 @@ const Organization = (props: Props) => {
|
||||
}
|
||||
|
||||
if (validation.error || teamApiError.current) {
|
||||
reportValidationError();
|
||||
reportValidationError(validation.error ? validation.error : teamApiError.current! as string);
|
||||
return;
|
||||
}
|
||||
props.next?.();
|
||||
|
||||
@@ -10,7 +10,6 @@ import MultiSelectCards from 'components/common/multi_select_cards';
|
||||
|
||||
import GithubSVG from 'components/common/svg_images_components/github_svg';
|
||||
import GitlabSVG from 'components/common/svg_images_components/gitlab_svg';
|
||||
import CelebrateSVG from 'components/common/svg_images_components/celebrate_svg';
|
||||
import JiraSVG from 'components/common/svg_images_components/jira_svg';
|
||||
import ZoomSVG from 'components/common/svg_images_components/zoom_svg';
|
||||
import TodoSVG from 'components/common/svg_images_components/todo_svg';
|
||||
@@ -31,6 +30,7 @@ type Props = PreparingWorkspacePageProps & {
|
||||
setOption: (option: keyof Form['plugins']) => void;
|
||||
className?: string;
|
||||
isSelfHosted: boolean;
|
||||
handleVisitMarketPlaceClick: () => void;
|
||||
}
|
||||
const Plugins = (props: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
@@ -46,32 +46,19 @@ const Plugins = (props: Props) => {
|
||||
className += ' ' + props.className;
|
||||
}
|
||||
|
||||
let title = (
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.cloud_plugins.title'}
|
||||
defaultMessage='Welcome to Mattermost!'
|
||||
id={'onboarding_wizard.self_hosted_plugins.title'}
|
||||
defaultMessage='What tools do you use?'
|
||||
/>
|
||||
);
|
||||
let description = (
|
||||
|
||||
const description = (
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.cloud_plugins.description'}
|
||||
defaultMessage={'Mattermost is better when integrated with the tools your team uses for collaboration. Popular tools are below, select the ones your team uses and we\'ll add them to your workspace. Additional set up may be needed later.'}
|
||||
id={'onboarding_wizard.self_hosted_plugins.description'}
|
||||
defaultMessage={'Choose the tools you work with, and we\'ll add them to your workspace. Additional set up may be needed later.'}
|
||||
/>
|
||||
);
|
||||
if (props.isSelfHosted) {
|
||||
title = (
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.self_hosted_plugins.title'}
|
||||
defaultMessage='What tools do you use?'
|
||||
/>
|
||||
);
|
||||
description = (
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.self_hosted_plugins.description'}
|
||||
defaultMessage={'Choose the tools you work with, and we\'ll add them to your workspace. Additional set up may be needed later.'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CSSTransition
|
||||
@@ -94,16 +81,6 @@ const Plugins = (props: Props) => {
|
||||
{props.previous}
|
||||
<Title>
|
||||
{title}
|
||||
{!props.isSelfHosted && (
|
||||
<div className='subtitle'>
|
||||
<CelebrateSVG/>
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.cloud_plugins.subtitle'}
|
||||
defaultMessage='(almost there!)'
|
||||
/>
|
||||
</div>
|
||||
|
||||
)}
|
||||
</Title>
|
||||
<Description>{description}</Description>
|
||||
<PageBody>
|
||||
@@ -178,6 +155,7 @@ const Plugins = (props: Props) => {
|
||||
<ExternalLink
|
||||
href='https://mattermost.com/marketplace/'
|
||||
location='preparing_workspace_plugins'
|
||||
onClick={props.handleVisitMarketPlaceClick}
|
||||
>
|
||||
{chunks}
|
||||
</ExternalLink>
|
||||
|
||||
@@ -133,6 +133,7 @@
|
||||
|
||||
// centering
|
||||
margin: 0 auto;
|
||||
margin-left: 300px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
|
||||
@@ -8,9 +8,11 @@ import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import {GeneralTypes} from 'mattermost-redux/action_types';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {sendEmailInvitesToTeamGracefully} from 'mattermost-redux/actions/teams';
|
||||
import {getFirstAdminSetupComplete as getFirstAdminSetupCompleteAction} from 'mattermost-redux/actions/general';
|
||||
import {ActionResult} from 'mattermost-redux/types/actions';
|
||||
import {Team} from '@mattermost/types/teams';
|
||||
import {getIsOnboardingFlowEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {isFirstAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getCurrentTeam, getMyTeams} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getFirstAdminSetupComplete, getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
@@ -108,24 +110,26 @@ const PreparingWorkspace = (props: Props) => {
|
||||
defaultMessage: 'Something went wrong. Please try again.',
|
||||
});
|
||||
const isUserFirstAdmin = useSelector(isFirstAdmin);
|
||||
const onboardingFlowEnabled = useSelector(getIsOnboardingFlowEnabled);
|
||||
|
||||
const currentTeam = useSelector(getCurrentTeam);
|
||||
const myTeams = useSelector(getMyTeams);
|
||||
|
||||
// In cloud instances created from portal,
|
||||
// new admin user has a team in myTeams but not in currentTeam.
|
||||
let team = currentTeam || myTeams?.[0];
|
||||
const team = currentTeam || myTeams?.[0];
|
||||
|
||||
const config = useSelector(getConfig);
|
||||
const pluginsEnabled = config.PluginsEnabled === 'true';
|
||||
const showOnMountTimeout = useRef<NodeJS.Timeout>();
|
||||
const configSiteUrl = config.SiteURL;
|
||||
const isConfigSiteUrlDefault = Boolean(config.SiteURL && config.SiteURL === Constants.DEFAULT_SITE_URL);
|
||||
const isSelfHosted = useSelector(getLicense).Cloud !== 'true';
|
||||
|
||||
const stepOrder = [
|
||||
isSelfHosted && WizardSteps.Organization,
|
||||
pluginsEnabled && WizardSteps.Plugins,
|
||||
isSelfHosted && WizardSteps.InviteMembers,
|
||||
WizardSteps.InviteMembers,
|
||||
WizardSteps.LaunchingWorkspace,
|
||||
].filter((x) => Boolean(x)) as WizardStep[];
|
||||
|
||||
@@ -225,16 +229,15 @@ const PreparingWorkspace = (props: Props) => {
|
||||
const sendFormStart = Date.now();
|
||||
setSubmissionState(SubmissionStates.Submitting);
|
||||
|
||||
if (form.organization && !isSelfHosted) {
|
||||
if (!form.teamMembers.skipped && !isConfigSiteUrlDefault && !isSelfHosted) {
|
||||
try {
|
||||
const {error, newTeam} = await createTeam(form.organization);
|
||||
if (error !== null) {
|
||||
redirectWithError(WizardSteps.Organization, genericSubmitError);
|
||||
const inviteResult = await dispatch(sendEmailInvitesToTeamGracefully(team.id, form.teamMembers.invites));
|
||||
if ((inviteResult as ActionResult).error) {
|
||||
redirectWithError(WizardSteps.InviteMembers, genericSubmitError);
|
||||
return;
|
||||
}
|
||||
team = newTeam as Team;
|
||||
} catch (e) {
|
||||
redirectWithError(WizardSteps.Organization, genericSubmitError);
|
||||
redirectWithError(WizardSteps.InviteMembers, genericSubmitError);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -267,6 +270,7 @@ const PreparingWorkspace = (props: Props) => {
|
||||
const goToChannels = () => {
|
||||
dispatch({type: GeneralTypes.SHOW_LAUNCHING_WORKSPACE, open: true});
|
||||
props.history.push(`/${team.name}/channels${Constants.DEFAULT_CHANNEL}`);
|
||||
trackEvent('first_admin_setup', 'admin_setup_complete');
|
||||
};
|
||||
|
||||
const sendFormEnd = Date.now();
|
||||
@@ -287,7 +291,7 @@ const PreparingWorkspace = (props: Props) => {
|
||||
}, [submissionState]);
|
||||
|
||||
const adminRevisitedPage = firstAdminSetupComplete && submissionState === SubmissionStates.Presubmit;
|
||||
const shouldRedirect = !isUserFirstAdmin || adminRevisitedPage;
|
||||
const shouldRedirect = !isUserFirstAdmin || adminRevisitedPage || !onboardingFlowEnabled;
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldRedirect) {
|
||||
@@ -434,16 +438,10 @@ const PreparingWorkspace = (props: Props) => {
|
||||
next={() => {
|
||||
const pluginChoices = {...form.plugins};
|
||||
delete pluginChoices.skipped;
|
||||
if (!isSelfHosted) {
|
||||
setSubmissionState(SubmissionStates.UserRequested);
|
||||
}
|
||||
makeNext(WizardSteps.Plugins)(pluginChoices);
|
||||
skipPlugins(false);
|
||||
}}
|
||||
skip={() => {
|
||||
if (!isSelfHosted) {
|
||||
setSubmissionState(SubmissionStates.UserRequested);
|
||||
}
|
||||
makeNext(WizardSteps.Plugins, true)();
|
||||
skipPlugins(true);
|
||||
}}
|
||||
@@ -460,6 +458,9 @@ const PreparingWorkspace = (props: Props) => {
|
||||
show={shouldShowPage(WizardSteps.Plugins)}
|
||||
transitionDirection={getTransitionDirection(WizardSteps.Plugins)}
|
||||
className='child-page'
|
||||
handleVisitMarketPlaceClick={() => {
|
||||
trackEvent('first_admin_setup', 'click_visit_marketplace_link');
|
||||
}}
|
||||
/>
|
||||
<InviteMembers
|
||||
onPageView={onPageViews[WizardSteps.InviteMembers]}
|
||||
@@ -485,6 +486,18 @@ const PreparingWorkspace = (props: Props) => {
|
||||
configSiteUrl={configSiteUrl}
|
||||
formUrl={form.url}
|
||||
browserSiteUrl={browserSiteUrl}
|
||||
emails={form.teamMembers.invites}
|
||||
setEmails={(emails: string[]) => {
|
||||
setForm({
|
||||
...form,
|
||||
teamMembers: {
|
||||
...form.teamMembers,
|
||||
invites: emails,
|
||||
},
|
||||
});
|
||||
}}
|
||||
inferredProtocol={form.inferredProtocol}
|
||||
isSelfHosted={isSelfHosted}
|
||||
/>
|
||||
<LaunchingWorkspace
|
||||
onPageView={onPageViews[WizardSteps.LaunchingWorkspace]}
|
||||
|
||||
@@ -524,23 +524,6 @@ class ProfilePopover extends React.PureComponent<ProfilePopoverProps, ProfilePop
|
||||
{userName}
|
||||
</div>,
|
||||
);
|
||||
const email = this.props.user.email || '';
|
||||
if (email && !this.props.user.is_bot && !haveOverrideProp) {
|
||||
dataContent.push(
|
||||
<div
|
||||
data-toggle='tooltip'
|
||||
title={email}
|
||||
key='user-popover-email'
|
||||
>
|
||||
<a
|
||||
href={'mailto:' + email}
|
||||
className='text-nowrap text-lowercase user-popover__email pb-1'
|
||||
>
|
||||
{email}
|
||||
</a>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
if (this.props.user.position && !haveOverrideProp) {
|
||||
const position = (this.props.user?.position || '').substring(
|
||||
0,
|
||||
@@ -561,6 +544,23 @@ class ProfilePopover extends React.PureComponent<ProfilePopoverProps, ProfilePop
|
||||
className='divider divider--expanded'
|
||||
/>,
|
||||
);
|
||||
const email = this.props.user.email || '';
|
||||
if (email && !this.props.user.is_bot && !haveOverrideProp) {
|
||||
dataContent.push(
|
||||
<div
|
||||
data-toggle='tooltip'
|
||||
title={email}
|
||||
key='user-popover-email'
|
||||
>
|
||||
<a
|
||||
href={'mailto:' + email}
|
||||
className='text-nowrap text-lowercase user-popover__email pb-1'
|
||||
>
|
||||
{email}
|
||||
</a>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
dataContent.push(
|
||||
<Pluggable
|
||||
key='profilePopoverPluggable2'
|
||||
|
||||
@@ -368,9 +368,6 @@ class ProcessPaymentSetup extends React.PureComponent<Props, State> {
|
||||
title={t(
|
||||
'admin.billing.subscription.complianceScreenFailed.title',
|
||||
)}
|
||||
subtitle={t(
|
||||
'admin.billing.subscription.complianceScreenFailed.subtitle',
|
||||
)}
|
||||
icon={
|
||||
<ComplianceScreenFailedSvg
|
||||
width={444}
|
||||
|
||||
@@ -940,9 +940,6 @@ class PurchaseModal extends React.PureComponent<Props, State> {
|
||||
title={t(
|
||||
'admin.billing.subscription.complianceScreenFailed.title',
|
||||
)}
|
||||
subtitle={t(
|
||||
'admin.billing.subscription.complianceScreenFailed.subtitle',
|
||||
)}
|
||||
icon={
|
||||
<ComplianceScreenFailedSvg
|
||||
width={321}
|
||||
|
||||
@@ -10,9 +10,10 @@ import classNames from 'classnames';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {rudderAnalytics, RudderTelemetryHandler} from 'mattermost-redux/client/rudder';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {Theme, getIsOnboardingFlowEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getCurrentUser, isCurrentUserSystemAdmin, checkIsFirstAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getActiveTeamsList} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {setUrl} from 'mattermost-redux/actions/general';
|
||||
import {setSystemEmojis} from 'mattermost-redux/actions/emojis';
|
||||
|
||||
@@ -89,8 +90,6 @@ import {ActionResult} from 'mattermost-redux/types/actions';
|
||||
|
||||
import WelcomePostRenderer from 'components/welcome_post_renderer';
|
||||
|
||||
import {getMyTeams} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import {applyLuxonDefaults} from './effects';
|
||||
|
||||
import RootProvider from './root_provider';
|
||||
@@ -360,8 +359,11 @@ export default class Root extends React.PureComponent<Props, State> {
|
||||
return;
|
||||
}
|
||||
|
||||
const myTeams = getMyTeams(storeState);
|
||||
if (myTeams.length > 0) {
|
||||
const teams = getActiveTeamsList(storeState);
|
||||
|
||||
const onboardingFlowEnabled = getIsOnboardingFlowEnabled(storeState);
|
||||
|
||||
if (teams.length > 0 || !onboardingFlowEnabled) {
|
||||
GlobalActions.redirectUserToDefaultTeam();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {connect} from 'react-redux';
|
||||
|
||||
import {getFirstAdminSetupComplete} from 'mattermost-redux/actions/general';
|
||||
import {getCurrentUserId, isCurrentUserSystemAdmin, isFirstAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getIsOnboardingFlowEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {GenericAction} from 'mattermost-redux/types/actions';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
@@ -13,7 +14,11 @@ import {GlobalState} from 'types/store';
|
||||
import RootRedirect, {Props} from './root_redirect';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const isElegibleForFirstAdmingOnboarding = isCurrentUserSystemAdmin(state);
|
||||
const onboardingFlowEnabled = getIsOnboardingFlowEnabled(state);
|
||||
let isElegibleForFirstAdmingOnboarding = onboardingFlowEnabled;
|
||||
if (isElegibleForFirstAdmingOnboarding) {
|
||||
isElegibleForFirstAdmingOnboarding = isCurrentUserSystemAdmin(state);
|
||||
}
|
||||
return {
|
||||
currentUserId: getCurrentUserId(state),
|
||||
isElegibleForFirstAdmingOnboarding,
|
||||
|
||||
@@ -250,10 +250,10 @@ export default class SearchableChannelList extends React.PureComponent {
|
||||
channelDropdown = (
|
||||
<div className='more-modal__dropdown'>
|
||||
<MenuWrapper id='channelsMoreDropdown'>
|
||||
<a>
|
||||
<button className='style--none'>
|
||||
<span>{this.props.shouldShowArchivedChannels ? localizeMessage('more_channels.show_archived_channels', 'Show: Archived Channels') : localizeMessage('more_channels.show_public_channels', 'Show: Public Channels')}</span>
|
||||
<span className='caret'/>
|
||||
</a>
|
||||
</button>
|
||||
<Menu
|
||||
openLeft={false}
|
||||
ariaLabel={localizeMessage('team_members_dropdown.menuAriaLabel', 'Change the role of a team member')}
|
||||
|
||||
@@ -316,15 +316,19 @@ describe('components/signup/Signup', () => {
|
||||
expect(signupContainer).toHaveTextContent('Interested in receiving Mattermost security, product, promotions, and company updates updates via newsletter?Sign up at https://mattermost.com/security-updates/.');
|
||||
});
|
||||
|
||||
it('should not show any newsletter related opt-in or text for cloud', async () => {
|
||||
it('should show newsletter related opt-in or text for cloud', async () => {
|
||||
jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => true);
|
||||
mockLicense = {IsLicensed: 'true', Cloud: 'true'};
|
||||
|
||||
renderWithIntlAndStore(
|
||||
const {container: signupContainer} = renderWithIntlAndStore(
|
||||
<BrowserRouter>
|
||||
<Signup/>
|
||||
</BrowserRouter>, {});
|
||||
|
||||
expect(() => screen.getByTestId('signup-body-card-form-check-newsletter')).toThrow();
|
||||
screen.getByTestId('signup-body-card-form-check-newsletter');
|
||||
const checkInput = screen.getByTestId('signup-body-card-form-check-newsletter');
|
||||
expect(checkInput).toHaveAttribute('type', 'checkbox');
|
||||
|
||||
expect(signupContainer).toHaveTextContent('I would like to receive Mattermost security updates via newsletter. By subscribing, I consent to receive emails from Mattermost with product updates, promotions, and company news. I have read the Privacy Policy and understand that I can unsubscribe at any time');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ import {getTeamInviteInfo} from 'mattermost-redux/actions/teams';
|
||||
import {createUser, loadMe, loadMeREST} from 'mattermost-redux/actions/users';
|
||||
import {DispatchFunc} from 'mattermost-redux/types/actions';
|
||||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getIsOnboardingFlowEnabled, isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {isEmail} from 'mattermost-redux/utils/helpers';
|
||||
|
||||
@@ -25,6 +25,7 @@ import {GlobalState} from 'types/store';
|
||||
|
||||
import {getGlobalItem} from 'selectors/storage';
|
||||
|
||||
import {redirectUserToDefaultTeam} from 'actions/global_actions';
|
||||
import {removeGlobalItem, setGlobalItem} from 'actions/storage';
|
||||
import {addUserToTeamFromInvite} from 'actions/team_actions';
|
||||
import {trackEvent} from 'actions/telemetry_actions.jsx';
|
||||
@@ -101,8 +102,9 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
TermsOfServiceLink,
|
||||
PrivacyPolicyLink,
|
||||
} = config;
|
||||
const {IsLicensed, Cloud} = useSelector(getLicense);
|
||||
const {IsLicensed} = useSelector(getLicense);
|
||||
const loggedIn = Boolean(useSelector(getCurrentUserId));
|
||||
const onboardingFlowEnabled = useSelector(getIsOnboardingFlowEnabled);
|
||||
const usedBefore = useSelector((state: GlobalState) => (!inviteId && !loggedIn && token ? getGlobalItem(state, token, null) : undefined));
|
||||
const graphQLEnabled = useSelector(isGraphQLEnabled);
|
||||
|
||||
@@ -111,7 +113,6 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
const passwordInput = useRef<HTMLInputElement>(null);
|
||||
|
||||
const isLicensed = IsLicensed === 'true';
|
||||
const isCloud = Cloud === 'true';
|
||||
const enableOpenServer = EnableOpenServer === 'true';
|
||||
const noAccounts = NoAccounts === 'true';
|
||||
const enableSignUpWithEmail = EnableSignUpWithEmail === 'true';
|
||||
@@ -308,7 +309,15 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
} else if (inviteId) {
|
||||
getInviteInfo(inviteId);
|
||||
} else if (loggedIn) {
|
||||
history.push('/');
|
||||
if (onboardingFlowEnabled) {
|
||||
// need info about whether admin or not,
|
||||
// and whether admin has already completed
|
||||
// first tiem onboarding. Instead of fetching and orchestrating that here,
|
||||
// let the default root component handle it.
|
||||
history.push('/');
|
||||
} else {
|
||||
redirectUserToDefaultTeam();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,12 +460,14 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
|
||||
if (redirectTo) {
|
||||
history.push(redirectTo);
|
||||
} else {
|
||||
} else if (onboardingFlowEnabled) {
|
||||
// need info about whether admin or not,
|
||||
// and whether admin has already completed
|
||||
// first tiem onboarding. Instead of fetching and orchestrating that here,
|
||||
// let the default root component handle it.
|
||||
history.push('/');
|
||||
} else {
|
||||
redirectUserToDefaultTeam();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -579,10 +590,6 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
const handleReturnButtonOnClick = () => history.replace('/');
|
||||
|
||||
const getNewsletterCheck = () => {
|
||||
if (isCloud) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (canReachCWS) {
|
||||
return (
|
||||
<CheckInput
|
||||
|
||||
@@ -6,6 +6,7 @@ import {bindActionCreators, Dispatch, ActionCreatorsMapObject} from 'redux';
|
||||
|
||||
import {getTermsOfService, updateMyTermsOfServiceStatus} from 'mattermost-redux/actions/users';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getIsOnboardingFlowEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {GlobalState} from '@mattermost/types/store';
|
||||
import {ActionFunc, GenericAction} from 'mattermost-redux/types/actions';
|
||||
@@ -25,7 +26,9 @@ type Actions = {
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const config = getConfig(state);
|
||||
const onboardingFlowEnabled = getIsOnboardingFlowEnabled(state);
|
||||
return {
|
||||
onboardingFlowEnabled,
|
||||
termsEnabled: config.EnableCustomTermsOfService === 'true',
|
||||
emojiMap: getEmojiMap(state),
|
||||
};
|
||||
|
||||
@@ -27,6 +27,7 @@ describe('components/terms_of_service/TermsOfService', () => {
|
||||
location: {search: ''},
|
||||
termsEnabled: true,
|
||||
emojiMap: {} as EmojiMap,
|
||||
onboardingFlowEnabled: false,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
|
||||
@@ -38,6 +38,7 @@ export interface TermsOfServiceProps {
|
||||
) => {data: UpdateMyTermsOfServiceStatusResponse};
|
||||
};
|
||||
emojiMap: EmojiMap;
|
||||
onboardingFlowEnabled: boolean;
|
||||
}
|
||||
|
||||
interface TermsOfServiceState {
|
||||
@@ -110,12 +111,14 @@ export default class TermsOfService extends React.PureComponent<TermsOfServicePr
|
||||
const redirectTo = query.get('redirect_to');
|
||||
if (redirectTo && redirectTo.match(/^\/([^/]|$)/)) {
|
||||
getHistory().push(redirectTo);
|
||||
} else {
|
||||
} else if (this.props.onboardingFlowEnabled) {
|
||||
// need info about whether admin or not,
|
||||
// and whether admin has already completed
|
||||
// first time onboarding. Instead of fetching and orchestrating that here,
|
||||
// let the default root component handle it.
|
||||
getHistory().push('/');
|
||||
} else {
|
||||
GlobalActions.redirectUserToDefaultTeam();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -70,7 +70,7 @@ function ThreadFooter({
|
||||
trackEvent('crt', 'replied_using_footer');
|
||||
e.stopPropagation();
|
||||
dispatch(selectPost({id: threadId, channel_id: channelId} as Post));
|
||||
}, [dispatch, replyClick, threadId, channelId]);
|
||||
}, [replyClick, threadId, channelId]);
|
||||
|
||||
const handleFollowing = useCallback((e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -52,7 +52,6 @@ exports[`components/threading/global_threads/thread_list should match snapshot 1
|
||||
>
|
||||
<Memo(Button)
|
||||
className="Button___large Button___icon"
|
||||
disabled={false}
|
||||
id="threads-list__mark-all-as-read"
|
||||
marginTop={true}
|
||||
onClick={[Function]}
|
||||
|
||||
@@ -233,7 +233,6 @@ const ThreadList = ({
|
||||
>
|
||||
<Button
|
||||
id={'threads-list__mark-all-as-read'}
|
||||
disabled={!someUnread}
|
||||
className={'Button___large Button___icon'}
|
||||
onClick={handleOpenMarkAllAsReadModal}
|
||||
marginTop={true}
|
||||
|
||||
@@ -52,7 +52,6 @@ function makeMapStateToProps() {
|
||||
directTeammate,
|
||||
lastPost,
|
||||
replyListIds,
|
||||
teamId: channel.team_id,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ type Props = {
|
||||
onCardClick: (post: Post) => void;
|
||||
post: Post;
|
||||
previousPostId: string;
|
||||
teamId: string;
|
||||
timestampProps?: Partial<TimestampProps>;
|
||||
id?: Post['id'];
|
||||
}
|
||||
@@ -27,7 +26,6 @@ function Reply({
|
||||
onCardClick,
|
||||
post,
|
||||
previousPostId,
|
||||
teamId,
|
||||
timestampProps,
|
||||
}: Props) {
|
||||
return (
|
||||
@@ -37,7 +35,6 @@ function Reply({
|
||||
isLastPost={isLastPost}
|
||||
post={post}
|
||||
previousPostId={previousPostId}
|
||||
teamId={teamId}
|
||||
timestampProps={timestampProps}
|
||||
location={Locations.RHS_COMMENT}
|
||||
/>
|
||||
|
||||
@@ -25,7 +25,6 @@ type Props = {
|
||||
listId: string;
|
||||
onCardClick: (post: Post) => void;
|
||||
previousPostId: string;
|
||||
teamId: string;
|
||||
timestampProps?: Partial<TimestampProps>;
|
||||
};
|
||||
|
||||
@@ -38,7 +37,6 @@ function ThreadViewerRow({
|
||||
listId,
|
||||
onCardClick,
|
||||
previousPostId,
|
||||
teamId,
|
||||
timestampProps,
|
||||
}: Props) {
|
||||
switch (true) {
|
||||
@@ -61,7 +59,6 @@ function ThreadViewerRow({
|
||||
postId={listId}
|
||||
isLastPost={isLastPost}
|
||||
handleCardClick={onCardClick}
|
||||
teamId={teamId}
|
||||
timestampProps={timestampProps}
|
||||
location={Locations.RHS_ROOT}
|
||||
/>
|
||||
@@ -87,7 +84,6 @@ function ThreadViewerRow({
|
||||
isLastPost={isLastPost}
|
||||
onCardClick={onCardClick}
|
||||
previousPostId={previousPostId}
|
||||
teamId={teamId}
|
||||
timestampProps={timestampProps}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -34,7 +34,6 @@ type Props = {
|
||||
onCardClick: (post: Post) => void;
|
||||
replyListIds: string[];
|
||||
selected: Post | FakePost;
|
||||
teamId: string;
|
||||
useRelativeTimestamp: boolean;
|
||||
isThreadView: boolean;
|
||||
}
|
||||
@@ -401,7 +400,6 @@ class ThreadViewerVirtualized extends PureComponent<Props, State> {
|
||||
listId={itemId}
|
||||
onCardClick={this.props.onCardClick}
|
||||
previousPostId={getPreviousPostId(data, index)}
|
||||
teamId={this.props.teamId}
|
||||
timestampProps={this.props.useRelativeTimestamp ? THREADING_TIME : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@ import {DispatchFunc} from 'mattermost-redux/types/actions';
|
||||
import {getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getCloudSubscription, getSubscriptionProduct} from 'mattermost-redux/selectors/entities/cloud';
|
||||
import {deprecateCloudFree} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
@@ -32,6 +33,7 @@ const MenuCloudTrial = ({id}: Props): JSX.Element | null => {
|
||||
const subscription = useSelector(getCloudSubscription);
|
||||
const subscriptionProduct = useSelector(getSubscriptionProduct);
|
||||
const license = useSelector(getLicense);
|
||||
const cloudFreeDeprecated = useSelector(deprecateCloudFree);
|
||||
const dispatch = useDispatch<DispatchFunc>();
|
||||
|
||||
const isCloud = license?.Cloud === 'true';
|
||||
@@ -109,7 +111,7 @@ const MenuCloudTrial = ({id}: Props): JSX.Element | null => {
|
||||
);
|
||||
|
||||
// menu option displayed when the workspace is not running any trial
|
||||
const noFreeTrialContent = noPriorTrial ? (
|
||||
const noFreeTrialContent = (noPriorTrial && !cloudFreeDeprecated) ? (
|
||||
<FormattedMessage
|
||||
id='menu.cloudFree.priorTrial.tryEnterprise'
|
||||
defaultMessage='Interested in a limitless plan with high-security features? <openModalLink>Try Enterprise free for 30 days</openModalLink>'
|
||||
|
||||
@@ -117,7 +117,7 @@ const Preview = ({template, className, pluginsEnabled}: PreviewProps) => {
|
||||
if (c.playbook) {
|
||||
playbooks.push(c.playbook);
|
||||
}
|
||||
if (c.integration) {
|
||||
if (c.integration && c.integration.recommended) {
|
||||
availableIntegrations.push(c.integration);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -232,7 +232,12 @@ const WorkTemplateModal = () => {
|
||||
|
||||
const execute = async (template: WorkTemplate, name = '', visibility: Visibility) => {
|
||||
const pbTemplates = [];
|
||||
for (const item of template.content) {
|
||||
for (const ctt in template.content) {
|
||||
if (!Object.hasOwn(template.content, ctt)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const item = template.content[ctt];
|
||||
if (item.playbook) {
|
||||
const pbTemplate = playbookTemplates.find((pb) => pb.title === item.playbook.template);
|
||||
if (pbTemplate) {
|
||||
@@ -241,11 +246,20 @@ const WorkTemplateModal = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// remove non recommended integrations
|
||||
const filteredTemplate = {...template};
|
||||
filteredTemplate.content = template.content.filter((item) => {
|
||||
if (!item.integration) {
|
||||
return true;
|
||||
}
|
||||
return item.integration.recommended;
|
||||
});
|
||||
|
||||
const req: ExecuteWorkTemplateRequest = {
|
||||
team_id: teamId,
|
||||
name,
|
||||
visibility,
|
||||
work_template: template,
|
||||
work_template: filteredTemplate,
|
||||
playbook_templates: pbTemplates,
|
||||
};
|
||||
|
||||
|
||||
@@ -312,11 +312,11 @@
|
||||
"admin.billing.subscription.cloudTrial.daysLeftOnTrial": "There are {daysLeftOnTrial} days left on your free trial",
|
||||
"admin.billing.subscription.cloudTrial.lastDay": "This is the last day of your free trial. Your access will expire on {userEndTrialDate} at {userEndTrialHour}.",
|
||||
"admin.billing.subscription.cloudTrial.moreThan3Days": "Your trial has started! There are {daysLeftOnTrial} days left",
|
||||
"admin.billing.subscription.cloudTrial.purchaseButton": "Purchase Now",
|
||||
"admin.billing.subscription.cloudTrial.subscribeButton": "Upgrade Now",
|
||||
"admin.billing.subscription.cloudTrialBadge.daysLeftOnTrial": "{daysLeftOnTrial} trial days left",
|
||||
"admin.billing.subscription.cloudYearlyBadge": "Annual",
|
||||
"admin.billing.subscription.complianceScreenFailed.button": "Continue with Cloud Free",
|
||||
"admin.billing.subscription.complianceScreenFailed.subtitle": "We will check things on our side and get back to you within 3 days once your Cloud subscription upgrade is approved. In the meantime, please feel free to continue using the free version of our product.",
|
||||
"admin.billing.subscription.complianceScreenFailed.button": "OK",
|
||||
"admin.billing.subscription.complianceScreenFailed.title": "Your transaction is being reviewed",
|
||||
"admin.billing.subscription.complianceScreenShippingSameAsBilling": "My shipping address is the same as my billing address",
|
||||
"admin.billing.subscription.constCloudCard.contactSupport": "Contact support",
|
||||
@@ -327,7 +327,7 @@
|
||||
"admin.billing.subscription.deleteWorkspaceModal.deleteButton": "Delete Workspace",
|
||||
"admin.billing.subscription.deleteWorkspaceModal.downgradeButton": "Downgrade To Free",
|
||||
"admin.billing.subscription.deleteWorkspaceModal.title": "Are you sure you want to delete?",
|
||||
"admin.billing.subscription.deleteWorkspaceModal.usage": "As part of your paid subscription to Mattermost {sku} you have created ",
|
||||
"admin.billing.subscription.deleteWorkspaceModal.usage": "As part of your subscription to Mattermost {sku} you have created ",
|
||||
"admin.billing.subscription.deleteWorkspaceModal.usageDetails": "{messageCount} messages and {fileSize} of files",
|
||||
"admin.billing.subscription.deleteWorkspaceModal.warning": "Deleting your workspace is final. Upon deleting, you'll lose all of the above with no ability to recover. If you downgrade to Free, you will not lose this information.",
|
||||
"admin.billing.subscription.deleteWorkspaceSection.delete": "Delete Workspace",
|
||||
@@ -382,8 +382,8 @@
|
||||
"admin.billing.subscription.planDetails.productName.unknown": "Unknown product",
|
||||
"admin.billing.subscription.planDetails.subheader": "Plan details",
|
||||
"admin.billing.subscription.planDetails.userCount": "{userCount} users",
|
||||
"admin.billing.subscription.privateCloudCard.cloudEnterprise.description": "At Mattermost, we work with you and your team to meet your needs throughout the product. If you are looking for an annual discount, please reach out to our sales team.",
|
||||
"admin.billing.subscription.privateCloudCard.cloudEnterprise.title": "Looking for an annual discount? ",
|
||||
"admin.billing.subscription.privateCloudCard.cloudEnterprise.description": "At Mattermost, we work with you and your organization to meet your needs throughout the product. If you’re considering a wider rollout, talk to us.",
|
||||
"admin.billing.subscription.privateCloudCard.cloudEnterprise.title": "Looking to rollout Mattermost for your entire organization? ",
|
||||
"admin.billing.subscription.privateCloudCard.cloudFree.description": "Optimize your processes with Guest Accounts, Office365 suite integrations, GitLab SSO and advanced permissions.",
|
||||
"admin.billing.subscription.privateCloudCard.cloudFree.title": "Upgrade to Cloud Professional",
|
||||
"admin.billing.subscription.privateCloudCard.cloudProfessional.description": "Advanced security and compliance features with premium support. See {pricingLink} for more details.",
|
||||
@@ -3057,8 +3057,9 @@
|
||||
"cloud_billing.nudge_to_yearly.announcement_bar": "Monthly billing will be discontinued in {days} days . Switch to annual billing",
|
||||
"cloud_billing.nudge_to_yearly.contact_sales": "Contact sales",
|
||||
"cloud_billing.nudge_to_yearly.description": "Monthly billing will be discontinued on {date}. To keep your workspace, switch to annual billing.",
|
||||
"cloud_billing.nudge_to_yearly.learn_more": "Update billing",
|
||||
"cloud_billing.nudge_to_yearly.learn_more": "Learn more",
|
||||
"cloud_billing.nudge_to_yearly.title": "Action required: Switch to annual billing to keep your workspace.",
|
||||
"cloud_billing.nudge_to_yearly.update_billing": "Update billing",
|
||||
"cloud_delinquency.banner.buttonText": "Update billing now",
|
||||
"cloud_delinquency.banner.end_user_notify_admin_button": "Notify admin",
|
||||
"cloud_delinquency.banner.end_user_notify_admin_title": "Your workspace has been downgraded. Notify your admin to fix billing issues",
|
||||
@@ -4037,6 +4038,7 @@
|
||||
"learn_more_about_trial.modal.useSsoDescription": "Sign on quickly and easily with our SSO feature that works with OpenID, SAML, Google, and O365.",
|
||||
"learn_more_about_trial.modal.useSsoTitle": "Use SSO (with OpenID, SAML, Google, O365)",
|
||||
"learn_more_trial_modal_step.learnMoreAboutFeature": "Learn more about this feature.",
|
||||
"learn_more_trial_modal.contact_sales": "Contact Sales",
|
||||
"learn_more_trial_modal.pretitle": "With Enterprise, you can...",
|
||||
"leave_private_channel_modal.leave": "Yes, leave channel",
|
||||
"leave_private_channel_modal.message": "Are you sure you wish to leave the private channel {channel}? You must be re-invited in order to re-join this channel in the future.",
|
||||
@@ -4319,14 +4321,16 @@
|
||||
"notify_here.question": "By using **@here** you are about to send notifications to up to **{totalMembers} other people**. Are you sure you want to do this?",
|
||||
"notify_here.question_timezone": "By using **@here** you are about to send notifications to up to **{totalMembers} other people** in **{timezones, number} {timezones, plural, one {timezone} other {timezones}}**. Are you sure you want to do this?",
|
||||
"numMembers": "{num, number} {num, plural, one {member} other {members}}",
|
||||
"onboarding_wizard.cloud_plugins.description": "Mattermost is better when integrated with the tools your team uses for collaboration. Popular tools are below, select the ones your team uses and we'll add them to your workspace. Additional set up may be needed later.",
|
||||
"onboarding_wizard.cloud_plugins.subtitle": "(almost there!)",
|
||||
"onboarding_wizard.cloud_plugins.title": "Welcome to Mattermost!",
|
||||
"onboarding_wizard.invite_members_cloud.title": "Who works with you?",
|
||||
"onboarding_wizard.invite_members.copied_link": "Link Copied",
|
||||
"onboarding_wizard.invite_members.copy_link": "Copy Link",
|
||||
"onboarding_wizard.invite_members.copy_link_input": "team invite link",
|
||||
"onboarding_wizard.invite_members.description": "Collaboration is tough by yourself. Invite a few team members. Separate each email address with a space or comma.",
|
||||
"onboarding_wizard.invite_members.description_link": "Collaboration is tough by yourself. Invite a few team members using the invitation link below.",
|
||||
"onboarding_wizard.invite_members.next": "Send invites",
|
||||
"onboarding_wizard.invite_members.next_link": "Finish setup",
|
||||
"onboarding_wizard.invite_members.placeholder": "Enter email addresses",
|
||||
"onboarding_wizard.invite_members.skip": "Skip",
|
||||
"onboarding_wizard.invite_members.title": "Invite your team members",
|
||||
"onboarding_wizard.launching_workspace.description": "It’ll be ready in a moment",
|
||||
"onboarding_wizard.launching_workspace.title": "Launching your workspace now",
|
||||
@@ -4482,7 +4486,6 @@
|
||||
"post_info.comment_icon.tooltip.reply": "Reply",
|
||||
"post_info.copy": "Copy Text",
|
||||
"post_info.del": "Delete",
|
||||
"post_info.dot_menu.tooltip.actions": "Actions",
|
||||
"post_info.dot_menu.tooltip.more": "More",
|
||||
"post_info.edit": "Edit",
|
||||
"post_info.edit.aria_label": "Select to restore an old message.",
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
const values = {
|
||||
INVITE_USER: 'invite_user',
|
||||
ADD_USER_TO_TEAM: 'add_user_to_team',
|
||||
USE_SLASH_COMMANDS: 'use_slash_commands',
|
||||
MANAGE_SLASH_COMMANDS: 'manage_slash_commands',
|
||||
MANAGE_OTHERS_SLASH_COMMANDS: 'manage_others_slash_commands',
|
||||
CREATE_PUBLIC_CHANNEL: 'create_public_channel',
|
||||
|
||||
@@ -245,6 +245,10 @@ export function isCustomGroupsEnabled(state: GlobalState): boolean {
|
||||
return getConfig(state).EnableCustomGroups === 'true';
|
||||
}
|
||||
|
||||
export function getIsOnboardingFlowEnabled(state: GlobalState): boolean {
|
||||
return getConfig(state).EnableOnboardingFlow === 'true';
|
||||
}
|
||||
|
||||
export function insightsAreEnabled(state: GlobalState): boolean {
|
||||
const isConfiguredForFeature = getConfig(state).InsightsEnabled === 'true';
|
||||
const featureIsEnabled = getFeatureFlagValue(state, 'InsightsEnabled') === 'true';
|
||||
@@ -300,6 +304,10 @@ export function deprecateCloudFree(state: GlobalState): boolean {
|
||||
return getFeatureFlagValue(state, 'DeprecateCloudFree') === 'true';
|
||||
}
|
||||
|
||||
export function cloudReverseTrial(state: GlobalState): boolean {
|
||||
return getFeatureFlagValue(state, 'CloudReverseTrial') === 'true';
|
||||
}
|
||||
|
||||
export function appsSidebarCategoryEnabled(state: GlobalState): boolean {
|
||||
return getFeatureFlagValue(state, 'AppsSidebarCategory') === 'true';
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ body {
|
||||
|
||||
&.admin-onboarding {
|
||||
background-image: url('images/admin-onboarding-background.jpg');
|
||||
background-position: 50%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
@@ -17,3 +17,7 @@
|
||||
-ms-flex-positive: 1;
|
||||
-ms-flex-preferred-size: 0;
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@@ -1136,7 +1136,6 @@ export const PermissionsScope = {
|
||||
[Permissions.INVITE_USER]: 'team_scope',
|
||||
[Permissions.INVITE_GUEST]: 'team_scope',
|
||||
[Permissions.ADD_USER_TO_TEAM]: 'team_scope',
|
||||
[Permissions.USE_SLASH_COMMANDS]: 'channel_scope',
|
||||
[Permissions.MANAGE_SLASH_COMMANDS]: 'team_scope',
|
||||
[Permissions.MANAGE_OTHERS_SLASH_COMMANDS]: 'team_scope',
|
||||
[Permissions.CREATE_PUBLIC_CHANNEL]: 'team_scope',
|
||||
@@ -1250,7 +1249,6 @@ export const DefaultRolePermissions = {
|
||||
Permissions.UPLOAD_FILE,
|
||||
Permissions.GET_PUBLIC_LINK,
|
||||
Permissions.CREATE_POST,
|
||||
Permissions.USE_SLASH_COMMANDS,
|
||||
Permissions.MANAGE_PRIVATE_CHANNEL_MEMBERS,
|
||||
Permissions.DELETE_POST,
|
||||
Permissions.EDIT_POST,
|
||||
@@ -1315,7 +1313,6 @@ export const DefaultRolePermissions = {
|
||||
Permissions.ADD_REACTION,
|
||||
Permissions.REMOVE_REACTION,
|
||||
Permissions.USE_CHANNEL_MENTIONS,
|
||||
Permissions.USE_SLASH_COMMANDS,
|
||||
Permissions.READ_CHANNEL,
|
||||
Permissions.UPLOAD_FILE,
|
||||
Permissions.CREATE_POST,
|
||||
@@ -2021,6 +2018,7 @@ export const ConsolePages = {
|
||||
WEB_SERVER: '/admin_console/environment/web_server',
|
||||
PUSH_NOTIFICATION_CENTER: '/admin_console/environment/push_notification_server',
|
||||
SMTP: '/admin_console/environment/smtp',
|
||||
PAYMENT_INFO: '/admin_console/billing/payment_info',
|
||||
BILLING_HISTORY: '/admin_console/billing/billing_history',
|
||||
};
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user