Merge branch 'master' into MM-51858-fix-workspace-deletion-telemetry

Этот коммит содержится в:
Mattermost Build
2023-04-27 17:36:11 +03:00
коммит произвёл GitHub
родитель 405b2091d6 1cc288168e
Коммит a599a5feab
149 изменённых файлов: 5627 добавлений и 1204 удалений

Просмотреть файл

@@ -3,7 +3,7 @@
import {Block, createBlock} from './block'
type AttachmentBlockFields = {
attachmentId: string
fileId: string
}
type AttachmentBlock = Block & {
@@ -18,7 +18,7 @@ function createAttachmentBlock(block?: Block): AttachmentBlock {
...createBlock(block),
type: 'attachment',
fields: {
attachmentId: block?.fields.attachmentId || '',
fileId: block?.fields.attachmentId || block?.fields.fileId || '',
},
isUploading: false,
uploadingPercent: 0,

Просмотреть файл

@@ -151,7 +151,7 @@ const CardDialog = (props: Props): JSX.Element => {
Utils.selectLocalFile(async (attachment) => {
const uploadingBlock = createBlock()
uploadingBlock.title = attachment.name
uploadingBlock.fields.attachmentId = attachment.name
uploadingBlock.fields.fileId = attachment.name
uploadingBlock.boardId = boardId
if (card) {
uploadingBlock.parentId = card.id
@@ -177,11 +177,11 @@ const CardDialog = (props: Props): JSX.Element => {
xhr.onload = () => {
if (xhr.status === 200 && xhr.readyState === 4) {
const json = JSON.parse(xhr.response)
const attachmentId = json.fileId
if (attachmentId) {
const fileId = json.fileId
if (fileId) {
removeUploadingAttachment(uploadingBlock)
const block = createAttachmentBlock()
block.fields.attachmentId = attachmentId || ''
block.fields.fileId = fileId || ''
block.title = attachment.name
sendFlashMessage({content: intl.formatMessage({id: 'AttachmentBlock.uploadSuccess', defaultMessage: 'Attachment uploaded.'}), severity: 'normal'})
resolve(block)

Просмотреть файл

@@ -39,7 +39,7 @@ describe('component/content/FileBlock', () => {
type: 'attachment',
title: 'test-title',
fields: {
attachmentId: 'test.txt',
fileId: 'test.txt',
},
createdBy: 'test-user-id',
createAt: 0,

Просмотреть файл

@@ -50,7 +50,7 @@ const AttachmentElement = (props: Props): JSX.Element|null => {
})
return
}
const attachmentInfo = await octoClient.getFileInfo(block.boardId, block.fields.attachmentId)
const attachmentInfo = await octoClient.getFileInfo(block.boardId, block.fields.fileId)
setFileInfo(attachmentInfo)
}
loadFile()
@@ -113,7 +113,7 @@ const AttachmentElement = (props: Props): JSX.Element|null => {
}
const attachmentDownloadHandler = async () => {
const attachment = await octoClient.getFileAsDataUrl(block.boardId, block.fields.attachmentId)
const attachment = await octoClient.getFileAsDataUrl(block.boardId, block.fields.fileId)
const anchor = document.createElement('a')
anchor.href = attachment.url || ''
anchor.download = fileInfo.name || ''

Просмотреть файл

@@ -25,6 +25,7 @@ import CompassIcon from 'src/widgets/icons/compassIcon'
import OptionsIcon from 'src/widgets/icons/options'
import Menu from 'src/widgets/menu'
import MenuWrapper from 'src/widgets/menuWrapper'
import {UserSettings} from 'src/userSettings'
import './sidebarCategory.scss'
import {Category, CategoryBoardMetadata, CategoryBoards} from 'src/store/sidebar'
@@ -202,12 +203,24 @@ const SidebarCategory = (props: Props) => {
setTimeout(() => {
showBoard(props.boards[nextBoardId as number].id)
}, 120)
} else {
setTimeout(() => {
const newPath = generatePath('/team/:teamId', {teamId: teamID,})
history.push(newPath)
}, 120)
}
},
async () => {
showBoard(deleteBoard.id)
},
)
if (
UserSettings.lastBoardId &&
UserSettings.lastBoardId[deleteBoard.teamId] == deleteBoard.id
) {
UserSettings.setLastBoardID(deleteBoard.teamId, null)
UserSettings.setLastViewId(deleteBoard.id, null)
}
}, [showBoard, deleteBoard, props.boards])
const updateCategory = useCallback(async (value: boolean) => {

Просмотреть файл

@@ -186,7 +186,9 @@ const BoardPage = (props: Props): JSX.Element => {
const joinBoard = async (myUser: IUser, boardTeamId: string, boardId: string, allowAdmin: boolean) => {
const member = await octoClient.joinBoard(boardId, allowAdmin)
if (!member) {
if (myUser.permissions?.find((s) => s === 'manage_system' || s === 'manage_team')) {
// if allowAdmin is true, then we failed to join the board
// as an admin, normally, this is deleted/missing board
if (!allowAdmin && myUser.permissions?.find((s) => s === 'manage_system' || s === 'manage_team')) {
setShowJoinBoardDialog(true)
return
}

Просмотреть файл

@@ -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 youre 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 youre 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 youre 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": "Itll 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',
};

Просмотреть файл

@@ -68,6 +68,24 @@ describe('ClientError', () => {
expect(copy.status_code).toEqual(error.status_code);
expect(copy.url).toEqual(error.url);
});
test('cause should be preserved when provided', () => {
const cause = new Error('the original error');
const error = new ClientError('https://example.com', {
message: 'This is a message',
server_error_id: 'test.app_error',
status_code: 418,
url: 'https://example.com/api/v4/error',
}, cause);
const copy = {...error};
expect(copy.message).toEqual(error.message);
expect(copy.server_error_id).toEqual(error.server_error_id);
expect(copy.status_code).toEqual(error.status_code);
expect(copy.url).toEqual(error.url);
expect(error.cause).toEqual(cause);
});
});
describe('trackEvent', () => {

Просмотреть файл

@@ -4168,7 +4168,7 @@ export default class Client4 {
throw new ClientError(this.getUrl(), {
message: 'Received invalid response from the server.',
url,
});
}, err);
}
if (headers.has(HEADER_X_VERSION_ID) && !headers.get('Cache-Control')) {
@@ -4311,8 +4311,8 @@ export class ClientError extends Error implements ServerError {
server_error_id?: string;
status_code?: number;
constructor(baseUrl: string, data: ServerError) {
super(data.message + ': ' + cleanUrlForLogging(baseUrl, data.url || ''));
constructor(baseUrl: string, data: ServerError, cause?: any) {
super(data.message + ': ' + cleanUrlForLogging(baseUrl, data.url || ''), {cause});
this.message = data.message;
this.url = data.url;

Просмотреть файл

@@ -2,7 +2,7 @@
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"target": "es6",
"target": "es2022",
"declaration": true,
"strict": true,
"resolveJsonModule": true,

Просмотреть файл

@@ -64,6 +64,7 @@ export interface Playbook {
}
export interface Integration {
id: string;
recommended: boolean;
name?: string;
icon?: string;
installed?: boolean;