Merge branch 'master' into MM-50966-in-product-expansion
Этот коммит содержится в:
@@ -4,6 +4,7 @@ exports[`components/FormattedMarkdownMessage should allow to disable links 1`] =
|
||||
<IntlProvider
|
||||
defaultFormats={Object {}}
|
||||
defaultLocale="en"
|
||||
fallbackOnEmptyString={true}
|
||||
formats={Object {}}
|
||||
locale="en"
|
||||
messages={
|
||||
@@ -14,6 +15,7 @@ exports[`components/FormattedMarkdownMessage should allow to disable links 1`] =
|
||||
}
|
||||
}
|
||||
onError={[Function]}
|
||||
onWarn={[Function]}
|
||||
textComponent={Symbol(react.fragment)}
|
||||
>
|
||||
<FormattedMarkdownMessage
|
||||
@@ -41,6 +43,7 @@ exports[`components/FormattedMarkdownMessage should backup to default 1`] = `
|
||||
<IntlProvider
|
||||
defaultFormats={Object {}}
|
||||
defaultLocale="en"
|
||||
fallbackOnEmptyString={true}
|
||||
formats={Object {}}
|
||||
locale="en"
|
||||
messages={
|
||||
@@ -51,6 +54,7 @@ exports[`components/FormattedMarkdownMessage should backup to default 1`] = `
|
||||
}
|
||||
}
|
||||
onError={[Function]}
|
||||
onWarn={[Function]}
|
||||
textComponent={Symbol(react.fragment)}
|
||||
>
|
||||
<FormattedMarkdownMessage
|
||||
@@ -72,6 +76,7 @@ exports[`components/FormattedMarkdownMessage should escape non-BR 1`] = `
|
||||
<IntlProvider
|
||||
defaultFormats={Object {}}
|
||||
defaultLocale="en"
|
||||
fallbackOnEmptyString={true}
|
||||
formats={Object {}}
|
||||
locale="en"
|
||||
messages={
|
||||
@@ -82,6 +87,7 @@ exports[`components/FormattedMarkdownMessage should escape non-BR 1`] = `
|
||||
}
|
||||
}
|
||||
onError={[Function]}
|
||||
onWarn={[Function]}
|
||||
textComponent={Symbol(react.fragment)}
|
||||
>
|
||||
<FormattedMarkdownMessage
|
||||
@@ -109,6 +115,7 @@ exports[`components/FormattedMarkdownMessage should render message 1`] = `
|
||||
<IntlProvider
|
||||
defaultFormats={Object {}}
|
||||
defaultLocale="en"
|
||||
fallbackOnEmptyString={true}
|
||||
formats={Object {}}
|
||||
locale="en"
|
||||
messages={
|
||||
@@ -119,6 +126,7 @@ exports[`components/FormattedMarkdownMessage should render message 1`] = `
|
||||
}
|
||||
}
|
||||
onError={[Function]}
|
||||
onWarn={[Function]}
|
||||
textComponent={Symbol(react.fragment)}
|
||||
>
|
||||
<FormattedMarkdownMessage
|
||||
@@ -140,6 +148,7 @@ exports[`components/FormattedMarkdownMessage values should work 1`] = `
|
||||
<IntlProvider
|
||||
defaultFormats={Object {}}
|
||||
defaultLocale="en"
|
||||
fallbackOnEmptyString={true}
|
||||
formats={Object {}}
|
||||
locale="en"
|
||||
messages={
|
||||
@@ -150,6 +159,7 @@ exports[`components/FormattedMarkdownMessage values should work 1`] = `
|
||||
}
|
||||
}
|
||||
onError={[Function]}
|
||||
onWarn={[Function]}
|
||||
textComponent={Symbol(react.fragment)}
|
||||
>
|
||||
<FormattedMarkdownMessage
|
||||
|
||||
@@ -1,980 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/AboutBuildModal should match snapshot for cloud edition 1`] = `
|
||||
<ContextProvider
|
||||
value={
|
||||
Object {
|
||||
"store": Object {
|
||||
"clearActions": [Function],
|
||||
"dispatch": [Function],
|
||||
"getActions": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
},
|
||||
"subscription": Subscription {
|
||||
"handleChangeWrapper": [Function],
|
||||
"listeners": Object {
|
||||
"notify": [Function],
|
||||
},
|
||||
"onStateChange": [Function],
|
||||
"parentSub": undefined,
|
||||
"store": Object {
|
||||
"clearActions": [Function],
|
||||
"dispatch": [Function],
|
||||
"getActions": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
},
|
||||
"unsubscribe": null,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<AboutBuildModalCloud
|
||||
config={
|
||||
Object {
|
||||
"BuildDate": "21 January 2017",
|
||||
"BuildEnterpriseReady": "true",
|
||||
"BuildHash": "abcdef1234567890",
|
||||
"BuildHashEnterprise": "0123456789abcdef",
|
||||
"BuildNumber": "3.6.2",
|
||||
"PrivacyPolicyLink": "https://about.custom.com/privacy-policy/",
|
||||
"SQLDriverName": "Postgres",
|
||||
"SchemaVersion": "77",
|
||||
"TermsOfServiceLink": "https://about.custom.com/default-terms/",
|
||||
"Version": "3.6.0",
|
||||
}
|
||||
}
|
||||
doHide={[MockFunction]}
|
||||
license={
|
||||
Object {
|
||||
"Cloud": "true",
|
||||
"Company": "Mattermost Inc",
|
||||
"IsLicensed": "true",
|
||||
}
|
||||
}
|
||||
onExited={[MockFunction]}
|
||||
show={true}
|
||||
/>
|
||||
</ContextProvider>
|
||||
`;
|
||||
|
||||
exports[`components/AboutBuildModal should match snapshot for enterprise edition 1`] = `
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-labelledby="aboutModalLabel"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
dialogClassName="a11y__modal about-modal"
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onExited={[MockFunction]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="dialog"
|
||||
show={true}
|
||||
>
|
||||
<ModalHeader
|
||||
bsClass="modal-header"
|
||||
closeButton={true}
|
||||
closeLabel="Close"
|
||||
>
|
||||
<ModalTitle
|
||||
bsClass="modal-title"
|
||||
componentClass="h1"
|
||||
id="aboutModalLabel"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="About {appTitle}"
|
||||
id="about.title"
|
||||
values={
|
||||
Object {
|
||||
"appTitle": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</ModalTitle>
|
||||
</ModalHeader>
|
||||
<ModalBody
|
||||
bsClass="modal-body"
|
||||
componentClass="div"
|
||||
>
|
||||
<div
|
||||
className="about-modal__content"
|
||||
>
|
||||
<div
|
||||
className="about-modal__logo"
|
||||
>
|
||||
<MattermostLogo />
|
||||
</div>
|
||||
<div>
|
||||
<h3
|
||||
className="about-modal__title"
|
||||
>
|
||||
<strong>
|
||||
Mattermost
|
||||
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Enterprise Edition"
|
||||
id="about.enterpriseEditione1"
|
||||
/>
|
||||
</strong>
|
||||
</h3>
|
||||
<p
|
||||
className="about-modal__subtitle pb-2"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Modern communication from behind your firewall."
|
||||
id="about.enterpriseEditionSt"
|
||||
/>
|
||||
</p>
|
||||
<div
|
||||
className="form-group less"
|
||||
>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Mattermost Version:"
|
||||
id="about.version"
|
||||
/>
|
||||
<span
|
||||
id="versionString"
|
||||
>
|
||||
3.6.2
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Database Schema Version:"
|
||||
id="about.dbversion"
|
||||
/>
|
||||
<span
|
||||
id="dbversionString"
|
||||
>
|
||||
77
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Database:"
|
||||
id="about.database"
|
||||
/>
|
||||
Postgres
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Licensed to:"
|
||||
id="about.licensed"
|
||||
/>
|
||||
<Nbsp />
|
||||
Mattermost Inc
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__footer"
|
||||
>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Learn more about Enterprise Edition at "
|
||||
id="about.enterpriseEditionLearn"
|
||||
/>
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/"
|
||||
location="about_build_modal"
|
||||
>
|
||||
mattermost.com
|
||||
</ExternalLink>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="about-modal__copyright"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Copyright 2015 - {currentYear} Mattermost, Inc. All rights reserved"
|
||||
id="about.copyright"
|
||||
values={
|
||||
Object {
|
||||
"currentYear": 2017,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__links"
|
||||
>
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/terms-of-use/"
|
||||
id="tosLink"
|
||||
location="about_build_modal"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Terms of Use"
|
||||
id="about.tos"
|
||||
/>
|
||||
</ExternalLink>
|
||||
-
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/privacy-policy/"
|
||||
id="privacyLink"
|
||||
location="about_build_modal"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Privacy Policy"
|
||||
id="about.privacy"
|
||||
/>
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__notice form-group pt-3"
|
||||
>
|
||||
<p>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Mattermost is made possible by the open source software used in our <linkServer>server</linkServer>, <linkDesktop>desktop</linkDesktop> and <linkMobile>mobile</linkMobile> apps."
|
||||
id="about.notice"
|
||||
values={
|
||||
Object {
|
||||
"linkDesktop": [Function],
|
||||
"linkMobile": [Function],
|
||||
"linkServer": [Function],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__hash"
|
||||
>
|
||||
<p>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Build Hash:"
|
||||
id="about.hash"
|
||||
/>
|
||||
<Nbsp />
|
||||
abcdef1234567890
|
||||
<br />
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="EE Build Hash:"
|
||||
id="about.hashee"
|
||||
/>
|
||||
<Nbsp />
|
||||
0123456789abcdef
|
||||
</p>
|
||||
<p>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Build Date:"
|
||||
id="about.date"
|
||||
/>
|
||||
<Nbsp />
|
||||
21 January 2017
|
||||
</p>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
`;
|
||||
|
||||
exports[`components/AboutBuildModal should match snapshot for team edition 1`] = `
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-labelledby="aboutModalLabel"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
dialogClassName="a11y__modal about-modal"
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onExited={[MockFunction]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="dialog"
|
||||
show={true}
|
||||
>
|
||||
<ModalHeader
|
||||
bsClass="modal-header"
|
||||
closeButton={true}
|
||||
closeLabel="Close"
|
||||
>
|
||||
<ModalTitle
|
||||
bsClass="modal-title"
|
||||
componentClass="h1"
|
||||
id="aboutModalLabel"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="About {appTitle}"
|
||||
id="about.title"
|
||||
values={
|
||||
Object {
|
||||
"appTitle": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</ModalTitle>
|
||||
</ModalHeader>
|
||||
<ModalBody
|
||||
bsClass="modal-body"
|
||||
componentClass="div"
|
||||
>
|
||||
<div
|
||||
className="about-modal__content"
|
||||
>
|
||||
<div
|
||||
className="about-modal__logo"
|
||||
>
|
||||
<MattermostLogo />
|
||||
</div>
|
||||
<div>
|
||||
<h3
|
||||
className="about-modal__title"
|
||||
>
|
||||
<strong>
|
||||
Mattermost
|
||||
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Team Edition"
|
||||
id="about.teamEditiont0"
|
||||
/>
|
||||
</strong>
|
||||
</h3>
|
||||
<p
|
||||
className="about-modal__subtitle pb-2"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="All your team communication in one place, instantly searchable and accessible anywhere."
|
||||
id="about.teamEditionSt"
|
||||
/>
|
||||
</p>
|
||||
<div
|
||||
className="form-group less"
|
||||
>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Mattermost Version:"
|
||||
id="about.version"
|
||||
/>
|
||||
<span
|
||||
id="versionString"
|
||||
>
|
||||
3.6.2
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Database Schema Version:"
|
||||
id="about.dbversion"
|
||||
/>
|
||||
<span
|
||||
id="dbversionString"
|
||||
>
|
||||
77
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Database:"
|
||||
id="about.database"
|
||||
/>
|
||||
Postgres
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__footer"
|
||||
>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Join the Mattermost community at "
|
||||
id="about.teamEditionLearn"
|
||||
/>
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/community/"
|
||||
location="about_build_modal"
|
||||
>
|
||||
mattermost.com/community/
|
||||
</ExternalLink>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="about-modal__copyright"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Copyright 2015 - {currentYear} Mattermost, Inc. All rights reserved"
|
||||
id="about.copyright"
|
||||
values={
|
||||
Object {
|
||||
"currentYear": 2017,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__links"
|
||||
>
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/terms-of-use/"
|
||||
id="tosLink"
|
||||
location="about_build_modal"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Terms of Use"
|
||||
id="about.tos"
|
||||
/>
|
||||
</ExternalLink>
|
||||
-
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/privacy-policy/"
|
||||
id="privacyLink"
|
||||
location="about_build_modal"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Privacy Policy"
|
||||
id="about.privacy"
|
||||
/>
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__notice form-group pt-3"
|
||||
>
|
||||
<p>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Mattermost is made possible by the open source software used in our <linkServer>server</linkServer>, <linkDesktop>desktop</linkDesktop> and <linkMobile>mobile</linkMobile> apps."
|
||||
id="about.notice"
|
||||
values={
|
||||
Object {
|
||||
"linkDesktop": [Function],
|
||||
"linkMobile": [Function],
|
||||
"linkServer": [Function],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__hash"
|
||||
>
|
||||
<p>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Build Hash:"
|
||||
id="about.hash"
|
||||
/>
|
||||
<Nbsp />
|
||||
abcdef1234567890
|
||||
<br />
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="EE Build Hash:"
|
||||
id="about.hashee"
|
||||
/>
|
||||
<Nbsp />
|
||||
</p>
|
||||
<p>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Build Date:"
|
||||
id="about.date"
|
||||
/>
|
||||
<Nbsp />
|
||||
21 January 2017
|
||||
</p>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
`;
|
||||
|
||||
exports[`components/AboutBuildModal should show ci if a ci build 1`] = `
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-labelledby="aboutModalLabel"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
dialogClassName="a11y__modal about-modal"
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onExited={[MockFunction]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="dialog"
|
||||
show={true}
|
||||
>
|
||||
<ModalHeader
|
||||
bsClass="modal-header"
|
||||
closeButton={true}
|
||||
closeLabel="Close"
|
||||
>
|
||||
<ModalTitle
|
||||
bsClass="modal-title"
|
||||
componentClass="h1"
|
||||
id="aboutModalLabel"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="About {appTitle}"
|
||||
id="about.title"
|
||||
values={
|
||||
Object {
|
||||
"appTitle": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</ModalTitle>
|
||||
</ModalHeader>
|
||||
<ModalBody
|
||||
bsClass="modal-body"
|
||||
componentClass="div"
|
||||
>
|
||||
<div
|
||||
className="about-modal__content"
|
||||
>
|
||||
<div
|
||||
className="about-modal__logo"
|
||||
>
|
||||
<MattermostLogo />
|
||||
</div>
|
||||
<div>
|
||||
<h3
|
||||
className="about-modal__title"
|
||||
>
|
||||
<strong>
|
||||
Mattermost
|
||||
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Team Edition"
|
||||
id="about.teamEditiont0"
|
||||
/>
|
||||
</strong>
|
||||
</h3>
|
||||
<p
|
||||
className="about-modal__subtitle pb-2"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="All your team communication in one place, instantly searchable and accessible anywhere."
|
||||
id="about.teamEditionSt"
|
||||
/>
|
||||
</p>
|
||||
<div
|
||||
className="form-group less"
|
||||
>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Mattermost Version:"
|
||||
id="about.version"
|
||||
/>
|
||||
<span
|
||||
id="versionString"
|
||||
>
|
||||
ci
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Database Schema Version:"
|
||||
id="about.dbversion"
|
||||
/>
|
||||
<span
|
||||
id="dbversionString"
|
||||
>
|
||||
77
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Build Number:"
|
||||
id="about.buildnumber"
|
||||
/>
|
||||
<span
|
||||
id="buildnumberString"
|
||||
>
|
||||
123
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Database:"
|
||||
id="about.database"
|
||||
/>
|
||||
Postgres
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__footer"
|
||||
>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Join the Mattermost community at "
|
||||
id="about.teamEditionLearn"
|
||||
/>
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/community/"
|
||||
location="about_build_modal"
|
||||
>
|
||||
mattermost.com/community/
|
||||
</ExternalLink>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="about-modal__copyright"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Copyright 2015 - {currentYear} Mattermost, Inc. All rights reserved"
|
||||
id="about.copyright"
|
||||
values={
|
||||
Object {
|
||||
"currentYear": 2017,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__links"
|
||||
>
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/terms-of-use/"
|
||||
id="tosLink"
|
||||
location="about_build_modal"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Terms of Use"
|
||||
id="about.tos"
|
||||
/>
|
||||
</ExternalLink>
|
||||
-
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/privacy-policy/"
|
||||
id="privacyLink"
|
||||
location="about_build_modal"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Privacy Policy"
|
||||
id="about.privacy"
|
||||
/>
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__notice form-group pt-3"
|
||||
>
|
||||
<p>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Mattermost is made possible by the open source software used in our <linkServer>server</linkServer>, <linkDesktop>desktop</linkDesktop> and <linkMobile>mobile</linkMobile> apps."
|
||||
id="about.notice"
|
||||
values={
|
||||
Object {
|
||||
"linkDesktop": [Function],
|
||||
"linkMobile": [Function],
|
||||
"linkServer": [Function],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__hash"
|
||||
>
|
||||
<p>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Build Hash:"
|
||||
id="about.hash"
|
||||
/>
|
||||
<Nbsp />
|
||||
abcdef1234567890
|
||||
<br />
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="EE Build Hash:"
|
||||
id="about.hashee"
|
||||
/>
|
||||
<Nbsp />
|
||||
</p>
|
||||
<p>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Build Date:"
|
||||
id="about.date"
|
||||
/>
|
||||
<Nbsp />
|
||||
21 January 2017
|
||||
</p>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
`;
|
||||
|
||||
exports[`components/AboutBuildModal should show dev if this is a dev build 1`] = `
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-labelledby="aboutModalLabel"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
dialogClassName="a11y__modal about-modal"
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onExited={[MockFunction]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="dialog"
|
||||
show={true}
|
||||
>
|
||||
<ModalHeader
|
||||
bsClass="modal-header"
|
||||
closeButton={true}
|
||||
closeLabel="Close"
|
||||
>
|
||||
<ModalTitle
|
||||
bsClass="modal-title"
|
||||
componentClass="h1"
|
||||
id="aboutModalLabel"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="About {appTitle}"
|
||||
id="about.title"
|
||||
values={
|
||||
Object {
|
||||
"appTitle": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</ModalTitle>
|
||||
</ModalHeader>
|
||||
<ModalBody
|
||||
bsClass="modal-body"
|
||||
componentClass="div"
|
||||
>
|
||||
<div
|
||||
className="about-modal__content"
|
||||
>
|
||||
<div
|
||||
className="about-modal__logo"
|
||||
>
|
||||
<MattermostLogo />
|
||||
</div>
|
||||
<div>
|
||||
<h3
|
||||
className="about-modal__title"
|
||||
>
|
||||
<strong>
|
||||
Mattermost
|
||||
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Team Edition"
|
||||
id="about.teamEditiont0"
|
||||
/>
|
||||
</strong>
|
||||
</h3>
|
||||
<p
|
||||
className="about-modal__subtitle pb-2"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="All your team communication in one place, instantly searchable and accessible anywhere."
|
||||
id="about.teamEditionSt"
|
||||
/>
|
||||
</p>
|
||||
<div
|
||||
className="form-group less"
|
||||
>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Mattermost Version:"
|
||||
id="about.version"
|
||||
/>
|
||||
<span
|
||||
id="versionString"
|
||||
>
|
||||
dev
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Database Schema Version:"
|
||||
id="about.dbversion"
|
||||
/>
|
||||
<span
|
||||
id="dbversionString"
|
||||
>
|
||||
77
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Database:"
|
||||
id="about.database"
|
||||
/>
|
||||
Postgres
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__footer"
|
||||
>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Join the Mattermost community at "
|
||||
id="about.teamEditionLearn"
|
||||
/>
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/community/"
|
||||
location="about_build_modal"
|
||||
>
|
||||
mattermost.com/community/
|
||||
</ExternalLink>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="about-modal__copyright"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Copyright 2015 - {currentYear} Mattermost, Inc. All rights reserved"
|
||||
id="about.copyright"
|
||||
values={
|
||||
Object {
|
||||
"currentYear": 2017,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__links"
|
||||
>
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/terms-of-use/"
|
||||
id="tosLink"
|
||||
location="about_build_modal"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Terms of Use"
|
||||
id="about.tos"
|
||||
/>
|
||||
</ExternalLink>
|
||||
-
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/privacy-policy/"
|
||||
id="privacyLink"
|
||||
location="about_build_modal"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Privacy Policy"
|
||||
id="about.privacy"
|
||||
/>
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__notice form-group pt-3"
|
||||
>
|
||||
<p>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Mattermost is made possible by the open source software used in our <linkServer>server</linkServer>, <linkDesktop>desktop</linkDesktop> and <linkMobile>mobile</linkMobile> apps."
|
||||
id="about.notice"
|
||||
values={
|
||||
Object {
|
||||
"linkDesktop": [Function],
|
||||
"linkMobile": [Function],
|
||||
"linkServer": [Function],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__hash"
|
||||
>
|
||||
<p>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Build Hash:"
|
||||
id="about.hash"
|
||||
/>
|
||||
<Nbsp />
|
||||
abcdef1234567890
|
||||
<br />
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="EE Build Hash:"
|
||||
id="about.hashee"
|
||||
/>
|
||||
<Nbsp />
|
||||
</p>
|
||||
<p>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Build Date:"
|
||||
id="about.date"
|
||||
/>
|
||||
<Nbsp />
|
||||
21 January 2017
|
||||
</p>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
`;
|
||||
@@ -2,8 +2,6 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import {shallow} from 'enzyme';
|
||||
import {Provider} from 'react-redux';
|
||||
|
||||
import mockStore from 'tests/test_store';
|
||||
@@ -12,11 +10,13 @@ import {ClientConfig, ClientLicense} from '@mattermost/types/config';
|
||||
|
||||
import AboutBuildModal from 'components/about_build_modal/about_build_modal';
|
||||
|
||||
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
|
||||
import {AboutLinks} from 'utils/constants';
|
||||
|
||||
import AboutBuildModalCloud from './about_build_modal_cloud/about_build_modal_cloud';
|
||||
import {screen} from '@testing-library/react';
|
||||
import {renderWithIntl} from 'tests/react_testing_utils';
|
||||
import store from 'stores/redux_store';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
describe('components/AboutBuildModal', () => {
|
||||
const RealDate: DateConstructor = Date;
|
||||
@@ -60,10 +60,17 @@ describe('components/AboutBuildModal', () => {
|
||||
});
|
||||
|
||||
test('should match snapshot for enterprise edition', () => {
|
||||
const wrapper = shallowAboutBuildModal({config, license});
|
||||
expect(wrapper.find('#versionString').text()).toBe('\u00a03.6.2');
|
||||
expect(wrapper.find('#dbversionString').text()).toBe('\u00a077');
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
renderAboutBuildModal({config, license});
|
||||
expect(screen.getByTestId('aboutModalVersion')).toHaveTextContent('Mattermost Version: 3.6.2');
|
||||
expect(screen.getByTestId('aboutModalDBVersionString')).toHaveTextContent('Database Schema Version: 77');
|
||||
expect(screen.getByText('Mattermost Enterprise Edition')).toBeInTheDocument();
|
||||
expect(screen.getByText('Modern communication from behind your firewall.')).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', {name: 'mattermost.com'})).toHaveAttribute('href', 'https://mattermost.com/?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=&sid=');
|
||||
expect(screen.getByText('EE Build Hash: 0123456789abcdef', {exact: false})).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByRole('link', {name: 'server'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-server/blob/master/NOTICE.txt');
|
||||
expect(screen.getByRole('link', {name: 'desktop'})).toHaveAttribute('href', 'https://github.com/mattermost/desktop/blob/master/NOTICE.txt');
|
||||
expect(screen.getByRole('link', {name: 'mobile'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-mobile/blob/master/NOTICE.txt');
|
||||
});
|
||||
|
||||
test('should match snapshot for team edition', () => {
|
||||
@@ -73,19 +80,25 @@ describe('components/AboutBuildModal', () => {
|
||||
BuildHashEnterprise: '',
|
||||
};
|
||||
|
||||
const wrapper = shallowAboutBuildModal({config: teamConfig, license: {}});
|
||||
expect(wrapper.find('#versionString').text()).toBe('\u00a03.6.2');
|
||||
expect(wrapper.find('#dbversionString').text()).toBe('\u00a077');
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
renderAboutBuildModal({config: teamConfig, license: {}});
|
||||
expect(screen.getByTestId('aboutModalVersion')).toHaveTextContent('Mattermost Version: 3.6.2');
|
||||
expect(screen.getByTestId('aboutModalDBVersionString')).toHaveTextContent('Database Schema Version: 77');
|
||||
expect(screen.getByText('Mattermost Team Edition')).toBeInTheDocument();
|
||||
expect(screen.getByText('All your team communication in one place, instantly searchable and accessible anywhere.')).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', {name: 'mattermost.com/community/'})).toHaveAttribute('href', 'https://mattermost.com/community/?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=&sid=');
|
||||
expect(screen.queryByText('EE Build Hash: 0123456789abcdef')).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByRole('link', {name: 'server'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-server/blob/master/NOTICE.txt');
|
||||
expect(screen.getByRole('link', {name: 'desktop'})).toHaveAttribute('href', 'https://github.com/mattermost/desktop/blob/master/NOTICE.txt');
|
||||
expect(screen.getByRole('link', {name: 'mobile'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-mobile/blob/master/NOTICE.txt');
|
||||
});
|
||||
|
||||
test('should match snapshot for cloud edition', () => {
|
||||
if (license !== null) {
|
||||
license.Cloud = 'true';
|
||||
}
|
||||
const store = mockStore();
|
||||
|
||||
const wrapper = shallow(
|
||||
renderWithIntl(
|
||||
<Provider store={store}>
|
||||
<AboutBuildModalCloud
|
||||
config={config}
|
||||
@@ -96,7 +109,14 @@ describe('components/AboutBuildModal', () => {
|
||||
/>
|
||||
</Provider>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
expect(screen.getByText('Mattermost Cloud')).toBeInTheDocument();
|
||||
expect(screen.getByText('High trust messaging for the enterprise')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('0123456789abcdef', {exact: false})).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', {name: 'server'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-server/blob/master/NOTICE.txt');
|
||||
expect(screen.getByRole('link', {name: 'desktop'})).toHaveAttribute('href', 'https://github.com/mattermost/desktop/blob/master/NOTICE.txt');
|
||||
expect(screen.getByRole('link', {name: 'mobile'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-mobile/blob/master/NOTICE.txt');
|
||||
});
|
||||
|
||||
test('should show dev if this is a dev build', () => {
|
||||
@@ -109,10 +129,18 @@ describe('components/AboutBuildModal', () => {
|
||||
BuildNumber: 'dev',
|
||||
};
|
||||
|
||||
const wrapper = shallowAboutBuildModal({config: sameBuildConfig, license: {}});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find('#versionString').text()).toBe('\u00a0dev');
|
||||
expect(wrapper.find('#dbversionString').text()).toBe('\u00a077');
|
||||
renderAboutBuildModal({config: sameBuildConfig, license: {}});
|
||||
|
||||
expect(screen.getByTestId('aboutModalVersion')).toHaveTextContent('Mattermost Version: dev');
|
||||
expect(screen.getByTestId('aboutModalDBVersionString')).toHaveTextContent('Database Schema Version: 77');
|
||||
expect(screen.getByText('Mattermost Team Edition')).toBeInTheDocument();
|
||||
expect(screen.getByText('All your team communication in one place, instantly searchable and accessible anywhere.')).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', {name: 'mattermost.com/community/'})).toHaveAttribute('href', 'https://mattermost.com/community/?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=&sid=');
|
||||
expect(screen.queryByText('EE Build Hash: 0123456789abcdef')).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByRole('link', {name: 'server'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-server/blob/master/NOTICE.txt');
|
||||
expect(screen.getByRole('link', {name: 'desktop'})).toHaveAttribute('href', 'https://github.com/mattermost/desktop/blob/master/NOTICE.txt');
|
||||
expect(screen.getByRole('link', {name: 'mobile'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-mobile/blob/master/NOTICE.txt');
|
||||
});
|
||||
|
||||
test('should show ci if a ci build', () => {
|
||||
@@ -125,11 +153,19 @@ describe('components/AboutBuildModal', () => {
|
||||
BuildNumber: '123',
|
||||
};
|
||||
|
||||
const wrapper = shallowAboutBuildModal({config: differentBuildConfig, license: {}});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find('#versionString').text()).toBe('\u00a0ci');
|
||||
expect(wrapper.find('#dbversionString').text()).toBe('\u00a077');
|
||||
expect(wrapper.find('#buildnumberString').text()).toBe('\u00a0123');
|
||||
renderAboutBuildModal({config: differentBuildConfig, license: {}});
|
||||
|
||||
expect(screen.getByTestId('aboutModalVersion')).toHaveTextContent('Mattermost Version: ci');
|
||||
expect(screen.getByTestId('aboutModalDBVersionString')).toHaveTextContent('Database Schema Version: 77');
|
||||
expect(screen.getByTestId('aboutModalBuildNumber')).toHaveTextContent('Build Number: 123');
|
||||
expect(screen.getByText('Mattermost Team Edition')).toBeInTheDocument();
|
||||
expect(screen.getByText('All your team communication in one place, instantly searchable and accessible anywhere.')).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', {name: 'mattermost.com/community/'})).toHaveAttribute('href', 'https://mattermost.com/community/?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=&sid=');
|
||||
expect(screen.queryByText('EE Build Hash: 0123456789abcdef')).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByRole('link', {name: 'server'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-server/blob/master/NOTICE.txt');
|
||||
expect(screen.getByRole('link', {name: 'desktop'})).toHaveAttribute('href', 'https://github.com/mattermost/desktop/blob/master/NOTICE.txt');
|
||||
expect(screen.getByRole('link', {name: 'mobile'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-mobile/blob/master/NOTICE.txt');
|
||||
});
|
||||
|
||||
test('should call onExited callback when the modal is hidden', () => {
|
||||
@@ -148,7 +184,7 @@ describe('components/AboutBuildModal', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
renderWithIntl(
|
||||
<Provider store={store}>
|
||||
<AboutBuildModal
|
||||
config={config}
|
||||
@@ -158,7 +194,7 @@ describe('components/AboutBuildModal', () => {
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
wrapper.find(Modal).first().props().onExited?.(document.createElement('div'));
|
||||
userEvent.click(screen.getByText('Close'));
|
||||
expect(onExited).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -176,7 +212,7 @@ describe('components/AboutBuildModal', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const wrapper = mountWithIntl(
|
||||
renderWithIntl(
|
||||
<Provider store={store}>
|
||||
<AboutBuildModal
|
||||
config={config}
|
||||
@@ -186,24 +222,15 @@ describe('components/AboutBuildModal', () => {
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find(AboutBuildModal).find('a#tosLink').props().href,
|
||||
).toBe(
|
||||
AboutLinks.TERMS_OF_SERVICE +
|
||||
'?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=currentUserId&sid=',
|
||||
);
|
||||
expect(
|
||||
wrapper.find(AboutBuildModal).find('a#privacyLink').props().href,
|
||||
).toBe(
|
||||
AboutLinks.PRIVACY_POLICY +
|
||||
'?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=currentUserId&sid=',
|
||||
);
|
||||
expect(screen.getByRole('link', {name: 'Terms of Use'})).toHaveAttribute('href', `${AboutLinks.TERMS_OF_SERVICE}?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=currentUserId&sid=`);
|
||||
|
||||
expect(wrapper.find(AboutBuildModal).find('a#tosLink').props().href).not.toBe(config?.TermsOfServiceLink);
|
||||
expect(wrapper.find(AboutBuildModal).find('a#privacyLink').props().href).not.toBe(config?.PrivacyPolicyLink);
|
||||
expect(screen.getByRole('link', {name: 'Privacy Policy'})).toHaveAttribute('href', `${AboutLinks.PRIVACY_POLICY}?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=currentUserId&sid=`);
|
||||
|
||||
expect(screen.getByRole('link', {name: 'Terms of Use'})).not.toHaveAttribute('href', config?.TermsOfServiceLink);
|
||||
expect(screen.getByRole('link', {name: 'Privacy Policy'})).not.toHaveAttribute('href', config?.PrivacyPolicyLink);
|
||||
});
|
||||
|
||||
function shallowAboutBuildModal(props = {}) {
|
||||
function renderAboutBuildModal(props = {}) {
|
||||
const onExited = jest.fn();
|
||||
const show = true;
|
||||
|
||||
@@ -215,6 +242,6 @@ describe('components/AboutBuildModal', () => {
|
||||
...props,
|
||||
};
|
||||
|
||||
return shallow(<AboutBuildModal {...allProps}/>);
|
||||
return renderWithIntl(<Provider store={store}><AboutBuildModal {...allProps}/></Provider>);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -49,6 +49,7 @@ export default class AboutBuildModal extends React.PureComponent<Props, State> {
|
||||
|
||||
doHide = () => {
|
||||
this.setState({show: false});
|
||||
this.props.onExited();
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -172,7 +173,7 @@ export default class AboutBuildModal extends React.PureComponent<Props, State> {
|
||||
|
||||
// Only show build number if it's a number (so only builds from Jenkins)
|
||||
let buildnumber: JSX.Element | null = (
|
||||
<div>
|
||||
<div data-testid='aboutModalBuildNumber'>
|
||||
<FormattedMessage
|
||||
id='about.buildnumber'
|
||||
defaultMessage='Build Number:'
|
||||
@@ -227,7 +228,7 @@ export default class AboutBuildModal extends React.PureComponent<Props, State> {
|
||||
{subTitle}
|
||||
</p>
|
||||
<div className='form-group less'>
|
||||
<div>
|
||||
<div data-testid='aboutModalVersion'>
|
||||
<FormattedMessage
|
||||
id='about.version'
|
||||
defaultMessage='Mattermost Version:'
|
||||
@@ -236,7 +237,7 @@ export default class AboutBuildModal extends React.PureComponent<Props, State> {
|
||||
{'\u00a0' + mmversion}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div data-testid='aboutModalDBVersionString'>
|
||||
<FormattedMessage
|
||||
id='about.dbversion'
|
||||
defaultMessage='Database Schema Version:'
|
||||
|
||||
@@ -6,8 +6,7 @@ import {Link, useRouteMatch, useLocation, matchPath} from 'react-router-dom';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import Icon from '@mattermost/compass-components/foundations/icon'; // eslint-disable-line no-restricted-imports
|
||||
|
||||
import {ChartLineIcon} from '@mattermost/compass-icons/components';
|
||||
import {insightsAreEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getIsRhsOpen, getRhsState} from 'selectors/rhs';
|
||||
|
||||
@@ -62,10 +61,7 @@ const ActivityAndInsightsLink = () => {
|
||||
tabIndex={0}
|
||||
>
|
||||
<span className='icon'>
|
||||
<Icon
|
||||
size={12}
|
||||
glyph={'chart-line'}
|
||||
/>
|
||||
<ChartLineIcon size={14}/>
|
||||
</span>
|
||||
<div className='SidebarChannelLinkLabel_wrapper'>
|
||||
<span className='SidebarChannelLinkLabel sidebar-item__name'>
|
||||
|
||||
@@ -353,6 +353,36 @@ exports[`components/DatabaseSettings should match snapshot 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="control-label col-sm-4"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Active Search Backend:"
|
||||
id="admin.database.search_backend.title"
|
||||
/>
|
||||
</label>
|
||||
<div
|
||||
className="col-sm-8"
|
||||
>
|
||||
<input
|
||||
className="form-control"
|
||||
disabled={true}
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="help-text"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Shows the currently active backend used for search. Values can be none, database, elasticsearch, bleve etc."
|
||||
id="admin.database.search_backend.help_text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsGroup>
|
||||
<div
|
||||
className="admin-console-save"
|
||||
|
||||
@@ -2869,7 +2869,7 @@ const AdminDefinition = {
|
||||
label: t('admin.customization.enableSVGsTitle'),
|
||||
label_default: 'Enable SVGs:',
|
||||
help_text: t('admin.customization.enableSVGsDesc'),
|
||||
help_text_default: 'Enable previews for SVG file attachments and allow them to appear in messages.',
|
||||
help_text_default: 'Enable previews for SVG file attachments and allow them to appear in messages.\n\nEnabling SVGs is not recommended in environments where not all users are trusted.',
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.POSTS)),
|
||||
},
|
||||
{
|
||||
@@ -2878,7 +2878,7 @@ const AdminDefinition = {
|
||||
label: t('admin.customization.enableLatexTitle'),
|
||||
label_default: 'Enable Latex Rendering:',
|
||||
help_text: t('admin.customization.enableLatexDesc'),
|
||||
help_text_default: 'Enable rendering of Latex in code blocks. If false, Latex code will be highlighted only.',
|
||||
help_text_default: 'Enable rendering of Latex in code blocks. If false, Latex code will be highlighted only.\n\nEnabling Latex is not recommended in environments where not all users are trusted.',
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.POSTS)),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/admin_console/billing/billing_history should match snapshot 1`] = `
|
||||
<ContextProvider
|
||||
value={
|
||||
Object {
|
||||
"store": Object {
|
||||
"clearActions": [Function],
|
||||
"dispatch": [Function],
|
||||
"getActions": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
},
|
||||
"subscription": Subscription {
|
||||
"handleChangeWrapper": [Function],
|
||||
"listeners": Object {
|
||||
"notify": [Function],
|
||||
},
|
||||
"onStateChange": [Function],
|
||||
"parentSub": undefined,
|
||||
"store": Object {
|
||||
"clearActions": [Function],
|
||||
"dispatch": [Function],
|
||||
"getActions": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
},
|
||||
"unsubscribe": null,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<BillingHistory />
|
||||
</ContextProvider>
|
||||
`;
|
||||
@@ -5,11 +5,9 @@ import React from 'react';
|
||||
|
||||
import {Provider} from 'react-redux';
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import {screen} from '@testing-library/react';
|
||||
|
||||
import {renderWithIntlAndStore} from 'tests/react_testing_utils';
|
||||
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
import {renderWithIntl, renderWithIntlAndStore} from 'tests/react_testing_utils';
|
||||
import mockStore from 'tests/test_store';
|
||||
|
||||
import {CloudLinks, HostedCustomerLinks} from 'utils/constants';
|
||||
@@ -35,7 +33,7 @@ const invoiceA = {
|
||||
quantity: 1,
|
||||
price_per_unit: 1000,
|
||||
description:
|
||||
'1 × Cloud Professional (at $10.00 / month)',
|
||||
'1 × Cloud Professional (at $10.00 / month)',
|
||||
type: 'onpremise',
|
||||
metadata: {},
|
||||
},
|
||||
@@ -59,7 +57,7 @@ const invoiceB = {
|
||||
quantity: 1,
|
||||
price_per_unit: 1000,
|
||||
description:
|
||||
'Trial period for Cloud Professional',
|
||||
'Trial period for Cloud Professional',
|
||||
type: 'onpremise',
|
||||
metadata: {},
|
||||
},
|
||||
@@ -98,13 +96,21 @@ describe('components/admin_console/billing/billing_history', () => {
|
||||
|
||||
const store = mockStore(state);
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
test('should match the default state of the component with given props', () => {
|
||||
renderWithIntl(
|
||||
<Provider store={store}>
|
||||
<BillingHistory/>
|
||||
</Provider>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
expect(screen.queryByText('Billing History')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Transactions')).toBeInTheDocument();
|
||||
expect(screen.queryByText('All of your invoices will be shown here')).toBeInTheDocument();
|
||||
expect(screen.getByTestId(invoiceA.number)).toHaveTextContent((invoiceA.total / 100.0).toString());
|
||||
expect(screen.getByTestId(invoiceB.number)).toHaveTextContent((invoiceB.total / 100.0).toString());
|
||||
|
||||
expect(screen.getByTestId(invoiceA.id)).toHaveTextContent('Pending');
|
||||
expect(screen.getByTestId(invoiceB.id)).toHaveTextContent('Paid');
|
||||
});
|
||||
|
||||
test('Billing history section shows template when no invoices have been emitted yet', () => {
|
||||
@@ -113,42 +119,54 @@ describe('components/admin_console/billing/billing_history', () => {
|
||||
entities: {...state.entities, cloud: {invoices: {}, errors: {}}},
|
||||
};
|
||||
const storeNoBillingHistory = mockStore(noBillingHistoryState);
|
||||
const wrapper = mountWithIntl(
|
||||
renderWithIntl(
|
||||
<Provider store={storeNoBillingHistory}>
|
||||
<BillingHistory/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const legend = wrapper.find(
|
||||
'.BillingHistory__cardHeaderText-bottom span',
|
||||
);
|
||||
expect(legend.text()).toBe(NO_INVOICES_LEGEND);
|
||||
expect(screen.queryByText('Date')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Description')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Total')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Status')).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByTestId(invoiceA.number)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(invoiceB.number)).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByTestId(invoiceA.id)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(invoiceB.id)).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByRole('link')).toHaveAttribute('href', 'https://docs.mattermost.com/cloud/cloud-billing/cloud-billing.html?utm_source=mattermost&utm_medium=in-product-cloud&utm_content=billing_history&uid=current_user_id&sid=');
|
||||
expect(screen.getByRole('link')).toHaveTextContent('See how billing works');
|
||||
expect(screen.getByTestId('no-invoices')).toHaveTextContent(NO_INVOICES_LEGEND);
|
||||
});
|
||||
|
||||
test('Billing history section shows two invoices to download', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
renderWithIntl(
|
||||
<Provider store={store}>
|
||||
<BillingHistory/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const invoiceTableRows = wrapper.find('table.BillingHistory__table tr.BillingHistory__table-row');
|
||||
expect(screen.queryByText('Date')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Description')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Total')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Status')).toBeInTheDocument();
|
||||
|
||||
expect(invoiceTableRows.length).toBe(2);
|
||||
expect(screen.getAllByTestId('billingHistoryTableRow')).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('Billing history section download button has the target property set as _self so it works well in desktop app', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
renderWithIntl(
|
||||
<Provider store={store}>
|
||||
<BillingHistory/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const invoiceTableRow = wrapper.find('table.BillingHistory__table tr.BillingHistory__table-row').at(0);
|
||||
|
||||
const downloadLink = invoiceTableRow.find('td.BillingHistory__table-invoice a');
|
||||
|
||||
expect(downloadLink.prop('target')).toBe('_self');
|
||||
expect(screen.getByTestId(`billingHistoryLink-${invoiceA.id}`)).toHaveAttribute('target', '_self');
|
||||
expect(screen.getByTestId(`billingHistoryLink-${invoiceB.id}`)).toHaveAttribute('target', '_self');
|
||||
expect(screen.getByTestId(`billingHistoryLink-${invoiceA.id}`)).toHaveAttribute('href', '/api/v4/cloud/subscription/invoices/in_1KNb3DI67GP2qpb4ueaJYBt8/pdf');
|
||||
expect(screen.getByTestId(`billingHistoryLink-${invoiceB.id}`)).toHaveAttribute('href', '/api/v4/cloud/subscription/invoices/in_1KIWNTI67GP2qpb4KjGj1KAy/pdf');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -191,30 +209,42 @@ describe('BillingHistory -- self-hosted', () => {
|
||||
entities: {...state.entities, hostedCustomer: {invoices: {invoices: {}, invoicesLoaded: true}, errors: {}}},
|
||||
};
|
||||
const storeNoBillingHistory = mockStore(noBillingHistoryState);
|
||||
const wrapper = mountWithIntl(
|
||||
renderWithIntl(
|
||||
<Provider store={storeNoBillingHistory}>
|
||||
<BillingHistory/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const legend = wrapper.find(
|
||||
'.BillingHistory__cardHeaderText-bottom span',
|
||||
);
|
||||
expect(legend.text()).toBe(NO_INVOICES_LEGEND);
|
||||
expect(screen.queryByText('Date')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Description')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Total')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Status')).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByTestId(invoiceA.number)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(invoiceB.number)).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByTestId(invoiceA.id)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(invoiceB.id)).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByRole('link')).toHaveAttribute('href', 'https://docs.mattermost.com/manage/self-hosted-billing.html?utm_source=mattermost&utm_medium=in-product&utm_content=billing_history&uid=current_user_id&sid=');
|
||||
expect(screen.getByRole('link')).toHaveTextContent('See how billing works');
|
||||
expect(screen.getByTestId('no-invoices')).toHaveTextContent(NO_INVOICES_LEGEND);
|
||||
});
|
||||
|
||||
test('Billing history section shows two invoices to download', () => {
|
||||
const store = mockStore(state);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
renderWithIntl(
|
||||
<Provider store={store}>
|
||||
<BillingHistory/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const invoiceTableRows = wrapper.find('table.BillingHistory__table tr.BillingHistory__table-row');
|
||||
|
||||
expect(invoiceTableRows.length).toBe(2);
|
||||
expect(screen.queryByText('Date')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Description')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Total')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Status')).toBeInTheDocument();
|
||||
expect(screen.getAllByTestId('billingHistoryTableRow')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ export const NoBillingHistorySection = (props: NoBillingHistorySectionProps) =>
|
||||
/>
|
||||
</div>
|
||||
<ExternalLink
|
||||
data-testid='billingHistoryLink'
|
||||
location='billing_history'
|
||||
href={props.selfHosted ? HostedCustomerLinks.SELF_HOSTED_BILLING : CloudLinks.BILLING_DOCS}
|
||||
className='BillingHistory__noHistory-link'
|
||||
@@ -84,7 +85,10 @@ const BillingHistory = () => {
|
||||
defaultMessage='Transactions'
|
||||
/>
|
||||
</div>
|
||||
<div className='BillingHistory__cardHeaderText-bottom'>
|
||||
<div
|
||||
data-testid='no-invoices'
|
||||
className='BillingHistory__cardHeaderText-bottom'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.billing.history.allPaymentsShowHere'
|
||||
defaultMessage='All of your invoices will be shown here'
|
||||
|
||||
@@ -176,7 +176,7 @@ export default function BillingHistoryTable({invoices}: BillingHistoryTableProps
|
||||
);
|
||||
}}
|
||||
>
|
||||
<td>
|
||||
<td data-testid='billingHistoryTableRow'>
|
||||
<FormattedDate
|
||||
value={new Date(invoice.period_start)}
|
||||
month='2-digit'
|
||||
@@ -191,7 +191,10 @@ export default function BillingHistoryTable({invoices}: BillingHistoryTableProps
|
||||
<InvoiceUserCount invoice={invoice}/>
|
||||
</div>
|
||||
</td>
|
||||
<td className='BillingHistory__table-total'>
|
||||
<td
|
||||
data-testid={invoice.number}
|
||||
className='BillingHistory__table-total'
|
||||
>
|
||||
<FormattedNumber
|
||||
value={invoice.total / 100.0}
|
||||
// eslint-disable-next-line react/style-prop-object
|
||||
@@ -199,9 +202,10 @@ export default function BillingHistoryTable({invoices}: BillingHistoryTableProps
|
||||
currency='USD'
|
||||
/>
|
||||
</td>
|
||||
<td>{getPaymentStatus(invoice.status)}</td>
|
||||
<td data-testid={invoice.id}>{getPaymentStatus(invoice.status)}</td>
|
||||
<td className='BillingHistory__table-invoice'>
|
||||
<a
|
||||
data-testid={`billingHistoryLink-${invoice.id}`}
|
||||
target='_self'
|
||||
rel='noopener noreferrer'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
import LimitReachedBanner from './limit_reached_banner';
|
||||
import CancelSubscription from './cancel_subscription';
|
||||
import {ToYearlyNudgeBanner} from './to_yearly_nudge_banner';
|
||||
import {ToPaidNudgeBanner} from './to_paid_plan_nudge_banner';
|
||||
|
||||
import './billing_subscriptions.scss';
|
||||
|
||||
@@ -136,6 +137,7 @@ const BillingSubscriptions = () => {
|
||||
/>
|
||||
{shouldShowPaymentFailedBanner() && paymentFailedBanner()}
|
||||
{<ToYearlyNudgeBanner/>}
|
||||
{<ToPaidNudgeBanner/>}
|
||||
{showCreditCardBanner &&
|
||||
isCardExpired &&
|
||||
creditCardExpiredBanner(setShowCreditCardBanner)}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
@import 'utils/mixins';
|
||||
|
||||
.ToPaidNudgeBanner {
|
||||
&__actions {
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
&__primary {
|
||||
font-size: 12px;
|
||||
|
||||
@include primary-button;
|
||||
|
||||
&:hover {
|
||||
color: var(--button-color);
|
||||
}
|
||||
}
|
||||
|
||||
&__secondary {
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
|
||||
@include tertiary-button;
|
||||
|
||||
&:hover {
|
||||
color: var(--button-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
// 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 {CloudProducts} from 'utils/constants';
|
||||
|
||||
import {ToPaidNudgeBanner, ToPaidPlanBannerDismissable} from './to_paid_plan_nudge_banner';
|
||||
|
||||
const initialState = {
|
||||
views: {
|
||||
announcementBar: {
|
||||
announcementBarState: {
|
||||
announcementBarCount: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
CWSURL: '',
|
||||
FeatureFlagDeprecateCloudFree: 'true',
|
||||
},
|
||||
license: {
|
||||
IsLicensed: 'true',
|
||||
Cloud: 'true',
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: 'current_user_id',
|
||||
profiles: {
|
||||
current_user_id: {roles: 'system_user'},
|
||||
},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {},
|
||||
},
|
||||
cloud: {},
|
||||
},
|
||||
};
|
||||
|
||||
describe('ToPaidPlanBannerDismissable', () => {
|
||||
test('should only show for admins on cloud free', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.users.profiles = {
|
||||
current_user_id: {roles: 'system_admin'},
|
||||
};
|
||||
state.entities.cloud = {
|
||||
subscription: {
|
||||
product_id: 'prod_starter',
|
||||
is_free_trial: 'false',
|
||||
trial_end_at: 1,
|
||||
},
|
||||
products: {
|
||||
prod_starter: {
|
||||
id: 'prod_starter',
|
||||
sku: CloudProducts.STARTER,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithIntlAndStore(<ToPaidPlanBannerDismissable/>, state);
|
||||
|
||||
screen.getByTestId('cloud-free-deprecation-announcement-bar');
|
||||
});
|
||||
|
||||
test('should NOT show for NON admins', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.users.profiles = {
|
||||
current_user_id: {roles: 'system_user'},
|
||||
};
|
||||
state.entities.cloud = {
|
||||
subscription: {
|
||||
product_id: 'prod_starter',
|
||||
is_free_trial: 'false',
|
||||
trial_end_at: 1,
|
||||
},
|
||||
products: {
|
||||
prod_starter: {
|
||||
id: 'prod_starter',
|
||||
sku: CloudProducts.STARTER,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithIntlAndStore(<ToPaidPlanBannerDismissable/>, state);
|
||||
|
||||
expect(() => screen.getByTestId('cloud-free-deprecation-announcement-bar')).toThrow();
|
||||
});
|
||||
|
||||
test('should NOT show for admins on cloud pro', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.users.profiles = {
|
||||
current_user_id: {roles: 'system_admin'},
|
||||
};
|
||||
state.entities.cloud = {
|
||||
subscription: {
|
||||
product_id: 'prod_pro',
|
||||
is_free_trial: 'false',
|
||||
trial_end_at: 1,
|
||||
},
|
||||
products: {
|
||||
prod_pro: {
|
||||
id: 'prod_pro',
|
||||
sku: CloudProducts.PROFESSIONAL,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithIntlAndStore(<ToPaidPlanBannerDismissable/>, state);
|
||||
|
||||
expect(() => screen.getByTestId('cloud-free-deprecation-announcement-bar')).toThrow();
|
||||
});
|
||||
|
||||
test('should NOT show for admins on cloud enterprise', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.users.profiles = {
|
||||
current_user_id: {roles: 'system_admin'},
|
||||
};
|
||||
state.entities.cloud = {
|
||||
subscription: {
|
||||
product_id: 'prod_enterprise',
|
||||
is_free_trial: 'false',
|
||||
trial_end_at: 1,
|
||||
},
|
||||
products: {
|
||||
prod_enterprise: {
|
||||
id: 'prod_enterprise',
|
||||
sku: CloudProducts.ENTERPRISE,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithIntlAndStore(<ToPaidPlanBannerDismissable/>, state);
|
||||
|
||||
expect(() => screen.getByTestId('cloud-free-deprecation-announcement-bar')).toThrow();
|
||||
});
|
||||
|
||||
test('should NOT show for admins when banner was dismissed in preferences', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.users.profiles = {
|
||||
current_user_id: {roles: 'system_admin'},
|
||||
};
|
||||
state.entities.preferences = {
|
||||
myPreferences: {
|
||||
'to_paid_plan_nudge--nudge_to_paid_plan_snoozed': {
|
||||
category: 'to_paid_plan_nudge',
|
||||
name: 'nudge_to_paid_plan_snoozed',
|
||||
value: '{"range": 0, "show": false}',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
state.entities.cloud = {
|
||||
subscription: {
|
||||
product_id: 'prod_starter',
|
||||
is_free_trial: 'false',
|
||||
trial_end_at: 1,
|
||||
},
|
||||
products: {
|
||||
prod_starter: {
|
||||
id: 'prod_starter',
|
||||
sku: CloudProducts.STARTER,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithIntlAndStore(<ToPaidPlanBannerDismissable/>, state);
|
||||
|
||||
expect(() => screen.getByTestId('cloud-free-deprecation-announcement-bar')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ToPaidNudgeBanner', () => {
|
||||
test('should show only for cloud free', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud = {
|
||||
subscription: {
|
||||
product_id: 'prod_starter',
|
||||
is_free_trial: 'false',
|
||||
trial_end_at: 1,
|
||||
},
|
||||
products: {
|
||||
prod_starter: {
|
||||
id: 'prod_starter',
|
||||
sku: CloudProducts.STARTER,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithIntlAndStore(<ToPaidNudgeBanner/>, state);
|
||||
|
||||
screen.getByTestId('cloud-free-deprecation-alert-banner');
|
||||
});
|
||||
|
||||
test('should NOT show for cloud professional', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud = {
|
||||
subscription: {
|
||||
product_id: 'prod_pro',
|
||||
is_free_trial: 'false',
|
||||
trial_end_at: 1,
|
||||
},
|
||||
products: {
|
||||
prod_pro: {
|
||||
id: 'prod_pro',
|
||||
sku: CloudProducts.PROFESSIONAL,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithIntlAndStore(<ToPaidNudgeBanner/>, state);
|
||||
|
||||
expect(() => screen.getByTestId('cloud-free-deprecation-alert-banner')).toThrow();
|
||||
});
|
||||
|
||||
test('should NOT show for cloud enterprise', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud = {
|
||||
subscription: {
|
||||
product_id: 'prod_ent',
|
||||
is_free_trial: 'false',
|
||||
trial_end_at: 1,
|
||||
},
|
||||
products: {
|
||||
prod_ent: {
|
||||
id: 'prod_ent',
|
||||
sku: CloudProducts.ENTERPRISE,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithIntlAndStore(<ToPaidNudgeBanner/>, state);
|
||||
|
||||
expect(() => screen.getByTestId('cloud-free-deprecation-alert-banner')).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,246 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect} from 'react';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
import {useIntl, FormattedMessage} from 'react-intl';
|
||||
import moment from 'moment';
|
||||
|
||||
import AlertBanner from 'components/alert_banner';
|
||||
import useOpenPricingModal from 'components/common/hooks/useOpenPricingModal';
|
||||
import useOpenCloudPurchaseModal from 'components/common/hooks/useOpenCloudPurchaseModal';
|
||||
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
|
||||
import AnnouncementBar from 'components/announcement_bar/default_announcement_bar';
|
||||
|
||||
import {getSubscriptionProduct as selectSubscriptionProduct} from 'mattermost-redux/selectors/entities/cloud';
|
||||
import {getCurrentUser, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {deprecateCloudFree, get as getPreference} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {AnnouncementBarTypes, CloudBanners, CloudProducts, Preferences} from 'utils/constants';
|
||||
import {t} from 'utils/i18n';
|
||||
|
||||
import {GlobalState} from '@mattermost/types/store';
|
||||
|
||||
import './to_paid_plan_nudge_banner.scss';
|
||||
|
||||
enum DismissShowRange {
|
||||
GreaterThanEqual90 = '>=90',
|
||||
BetweenNinetyAnd60 = '89-61',
|
||||
SixtyTo31 = '60-31',
|
||||
ThirtyTo11 = '30-11',
|
||||
TenTo1 = '10-1',
|
||||
Zero = '0'
|
||||
}
|
||||
|
||||
const cloudFreeCloseMoment = '20230727';
|
||||
|
||||
interface ToPaidPlanDismissPreference {
|
||||
|
||||
// range represents the range for the days to the deprecation of cloud free e.g. in 30 to 10 days to deprecate cloud free
|
||||
// Incase of dismissing the banner, range represents the time (days) period when this banner was dismissed.
|
||||
// This is important because in case the banner was dismissed for a certain period, it helps us know that we should not show it again for that period.
|
||||
range: DismissShowRange;
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
export const ToPaidPlanBannerDismissable = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const openPricingModal = useOpenPricingModal();
|
||||
|
||||
const currentUser = useSelector(getCurrentUser);
|
||||
const isAdmin = useSelector(isCurrentUserSystemAdmin);
|
||||
const product = useSelector(selectSubscriptionProduct);
|
||||
const cloudFreeDeprecated = useSelector(deprecateCloudFree);
|
||||
const currentProductStarter = product?.sku === CloudProducts.STARTER;
|
||||
|
||||
const now = moment(Date.now());
|
||||
const cloudFreeEndDate = moment(cloudFreeCloseMoment, 'YYYYMMDD');
|
||||
const daysToCloudFreeEnd = cloudFreeEndDate.diff(now, 'days');
|
||||
|
||||
const snoozePreferenceVal = useSelector((state: GlobalState) => getPreference(state, Preferences.TO_PAID_PLAN_NUDGE, CloudBanners.NUDGE_TO_PAID_PLAN_SNOOZED, '{"range": 0, "show": true}'));
|
||||
const snoozeInfo = JSON.parse(snoozePreferenceVal) as ToPaidPlanDismissPreference;
|
||||
const show = snoozeInfo.show;
|
||||
|
||||
const snoozedForRange = (range: DismissShowRange) => {
|
||||
return snoozeInfo.range === range;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!snoozeInfo.show) {
|
||||
if (daysToCloudFreeEnd >= 90 && !snoozedForRange(DismissShowRange.GreaterThanEqual90)) {
|
||||
showBanner(true);
|
||||
}
|
||||
|
||||
if (daysToCloudFreeEnd < 90 && daysToCloudFreeEnd > 60 && !snoozedForRange(DismissShowRange.BetweenNinetyAnd60)) {
|
||||
showBanner(true);
|
||||
}
|
||||
|
||||
if (daysToCloudFreeEnd <= 60 && daysToCloudFreeEnd > 30 && !snoozedForRange(DismissShowRange.SixtyTo31)) {
|
||||
showBanner(true);
|
||||
}
|
||||
|
||||
if (daysToCloudFreeEnd <= 30 && daysToCloudFreeEnd > 10 && !snoozedForRange(DismissShowRange.ThirtyTo11)) {
|
||||
showBanner(true);
|
||||
}
|
||||
|
||||
if (daysToCloudFreeEnd <= 10) {
|
||||
showBanner(true);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const showBanner = (show = false) => {
|
||||
let dRange = DismissShowRange.Zero;
|
||||
if (daysToCloudFreeEnd >= 90) {
|
||||
dRange = DismissShowRange.GreaterThanEqual90;
|
||||
}
|
||||
|
||||
if (daysToCloudFreeEnd < 90 && daysToCloudFreeEnd > 60) {
|
||||
dRange = DismissShowRange.BetweenNinetyAnd60;
|
||||
}
|
||||
|
||||
if (daysToCloudFreeEnd <= 60 && daysToCloudFreeEnd > 30) {
|
||||
dRange = DismissShowRange.SixtyTo31;
|
||||
}
|
||||
|
||||
if (daysToCloudFreeEnd <= 30 && daysToCloudFreeEnd > 10) {
|
||||
dRange = DismissShowRange.ThirtyTo11;
|
||||
}
|
||||
|
||||
// ideally this case should not happen because snooze button is not shown when TenTo1 days are remaining
|
||||
if (daysToCloudFreeEnd <= 10 && daysToCloudFreeEnd > 0) {
|
||||
dRange = DismissShowRange.TenTo1;
|
||||
}
|
||||
|
||||
const snoozeInfo: ToPaidPlanDismissPreference = {
|
||||
range: dRange,
|
||||
show,
|
||||
};
|
||||
|
||||
dispatch(savePreferences(currentUser.id, [{
|
||||
category: Preferences.TO_PAID_PLAN_NUDGE,
|
||||
name: CloudBanners.NUDGE_TO_PAID_PLAN_SNOOZED,
|
||||
user_id: currentUser.id,
|
||||
value: JSON.stringify(snoozeInfo),
|
||||
}]));
|
||||
};
|
||||
|
||||
if (!cloudFreeDeprecated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isAdmin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!currentProductStarter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let message = {
|
||||
id: 'cloud_billing.nudge_to_paid.announcement_bar',
|
||||
defaultMessage: 'Cloud Free will be deprecated on {date}. To keep your workspace, upgrade to a paid plan',
|
||||
values: {
|
||||
date: moment(cloudFreeCloseMoment, 'YYYYMMDD').format('MMMM DD, YYYY'),
|
||||
},
|
||||
};
|
||||
|
||||
if (daysToCloudFreeEnd < 0) {
|
||||
message = {
|
||||
id: 'cloud_billing.nudge_to_paid.announcement_bar_deprecated',
|
||||
defaultMessage: 'Cloud Free was deprecated. To keep your workspace, upgrade to a paid plan',
|
||||
} as any;
|
||||
}
|
||||
|
||||
const announcementType = (daysToCloudFreeEnd <= 10) ? AnnouncementBarTypes.CRITICAL : AnnouncementBarTypes.ANNOUNCEMENT;
|
||||
|
||||
return (
|
||||
<AnnouncementBar
|
||||
id='cloud-free-deprecation-announcement-bar'
|
||||
type={announcementType}
|
||||
showCloseButton={daysToCloudFreeEnd > 10}
|
||||
onButtonClick={openPricingModal}
|
||||
modalButtonText={t('cloud_billing.nudge_to_paid.view_plans')}
|
||||
modalButtonDefaultText='View plans'
|
||||
message={<FormattedMessage {...message}/>}
|
||||
showLinkAsButton={true}
|
||||
handleClose={showBanner}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ToPaidNudgeBanner = () => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const [openSalesLink] = useOpenSalesLink();
|
||||
const openPurchaseModal = useOpenCloudPurchaseModal({});
|
||||
|
||||
const product = useSelector(selectSubscriptionProduct);
|
||||
const cloudFreeDeprecated = useSelector(deprecateCloudFree);
|
||||
const currentProductStarter = product?.sku === CloudProducts.STARTER;
|
||||
|
||||
if (!cloudFreeDeprecated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!currentProductStarter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = moment(Date.now());
|
||||
const cloudFreeEndDate = moment(cloudFreeCloseMoment, 'YYYYMMDD');
|
||||
const daysToCloudFreeEnd = cloudFreeEndDate.diff(now, 'days');
|
||||
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='cloud_billing.nudge_to_paid.title'
|
||||
defaultMessage='Upgrade to paid plan to keep your workspace'
|
||||
/>
|
||||
);
|
||||
|
||||
const description = (
|
||||
<FormattedMessage
|
||||
id='cloud_billing.nudge_to_paid.description'
|
||||
defaultMessage='Cloud Free will be deprecated in {days} days. Upgrade to a paid plan or contact sales.'
|
||||
values={{days: daysToCloudFreeEnd < 0 ? 0 : daysToCloudFreeEnd}}
|
||||
/>
|
||||
);
|
||||
|
||||
const viewPlansAction = (
|
||||
<button
|
||||
onClick={() => openPurchaseModal({trackingLocation: 'to_paid_plan_nudge_banner'})}
|
||||
className='btn ToPaidNudgeBanner__primary'
|
||||
>
|
||||
{formatMessage({id: 'cloud_billing.nudge_to_paid.learn_more', defaultMessage: 'Upgrade'})}
|
||||
</button>
|
||||
);
|
||||
|
||||
const contactSalesAction = (
|
||||
<button
|
||||
onClick={openSalesLink}
|
||||
className='btn ToPaidNudgeBanner__secondary'
|
||||
>
|
||||
{formatMessage({id: 'cloud_billing.nudge_to_paid.contact_sales', defaultMessage: 'Contact sales'})}
|
||||
</button>
|
||||
);
|
||||
|
||||
const bannerMode = (daysToCloudFreeEnd <= 10) ? 'danger' : 'info';
|
||||
|
||||
return (
|
||||
<AlertBanner
|
||||
id='cloud-free-deprecation-alert-banner'
|
||||
mode={bannerMode}
|
||||
title={title}
|
||||
message={description}
|
||||
className='ToYearlyNudgeBanner'
|
||||
actionButtonLeft={viewPlansAction}
|
||||
actionButtonRight={contactSalesAction}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -2,11 +2,9 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {screen} from '@testing-library/react';
|
||||
|
||||
import {Provider} from 'react-redux';
|
||||
|
||||
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
import mockStore from 'tests/test_store';
|
||||
import {renderWithIntlAndStore} from 'tests/react_testing_utils';
|
||||
import {CloudProducts, RecurringIntervals} from 'utils/constants';
|
||||
|
||||
import {ToYearlyNudgeBanner, ToYearlyNudgeBannerDismissable} from './to_yearly_nudge_banner';
|
||||
@@ -42,7 +40,7 @@ const initialState = {
|
||||
},
|
||||
};
|
||||
|
||||
describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () => {
|
||||
describe('ToYearlyNudgeBannerDismissable', () => {
|
||||
test('should show for admins cloud professional monthly', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.users.profiles = {
|
||||
@@ -63,14 +61,9 @@ describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () =
|
||||
},
|
||||
};
|
||||
|
||||
const store = mockStore(state);
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<ToYearlyNudgeBannerDismissable/>
|
||||
</Provider>,
|
||||
);
|
||||
renderWithIntlAndStore(<ToYearlyNudgeBannerDismissable/>, state);
|
||||
|
||||
expect(wrapper.find('AnnouncementBar').exists()).toBe(true);
|
||||
screen.getByTestId('cloud-pro-monthly-deprecation-announcement-bar');
|
||||
});
|
||||
|
||||
test('should NOT show for NON admins', () => {
|
||||
@@ -93,14 +86,9 @@ describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () =
|
||||
},
|
||||
};
|
||||
|
||||
const store = mockStore(state);
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<ToYearlyNudgeBannerDismissable/>
|
||||
</Provider>,
|
||||
);
|
||||
renderWithIntlAndStore(<ToYearlyNudgeBannerDismissable/>, state);
|
||||
|
||||
expect(wrapper.find('AnnouncementBar').exists()).toBe(false);
|
||||
expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-announcement-bar')).toThrow();
|
||||
});
|
||||
|
||||
test('should NOT show for admins on cloud free', () => {
|
||||
@@ -123,14 +111,9 @@ describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () =
|
||||
},
|
||||
};
|
||||
|
||||
const store = mockStore(state);
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<ToYearlyNudgeBannerDismissable/>
|
||||
</Provider>,
|
||||
);
|
||||
renderWithIntlAndStore(<ToYearlyNudgeBannerDismissable/>, state);
|
||||
|
||||
expect(wrapper.find('AnnouncementBar').exists()).toBe(false);
|
||||
expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-announcement-bar')).toThrow();
|
||||
});
|
||||
|
||||
test('should NOT show for admins on cloud enterprise', () => {
|
||||
@@ -153,14 +136,9 @@ describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () =
|
||||
},
|
||||
};
|
||||
|
||||
const store = mockStore(state);
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<ToYearlyNudgeBannerDismissable/>
|
||||
</Provider>,
|
||||
);
|
||||
renderWithIntlAndStore(<ToYearlyNudgeBannerDismissable/>, state);
|
||||
|
||||
expect(wrapper.find('AnnouncementBar').exists()).toBe(false);
|
||||
expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-announcement-bar')).toThrow();
|
||||
});
|
||||
|
||||
test('should NOT show for admins on cloud pro annual', () => {
|
||||
@@ -182,15 +160,9 @@ describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () =
|
||||
},
|
||||
},
|
||||
};
|
||||
renderWithIntlAndStore(<ToYearlyNudgeBannerDismissable/>, state);
|
||||
|
||||
const store = mockStore(state);
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<ToYearlyNudgeBannerDismissable/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(wrapper.find('AnnouncementBar').exists()).toBe(false);
|
||||
expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-announcement-bar')).toThrow();
|
||||
});
|
||||
|
||||
test('should NOT show for admins when banner was dismissed in preferences', () => {
|
||||
@@ -200,10 +172,10 @@ describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () =
|
||||
};
|
||||
state.entities.preferences = {
|
||||
myPreferences: {
|
||||
'cloud_yearly_nudge_banner--nudge_to_yearly_banner_dismissed': {
|
||||
category: 'cloud_yearly_nudge_banner',
|
||||
name: 'nudge_to_yearly_banner_dismissed',
|
||||
value: 'true',
|
||||
'to_cloud_yearly_plan_nudge--nudge_to_cloud_yearly_plan_snoozed': {
|
||||
category: 'to_cloud_yearly_plan_nudge',
|
||||
name: 'nudge_to_cloud_yearly_plan_snoozed',
|
||||
value: '{"range": 0, "show": false}',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -221,19 +193,13 @@ describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () =
|
||||
},
|
||||
},
|
||||
};
|
||||
renderWithIntlAndStore(<ToYearlyNudgeBannerDismissable/>, state);
|
||||
|
||||
const store = mockStore(state);
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<ToYearlyNudgeBannerDismissable/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(wrapper.find('AnnouncementBar').exists()).toBe(false);
|
||||
expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-announcement-bar')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('components/admin_console/billing/ToYearlyNudgeBanner', () => {
|
||||
describe('ToYearlyNudgeBanner', () => {
|
||||
test('should show for cloud professional monthly', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud = {
|
||||
@@ -251,14 +217,9 @@ describe('components/admin_console/billing/ToYearlyNudgeBanner', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const store = mockStore(state);
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<ToYearlyNudgeBanner/>
|
||||
</Provider>,
|
||||
);
|
||||
renderWithIntlAndStore(<ToYearlyNudgeBanner/>, state);
|
||||
|
||||
expect(wrapper.find('AlertBanner').exists()).toBe(true);
|
||||
screen.getByTestId('cloud-pro-monthly-deprecation-alert-banner');
|
||||
});
|
||||
|
||||
test('should NOT show for non cloud professional monthly', () => {
|
||||
@@ -278,13 +239,9 @@ describe('components/admin_console/billing/ToYearlyNudgeBanner', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const store = mockStore(state);
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<ToYearlyNudgeBanner/>
|
||||
</Provider>,
|
||||
);
|
||||
renderWithIntlAndStore(<ToYearlyNudgeBanner/>, state);
|
||||
|
||||
expect(wrapper.find('AlertBanner').exists()).toBe(false);
|
||||
expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-alert-banner')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import React, {useEffect} from 'react';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
import {useIntl, FormattedMessage} from 'react-intl';
|
||||
import moment from 'moment';
|
||||
|
||||
import AlertBanner from 'components/alert_banner';
|
||||
import useOpenCloudPurchaseModal from 'components/common/hooks/useOpenCloudPurchaseModal';
|
||||
@@ -22,12 +23,35 @@ import {GlobalState} from '@mattermost/types/store';
|
||||
|
||||
import './to_yearly_nudge_banner.scss';
|
||||
|
||||
enum DismissShowRange {
|
||||
GreaterThanEqual90 = '>=90',
|
||||
BetweenNinetyAnd60 = '89-61',
|
||||
SixtyTo31 = '60-31',
|
||||
ThirtyTo11 = '30-11',
|
||||
TenTo1 = '10-1',
|
||||
Zero = '0'
|
||||
}
|
||||
|
||||
const cloudProMonthlyCloseMoment = '20230727';
|
||||
|
||||
interface ToYearlyPlanDismissPreference {
|
||||
|
||||
// range represents the range for the days to the deprecation of cloud free e.g. in 30 to 10 days to deprecate cloud free
|
||||
// Incase of dismissing the banner, range represents the time (days) period when this banner was dismissed.
|
||||
// This is important because in case the banner was dismissed for a certain period, it helps us know that we should not show it again for that period.
|
||||
range: DismissShowRange;
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
const ToYearlyNudgeBannerDismissable = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const openPurchaseModal = useOpenCloudPurchaseModal({});
|
||||
|
||||
const nudgeDismissed = useSelector((state: GlobalState) => getPreference(state, Preferences.CLOUD_YEARLY_NUDGE_BANNER, CloudBanners.NUDGE_TO_YEARLY_BANNER_DISMISSED)) === 'true';
|
||||
const snoozePreferenceVal = useSelector((state: GlobalState) => getPreference(state, Preferences.TO_CLOUD_YEARLY_PLAN_NUDGE, CloudBanners.NUDGE_TO_CLOUD_YEARLY_PLAN_SNOOZED, '{"range": 0, "show": true}'));
|
||||
const snoozeInfo = JSON.parse(snoozePreferenceVal) as ToYearlyPlanDismissPreference;
|
||||
const show = snoozeInfo.show;
|
||||
|
||||
const currentUser = useSelector(getCurrentUser);
|
||||
const isAdmin = useSelector(isCurrentUserSystemAdmin);
|
||||
const product = useSelector(selectSubscriptionProduct);
|
||||
@@ -35,16 +59,75 @@ const ToYearlyNudgeBannerDismissable = () => {
|
||||
const currentProductIsMonthly = product?.recurring_interval === RecurringIntervals.MONTH;
|
||||
const currentProductProMonthly = currentProductProfessional && currentProductIsMonthly;
|
||||
|
||||
const savedDismissedPref = () => {
|
||||
const now = moment(Date.now());
|
||||
const proMonthlyEndDate = moment(cloudProMonthlyCloseMoment, 'YYYYMMDD');
|
||||
const daysToProMonthlyEnd = proMonthlyEndDate.diff(now, 'days');
|
||||
|
||||
const snoozedForRange = (range: DismissShowRange) => {
|
||||
return snoozeInfo.range === range;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!snoozeInfo.show) {
|
||||
if (daysToProMonthlyEnd >= 90 && !snoozedForRange(DismissShowRange.GreaterThanEqual90)) {
|
||||
showBanner(true);
|
||||
}
|
||||
|
||||
if (daysToProMonthlyEnd < 90 && daysToProMonthlyEnd > 60 && !snoozedForRange(DismissShowRange.BetweenNinetyAnd60)) {
|
||||
showBanner(true);
|
||||
}
|
||||
|
||||
if (daysToProMonthlyEnd <= 60 && daysToProMonthlyEnd > 30 && !snoozedForRange(DismissShowRange.SixtyTo31)) {
|
||||
showBanner(true);
|
||||
}
|
||||
|
||||
if (daysToProMonthlyEnd <= 30 && daysToProMonthlyEnd > 10 && !snoozedForRange(DismissShowRange.ThirtyTo11)) {
|
||||
showBanner(true);
|
||||
}
|
||||
|
||||
if (daysToProMonthlyEnd <= 10) {
|
||||
showBanner(true);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const showBanner = (show = false) => {
|
||||
let dRange = DismissShowRange.Zero;
|
||||
if (daysToProMonthlyEnd >= 90) {
|
||||
dRange = DismissShowRange.GreaterThanEqual90;
|
||||
}
|
||||
|
||||
if (daysToProMonthlyEnd < 90 && daysToProMonthlyEnd > 60) {
|
||||
dRange = DismissShowRange.BetweenNinetyAnd60;
|
||||
}
|
||||
|
||||
if (daysToProMonthlyEnd <= 60 && daysToProMonthlyEnd > 30) {
|
||||
dRange = DismissShowRange.SixtyTo31;
|
||||
}
|
||||
|
||||
if (daysToProMonthlyEnd <= 30 && daysToProMonthlyEnd > 10) {
|
||||
dRange = DismissShowRange.ThirtyTo11;
|
||||
}
|
||||
|
||||
// ideally this case should not happen because snooze button is not shown when TenTo1 days are remaining
|
||||
if (daysToProMonthlyEnd <= 10 && daysToProMonthlyEnd > 0) {
|
||||
dRange = DismissShowRange.TenTo1;
|
||||
}
|
||||
|
||||
const snoozeInfo: ToYearlyPlanDismissPreference = {
|
||||
range: dRange,
|
||||
show,
|
||||
};
|
||||
|
||||
dispatch(savePreferences(currentUser.id, [{
|
||||
category: Preferences.CLOUD_YEARLY_NUDGE_BANNER,
|
||||
name: CloudBanners.NUDGE_TO_YEARLY_BANNER_DISMISSED,
|
||||
category: Preferences.TO_CLOUD_YEARLY_PLAN_NUDGE,
|
||||
name: CloudBanners.NUDGE_TO_CLOUD_YEARLY_PLAN_SNOOZED,
|
||||
user_id: currentUser.id,
|
||||
value: 'true',
|
||||
value: JSON.stringify(snoozeInfo),
|
||||
}]));
|
||||
};
|
||||
|
||||
if (nudgeDismissed) {
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -56,21 +139,30 @@ const ToYearlyNudgeBannerDismissable = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const message = {
|
||||
id: 'cloud_billing.nudge_to_yearly.announcement_bar',
|
||||
defaultMessage: 'Simplify your billing and switch to an annual plan today',
|
||||
};
|
||||
const message = (
|
||||
<FormattedMessage
|
||||
id='cloud_billing.nudge_to_yearly.announcement_bar'
|
||||
defaultMessage='Monthly billing will be discontinued in {days} days . Switch to annual billing'
|
||||
values={{
|
||||
days: daysToProMonthlyEnd,
|
||||
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const announcementType = (daysToProMonthlyEnd <= 10) ? AnnouncementBarTypes.CRITICAL : AnnouncementBarTypes.ANNOUNCEMENT;
|
||||
|
||||
return (
|
||||
<AnnouncementBar
|
||||
type={AnnouncementBarTypes.ANNOUNCEMENT}
|
||||
showCloseButton={true}
|
||||
id='cloud-pro-monthly-deprecation-announcement-bar'
|
||||
type={announcementType}
|
||||
showCloseButton={daysToProMonthlyEnd > 10}
|
||||
onButtonClick={() => openPurchaseModal({trackingLocation: 'to_yearly_nudge_annoucement_bar'})}
|
||||
modalButtonText={t('cloud_billing.nudge_to_yearly.learn_more')}
|
||||
modalButtonDefaultText='Learn more'
|
||||
message={<FormattedMessage {...message}/>}
|
||||
message={message}
|
||||
showLinkAsButton={true}
|
||||
handleClose={savedDismissedPref}
|
||||
handleClose={showBanner}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -90,17 +182,22 @@ const ToYearlyNudgeBanner = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = moment(Date.now());
|
||||
const proMonthlyEndDate = moment(cloudProMonthlyCloseMoment, 'YYYYMMDD');
|
||||
const daysToProMonthlyEnd = proMonthlyEndDate.diff(now, 'days');
|
||||
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='cloud_billing.nudge_to_yearly.title'
|
||||
defaultMessage='Switch to an annual plan today'
|
||||
defaultMessage='Action required: Switch to annual billing to keep your workspace.'
|
||||
/>
|
||||
);
|
||||
|
||||
const description = (
|
||||
<FormattedMessage
|
||||
id='cloud_billing.nudge_to_yearly.description'
|
||||
defaultMessage='Simplify your billing by switching to an annual subscription.'
|
||||
defaultMessage='Monthly billing will be discontinued on {date}. To keep your workspace, switch to annual billing.'
|
||||
values={{date: moment(cloudProMonthlyCloseMoment, 'YYYYMMDD').format('MMMM DD, YYYY')}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -122,9 +219,12 @@ const ToYearlyNudgeBanner = () => {
|
||||
</button>
|
||||
);
|
||||
|
||||
const bannerMode = (daysToProMonthlyEnd <= 10) ? 'danger' : 'info';
|
||||
|
||||
return (
|
||||
<AlertBanner
|
||||
mode='info'
|
||||
id='cloud-pro-monthly-deprecation-alert-banner'
|
||||
mode={bannerMode}
|
||||
title={title}
|
||||
message={description}
|
||||
className='ToYearlyNudgeBanner'
|
||||
|
||||
@@ -6,7 +6,7 @@ import React from 'react';
|
||||
import {injectIntl, WrappedComponentProps} from 'react-intl';
|
||||
|
||||
import {Feedback} from '@mattermost/types/cloud';
|
||||
import FeedbackModal from 'components/feedback_modal/feedback';
|
||||
import FeedbackModal, {FeedbackOption} from 'components/feedback_modal/feedback';
|
||||
|
||||
type Props = {
|
||||
onSubmit: (deleteFeedback: Feedback) => void;
|
||||
@@ -28,23 +28,35 @@ const DeleteFeedbackModal = (props: Props) => {
|
||||
defaultMessage: 'Delete Workspace',
|
||||
});
|
||||
|
||||
const deleteFeedbackOptions = [
|
||||
props.intl.formatMessage({
|
||||
id: 'feedback.deleteWorkspace.feedbackNoValue',
|
||||
defaultMessage: 'No longer found value',
|
||||
}),
|
||||
props.intl.formatMessage({
|
||||
id: 'feedback.deleteWorkspace.feedbackMoving',
|
||||
defaultMessage: 'Moving to a different solution',
|
||||
}),
|
||||
props.intl.formatMessage({
|
||||
id: 'feedback.deleteWorkspace.feedbackMistake',
|
||||
defaultMessage: 'Created a workspace by mistake',
|
||||
}),
|
||||
props.intl.formatMessage({
|
||||
id: 'feedback.deleteWorkspace.feedbackHosting',
|
||||
defaultMessage: 'Moving to hosting my own Mattermost instance (self-hosted)',
|
||||
}),
|
||||
const deleteFeedbackOptions: FeedbackOption[] = [
|
||||
{
|
||||
translatedMessage: props.intl.formatMessage({
|
||||
id: 'feedback.deleteWorkspace.feedbackNoValue',
|
||||
defaultMessage: 'No longer found value',
|
||||
}),
|
||||
submissionValue: 'No longer found value',
|
||||
},
|
||||
{
|
||||
translatedMessage: props.intl.formatMessage({
|
||||
id: 'feedback.deleteWorkspace.feedbackMoving',
|
||||
defaultMessage: 'Moving to a different solution',
|
||||
}),
|
||||
submissionValue: 'Moving to a different solution',
|
||||
},
|
||||
{
|
||||
translatedMessage: props.intl.formatMessage({
|
||||
id: 'feedback.deleteWorkspace.feedbackMistake',
|
||||
defaultMessage: 'Created a workspace by mistake',
|
||||
}),
|
||||
submissionValue: 'Created a workspace by mistake',
|
||||
},
|
||||
{
|
||||
translatedMessage: props.intl.formatMessage({
|
||||
id: 'feedback.deleteWorkspace.feedbackHosting',
|
||||
defaultMessage: 'Moving to hosting my own Mattermost instance (self-hosted)',
|
||||
}),
|
||||
submissionValue: 'Moving to hosting my own Mattermost instance (self-hosted)',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {recycleDatabaseConnection} from 'actions/admin_actions.jsx';
|
||||
import {recycleDatabaseConnection, ping} from 'actions/admin_actions';
|
||||
import * as Utils from 'utils/utils';
|
||||
import {t} from 'utils/i18n';
|
||||
|
||||
@@ -19,6 +19,15 @@ import TextSetting from './text_setting';
|
||||
import MigrationsTable from './database';
|
||||
|
||||
export default class DatabaseSettings extends AdminSettings {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
...this.state,
|
||||
searchBackend: '',
|
||||
};
|
||||
}
|
||||
|
||||
getConfigFromState = (config) => {
|
||||
// driverName and dataSource are read-only from the UI
|
||||
|
||||
@@ -34,6 +43,17 @@ export default class DatabaseSettings extends AdminSettings {
|
||||
return config;
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.getSearchBackend().then((searchBackend) => {
|
||||
this.setState({searchBackend});
|
||||
});
|
||||
}
|
||||
|
||||
async getSearchBackend() {
|
||||
const res = await ping()();
|
||||
return res.ActiveSearchBackend;
|
||||
}
|
||||
|
||||
getStateFromConfig(config) {
|
||||
return {
|
||||
driverName: config.SqlSettings.DriverName,
|
||||
@@ -368,6 +388,30 @@ export default class DatabaseSettings extends AdminSettings {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.database.search_backend.title'
|
||||
defaultMessage='Active Search Backend:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
value={this.state.searchBackend}
|
||||
disabled={true}
|
||||
/>
|
||||
<div className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.database.search_backend.help_text'
|
||||
defaultMessage='Shows the currently active backend used for search. Values can be none, database, elasticsearch, bleve etc.'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsGroup>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,8 +7,14 @@ import {shallow} from 'enzyme';
|
||||
import DatabaseSettings from 'components/admin_console/database_settings.jsx';
|
||||
|
||||
jest.mock('actions/admin_actions.jsx', () => {
|
||||
const pingFn = () => {
|
||||
return jest.fn(() => {
|
||||
return {ActiveSearchBackend: 'none'};
|
||||
});
|
||||
};
|
||||
return {
|
||||
recycleDatabaseConnection: jest.fn(),
|
||||
ping: pingFn,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -671,7 +671,7 @@ export default class SchemaAdminSettings extends React.PureComponent {
|
||||
};
|
||||
|
||||
handleGeneratedChange = (id, s) => {
|
||||
this.handleChange(id, s.replace('+', '-').replace('/', '_'));
|
||||
this.handleChange(id, s.replace(/\+/g, '-').replace(/\//g, '_'));
|
||||
};
|
||||
|
||||
handleChange = (id, value, confirm = false, doSubmit = false, warning = false) => {
|
||||
|
||||
@@ -213,9 +213,11 @@ exports[`admin_console/team_channel_settings/team/TeamProfile__Cloud restore sho
|
||||
content="name"
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
@@ -242,6 +244,7 @@ exports[`admin_console/team_channel_settings/team/TeamProfile__Cloud restore sho
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
@@ -457,9 +460,11 @@ exports[`admin_console/team_channel_settings/team/TeamProfile__Cloud should matc
|
||||
content="name"
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
@@ -486,6 +491,7 @@ exports[`admin_console/team_channel_settings/team/TeamProfile__Cloud should matc
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
@@ -603,9 +609,11 @@ exports[`admin_console/team_channel_settings/team/TeamProfile__Cloud should matc
|
||||
id="sharedTooltip"
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
@@ -632,6 +640,7 @@ exports[`admin_console/team_channel_settings/team/TeamProfile__Cloud should matc
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
@@ -834,9 +843,11 @@ exports[`admin_console/team_channel_settings/team/TeamProfile__Cloud should matc
|
||||
content="name"
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
@@ -863,6 +874,7 @@ exports[`admin_console/team_channel_settings/team/TeamProfile__Cloud should matc
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
|
||||
@@ -6,7 +6,7 @@ import {shallow} from 'enzyme';
|
||||
|
||||
import ChipsList, {ChipsInfoType} from 'components/admin_console/workspace-optimization/chips_list';
|
||||
|
||||
import {ItemStatus} from './dashboard.data';
|
||||
import {ItemStatus} from './dashboard.type';
|
||||
|
||||
describe('components/admin_console/workspace-optimization/chips_list', () => {
|
||||
const overallScoreChips: ChipsInfoType = {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import Chip from 'components/common/chip/chip';
|
||||
|
||||
import {ItemStatus} from './dashboard.data';
|
||||
import {ItemStatus} from './dashboard.type';
|
||||
|
||||
import './dashboard.scss';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import React, {useEffect, useMemo, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
@@ -14,56 +14,23 @@ import {
|
||||
AccountMultipleOutlineIcon,
|
||||
} from '@mattermost/compass-icons/components';
|
||||
|
||||
import {getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getLicense, getServerVersion} from 'mattermost-redux/selectors/entities/general';
|
||||
|
||||
import {GlobalState} from '@mattermost/types/store';
|
||||
|
||||
import {CloudLinks, ConsolePages, DocLinks} from 'utils/constants';
|
||||
import {ConsolePages} from 'utils/constants';
|
||||
import {daysToLicenseExpire, isEnterpriseOrE20License, getIsStarterLicense} from '../../../utils/license_utils';
|
||||
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
|
||||
import {AdminConfig} from '@mattermost/types/config';
|
||||
import {runConfigChecks} from './dashboard_checks/config';
|
||||
import {DataModel, ItemStatus, Options} from './dashboard.type';
|
||||
import {runAccessChecks} from './dashboard_checks/access';
|
||||
import {runDataPrivacyChecks} from './dashboard_checks/data_privacy';
|
||||
import {runPerformanceChecks} from './dashboard_checks/performance';
|
||||
import {runEaseOfUseChecks} from './dashboard_checks/easy_management';
|
||||
import {runUpdateChecks} from './dashboard_checks/updates';
|
||||
|
||||
export type DataModel = {
|
||||
[key: string]: {
|
||||
title: string;
|
||||
description: string;
|
||||
descriptionOk: string;
|
||||
items: ItemModel[];
|
||||
icon: React.ReactNode;
|
||||
hide?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export enum ItemStatus {
|
||||
NONE = 'none',
|
||||
OK = 'ok',
|
||||
INFO = 'info',
|
||||
WARNING = 'warning',
|
||||
ERROR = 'error',
|
||||
}
|
||||
|
||||
export type ItemModel = {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
status: ItemStatus;
|
||||
scoreImpact: number;
|
||||
impactModifier: number;
|
||||
configUrl?: string;
|
||||
configText?: string;
|
||||
telemetryAction?: string;
|
||||
infoUrl?: string;
|
||||
infoText?: string;
|
||||
}
|
||||
|
||||
export type UpdatesParam = {
|
||||
serverVersion: {
|
||||
type: string;
|
||||
status: ItemStatus;
|
||||
description: string;
|
||||
};
|
||||
}
|
||||
|
||||
const impactModifiers: Record<ItemStatus, number> = {
|
||||
export const impactModifiers: Record<ItemStatus, number> = {
|
||||
[ItemStatus.NONE]: 1,
|
||||
[ItemStatus.OK]: 1,
|
||||
[ItemStatus.INFO]: 0.5,
|
||||
@@ -71,11 +38,191 @@ const impactModifiers: Record<ItemStatus, number> = {
|
||||
[ItemStatus.ERROR]: 0,
|
||||
};
|
||||
|
||||
const useMetricsData = () => {
|
||||
const getUpdatesData = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
) => ({
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.updates.title',
|
||||
defaultMessage: 'Server updates',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.updates.description',
|
||||
defaultMessage: 'An update is available.',
|
||||
}),
|
||||
descriptionOk: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.updates.descriptionOk',
|
||||
defaultMessage: 'Your workspace is completely up to date!',
|
||||
}),
|
||||
icon: (
|
||||
<div className='icon'>
|
||||
<ArrowUpBoldCircleOutlineIcon
|
||||
size={20}
|
||||
color={'var(--sys-center-channel-color)'}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
items: await runUpdateChecks(config, formatMessage, options),
|
||||
});
|
||||
|
||||
const getConfigurationData = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
) => ({
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.configuration.title',
|
||||
defaultMessage: 'Configuration',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.configuration.description',
|
||||
defaultMessage: 'You have configuration issues to resolve',
|
||||
}),
|
||||
hide: options.isCloud,
|
||||
descriptionOk: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.configuration.descriptionOk',
|
||||
defaultMessage: 'You\'ve successfully configured SSL and Session Lengths!',
|
||||
}),
|
||||
icon: (
|
||||
<div className='icon'>
|
||||
<TuneIcon
|
||||
size={20}
|
||||
color={'var(--sys-center-channel-color)'}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
items: await runConfigChecks(config, formatMessage, options),
|
||||
});
|
||||
|
||||
const getAccessData = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
) => ({
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.access.title',
|
||||
defaultMessage: 'Workspace access',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.access.description',
|
||||
defaultMessage: 'Web server configuration may be affecting access to your Mattermost workspace.',
|
||||
}),
|
||||
hide: options.isCloud,
|
||||
descriptionOk: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.access.descriptionOk',
|
||||
defaultMessage: 'Your web server configuration is passing a live URL test!',
|
||||
}),
|
||||
icon: (
|
||||
<div className='icon'>
|
||||
<ServerVariantIcon
|
||||
size={20}
|
||||
color={'var(--sys-center-channel-color)'}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
items: await runAccessChecks(config, formatMessage),
|
||||
});
|
||||
|
||||
const getPerformanceData = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
) => ({
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.performance.title',
|
||||
defaultMessage: 'Performance',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.performance.description',
|
||||
defaultMessage: 'Your server would benefit from some performance tweaks.',
|
||||
}),
|
||||
hide: options.isCloud,
|
||||
descriptionOk: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.performance.descriptionOk',
|
||||
defaultMessage: 'Your search performance suits your workspace usage!',
|
||||
}),
|
||||
icon: (
|
||||
<div className='icon'>
|
||||
<ChartLineIcon
|
||||
size={20}
|
||||
color={'var(--sys-center-channel-color)'}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
items: await runPerformanceChecks(config, formatMessage, options),
|
||||
});
|
||||
|
||||
const getDataPrivacyData = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
) => ({
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.data_privacy.title',
|
||||
defaultMessage: 'Data privacy',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.data_privacy.description',
|
||||
defaultMessage: 'Get better insight and control over your data.',
|
||||
}),
|
||||
descriptionOk: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.data_privacy.descriptionOk',
|
||||
defaultMessage: 'You\'ve enabled data retention and compliance features!',
|
||||
}),
|
||||
icon: (
|
||||
<div className='icon'>
|
||||
<LockOutlineIcon
|
||||
size={20}
|
||||
color={'var(--sys-center-channel-color)'}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
items: await runDataPrivacyChecks(config, formatMessage, options),
|
||||
});
|
||||
|
||||
const getEaseOfManagementData = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
) => ({
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.ease_of_management.title',
|
||||
defaultMessage: 'Ease of management',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.ease_of_management.description',
|
||||
defaultMessage: 'Make it easier to manage your Mattermost workspace.',
|
||||
}),
|
||||
descriptionOk: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.ease_of_management.descriptionOk',
|
||||
defaultMessage: 'Your user authentication setup is appropriate based on your current usage!',
|
||||
}),
|
||||
icon: (
|
||||
<div className='icon'>
|
||||
<AccountMultipleOutlineIcon
|
||||
size={20}
|
||||
color={'var(--sys-center-channel-color)'}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
items: await runEaseOfUseChecks(config, formatMessage, options),
|
||||
});
|
||||
|
||||
const useMetricsData = (
|
||||
config: Partial<AdminConfig>,
|
||||
) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [data, setData] = useState<DataModel | undefined>(undefined);
|
||||
|
||||
const {formatMessage} = useIntl();
|
||||
const prevTrialLicense = useSelector((state: GlobalState) => state.entities.admin.prevTrialLicense);
|
||||
const license = useSelector(getLicense);
|
||||
|
||||
// get the currently installed server version
|
||||
const installedVersion = useSelector((state: GlobalState) => getServerVersion(state));
|
||||
const analytics = useSelector((state: GlobalState) => state.entities.admin.analytics) as unknown as Options['analytics'];
|
||||
|
||||
const canStartTrial = license?.IsLicensed !== 'true' && prevTrialLicense?.IsLicensed !== 'true';
|
||||
const daysUntilExpiration = daysToLicenseExpire(license) || -1;
|
||||
|
||||
@@ -87,358 +234,46 @@ const useMetricsData = () => {
|
||||
|
||||
const [, contactSalesLink] = useOpenSalesLink();
|
||||
|
||||
const trialOrEnterpriseCtaConfig = {
|
||||
const trialOrEnterpriseCtaConfig = useMemo(() => ({
|
||||
configUrl: canStartTrial ? ConsolePages.LICENSE : contactSalesLink,
|
||||
configText: canStartTrial ? formatMessage({id: 'admin.reporting.workspace_optimization.cta.startTrial', defaultMessage: 'Start trial'}) : formatMessage({id: 'admin.reporting.workspace_optimization.cta.upgradeLicense', defaultMessage: 'Contact sales'}),
|
||||
}), [canStartTrial, contactSalesLink, formatMessage]);
|
||||
|
||||
const options: Options = useMemo(() => ({
|
||||
isLicensed,
|
||||
isEnterpriseLicense,
|
||||
trialOrEnterpriseCtaConfig,
|
||||
isStarterLicense,
|
||||
isCloud,
|
||||
analytics,
|
||||
installedVersion,
|
||||
}), [isLicensed, isEnterpriseLicense, trialOrEnterpriseCtaConfig, isStarterLicense, isCloud, analytics, installedVersion]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
const refreshData = async () => {
|
||||
const data = {
|
||||
updates: await getUpdatesData(config, formatMessage, options),
|
||||
configuration: await getConfigurationData(config, formatMessage, options),
|
||||
access: await getAccessData(config, formatMessage, options),
|
||||
performance: await getPerformanceData(config, formatMessage, options),
|
||||
dataPrivacy: await getDataPrivacyData(config, formatMessage, options),
|
||||
easyManagement: await getEaseOfManagementData(config, formatMessage, options),
|
||||
};
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
refreshData().then((data) => {
|
||||
setData(data);
|
||||
setLoading(false);
|
||||
});
|
||||
}, [config, formatMessage, options]);
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
};
|
||||
|
||||
const getUpdatesData = (data: UpdatesParam) => ({
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.updates.title',
|
||||
defaultMessage: 'Server updates',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.updates.description',
|
||||
defaultMessage: 'An update is available.',
|
||||
}),
|
||||
descriptionOk: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.updates.descriptionOk',
|
||||
defaultMessage: 'Your workspace is completely up to date!',
|
||||
}),
|
||||
icon: (
|
||||
<div className='icon'>
|
||||
<ArrowUpBoldCircleOutlineIcon
|
||||
size={20}
|
||||
color={'var(--sys-center-channel-color)'}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
items: [
|
||||
{
|
||||
id: 'server_version',
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.updates.server_version.status.title',
|
||||
defaultMessage: '{type} version update available.',
|
||||
}, {type: data.serverVersion.type}),
|
||||
description: data.serverVersion.description,
|
||||
configUrl: CloudLinks.DOWNLOAD_UPDATE,
|
||||
configText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.downloadUpdate', defaultMessage: 'Download update'}),
|
||||
infoUrl: DocLinks.UPGRADE_SERVER,
|
||||
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
telemetryAction: 'server-version',
|
||||
status: data.serverVersion.status,
|
||||
scoreImpact: 15,
|
||||
impactModifier: impactModifiers[data.serverVersion.status],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
type ConfigurationParam = {
|
||||
ssl: {
|
||||
status: ItemStatus;
|
||||
};
|
||||
sessionLength: {
|
||||
status: ItemStatus;
|
||||
};
|
||||
}
|
||||
|
||||
const getConfigurationData = (data: ConfigurationParam) => ({
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.configuration.title',
|
||||
defaultMessage: 'Configuration',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.configuration.description',
|
||||
defaultMessage: 'You have configuration issues to resolve',
|
||||
}),
|
||||
hide: isCloud,
|
||||
descriptionOk: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.configuration.descriptionOk',
|
||||
defaultMessage: 'You\'ve successfully configured SSL and Session Lengths!',
|
||||
}),
|
||||
icon: (
|
||||
<div className='icon'>
|
||||
<TuneIcon
|
||||
size={20}
|
||||
color={'var(--sys-center-channel-color)'}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
items: [
|
||||
{
|
||||
id: 'ssl',
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.configuration.ssl.title',
|
||||
defaultMessage: 'Configure SSL to make your server more secure',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.configuration.ssl.description',
|
||||
defaultMessage: 'We strongly recommend securing your Mattermost workspace by configuring SSL in production environments.',
|
||||
}),
|
||||
infoUrl: DocLinks.SSL_CERTIFICATE,
|
||||
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
telemetryAction: 'ssl',
|
||||
status: data.ssl.status,
|
||||
scoreImpact: 25,
|
||||
impactModifier: impactModifiers[data.ssl.status],
|
||||
},
|
||||
{
|
||||
id: 'session-length',
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.configuration.session_length.title',
|
||||
defaultMessage: 'Session lengths is set to default',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.configuration.session_length.description',
|
||||
defaultMessage: 'Your session length is set to the default of 30 days. A longer session length provides convenience, and a shorter session provides tighter security. We recommend adjusting this based on your organization\'s security policies.',
|
||||
}),
|
||||
configUrl: ConsolePages.SESSION_LENGTHS,
|
||||
configText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.configureSessionLength', defaultMessage: 'Configure session length'}),
|
||||
infoUrl: DocLinks.SESSION_LENGTHS,
|
||||
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
telemetryAction: 'session-length',
|
||||
status: data.sessionLength.status,
|
||||
scoreImpact: 8,
|
||||
impactModifier: impactModifiers[data.sessionLength.status],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
type AccessParam = {
|
||||
siteUrl: {
|
||||
status: ItemStatus;
|
||||
};
|
||||
}
|
||||
|
||||
const getAccessData = (data: AccessParam) => ({
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.access.title',
|
||||
defaultMessage: 'Workspace access',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.access.description',
|
||||
defaultMessage: 'Web server configuration may be affecting access to your Mattermost workspace.',
|
||||
}),
|
||||
hide: isCloud,
|
||||
descriptionOk: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.access.descriptionOk',
|
||||
defaultMessage: 'Your web server configuration is passing a live URL test!',
|
||||
}),
|
||||
icon: (
|
||||
<div className='icon'>
|
||||
<ServerVariantIcon
|
||||
size={20}
|
||||
color={'var(--sys-center-channel-color)'}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
items: [
|
||||
{
|
||||
id: 'site-url',
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.access.site_url.title',
|
||||
defaultMessage: 'Misconfigured web server',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.access.site_url.description',
|
||||
defaultMessage: 'Your web server settings aren\'t passing a live URL test which means your workspace may not be accessible to users. We recommend updating your web server settings.',
|
||||
}),
|
||||
configUrl: ConsolePages.WEB_SERVER,
|
||||
configText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.configureWebServer', defaultMessage: 'Configure web server'}),
|
||||
infoUrl: DocLinks.SITE_URL,
|
||||
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
telemetryAction: 'site-url',
|
||||
status: data.siteUrl.status,
|
||||
scoreImpact: 12,
|
||||
impactModifier: impactModifiers[data.siteUrl.status],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
type PerformanceParam = {
|
||||
search: {
|
||||
status: ItemStatus;
|
||||
};
|
||||
}
|
||||
|
||||
const getPerformanceData = (data: PerformanceParam) => ({
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.performance.title',
|
||||
defaultMessage: 'Performance',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.performance.description',
|
||||
defaultMessage: 'Your server would benefit from some performance tweaks.',
|
||||
}),
|
||||
hide: isCloud,
|
||||
descriptionOk: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.performance.descriptionOk',
|
||||
defaultMessage: 'Your search performance suits your workspace usage!',
|
||||
}),
|
||||
icon: (
|
||||
<div className='icon'>
|
||||
<ChartLineIcon
|
||||
size={20}
|
||||
color={'var(--sys-center-channel-color)'}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
items: [
|
||||
{
|
||||
id: 'search',
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.performance.search.title',
|
||||
defaultMessage: 'Search performance',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.performance.search.description',
|
||||
defaultMessage: 'Your server has reached over 500 users and 2 million posts which can result in slow search performance. We recommend enabling Elasticsearch for better performance.',
|
||||
}),
|
||||
...(isLicensed && isEnterpriseLicense ? {
|
||||
configUrl: ConsolePages.ELASTICSEARCH,
|
||||
configText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.configureElasticsearch', defaultMessage: 'Try Elasticsearch'}),
|
||||
} : trialOrEnterpriseCtaConfig),
|
||||
infoUrl: DocLinks.ELASTICSEARCH,
|
||||
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
telemetryAction: 'search-optimization',
|
||||
status: data.search.status,
|
||||
scoreImpact: 20,
|
||||
impactModifier: impactModifiers[data.search.status],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
type DataPrivacyParam = {
|
||||
retention: {
|
||||
status: ItemStatus;
|
||||
};
|
||||
}
|
||||
|
||||
// TBD
|
||||
const getDataPrivacyData = (data: DataPrivacyParam) => ({
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.data_privacy.title',
|
||||
defaultMessage: 'Data privacy',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.data_privacy.description',
|
||||
defaultMessage: 'Get better insight and control over your data.',
|
||||
}),
|
||||
descriptionOk: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.data_privacy.descriptionOk',
|
||||
defaultMessage: 'You\'ve enabled data retention and compliance features!',
|
||||
}),
|
||||
icon: (
|
||||
<div className='icon'>
|
||||
<LockOutlineIcon
|
||||
size={20}
|
||||
color={'var(--sys-center-channel-color)'}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
items: [
|
||||
{
|
||||
id: 'data-retention',
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.data_privacy.retention.title',
|
||||
defaultMessage: 'Become more data aware',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.data_privacy.retention.description',
|
||||
defaultMessage: 'Organizations in highly regulated industries require more control and insight with their data. We recommend enabling Data Retention and Compliance features.',
|
||||
}),
|
||||
...(isLicensed && isEnterpriseLicense ? {
|
||||
configUrl: ConsolePages.DATA_RETENTION,
|
||||
configText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.configureDataRetention', defaultMessage: 'Try data retention'}),
|
||||
} : trialOrEnterpriseCtaConfig),
|
||||
infoUrl: DocLinks.DATA_RETENTION_POLICY,
|
||||
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
telemetryAction: 'data-retention',
|
||||
status: data.retention.status,
|
||||
scoreImpact: 16,
|
||||
impactModifier: impactModifiers[data.retention.status],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
type EaseOfManagementParam = {
|
||||
ldap: {
|
||||
status: ItemStatus;
|
||||
};
|
||||
guestAccounts?: {
|
||||
status: ItemStatus;
|
||||
};
|
||||
}
|
||||
|
||||
// TBD
|
||||
const getEaseOfManagementData = (data: EaseOfManagementParam) => ({
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.ease_of_management.title',
|
||||
defaultMessage: 'Ease of management',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.ease_of_management.description',
|
||||
defaultMessage: 'Make it easier to manage your Mattermost workspace.',
|
||||
}),
|
||||
descriptionOk: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.ease_of_management.descriptionOk',
|
||||
defaultMessage: 'Your user authentication setup is appropriate based on your current usage!',
|
||||
}),
|
||||
icon: (
|
||||
<div className='icon'>
|
||||
<AccountMultipleOutlineIcon
|
||||
size={20}
|
||||
color={'var(--sys-center-channel-color)'}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
items: [
|
||||
{
|
||||
id: 'ad-ldap',
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.ease_of_management.ldap.title',
|
||||
defaultMessage: 'AD/LDAP integration recommended',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.ease_of_management.ldap.description',
|
||||
defaultMessage: 'You\'ve reached over 100 users! We recommend setting up AD/LDAP user authentication for easier onboarding as well as automated deactivations and role assignments.',
|
||||
}),
|
||||
...(isLicensed && !isStarterLicense ? {
|
||||
configUrl: ConsolePages.AD_LDAP,
|
||||
configText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.configureLDAP', defaultMessage: 'Try AD/LDAP'}),
|
||||
} : trialOrEnterpriseCtaConfig),
|
||||
infoUrl: DocLinks.AD_LDAP,
|
||||
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
telemetryAction: 'ad-ldap',
|
||||
status: data.ldap.status,
|
||||
scoreImpact: 22,
|
||||
impactModifier: impactModifiers[data.ldap.status],
|
||||
},
|
||||
|
||||
// commented out for now.
|
||||
// @see discussion here: https://github.com/mattermost/mattermost-webapp/pull/9822#discussion_r806879385
|
||||
// {
|
||||
// id: 'guest-accounts',
|
||||
// title: formatMessage({
|
||||
// id: 'admin.reporting.workspace_optimization.ease_of_management.guests_accounts.title',
|
||||
// defaultMessage: 'Guest Accounts recommended',
|
||||
// }),
|
||||
// description: formatMessage({
|
||||
// id: 'admin.reporting.workspace_optimization.ease_of_management.guests_accounts.description',
|
||||
// defaultMessage: 'Several user accounts are using different domains than your Site URL. You can control user access to channels and teams with guest accounts. We recommend starting an Enterprise trial and enabling Guest Access.',
|
||||
// }),
|
||||
// ...trialOrEnterpriseCtaConfig,
|
||||
// infoUrl: 'https://docs.mattermost.com/onboard/guest-accounts.html',
|
||||
// infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
// telemetryAction: 'guest-accounts',
|
||||
// status: data.guestAccounts.status,
|
||||
// scoreImpact: 6,
|
||||
// impactModifier: impactModifiers[data.guestAccounts.status],
|
||||
// },
|
||||
],
|
||||
});
|
||||
|
||||
return {getAccessData, getConfigurationData, getUpdatesData, getPerformanceData, getDataPrivacyData, getEaseOfManagementData, isLicensed, isEnterpriseLicense};
|
||||
};
|
||||
|
||||
export default useMetricsData;
|
||||
|
||||
@@ -4,17 +4,11 @@
|
||||
import {CheckIcon} from '@mattermost/compass-icons/components';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {GlobalState} from '@mattermost/types/store';
|
||||
import {getServerVersion} from 'mattermost-redux/selectors/entities/general';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import Accordion, {AccordionItemType} from 'components/common/accordion/accordion';
|
||||
|
||||
import {elasticsearchTest, ldapTest, testSiteURL} from '../../../actions/admin_actions';
|
||||
import LoadingScreen from '../../loading_screen';
|
||||
import FormattedAdminHeader from '../../widgets/admin_console/formatted_admin_header';
|
||||
import {Props} from '../admin_console';
|
||||
@@ -22,10 +16,11 @@ import {Props} from '../admin_console';
|
||||
import ChipsList, {ChipsInfoType} from './chips_list';
|
||||
import CtaButtons from './cta_buttons';
|
||||
|
||||
import useMetricsData, {DataModel, ItemStatus, UpdatesParam} from './dashboard.data';
|
||||
import useMetricsData from './dashboard.data';
|
||||
|
||||
import './dashboard.scss';
|
||||
import OverallScore from './overall-score';
|
||||
import {ItemStatus} from './dashboard.type';
|
||||
|
||||
const AccordionItem = styled.div`
|
||||
padding: 12px;
|
||||
@@ -49,180 +44,7 @@ const successIcon = (
|
||||
);
|
||||
|
||||
const WorkspaceOptimizationDashboard = (props: Props) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [versionData, setVersionData] = useState<UpdatesParam['serverVersion']>({type: '', description: '', status: ItemStatus.NONE});
|
||||
|
||||
// const [guestAccountStatus, setGuestAccountStatus] = useState<ItemStatus>('none');
|
||||
const [liveUrlStatus, setLiveUrlStatus] = useState<ItemStatus>(ItemStatus.ERROR);
|
||||
const [elastisearchStatus, setElasticsearchStatus] = useState<ItemStatus>(ItemStatus.INFO);
|
||||
const [ldapStatus, setLdapStatus] = useState<ItemStatus>(ItemStatus.INFO);
|
||||
const [dataRetentionStatus, setDataRetentionStatus] = useState<ItemStatus>(ItemStatus.INFO);
|
||||
const {formatMessage} = useIntl();
|
||||
const {getAccessData, getConfigurationData, getUpdatesData, getPerformanceData, getDataPrivacyData, getEaseOfManagementData, isLicensed, isEnterpriseLicense} = useMetricsData();
|
||||
|
||||
// get the currently installed server version
|
||||
const installedVersion = useSelector((state: GlobalState) => getServerVersion(state));
|
||||
const analytics = useSelector((state: GlobalState) => state.entities.admin.analytics);
|
||||
const {TOTAL_USERS: totalUsers, TOTAL_POSTS: totalPosts} = analytics!;
|
||||
|
||||
// gather locally available data
|
||||
const {
|
||||
ServiceSettings,
|
||||
DataRetentionSettings,
|
||||
ElasticsearchSettings,
|
||||
LdapSettings,
|
||||
|
||||
// TeamSettings,
|
||||
// GuestAccountsSettings,
|
||||
} = props.config;
|
||||
const {location} = document;
|
||||
|
||||
const sessionLengthWebInHours = ServiceSettings?.SessionLengthWebInHours || -1;
|
||||
|
||||
const testURL = () => {
|
||||
if (!ServiceSettings?.SiteURL) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const onSuccess = ({status}: any) => setLiveUrlStatus(status === 'OK' ? ItemStatus.OK : ItemStatus.ERROR);
|
||||
const onError = () => setLiveUrlStatus(ItemStatus.ERROR);
|
||||
return testSiteURL(onSuccess, onError, ServiceSettings?.SiteURL);
|
||||
};
|
||||
|
||||
const testDataRetention = async () => {
|
||||
if (!isLicensed || !isEnterpriseLicense) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (DataRetentionSettings?.EnableMessageDeletion || DataRetentionSettings?.EnableFileDeletion) {
|
||||
setDataRetentionStatus(ItemStatus.OK);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const result = await fetch(`${Client4.getBaseRoute()}/data_retention/policies?page=0&per_page=0`).then((result) => result.json());
|
||||
|
||||
setDataRetentionStatus(result.total_count > 0 ? ItemStatus.OK : ItemStatus.INFO);
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const fetchVersion = async () => {
|
||||
const result = await fetch(`${Client4.getBaseRoute()}/latest_version`).then((result) => result.json());
|
||||
|
||||
if (result.tag_name) {
|
||||
const sanitizedVersion = result.tag_name.startsWith('v') ? result.tag_name.slice(1) : result.tag_name;
|
||||
const newVersionParts = sanitizedVersion.split('.');
|
||||
const installedVersionParts = installedVersion.split('.').slice(0, 3);
|
||||
|
||||
// quick general check if a newer version is available
|
||||
let type = '';
|
||||
let status: ItemStatus = ItemStatus.OK;
|
||||
|
||||
if (newVersionParts.join('') > installedVersionParts.join('')) {
|
||||
// get correct values to be inserted into the accordion item
|
||||
switch (true) {
|
||||
case newVersionParts[0] > installedVersionParts[0]:
|
||||
type = formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.updates.server_version.update_type.major',
|
||||
defaultMessage: 'Major',
|
||||
});
|
||||
status = ItemStatus.ERROR;
|
||||
break;
|
||||
case newVersionParts[1] > installedVersionParts[1]:
|
||||
type = formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.updates.server_version.update_type.minor',
|
||||
defaultMessage: 'Minor',
|
||||
});
|
||||
status = ItemStatus.WARNING;
|
||||
break;
|
||||
case newVersionParts[2] > installedVersionParts[2]:
|
||||
type = formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.updates.server_version.update_type.patch',
|
||||
defaultMessage: 'Patch',
|
||||
});
|
||||
status = ItemStatus.INFO;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setVersionData({type, description: result.body, status});
|
||||
}
|
||||
};
|
||||
|
||||
const testElasticsearch = () => {
|
||||
if (!isLicensed || !isEnterpriseLicense || !(ElasticsearchSettings?.EnableIndexing && ElasticsearchSettings?.EnableSearching)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const onSuccess = ({status}: any) => setElasticsearchStatus(status === 'OK' ? ItemStatus.OK : ItemStatus.INFO);
|
||||
const onError = () => setElasticsearchStatus(ItemStatus.INFO);
|
||||
|
||||
return elasticsearchTest(props.config, onSuccess, onError);
|
||||
};
|
||||
|
||||
const testLdap = () => {
|
||||
if (!isLicensed || !LdapSettings?.Enable) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const onSuccess = ({status}: any) => setLdapStatus(status === 'OK' ? ItemStatus.OK : ItemStatus.INFO);
|
||||
const onError = () => setLdapStatus(ItemStatus.INFO);
|
||||
|
||||
return ldapTest(onSuccess, onError);
|
||||
};
|
||||
|
||||
// commented out for now.
|
||||
// @see discussion here: https://github.com/mattermost/mattermost-webapp/pull/9822#discussion_r806879385
|
||||
// const fetchGuestAccounts = async () => {
|
||||
// if (TeamSettings?.EnableOpenServer && GuestAccountsSettings?.Enable) {
|
||||
// let usersArray = await fetch(`${Client4.getBaseRoute()}/users/invalid_emails`).then((result) => result.json());
|
||||
//
|
||||
// // this setting is just a string with a list of domains, or an empty string
|
||||
// if (GuestAccountsSettings?.RestrictCreationToDomains) {
|
||||
// const domainList = GuestAccountsSettings?.RestrictCreationToDomains;
|
||||
// usersArray = usersArray.filter(({email}: Record<string, unknown>) => domainList.includes((email as string).split('@')[1]));
|
||||
// }
|
||||
//
|
||||
// // if guest accounts make up more than 5% of the user base show the info accordion
|
||||
// if (usersArray.length > (totalUsers as number * 0.05)) {
|
||||
// setGuestAccountStatus(ItemStatus.INFO);
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// setGuestAccountStatus(ItemStatus.OK);
|
||||
// };
|
||||
|
||||
useEffect(() => {
|
||||
const promises = [];
|
||||
promises.push(testURL());
|
||||
promises.push(testLdap());
|
||||
promises.push(fetchVersion());
|
||||
promises.push(testElasticsearch());
|
||||
promises.push(testDataRetention());
|
||||
|
||||
// promises.push(fetchGuestAccounts());
|
||||
Promise.all(promises).then(() => setLoading(false));
|
||||
}, [props.config, isLicensed, isEnterpriseLicense]);
|
||||
|
||||
const data: DataModel = {
|
||||
updates: getUpdatesData({serverVersion: versionData}),
|
||||
configuration: getConfigurationData({
|
||||
ssl: {status: location.protocol === 'https:' ? ItemStatus.OK : ItemStatus.ERROR},
|
||||
sessionLength: {status: sessionLengthWebInHours === 720 ? ItemStatus.INFO : ItemStatus.OK},
|
||||
}),
|
||||
access: getAccessData({siteUrl: {status: liveUrlStatus}}),
|
||||
performance: getPerformanceData({
|
||||
search: {
|
||||
status: totalPosts < 2_000_000 && totalUsers < 500 ? ItemStatus.OK : elastisearchStatus,
|
||||
},
|
||||
}),
|
||||
dataPrivacy: getDataPrivacyData({retention: {status: dataRetentionStatus}}),
|
||||
easyManagement: getEaseOfManagementData({
|
||||
ldap: {status: totalUsers < 100 ? ItemStatus.OK : ldapStatus},
|
||||
|
||||
// guestAccounts: {status: guestAccountStatus},
|
||||
}),
|
||||
};
|
||||
const {data, loading} = useMetricsData(props.config);
|
||||
|
||||
const overallScoreChips: ChipsInfoType = {
|
||||
[ItemStatus.INFO]: 0,
|
||||
@@ -236,7 +58,7 @@ const WorkspaceOptimizationDashboard = (props: Props) => {
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const accData: AccordionItemType[] = Object.entries(data).filter(([_, y]) => !y.hide).map(([accordionKey, accordionData]) => {
|
||||
const accordionItemsData: AccordionItemType[] | undefined = data && Object.entries(data).filter(([_, y]) => !y.hide).map(([accordionKey, accordionData]) => {
|
||||
const accordionDataChips: ChipsInfoType = {
|
||||
[ItemStatus.INFO]: 0,
|
||||
[ItemStatus.WARNING]: 0,
|
||||
@@ -297,7 +119,7 @@ const WorkspaceOptimizationDashboard = (props: Props) => {
|
||||
};
|
||||
});
|
||||
|
||||
return loading ? <LoadingScreen/> : (
|
||||
return loading || !accordionItemsData ? <LoadingScreen/> : (
|
||||
<div className='WorkspaceOptimizationDashboard wrapper--fixed'>
|
||||
<FormattedAdminHeader
|
||||
id={'admin.reporting.workspace_optimization.title'}
|
||||
@@ -314,7 +136,7 @@ const WorkspaceOptimizationDashboard = (props: Props) => {
|
||||
chartValue={Math.floor((overallScore.current / overallScore.max) * 100)}
|
||||
/>
|
||||
<Accordion
|
||||
accordionItemsData={accData}
|
||||
accordionItemsData={accordionItemsData}
|
||||
expandMultiple={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export type DataModel = {
|
||||
[key: string]: {
|
||||
title: string;
|
||||
description: string;
|
||||
descriptionOk: string;
|
||||
items: ItemModel[];
|
||||
icon: React.ReactNode;
|
||||
hide?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export enum ItemStatus {
|
||||
|
||||
/** Return NONE if it's not relevant, not configured, or not an option */
|
||||
NONE = 'none',
|
||||
|
||||
/** Return OK if all checks for this have passed */
|
||||
OK = 'ok',
|
||||
|
||||
/** Return info if it might not be relevant to them, but they could utilize it */
|
||||
INFO = 'info',
|
||||
WARNING = 'warning',
|
||||
ERROR = 'error',
|
||||
}
|
||||
|
||||
export type ItemModel = {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
status: ItemStatus;
|
||||
scoreImpact: number;
|
||||
impactModifier: number;
|
||||
configUrl?: string;
|
||||
configText?: string;
|
||||
telemetryAction?: string;
|
||||
infoUrl?: string;
|
||||
infoText?: string;
|
||||
}
|
||||
|
||||
export type UpdatesParam = {
|
||||
serverVersion: {
|
||||
type: string;
|
||||
status: ItemStatus;
|
||||
description: string;
|
||||
};
|
||||
}
|
||||
|
||||
type Analytics = {
|
||||
DAILY_ACTIVE_USERS: number;
|
||||
MONTHLY_ACTIVE_USERS: number;
|
||||
TOTAL_INACTIVE_USERS: number;
|
||||
TOTAL_MASTER_DB_CONNECTIONS: number;
|
||||
TOTAL_POSTS: number;
|
||||
TOTAL_PRIVATE_GROUPS: number;
|
||||
TOTAL_PUBLIC_CHANNELS: number;
|
||||
TOTAL_READ_DB_CONNECTIONS: number;
|
||||
TOTAL_TEAMS: number;
|
||||
TOTAL_USERS: number;
|
||||
TOTAL_WEBSOCKET_CONNECTIONS: number;
|
||||
}
|
||||
export type Options = {
|
||||
isLicensed: boolean;
|
||||
isEnterpriseLicense: boolean;
|
||||
trialOrEnterpriseCtaConfig: {
|
||||
configUrl: string;
|
||||
configText: string;
|
||||
};
|
||||
isCloud: boolean;
|
||||
isStarterLicense: boolean;
|
||||
analytics: Analytics | undefined;
|
||||
installedVersion: string;
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useIntl} from 'react-intl';
|
||||
import {ItemModel, ItemStatus} from '../dashboard.type';
|
||||
import {ConsolePages, DocLinks} from 'utils/constants';
|
||||
import {impactModifiers} from '../dashboard.data';
|
||||
import {AdminConfig} from '@mattermost/types/config';
|
||||
import {testSiteURL} from 'actions/admin_actions';
|
||||
|
||||
/**
|
||||
*
|
||||
* @description Checking to see if the siteURL is configured correctly by running it through the same "check siteURL" button that exists on the webserver page.
|
||||
*/
|
||||
const siteURLCheck = async (config: Partial<AdminConfig>, formatMessage: ReturnType<typeof useIntl>['formatMessage']): Promise<ItemModel> => {
|
||||
let status = ItemStatus.OK;
|
||||
const testURL = async () => {
|
||||
if (!config.ServiceSettings?.SiteURL) {
|
||||
status = ItemStatus.ERROR;
|
||||
}
|
||||
|
||||
const onSuccess = ({status: s}: any) => {
|
||||
if (s === 'OK') {
|
||||
status = ItemStatus.OK;
|
||||
}
|
||||
};
|
||||
const onError = () => {
|
||||
status = ItemStatus.ERROR;
|
||||
};
|
||||
await testSiteURL(onSuccess, onError, config.ServiceSettings?.SiteURL);
|
||||
};
|
||||
|
||||
await testURL();
|
||||
return {
|
||||
id: 'site-url',
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.access.site_url.title',
|
||||
defaultMessage: 'Misconfigured web server',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.access.site_url.description',
|
||||
defaultMessage: 'Your web server settings aren\'t passing a live URL test which means your workspace may not be accessible to users. We recommend updating your web server settings.',
|
||||
}),
|
||||
configUrl: ConsolePages.WEB_SERVER,
|
||||
configText: formatMessage({id: 'admin.reporting.workspace_optimization.access.site_url.cta', defaultMessage: 'Configure web server'}),
|
||||
infoUrl: DocLinks.SITE_URL,
|
||||
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
telemetryAction: 'site-url',
|
||||
status,
|
||||
scoreImpact: 12,
|
||||
impactModifier: impactModifiers[status],
|
||||
};
|
||||
};
|
||||
|
||||
const checks = [
|
||||
siteURLCheck,
|
||||
];
|
||||
|
||||
export const runAccessChecks = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
) => {
|
||||
const results = await Promise.all(checks.map((check) => check(config, formatMessage)));
|
||||
return results;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
import {AdminConfig} from '@mattermost/types/config';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {ConsolePages, DocLinks} from 'utils/constants';
|
||||
import {ItemModel, ItemStatus, Options} from '../dashboard.type';
|
||||
import {impactModifiers} from '../dashboard.data';
|
||||
|
||||
/**
|
||||
*
|
||||
* @description This checks to see if the user's active session is done over https. This does not check if the server is configured to use https.
|
||||
*/
|
||||
const ssl = (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
): ItemModel => {
|
||||
const status = document.location.protocol === 'https:' ? ItemStatus.OK : ItemStatus.ERROR;
|
||||
|
||||
return {
|
||||
id: 'ssl',
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.configuration.ssl.title',
|
||||
defaultMessage: 'Configure SSL to make your server more secure',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.configuration.ssl.description',
|
||||
defaultMessage: 'We strongly recommend securing your Mattermost workspace by configuring SSL in production environments.',
|
||||
}),
|
||||
infoUrl: DocLinks.SSL_CERTIFICATE,
|
||||
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
telemetryAction: 'ssl',
|
||||
status,
|
||||
scoreImpact: 25,
|
||||
impactModifier: impactModifiers[status],
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @description This checks to see if the user has adjusted the default session lengths to something other than 720 hours.
|
||||
*/
|
||||
const sessionLength = (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
): ItemModel => {
|
||||
const status = config.ServiceSettings?.SessionLengthMobileInHours === 720 ? ItemStatus.INFO : ItemStatus.OK;
|
||||
return {
|
||||
id: 'session-length',
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.configuration.session_length.title',
|
||||
defaultMessage: 'Session lengths is set to default',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.configuration.session_length.description',
|
||||
defaultMessage: 'Your session length is set to the default of 30 days. A longer session length provides convenience, and a shorter session provides tighter security. We recommend adjusting this based on your organization\'s security policies.',
|
||||
}),
|
||||
configUrl: ConsolePages.SESSION_LENGTHS,
|
||||
configText: formatMessage({id: 'admin.reporting.workspace_optimization.configuration.session_length.cta', defaultMessage: 'Configure session length'}),
|
||||
infoUrl: DocLinks.SESSION_LENGTHS,
|
||||
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
telemetryAction: 'session-length',
|
||||
status,
|
||||
scoreImpact: 8,
|
||||
impactModifier: impactModifiers[status],
|
||||
};
|
||||
};
|
||||
|
||||
export const runConfigChecks = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
) => {
|
||||
const checks = [
|
||||
ssl,
|
||||
sessionLength,
|
||||
];
|
||||
const results = await Promise.all(checks.map((check) => check(config, formatMessage, options)));
|
||||
return results;
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useIntl} from 'react-intl';
|
||||
import {impactModifiers} from '../dashboard.data';
|
||||
import {ConsolePages, DocLinks} from 'utils/constants';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {ItemStatus, Options} from '../dashboard.type';
|
||||
import {AdminConfig} from '@mattermost/types/config';
|
||||
|
||||
/**
|
||||
*
|
||||
* @description Checks if they they have a global policy deletion enabled, or if a custom policy has been created.
|
||||
*/
|
||||
const dataRetentionCheck = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
) => {
|
||||
const testDataRetention = async (
|
||||
config: Partial<AdminConfig>,
|
||||
options: Options,
|
||||
) => {
|
||||
if (!options.isLicensed || !options.isEnterpriseLicense) {
|
||||
return ItemStatus.INFO;
|
||||
}
|
||||
|
||||
if (config.DataRetentionSettings?.EnableMessageDeletion || config.DataRetentionSettings?.EnableFileDeletion) {
|
||||
return ItemStatus.OK;
|
||||
}
|
||||
|
||||
const policyCount: {total_count: number} = await fetch(`${Client4.getBaseRoute()}/data_retention/policies_count`).then((result) => result.json());
|
||||
return policyCount.total_count > 0 ? ItemStatus.OK : ItemStatus.INFO;
|
||||
};
|
||||
|
||||
const status = await testDataRetention(config, options);
|
||||
return {
|
||||
id: 'data-retention',
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.data_privacy.retention.title',
|
||||
defaultMessage: 'Become more data aware',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.data_privacy.retention.description',
|
||||
defaultMessage: 'Organizations in highly regulated industries require more control and insight with their data. We recommend enabling Data Retention and Compliance features.',
|
||||
}),
|
||||
...(options.isLicensed && options.isEnterpriseLicense ? {
|
||||
configUrl: ConsolePages.DATA_RETENTION,
|
||||
configText: formatMessage({id: 'admin.reporting.workspace_optimization.data_privacy.retention.cta', defaultMessage: 'Try data retention'}),
|
||||
} : options.trialOrEnterpriseCtaConfig),
|
||||
infoUrl: DocLinks.DATA_RETENTION_POLICY,
|
||||
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
telemetryAction: 'data-retention',
|
||||
status,
|
||||
scoreImpact: 16,
|
||||
impactModifier: impactModifiers[status],
|
||||
};
|
||||
};
|
||||
|
||||
export const runDataPrivacyChecks = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
) => {
|
||||
const checks = [
|
||||
dataRetentionCheck,
|
||||
];
|
||||
const results = await Promise.all(checks.map((check) => check(config, formatMessage, options)));
|
||||
return results;
|
||||
};
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {AdminConfig} from '@mattermost/types/config';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {ItemModel, ItemStatus, Options} from '../dashboard.type';
|
||||
import {ConsolePages, DocLinks} from 'utils/constants';
|
||||
import {impactModifiers} from '../dashboard.data';
|
||||
|
||||
// import {Client4} from 'mattermost-redux/client';
|
||||
// import {AnalyticsRow} from '@mattermost/types/admin';
|
||||
import {ldapTest} from 'actions/admin_actions';
|
||||
|
||||
const usesLDAP = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
): Promise<ItemModel> => {
|
||||
const testLdap = async (
|
||||
config: Partial<AdminConfig>,
|
||||
options: Options,
|
||||
): Promise<ItemStatus> => {
|
||||
let check = ItemStatus.INFO;
|
||||
|
||||
if (!options.isLicensed || !config.LdapSettings?.Enable) {
|
||||
return check;
|
||||
}
|
||||
|
||||
const onSuccess = ({status}: any) => {
|
||||
if (status === 'OK') {
|
||||
check = ItemStatus.OK;
|
||||
}
|
||||
};
|
||||
|
||||
await ldapTest(onSuccess);
|
||||
|
||||
return check;
|
||||
};
|
||||
|
||||
// something feels flawed in this check.
|
||||
const status = options.analytics?.TOTAL_USERS as number > 100 ? await testLdap(config, options) : ItemStatus.OK;
|
||||
|
||||
return {
|
||||
id: 'ad-ldap',
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.ease_of_management.ldap.title',
|
||||
defaultMessage: 'AD/LDAP integration recommended',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.ease_of_management.ldap.description',
|
||||
defaultMessage: 'You\'ve reached over 100 users! We recommend setting up AD/LDAP user authentication for easier onboarding as well as automated deactivations and role assignments.',
|
||||
}),
|
||||
...(options.isLicensed && !options.isStarterLicense ? {
|
||||
configUrl: ConsolePages.AD_LDAP,
|
||||
configText: formatMessage({id: 'admin.reporting.workspace_optimization.ease_of_management.ldap.cta', defaultMessage: 'Try AD/LDAP'}),
|
||||
} : options.trialOrEnterpriseCtaConfig),
|
||||
infoUrl: DocLinks.AD_LDAP,
|
||||
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
telemetryAction: 'ad-ldap',
|
||||
status,
|
||||
scoreImpact: 22,
|
||||
impactModifier: impactModifiers[status],
|
||||
};
|
||||
};
|
||||
|
||||
// // commented out for now.
|
||||
// // @see discussion here: https://github.com/mattermost/mattermost-webapp/pull/9822#discussion_r806879385
|
||||
// const fetchGuestAccounts = async (
|
||||
// config: Partial<AdminConfig>,
|
||||
// analytics: Record<string, number | AnalyticsRow[]> | undefined,
|
||||
// ) => {
|
||||
// if (config.TeamSettings?.EnableOpenServer && config.GuestAccountsSettings?.Enable) {
|
||||
// let usersArray = await fetch(`${Client4.getBaseRoute()}/users/invalid_emails`).then((result) => result.json());
|
||||
|
||||
// // this setting is just a string with a list of domains, or an empty string
|
||||
// if (config.GuestAccountsSettings?.RestrictCreationToDomains) {
|
||||
// const domainList = config.GuestAccountsSettings?.RestrictCreationToDomains;
|
||||
// usersArray = usersArray.filter(({email}: Record<string, unknown>) => domainList.includes((email as string).split('@')[1]));
|
||||
// }
|
||||
|
||||
// // if guest accounts make up more than 5% of the user base show the info accordion
|
||||
// if (analytics && usersArray.length > (analytics.totalUsers as number * 0.05)) {
|
||||
// return ItemStatus.INFO;
|
||||
// }
|
||||
// }
|
||||
|
||||
// return ItemStatus.OK;
|
||||
// };
|
||||
|
||||
// const guestAccounts = async (
|
||||
// config: Partial<AdminConfig>,
|
||||
// formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
// options: Options,
|
||||
// ): Promise<ItemModel> => {
|
||||
// const status = await fetchGuestAccounts(config, options.analytics);
|
||||
// return {
|
||||
// id: 'guest-accounts',
|
||||
// title: formatMessage({
|
||||
// id: 'admin.reporting.workspace_optimization.ease_of_management.guests_accounts.title',
|
||||
// defaultMessage: 'Guest Accounts recommended',
|
||||
// }),
|
||||
// description: formatMessage({
|
||||
// id: 'admin.reporting.workspace_optimization.ease_of_management.guests_accounts.description',
|
||||
// defaultMessage: 'Several user accounts are using different domains than your Site URL. You can control user access to channels and teams with guest accounts. We recommend starting an Enterprise trial and enabling Guest Access.',
|
||||
// }),
|
||||
// ...options.trialOrEnterpriseCtaConfig,
|
||||
// infoUrl: 'https://docs.mattermost.com/onboard/guest-accounts.html',
|
||||
// infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
// telemetryAction: 'guest-accounts',
|
||||
// status,
|
||||
// scoreImpact: 6,
|
||||
// impactModifier: impactModifiers[status],
|
||||
// };
|
||||
// };
|
||||
|
||||
export const runEaseOfUseChecks = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
): Promise<ItemModel[]> => {
|
||||
const checks = [
|
||||
usesLDAP,
|
||||
|
||||
// guestAccounts,
|
||||
];
|
||||
|
||||
const results = await Promise.all(checks.map((check) => check(config, formatMessage, options)));
|
||||
return results;
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {AdminConfig} from '@mattermost/types/config';
|
||||
import {ItemModel, ItemStatus, Options} from '../dashboard.type';
|
||||
import {elasticsearchTest} from 'actions/admin_actions';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {ConsolePages, DocLinks} from 'utils/constants';
|
||||
import {impactModifiers} from '../dashboard.data';
|
||||
|
||||
const search = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
): Promise<ItemModel> => {
|
||||
const testElasticsearch = async (
|
||||
config: Partial<AdminConfig>,
|
||||
options: Options,
|
||||
) => {
|
||||
let check = ItemStatus.INFO;
|
||||
|
||||
if (!options.isLicensed || !options.isEnterpriseLicense || !(config.ElasticsearchSettings?.EnableIndexing && config.ElasticsearchSettings?.EnableSearching)) {
|
||||
return check;
|
||||
}
|
||||
|
||||
const onSuccess = ({status}: any) => {
|
||||
if (status === 'OK') {
|
||||
check = ItemStatus.OK;
|
||||
}
|
||||
};
|
||||
await elasticsearchTest(config, onSuccess);
|
||||
return check;
|
||||
};
|
||||
|
||||
const totalPosts = options.analytics?.TOTAL_POSTS as number;
|
||||
const totalUsers = options.analytics?.TOTAL_USERS as number;
|
||||
const status = totalPosts < 2_000_000 && totalUsers < 500 ? ItemStatus.OK : await testElasticsearch(config, options);
|
||||
return {
|
||||
id: 'search',
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.performance.search.title',
|
||||
defaultMessage: 'Search performance',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.performance.search.description',
|
||||
defaultMessage: 'Your server has reached over 500 users and 2 million posts which can result in slow search performance. We recommend enabling Elasticsearch for better performance.',
|
||||
}),
|
||||
...(options.isLicensed && options.isEnterpriseLicense ? {
|
||||
configUrl: ConsolePages.ELASTICSEARCH,
|
||||
configText: formatMessage({id: 'admin.reporting.workspace_optimization.search.cta', defaultMessage: 'Try Elasticsearch'}),
|
||||
} : options.trialOrEnterpriseCtaConfig),
|
||||
infoUrl: DocLinks.ELASTICSEARCH,
|
||||
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
telemetryAction: 'search-optimization',
|
||||
status,
|
||||
scoreImpact: 20,
|
||||
impactModifier: impactModifiers[status],
|
||||
};
|
||||
};
|
||||
|
||||
export const runPerformanceChecks = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
) => {
|
||||
const checks = [
|
||||
search,
|
||||
];
|
||||
|
||||
const results = await Promise.all(checks.map((check) => check(config, formatMessage, options)));
|
||||
return results;
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {ItemStatus, Options} from '../dashboard.type';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {AdminConfig} from '@mattermost/types/config';
|
||||
import {CloudLinks, DocLinks} from 'utils/constants';
|
||||
import {impactModifiers} from '../dashboard.data';
|
||||
|
||||
const testServerVersion = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
) => {
|
||||
const fetchVersion = async (
|
||||
installedVersion: string,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
) => {
|
||||
const result = await fetch(`${Client4.getBaseRoute()}/latest_version`).then((result) => result.json());
|
||||
|
||||
if (result.tag_name) {
|
||||
const sanitizedVersion = result.tag_name.startsWith('v') ? result.tag_name.slice(1) : result.tag_name;
|
||||
const newVersionParts = sanitizedVersion.split('.');
|
||||
const installedVersionParts = installedVersion.split('.').slice(0, 3);
|
||||
|
||||
// quick general check if a newer version is available
|
||||
let type = '';
|
||||
let status: ItemStatus = ItemStatus.OK;
|
||||
|
||||
if (newVersionParts.join('') > installedVersionParts.join('')) {
|
||||
// get correct values to be inserted into the accordion item
|
||||
switch (true) {
|
||||
case newVersionParts[0] > installedVersionParts[0]:
|
||||
type = formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.updates.server_version.update_type.major',
|
||||
defaultMessage: 'Major',
|
||||
});
|
||||
status = ItemStatus.ERROR;
|
||||
break;
|
||||
case newVersionParts[1] > installedVersionParts[1]:
|
||||
type = formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.updates.server_version.update_type.minor',
|
||||
defaultMessage: 'Minor',
|
||||
});
|
||||
status = ItemStatus.WARNING;
|
||||
break;
|
||||
case newVersionParts[2] > installedVersionParts[2]:
|
||||
type = formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.updates.server_version.update_type.patch',
|
||||
defaultMessage: 'Patch',
|
||||
});
|
||||
status = ItemStatus.INFO;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {type, description: result.body, status};
|
||||
}
|
||||
|
||||
return {type: '', description: '', status: ItemStatus.OK};
|
||||
};
|
||||
|
||||
const serverVersion = await fetchVersion(options.installedVersion, formatMessage);
|
||||
return {
|
||||
id: 'server_version',
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.updates.server_version.status.title',
|
||||
defaultMessage: '{type} version update available.',
|
||||
}, {type: serverVersion.type}),
|
||||
description: serverVersion.description,
|
||||
configUrl: CloudLinks.DOWNLOAD_UPDATE,
|
||||
configText: formatMessage({id: 'admin.reporting.workspace_optimization.updates.server_version.cta', defaultMessage: 'Download update'}),
|
||||
infoUrl: DocLinks.UPGRADE_SERVER,
|
||||
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
telemetryAction: 'server-version',
|
||||
status: serverVersion.status,
|
||||
scoreImpact: 15,
|
||||
impactModifier: impactModifiers[serverVersion.status],
|
||||
};
|
||||
};
|
||||
|
||||
export const runUpdateChecks = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
) => {
|
||||
const checks = [
|
||||
testServerVersion,
|
||||
];
|
||||
|
||||
const results = await Promise.all(checks.map((check) => check(config, formatMessage, options)));
|
||||
return results;
|
||||
};
|
||||
@@ -13,6 +13,7 @@ import * as GlobalActions from 'actions/global_actions';
|
||||
|
||||
import Constants, {AdvancedTextEditor as AdvancedTextEditorConst, Locations, ModalIdentifiers, Preferences} from 'utils/constants';
|
||||
import {PreferenceType} from '@mattermost/types/preferences';
|
||||
import * as Keyboard from 'utils/keyboard';
|
||||
import * as UserAgent from 'utils/user_agent';
|
||||
import * as Utils from 'utils/utils';
|
||||
import {
|
||||
@@ -819,11 +820,11 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
|
||||
handleKeyDown = (e: React.KeyboardEvent<TextboxElement>) => {
|
||||
const ctrlOrMetaKeyPressed = e.ctrlKey || e.metaKey;
|
||||
const lastMessageReactionKeyCombo = ctrlOrMetaKeyPressed && e.shiftKey && Utils.isKeyPressed(e, KeyCodes.BACK_SLASH);
|
||||
const lastMessageReactionKeyCombo = ctrlOrMetaKeyPressed && e.shiftKey && Keyboard.isKeyPressed(e, KeyCodes.BACK_SLASH);
|
||||
|
||||
const ctrlKeyCombo = Utils.cmdOrCtrlPressed(e) && !e.altKey && !e.shiftKey;
|
||||
const ctrlAltCombo = Utils.cmdOrCtrlPressed(e, true) && e.altKey;
|
||||
const shiftAltCombo = !Utils.cmdOrCtrlPressed(e) && e.shiftKey && e.altKey;
|
||||
const ctrlKeyCombo = Keyboard.cmdOrCtrlPressed(e) && !e.altKey && !e.shiftKey;
|
||||
const ctrlAltCombo = Keyboard.cmdOrCtrlPressed(e, true) && e.altKey;
|
||||
const shiftAltCombo = !Keyboard.cmdOrCtrlPressed(e) && e.shiftKey && e.altKey;
|
||||
|
||||
// listen for line break key combo and insert new line character
|
||||
if (Utils.isUnhandledLineBreakKeyCombo(e)) {
|
||||
@@ -838,7 +839,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
|
||||
if (
|
||||
(this.props.ctrlSend || this.props.codeBlockOnCtrlEnter) &&
|
||||
Utils.isKeyPressed(e, KeyCodes.ENTER) &&
|
||||
Keyboard.isKeyPressed(e, KeyCodes.ENTER) &&
|
||||
(e.ctrlKey || e.metaKey)
|
||||
) {
|
||||
this.setShowPreview(false);
|
||||
@@ -849,7 +850,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
const draft = this.state.draft!;
|
||||
const {message} = draft;
|
||||
|
||||
if (Utils.isKeyPressed(e, KeyCodes.ESCAPE)) {
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.ESCAPE)) {
|
||||
this.textboxRef.current?.blur();
|
||||
}
|
||||
|
||||
@@ -858,7 +859,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
!e.metaKey &&
|
||||
!e.altKey &&
|
||||
!e.shiftKey &&
|
||||
Utils.isKeyPressed(e, KeyCodes.UP) &&
|
||||
Keyboard.isKeyPressed(e, KeyCodes.UP) &&
|
||||
message === ''
|
||||
) {
|
||||
e.preventDefault();
|
||||
@@ -879,13 +880,13 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
} = e.target as TextboxElement;
|
||||
|
||||
if (ctrlKeyCombo) {
|
||||
if (Utils.isKeyPressed(e, KeyCodes.UP)) {
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.UP)) {
|
||||
e.preventDefault();
|
||||
this.props.onMoveHistoryIndexBack();
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.DOWN)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.DOWN)) {
|
||||
e.preventDefault();
|
||||
this.props.onMoveHistoryIndexForward();
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.B)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.B)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
@@ -894,7 +895,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.I)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.I)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
@@ -905,7 +906,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
});
|
||||
}
|
||||
} else if (ctrlAltCombo) {
|
||||
if (Utils.isKeyPressed(e, KeyCodes.K)) {
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.K)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
@@ -914,7 +915,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.C)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.C)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
@@ -923,21 +924,21 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.E)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.E)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.toggleEmojiPicker();
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.T)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.T)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.toggleAdvanceTextEditor();
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.P) && draft.message.length) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.P) && draft.message.length) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.setShowPreview(!this.props.shouldShowPreview);
|
||||
}
|
||||
} else if (shiftAltCombo) {
|
||||
if (Utils.isKeyPressed(e, KeyCodes.X)) {
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.X)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
@@ -946,7 +947,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.SEVEN)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.SEVEN)) {
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'ol',
|
||||
@@ -954,7 +955,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.EIGHT)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.EIGHT)) {
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'ul',
|
||||
@@ -962,7 +963,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.NINE)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.NINE)) {
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'quote',
|
||||
|
||||
@@ -22,6 +22,7 @@ import Constants, {
|
||||
Preferences,
|
||||
AdvancedTextEditor as AdvancedTextEditorConst,
|
||||
} from 'utils/constants';
|
||||
import * as Keyboard from 'utils/keyboard';
|
||||
import {
|
||||
containsAtChannel,
|
||||
specialMentionsInText,
|
||||
@@ -34,7 +35,6 @@ import {
|
||||
} from 'utils/post_utils';
|
||||
import {getTable, hasHtmlLink, formatMarkdownMessage, formatGithubCodePaste, isGitHubCodeBlock} from 'utils/paste';
|
||||
import * as UserAgent from 'utils/user_agent';
|
||||
import {isMac} from 'utils/utils';
|
||||
import * as Utils from 'utils/utils';
|
||||
import EmojiMap from 'utils/emoji_map';
|
||||
import {applyMarkdown, ApplyMarkdownOptions} from 'utils/markdown/apply_markdown';
|
||||
@@ -1096,7 +1096,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
|
||||
documentKeyHandler = (e: KeyboardEvent) => {
|
||||
const ctrlOrMetaKeyPressed = e.ctrlKey || e.metaKey;
|
||||
const lastMessageReactionKeyCombo = ctrlOrMetaKeyPressed && e.shiftKey && Utils.isKeyPressed(e, KeyCodes.BACK_SLASH);
|
||||
const lastMessageReactionKeyCombo = ctrlOrMetaKeyPressed && e.shiftKey && Keyboard.isKeyPressed(e, KeyCodes.BACK_SLASH);
|
||||
if (lastMessageReactionKeyCombo) {
|
||||
this.reactToLastMessage(e);
|
||||
return;
|
||||
@@ -1134,12 +1134,12 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
|
||||
const ctrlOrMetaKeyPressed = e.ctrlKey || e.metaKey;
|
||||
const ctrlEnterKeyCombo = (this.props.ctrlSend || this.props.codeBlockOnCtrlEnter) &&
|
||||
Utils.isKeyPressed(e, KeyCodes.ENTER) &&
|
||||
Keyboard.isKeyPressed(e, KeyCodes.ENTER) &&
|
||||
ctrlOrMetaKeyPressed;
|
||||
|
||||
const ctrlKeyCombo = Utils.cmdOrCtrlPressed(e) && !e.altKey && !e.shiftKey;
|
||||
const ctrlAltCombo = Utils.cmdOrCtrlPressed(e, true) && e.altKey;
|
||||
const shiftAltCombo = !Utils.cmdOrCtrlPressed(e) && e.shiftKey && e.altKey;
|
||||
const ctrlKeyCombo = Keyboard.cmdOrCtrlPressed(e) && !e.altKey && !e.shiftKey;
|
||||
const ctrlAltCombo = Keyboard.cmdOrCtrlPressed(e, true) && e.altKey;
|
||||
const shiftAltCombo = !Keyboard.cmdOrCtrlPressed(e) && e.shiftKey && e.altKey;
|
||||
|
||||
// listen for line break key combo and insert new line character
|
||||
if (Utils.isUnhandledLineBreakKeyCombo(e)) {
|
||||
@@ -1155,7 +1155,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
|
||||
const {message} = this.state;
|
||||
|
||||
if (Utils.isKeyPressed(e, KeyCodes.ESCAPE)) {
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.ESCAPE)) {
|
||||
this.textboxRef.current?.blur();
|
||||
}
|
||||
|
||||
@@ -1164,7 +1164,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
!e.metaKey &&
|
||||
!e.altKey &&
|
||||
!e.shiftKey &&
|
||||
Utils.isKeyPressed(e, KeyCodes.UP) &&
|
||||
Keyboard.isKeyPressed(e, KeyCodes.UP) &&
|
||||
message === ''
|
||||
) {
|
||||
e.preventDefault();
|
||||
@@ -1182,15 +1182,15 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
} = e.target as TextboxElement;
|
||||
|
||||
if (ctrlKeyCombo) {
|
||||
if (draftMessageIsEmpty && Utils.isKeyPressed(e, KeyCodes.UP)) {
|
||||
if (draftMessageIsEmpty && Keyboard.isKeyPressed(e, KeyCodes.UP)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.loadPrevMessage(e);
|
||||
} else if (draftMessageIsEmpty && Utils.isKeyPressed(e, KeyCodes.DOWN)) {
|
||||
} else if (draftMessageIsEmpty && Keyboard.isKeyPressed(e, KeyCodes.DOWN)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.loadNextMessage(e);
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.B)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.B)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
@@ -1199,7 +1199,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.I)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.I)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
@@ -1210,7 +1210,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
});
|
||||
}
|
||||
} else if (ctrlAltCombo) {
|
||||
if (Utils.isKeyPressed(e, KeyCodes.K)) {
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.K)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
@@ -1219,7 +1219,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.C)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.C)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
@@ -1228,21 +1228,21 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.E)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.E)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.toggleEmojiPicker();
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.T)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.T)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.toggleAdvanceTextEditor();
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.P) && message.length) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.P) && message.length) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.setShowPreview(!this.props.shouldShowPreview);
|
||||
}
|
||||
} else if (shiftAltCombo) {
|
||||
if (Utils.isKeyPressed(e, KeyCodes.X)) {
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.X)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
@@ -1251,7 +1251,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.SEVEN)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.SEVEN)) {
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'ol',
|
||||
@@ -1259,7 +1259,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.EIGHT)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.EIGHT)) {
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'ul',
|
||||
@@ -1267,7 +1267,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.NINE)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.NINE)) {
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'quote',
|
||||
@@ -1277,21 +1277,21 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
});
|
||||
}
|
||||
}
|
||||
const upKeyOnly = !ctrlOrMetaKeyPressed && !e.altKey && !e.shiftKey && Utils.isKeyPressed(e, KeyCodes.UP);
|
||||
const shiftUpKeyCombo = !ctrlOrMetaKeyPressed && !e.altKey && e.shiftKey && Utils.isKeyPressed(e, KeyCodes.UP);
|
||||
const ctrlShiftCombo = Utils.cmdOrCtrlPressed(e, true) && e.shiftKey;
|
||||
const upKeyOnly = !ctrlOrMetaKeyPressed && !e.altKey && !e.shiftKey && Keyboard.isKeyPressed(e, KeyCodes.UP);
|
||||
const shiftUpKeyCombo = !ctrlOrMetaKeyPressed && !e.altKey && e.shiftKey && Keyboard.isKeyPressed(e, KeyCodes.UP);
|
||||
const ctrlShiftCombo = Keyboard.cmdOrCtrlPressed(e, true) && e.shiftKey;
|
||||
|
||||
if (upKeyOnly && messageIsEmpty) {
|
||||
this.editLastPost(e);
|
||||
} else if (shiftUpKeyCombo && messageIsEmpty) {
|
||||
this.replyToLastPost(e);
|
||||
} else if (ctrlShiftCombo && Utils.isKeyPressed(e, KeyCodes.E)) {
|
||||
} else if (ctrlShiftCombo && Keyboard.isKeyPressed(e, KeyCodes.E)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.toggleEmojiPicker();
|
||||
} else if (((isMac() && ctrlShiftCombo) || (!isMac() && ctrlAltCombo)) && Utils.isKeyPressed(e, KeyCodes.P) && this.state.message.length) {
|
||||
} else if (((UserAgent.isMac() && ctrlShiftCombo) || (!UserAgent.isMac() && ctrlAltCombo)) && Keyboard.isKeyPressed(e, KeyCodes.P) && this.state.message.length) {
|
||||
this.setShowPreview(!this.props.shouldShowPreview);
|
||||
} else if (ctrlAltCombo && Utils.isKeyPressed(e, KeyCodes.T)) {
|
||||
} else if (ctrlAltCombo && Keyboard.isKeyPressed(e, KeyCodes.T)) {
|
||||
this.toggleAdvanceTextEditor();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ import './alert_banner.scss';
|
||||
export type ModeType = 'danger' | 'warning' | 'info' | 'success';
|
||||
|
||||
export type AlertBannerProps = {
|
||||
id?: string;
|
||||
mode: ModeType;
|
||||
title?: React.ReactNode;
|
||||
message?: React.ReactNode;
|
||||
@@ -35,6 +36,7 @@ export type AlertBannerProps = {
|
||||
}
|
||||
|
||||
const AlertBanner = ({
|
||||
id,
|
||||
mode,
|
||||
title,
|
||||
message,
|
||||
@@ -70,6 +72,7 @@ const AlertBanner = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid={id}
|
||||
className={classNames(
|
||||
'AlertBanner',
|
||||
mode,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import {ToYearlyNudgeBannerDismissable} from 'components/admin_console/billing/billing_subscriptions/to_yearly_nudge_banner';
|
||||
import {ToPaidPlanBannerDismissable} from 'components/admin_console/billing/billing_subscriptions/to_paid_plan_nudge_banner';
|
||||
|
||||
import {ClientLicense, ClientConfig, WarnMetricStatus} from '@mattermost/types/config';
|
||||
import withGetCloudSubscription from '../common/hocs/cloud/with_get_cloud_subscription';
|
||||
@@ -72,6 +73,7 @@ class AnnouncementBarController extends React.PureComponent<Props> {
|
||||
let cloudDelinquencyAnnouncementBar = null;
|
||||
let notifyAdminDowngradeDelinquencyBar = null;
|
||||
let toYearlyNudgeBannerDismissable = null;
|
||||
let toPaidPlanNudgeBannerDismissable = null;
|
||||
if (this.props.license?.Cloud === 'true') {
|
||||
paymentAnnouncementBar = (
|
||||
<PaymentAnnouncementBar/>
|
||||
@@ -89,6 +91,7 @@ class AnnouncementBarController extends React.PureComponent<Props> {
|
||||
<NotifyAdminDowngradeDelinquencyBar/>
|
||||
);
|
||||
toYearlyNudgeBannerDismissable = (<ToYearlyNudgeBannerDismissable/>);
|
||||
toPaidPlanNudgeBannerDismissable = (<ToPaidPlanBannerDismissable/>);
|
||||
}
|
||||
|
||||
let autoStartTrialModal = null;
|
||||
@@ -108,6 +111,7 @@ class AnnouncementBarController extends React.PureComponent<Props> {
|
||||
{cloudDelinquencyAnnouncementBar}
|
||||
{notifyAdminDowngradeDelinquencyBar}
|
||||
{toYearlyNudgeBannerDismissable}
|
||||
{toPaidPlanNudgeBannerDismissable}
|
||||
{this.props.license?.Cloud !== 'true' && <OverageUsersBanner/>}
|
||||
{autoStartTrialModal}
|
||||
<ShowThreeDaysLeftTrialModal/>
|
||||
|
||||
@@ -18,6 +18,8 @@ import PricingModal from 'components/pricing_modal';
|
||||
|
||||
import {ModalData} from 'types/actions';
|
||||
|
||||
import {AlertCircleOutlineIcon, AlertOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
|
||||
import {
|
||||
Preferences,
|
||||
CloudBanners,
|
||||
@@ -38,6 +40,7 @@ type Props = {
|
||||
daysLeftOnTrial: number;
|
||||
isCloud: boolean;
|
||||
subscription?: Subscription;
|
||||
reverseTrial: boolean;
|
||||
actions: {
|
||||
savePreferences: (userId: string, preferences: PreferenceType[]) => void;
|
||||
getCloudSubscription: () => void;
|
||||
@@ -132,7 +135,7 @@ class CloudTrialAnnouncementBar extends React.PureComponent<Props> {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trialMoreThan3DaysMsg = (
|
||||
let trialMoreThan7DaysMsg = (
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.cloudTrial.moreThan3Days'
|
||||
defaultMessage='Your trial has started! There are {daysLeftOnTrial} days left'
|
||||
@@ -140,7 +143,26 @@ class CloudTrialAnnouncementBar extends React.PureComponent<Props> {
|
||||
/>
|
||||
);
|
||||
|
||||
const trialLessThan3DaysMsg = (
|
||||
let modalButtonText = t('admin.billing.subscription.cloudTrial.subscribeButton');
|
||||
let modalButtonDefaultText = 'Upgrade Now';
|
||||
|
||||
if (this.props.reverseTrial) {
|
||||
modalButtonText = t('admin.billing.subscription.cloudReverseTrial.subscribeButton');
|
||||
modalButtonDefaultText = 'Review your options';
|
||||
}
|
||||
|
||||
if (this.props.reverseTrial) {
|
||||
const trialEnd = getLocaleDateFromUTC((this.props.subscription?.trial_end_at as number / 1000), 'MMMM Do');
|
||||
trialMoreThan7DaysMsg = (
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.cloudTrial.moreThan3Days'
|
||||
defaultMessage='Your 30 day Enterprise trial has started! Purchase before {trialEnd} to keep your workspace.'
|
||||
values={{trialEnd}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let trialLessThan7DaysMsg = (
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.cloudTrial.daysLeftOnTrial'
|
||||
defaultMessage='There are {daysLeftOnTrial} days left on your free trial'
|
||||
@@ -148,10 +170,20 @@ class CloudTrialAnnouncementBar extends React.PureComponent<Props> {
|
||||
/>
|
||||
);
|
||||
|
||||
const userEndTrialDate = getLocaleDateFromUTC((this.props.subscription?.trial_end_at as number / 1000), 'MMMM Do YYYY');
|
||||
const userEndTrialHour = getLocaleDateFromUTC((this.props.subscription?.trial_end_at as number / 1000), 'HH:mm:ss', this.props.currentUser.timezone?.automaticTimezone as string);
|
||||
if (this.props.reverseTrial) {
|
||||
trialLessThan7DaysMsg = (
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.cloudReverseTrial.daysLeftOnTrial'
|
||||
defaultMessage='{daysLeftOnTrial} days left on your trial. Purchase a plan or contact sales to keep your workspace.'
|
||||
values={{daysLeftOnTrial}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const trialLastDaysMsg = (
|
||||
const userEndTrialDate = getLocaleDateFromUTC((this.props.subscription?.trial_end_at as number / 1000), 'MMMM Do YYYY');
|
||||
const userEndTrialHour = getLocaleDateFromUTC((this.props.subscription?.trial_end_at as number / 1000), 'HH:mm', this.props.currentUser.timezone?.automaticTimezone as string);
|
||||
|
||||
let trialLastDaysMsg = (
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.cloudTrial.lastDay'
|
||||
defaultMessage='This is the last day of your free trial. Your access will expire on {userEndTrialDate} at {userEndTrialHour}.'
|
||||
@@ -159,21 +191,28 @@ class CloudTrialAnnouncementBar extends React.PureComponent<Props> {
|
||||
/>
|
||||
);
|
||||
|
||||
if (this.props.reverseTrial) {
|
||||
trialLastDaysMsg = (
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.cloudReverseTrial.lastDay'
|
||||
defaultMessage='This is the last day of your trial. Purchase a plan before {userEndTrialHour} or contact sales'
|
||||
values={{userEndTrialHour}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let bannerMessage;
|
||||
let icon;
|
||||
switch (daysLeftOnTrial) {
|
||||
case TrialPeriodDays.TRIAL_WARNING_THRESHOLD:
|
||||
case TrialPeriodDays.TRIAL_2_DAYS:
|
||||
bannerMessage = trialLessThan3DaysMsg;
|
||||
break;
|
||||
case TrialPeriodDays.TRIAL_1_DAY:
|
||||
case TrialPeriodDays.TRIAL_0_DAYS:
|
||||
|
||||
if (daysLeftOnTrial >= TrialPeriodDays.TRIAL_2_DAYS && daysLeftOnTrial <= TrialPeriodDays.TRIAL_WARNING_THRESHOLD) {
|
||||
bannerMessage = trialLessThan7DaysMsg;
|
||||
icon = <AlertCircleOutlineIcon size={18}/>;
|
||||
} else if (daysLeftOnTrial <= TrialPeriodDays.TRIAL_1_DAY && daysLeftOnTrial >= TrialPeriodDays.TRIAL_0_DAYS) {
|
||||
bannerMessage = trialLastDaysMsg;
|
||||
break;
|
||||
default:
|
||||
bannerMessage = trialMoreThan3DaysMsg;
|
||||
icon = <i className='icon-check-outline-circle'/>;
|
||||
break;
|
||||
icon = <AlertOutlineIcon size={18}/>;
|
||||
} else {
|
||||
bannerMessage = trialMoreThan7DaysMsg;
|
||||
icon = <AlertCircleOutlineIcon size={18}/>;
|
||||
}
|
||||
|
||||
const dismissable = this.isDismissable();
|
||||
@@ -184,8 +223,8 @@ class CloudTrialAnnouncementBar extends React.PureComponent<Props> {
|
||||
showCloseButton={dismissable}
|
||||
handleClose={this.handleClose}
|
||||
onButtonClick={this.showModal}
|
||||
modalButtonText={t('admin.billing.subscription.cloudTrial.subscribeButton')}
|
||||
modalButtonDefaultText={'Upgrade Now'}
|
||||
modalButtonText={modalButtonText}
|
||||
modalButtonDefaultText={modalButtonDefaultText}
|
||||
message={bannerMessage}
|
||||
showLinkAsButton={true}
|
||||
icon={icon}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {Preferences, TrialPeriodDays} from 'utils/constants';
|
||||
import {getRemainingDaysFromFutureTimestamp} from 'utils/utils';
|
||||
|
||||
import CloudTrialAnnouncementBar from './cloud_trial_announcement_bar';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/admin';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const getCategory = makeGetCategory();
|
||||
@@ -29,6 +30,7 @@ function mapStateToProps(state: GlobalState) {
|
||||
const isCloud = getLicense(state).Cloud === 'true';
|
||||
let isFreeTrial = false;
|
||||
let daysLeftOnTrial = 0;
|
||||
const config = getConfig(state);
|
||||
|
||||
if (isCloud && subscription?.is_free_trial === 'true') {
|
||||
isFreeTrial = true;
|
||||
@@ -46,6 +48,7 @@ function mapStateToProps(state: GlobalState) {
|
||||
isCloud,
|
||||
subscription,
|
||||
preferences: getCategory(state, Preferences.CLOUD_TRIAL_BANNER),
|
||||
reverseTrial: Boolean(config.FeatureFlags?.CloudReverseTrial),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import React, {ReactNode} from 'react';
|
||||
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
|
||||
import {Link} from 'react-router-dom';
|
||||
|
||||
@@ -395,7 +395,7 @@ const ConfigurationAnnouncementBar = (props: Props) => {
|
||||
defaultMessage = 'Please configure your <linkSite>site URL</linkSite> on the <linkConsole>System Console</linkConsole>.';
|
||||
}
|
||||
|
||||
const values = {
|
||||
const values: Record<string, ReactNode> = {
|
||||
linkSite: (msg: string) => (
|
||||
<ExternalLink
|
||||
href={props.siteURL}
|
||||
|
||||
@@ -19,6 +19,7 @@ import ToggleModalButton from 'components/toggle_modal_button';
|
||||
import {trackEvent} from 'actions/telemetry_actions.jsx';
|
||||
|
||||
type Props = {
|
||||
id?: string;
|
||||
showCloseButton: boolean;
|
||||
color: string;
|
||||
textColor: string;
|
||||
@@ -172,6 +173,7 @@ export default class AnnouncementBar extends React.PureComponent<Props, State> {
|
||||
style={barStyle}
|
||||
// eslint-disable-next-line react/no-unknown-property
|
||||
css={{gridArea: 'announcement'}}
|
||||
data-testid={this.props.id}
|
||||
>
|
||||
<OverlayTrigger
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
|
||||
@@ -5,15 +5,13 @@ import React, {useCallback} from 'react';
|
||||
import {useDispatch} from 'react-redux';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Tooltip} from 'react-bootstrap';
|
||||
|
||||
import Icon from '@mattermost/compass-components/foundations/icon'; // eslint-disable-line no-restricted-imports
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
import MarketplaceModal from 'components/plugin_marketplace/marketplace_modal';
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
|
||||
import {Constants, ModalIdentifiers} from 'utils/constants';
|
||||
import {ViewGridPlusOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
|
||||
const AppBarMarketplace = () => {
|
||||
const {formatMessage} = useIntl();
|
||||
@@ -48,10 +46,7 @@ const AppBarMarketplace = () => {
|
||||
aria-label={label}
|
||||
onClick={handleOpenMarketplace}
|
||||
>
|
||||
<Icon
|
||||
size={16}
|
||||
glyph={'view-grid-plus-outline'}
|
||||
/>
|
||||
<ViewGridPlusOutlineIcon size={18}/>
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
|
||||
@@ -12,9 +12,10 @@ import {Group} from '@mattermost/types/groups';
|
||||
import ProfilePopover from 'components/profile_popover';
|
||||
|
||||
import {popOverOverlayPosition} from 'utils/position_utils';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
import {getUserOrGroupFromMentionName} from 'utils/post_utils';
|
||||
import Constants from 'utils/constants';
|
||||
import {getViewportSize, isKeyPressed} from 'utils/utils';
|
||||
import {getViewportSize} from 'utils/utils';
|
||||
|
||||
import AtMentionGroup from 'components/at_mention/at_mention_group';
|
||||
|
||||
|
||||
@@ -12,8 +12,9 @@ import ProfilePopover from 'components/profile_popover';
|
||||
import UserGroupPopover from 'components/user_group_popover';
|
||||
|
||||
import Constants, {A11yCustomEventTypes, A11yFocusEventDetail} from 'utils/constants';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
import {popOverOverlayPosition} from 'utils/position_utils';
|
||||
import {getViewportSize, isKeyPressed} from 'utils/utils';
|
||||
import {getViewportSize} from 'utils/utils';
|
||||
|
||||
import {MAX_LIST_HEIGHT, getListHeight, VIEWPORT_SCALE_FACTOR} from 'components/user_group_popover/group_member_list/group_member_list';
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {FormattedMessage} from 'react-intl';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
import {isKeyPressed} from 'utils/utils';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
|
||||
const Title = styled.div`
|
||||
flex:1;
|
||||
|
||||
@@ -29,9 +29,11 @@ exports[`components/pricing_modal/downgrade_team_removal_modal matches snapshot
|
||||
forwardedRef={null}
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
@@ -58,6 +60,7 @@ exports[`components/pricing_modal/downgrade_team_removal_modal matches snapshot
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
|
||||
@@ -29,9 +29,11 @@ exports[`components/pricing_modal/downgrade_team_removal_modal matches snapshot
|
||||
forwardedRef={null}
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
@@ -58,6 +60,7 @@ exports[`components/pricing_modal/downgrade_team_removal_modal matches snapshot
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
|
||||
@@ -4,10 +4,12 @@ exports[`codeBlock should render html code block with proper indentation after s
|
||||
<IntlProvider
|
||||
defaultFormats={Object {}}
|
||||
defaultLocale="en"
|
||||
fallbackOnEmptyString={true}
|
||||
formats={Object {}}
|
||||
locale="en"
|
||||
messages={Object {}}
|
||||
onError={[Function]}
|
||||
onWarn={[Function]}
|
||||
textComponent={Symbol(react.fragment)}
|
||||
>
|
||||
<CodeBlock
|
||||
@@ -68,9 +70,11 @@ exports[`codeBlock should render html code block with proper indentation after s
|
||||
id="copyButton"
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
@@ -97,6 +101,7 @@ exports[`codeBlock should render html code block with proper indentation after s
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": Symbol(react.fragment),
|
||||
"timeZone": undefined,
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
@@ -228,10 +233,12 @@ exports[`codeBlock should render typescript code block after syntax highlighting
|
||||
<IntlProvider
|
||||
defaultFormats={Object {}}
|
||||
defaultLocale="en"
|
||||
fallbackOnEmptyString={true}
|
||||
formats={Object {}}
|
||||
locale="en"
|
||||
messages={Object {}}
|
||||
onError={[Function]}
|
||||
onWarn={[Function]}
|
||||
textComponent={Symbol(react.fragment)}
|
||||
>
|
||||
<CodeBlock
|
||||
@@ -292,9 +299,11 @@ const myFunction = () => {
|
||||
id="copyButton"
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
@@ -321,6 +330,7 @@ const myFunction = () => {
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": Symbol(react.fragment),
|
||||
"timeZone": undefined,
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
@@ -452,10 +462,12 @@ exports[`codeBlock should render unknown language after syntax highlighting 1`]
|
||||
<IntlProvider
|
||||
defaultFormats={Object {}}
|
||||
defaultLocale="en"
|
||||
fallbackOnEmptyString={true}
|
||||
formats={Object {}}
|
||||
locale="en"
|
||||
messages={Object {}}
|
||||
onError={[Function]}
|
||||
onWarn={[Function]}
|
||||
textComponent={Symbol(react.fragment)}
|
||||
>
|
||||
<CodeBlock
|
||||
@@ -514,9 +526,11 @@ it shouldn't highlight, it's just garbage
|
||||
id="copyButton"
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
@@ -543,6 +557,7 @@ it shouldn't highlight, it's just garbage
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": Symbol(react.fragment),
|
||||
"timeZone": undefined,
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
|
||||
@@ -25,7 +25,8 @@ import {GlobalState} from 'types/store';
|
||||
import {getCurrentMomentForTimezone} from 'utils/timezone';
|
||||
import {A11yCustomEventTypes, A11yFocusEventDetail, Constants, ModalIdentifiers} from 'utils/constants';
|
||||
import {t} from 'utils/i18n';
|
||||
import {isKeyPressed, localizeMessage} from 'utils/utils';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
import {localizeMessage} from 'utils/utils';
|
||||
|
||||
import CustomStatusSuggestion from 'components/custom_status/custom_status_suggestion';
|
||||
import ExpiryMenu from 'components/custom_status/expiry_menu';
|
||||
|
||||
@@ -17,7 +17,8 @@ import DatePicker from 'components/date_picker';
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
import Timestamp from 'components/timestamp';
|
||||
import {getCurrentLocale} from 'selectors/i18n';
|
||||
import {isKeyPressed, localizeMessage} from 'utils/utils';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
import {localizeMessage} from 'utils/utils';
|
||||
import {getCurrentMomentForTimezone} from 'utils/timezone';
|
||||
import Constants, {A11yCustomEventTypes, A11yFocusEventDetail} from 'utils/constants';
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@ import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
||||
|
||||
import './dnd_custom_time_picker_modal.scss';
|
||||
import {toUTCUnix} from 'utils/datetime';
|
||||
import {isKeyPressed, localizeMessage} from 'utils/utils';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
import {localizeMessage} from 'utils/utils';
|
||||
import Input from 'components/widgets/inputs/input/input';
|
||||
import DatePicker from 'components/date_picker';
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import Permissions from 'mattermost-redux/constants/permissions';
|
||||
import {Locations, ModalIdentifiers, Constants, TELEMETRY_LABELS} from 'utils/constants';
|
||||
import DeletePostModal from 'components/delete_post_modal';
|
||||
import DelayedAction from 'utils/delayed_action';
|
||||
import * as Keyboard from 'utils/keyboard';
|
||||
import * as PostUtils from 'utils/post_utils';
|
||||
import * as Menu from 'components/menu';
|
||||
import * as Utils from 'utils/utils';
|
||||
@@ -337,61 +338,61 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
|
||||
const isShiftKeyPressed = e.shiftKey;
|
||||
|
||||
switch (true) {
|
||||
case Utils.isKeyPressed(e, Constants.KeyCodes.R):
|
||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.R):
|
||||
this.handleCommentClick(e);
|
||||
this.handleDropdownOpened(false);
|
||||
break;
|
||||
|
||||
// edit post
|
||||
case Utils.isKeyPressed(e, Constants.KeyCodes.E):
|
||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.E):
|
||||
this.handleEditMenuItemActivated(e);
|
||||
this.handleDropdownOpened(false);
|
||||
break;
|
||||
|
||||
// follow thread
|
||||
case Utils.isKeyPressed(e, Constants.KeyCodes.F) && !isShiftKeyPressed:
|
||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.F) && !isShiftKeyPressed:
|
||||
this.handleSetThreadFollow(e);
|
||||
this.handleDropdownOpened(false);
|
||||
break;
|
||||
|
||||
// forward post
|
||||
case Utils.isKeyPressed(e, Constants.KeyCodes.F) && isShiftKeyPressed:
|
||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.F) && isShiftKeyPressed:
|
||||
this.handleForwardMenuItemActivated(e);
|
||||
this.handleDropdownOpened(false);
|
||||
break;
|
||||
|
||||
// copy link
|
||||
case Utils.isKeyPressed(e, Constants.KeyCodes.K):
|
||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.K):
|
||||
this.copyLink(e);
|
||||
this.handleDropdownOpened(false);
|
||||
break;
|
||||
|
||||
// copy text
|
||||
case Utils.isKeyPressed(e, Constants.KeyCodes.C):
|
||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.C):
|
||||
this.copyText(e);
|
||||
this.handleDropdownOpened(false);
|
||||
break;
|
||||
|
||||
// delete post
|
||||
case Utils.isKeyPressed(e, Constants.KeyCodes.DELETE):
|
||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.DELETE):
|
||||
this.handleDeleteMenuItemActivated(e);
|
||||
this.handleDropdownOpened(false);
|
||||
break;
|
||||
|
||||
// pin / unpin
|
||||
case Utils.isKeyPressed(e, Constants.KeyCodes.P):
|
||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.P):
|
||||
this.handlePinMenuItemActivated(e);
|
||||
this.handleDropdownOpened(false);
|
||||
break;
|
||||
|
||||
// save / unsave
|
||||
case Utils.isKeyPressed(e, Constants.KeyCodes.S):
|
||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.S):
|
||||
this.handleFlagMenuItemActivated(e);
|
||||
this.handleDropdownOpened(false);
|
||||
break;
|
||||
|
||||
// mark as unread
|
||||
case Utils.isKeyPressed(e, Constants.KeyCodes.U):
|
||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.U):
|
||||
this.handleMarkPostAsUnread(e);
|
||||
this.handleDropdownOpened(false);
|
||||
break;
|
||||
|
||||
@@ -6,7 +6,7 @@ import {connect} from 'react-redux';
|
||||
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import {getChannelURL} from 'utils/utils';
|
||||
import {getChannelURL} from 'selectors/urls';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
|
||||
@@ -73,9 +73,11 @@ exports[`components/drafts/panel/panel_body should have called handleFormattedTe
|
||||
hide={[Function]}
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
@@ -102,6 +104,7 @@ exports[`components/drafts/panel/panel_body should have called handleFormattedTe
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
@@ -351,9 +354,11 @@ exports[`components/drafts/panel/panel_body should match snapshot 1`] = `
|
||||
hide={[Function]}
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
@@ -380,6 +385,7 @@ exports[`components/drafts/panel/panel_body should match snapshot 1`] = `
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
@@ -635,9 +641,11 @@ exports[`components/drafts/panel/panel_body should match snapshot for priority 1
|
||||
hide={[Function]}
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
@@ -664,6 +672,7 @@ exports[`components/drafts/panel/panel_body should match snapshot for priority 1
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
@@ -989,9 +998,11 @@ exports[`components/drafts/panel/panel_body should match snapshot for requested_
|
||||
hide={[Function]}
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
@@ -1018,6 +1029,7 @@ exports[`components/drafts/panel/panel_body should match snapshot for requested_
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
|
||||
@@ -13,8 +13,9 @@ import Textbox, {TextboxElement} from 'components/textbox';
|
||||
import TextboxClass from 'components/textbox/textbox';
|
||||
import TextboxLinks from 'components/textbox/textbox_links';
|
||||
import Constants from 'utils/constants';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
import {isMobile} from 'utils/user_agent';
|
||||
import {insertLineBreakFromKeyEvent, isKeyPressed, isUnhandledLineBreakKeyCombo, localizeMessage} from 'utils/utils';
|
||||
import {insertLineBreakFromKeyEvent, isUnhandledLineBreakKeyCombo, localizeMessage} from 'utils/utils';
|
||||
|
||||
const KeyCodes = Constants.KeyCodes;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {Channel} from '@mattermost/types/channels';
|
||||
import {ActionResult} from 'mattermost-redux/types/actions';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
import * as Keyboard from 'utils/keyboard';
|
||||
import * as Utils from 'utils/utils';
|
||||
|
||||
type Actions = {
|
||||
@@ -68,10 +69,10 @@ export class EditChannelPurposeModal extends React.PureComponent<Props, State> {
|
||||
if (Utils.isUnhandledLineBreakKeyCombo(e)) {
|
||||
e.preventDefault();
|
||||
this.setState({purpose: Utils.insertLineBreakFromKeyEvent(e as React.KeyboardEvent<HTMLTextAreaElement>)});
|
||||
} else if (ctrlSend && Utils.isKeyPressed(e, Constants.KeyCodes.ENTER) && e.ctrlKey) {
|
||||
} else if (ctrlSend && Keyboard.isKeyPressed(e, Constants.KeyCodes.ENTER) && e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
this.handleSave();
|
||||
} else if (!ctrlSend && Utils.isKeyPressed(e, Constants.KeyCodes.ENTER) && !e.shiftKey && !e.altKey) {
|
||||
} else if (!ctrlSend && Keyboard.isKeyPressed(e, Constants.KeyCodes.ENTER) && !e.shiftKey && !e.altKey) {
|
||||
e.preventDefault();
|
||||
this.handleSave();
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {Post} from '@mattermost/types/posts';
|
||||
import {Emoji, SystemEmoji} from '@mattermost/types/emojis';
|
||||
|
||||
import {AppEvents, Constants, ModalIdentifiers, StoragePrefixes} from 'utils/constants';
|
||||
import * as Keyboard from 'utils/keyboard';
|
||||
import {
|
||||
formatGithubCodePaste,
|
||||
formatMarkdownMessage,
|
||||
@@ -220,7 +221,13 @@ const EditPost = ({editingPost, actions, canEditPost, config, channelId, draft,
|
||||
actions.unsetEditingPost();
|
||||
};
|
||||
|
||||
const handleAutomatedRefocusAndExit = () => handleRefocusAndExit(editingPost.refocusId || null);
|
||||
const handleAutomatedRefocusAndExit = () => {
|
||||
draftRef.current = {
|
||||
...draftRef.current,
|
||||
message: '',
|
||||
};
|
||||
handleRefocusAndExit(editingPost.refocusId || null);
|
||||
};
|
||||
|
||||
const handleEdit = async () => {
|
||||
if (!editingPost.post || isSaveDisabled()) {
|
||||
@@ -302,13 +309,13 @@ const EditPost = ({editingPost, actions, canEditPost, config, channelId, draft,
|
||||
const {ctrlSend, codeBlockOnCtrlEnter} = rest;
|
||||
|
||||
const ctrlOrMetaKeyPressed = e.ctrlKey || e.metaKey;
|
||||
const ctrlKeyCombo = Utils.cmdOrCtrlPressed(e) && !e.altKey && !e.shiftKey;
|
||||
const ctrlAltCombo = Utils.cmdOrCtrlPressed(e, true) && e.altKey;
|
||||
const ctrlKeyCombo = Keyboard.cmdOrCtrlPressed(e) && !e.altKey && !e.shiftKey;
|
||||
const ctrlAltCombo = Keyboard.cmdOrCtrlPressed(e, true) && e.altKey;
|
||||
const ctrlEnterKeyCombo =
|
||||
(ctrlSend || codeBlockOnCtrlEnter) &&
|
||||
Utils.isKeyPressed(e, KeyCodes.ENTER) &&
|
||||
Keyboard.isKeyPressed(e, KeyCodes.ENTER) &&
|
||||
ctrlOrMetaKeyPressed;
|
||||
const markdownLinkKey = Utils.isKeyPressed(e, KeyCodes.K);
|
||||
const markdownLinkKey = Keyboard.isKeyPressed(e, KeyCodes.K);
|
||||
|
||||
// listen for line break key combo and insert new line character
|
||||
if (Utils.isUnhandledLineBreakKeyCombo(e)) {
|
||||
@@ -316,7 +323,7 @@ const EditPost = ({editingPost, actions, canEditPost, config, channelId, draft,
|
||||
setEditText(Utils.insertLineBreakFromKeyEvent(e as React.KeyboardEvent<HTMLTextAreaElement>));
|
||||
} else if (ctrlEnterKeyCombo) {
|
||||
handleEdit();
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.ESCAPE) && !showEmojiPicker) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.ESCAPE) && !showEmojiPicker) {
|
||||
handleAutomatedRefocusAndExit();
|
||||
} else if (ctrlAltCombo && markdownLinkKey) {
|
||||
applyHotkeyMarkdown({
|
||||
@@ -325,14 +332,14 @@ const EditPost = ({editingPost, actions, canEditPost, config, channelId, draft,
|
||||
selectionEnd: e.currentTarget.selectionEnd,
|
||||
message: e.currentTarget.value,
|
||||
});
|
||||
} else if (ctrlKeyCombo && Utils.isKeyPressed(e, KeyCodes.B)) {
|
||||
} else if (ctrlKeyCombo && Keyboard.isKeyPressed(e, KeyCodes.B)) {
|
||||
applyHotkeyMarkdown({
|
||||
markdownMode: 'bold',
|
||||
selectionStart: e.currentTarget.selectionStart,
|
||||
selectionEnd: e.currentTarget.selectionEnd,
|
||||
message: e.currentTarget.value,
|
||||
});
|
||||
} else if (ctrlKeyCombo && Utils.isKeyPressed(e, KeyCodes.I)) {
|
||||
} else if (ctrlKeyCombo && Keyboard.isKeyPressed(e, KeyCodes.I)) {
|
||||
applyHotkeyMarkdown({
|
||||
markdownMode: 'italic',
|
||||
selectionStart: e.currentTarget.selectionStart,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {FormattedMessage} from 'react-intl';
|
||||
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
|
||||
import {isMac} from 'utils/utils';
|
||||
import {isMac} from 'utils/user_agent';
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -6,7 +6,7 @@ import React from 'react';
|
||||
import {injectIntl, WrappedComponentProps} from 'react-intl';
|
||||
|
||||
import {Feedback} from '@mattermost/types/cloud';
|
||||
import FeedbackModal from 'components/feedback_modal/feedback';
|
||||
import FeedbackModal, {FeedbackOption} from 'components/feedback_modal/feedback';
|
||||
|
||||
type Props = {
|
||||
onSubmit: (downgradeFeedback: Feedback) => void;
|
||||
@@ -28,23 +28,35 @@ const DowngradeFeedbackModal = (props: Props) => {
|
||||
defaultMessage: 'Downgrade',
|
||||
});
|
||||
|
||||
const downgradeFeedbackOptions = [
|
||||
props.intl.formatMessage({
|
||||
id: 'feedback.downgradeWorkspace.technicalIssues',
|
||||
defaultMessage: 'Experienced technical issues',
|
||||
}),
|
||||
props.intl.formatMessage({
|
||||
id: 'feedback.downgradeWorkspace.noLongerNeeded',
|
||||
defaultMessage: 'No longer need Cloud Professional features',
|
||||
}),
|
||||
props.intl.formatMessage({
|
||||
id: 'feedback.downgradeWorkspace.exploringOptions',
|
||||
defaultMessage: 'Exploring other solutions',
|
||||
}),
|
||||
props.intl.formatMessage({
|
||||
id: 'feedback.downgradeWorkspace.tooExpensive',
|
||||
defaultMessage: 'Too expensive',
|
||||
}),
|
||||
const downgradeFeedbackOptions: FeedbackOption[] = [
|
||||
{
|
||||
translatedMessage: props.intl.formatMessage({
|
||||
id: 'feedback.downgradeWorkspace.technicalIssues',
|
||||
defaultMessage: 'Experienced technical issues',
|
||||
}),
|
||||
submissionValue: 'Experienced technical issues',
|
||||
},
|
||||
{
|
||||
translatedMessage: props.intl.formatMessage({
|
||||
id: 'feedback.downgradeWorkspace.noLongerNeeded',
|
||||
defaultMessage: 'No longer need Cloud Professional features',
|
||||
}),
|
||||
submissionValue: 'No longer need Cloud Professional features',
|
||||
},
|
||||
{
|
||||
translatedMessage: props.intl.formatMessage({
|
||||
id: 'feedback.downgradeWorkspace.exploringOptions',
|
||||
defaultMessage: 'Exploring other solutions',
|
||||
}),
|
||||
submissionValue: 'Exploring other solutions',
|
||||
},
|
||||
{
|
||||
translatedMessage: props.intl.formatMessage({
|
||||
id: 'feedback.downgradeWorkspace.tooExpensive',
|
||||
defaultMessage: 'Too expensive',
|
||||
}),
|
||||
submissionValue: 'Too expensive',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -15,18 +15,23 @@ import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
import './feedback.scss';
|
||||
|
||||
export interface FeedbackOption {
|
||||
translatedMessage: string;
|
||||
submissionValue: string;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
onSubmit: (deleteFeedback: Feedback) => void;
|
||||
title: string;
|
||||
submitText: string;
|
||||
feedbackOptions: string[];
|
||||
feedbackOptions: FeedbackOption[];
|
||||
freeformTextPlaceholder: string;
|
||||
} & WrappedComponentProps
|
||||
|
||||
function FeedbackModal(props: Props) {
|
||||
const maxFreeFormTextLength = 500;
|
||||
const optionOther = props.intl.formatMessage({id: 'feedback.other', defaultMessage: 'Other'});
|
||||
const feedbackModalOptions: string[] = [
|
||||
const optionOther = {translatedMessage: props.intl.formatMessage({id: 'feedback.other', defaultMessage: 'Other'}), submissionValue: 'Other'};
|
||||
const feedbackModalOptions: FeedbackOption[] = [
|
||||
...props.feedbackOptions,
|
||||
optionOther,
|
||||
];
|
||||
@@ -34,9 +39,9 @@ function FeedbackModal(props: Props) {
|
||||
const [reason, setReason] = useState('');
|
||||
const [comments, setComments] = useState('');
|
||||
const reasonNotSelected = reason === '';
|
||||
const reasonOther = reason === optionOther;
|
||||
const reasonOther = reason === optionOther.submissionValue;
|
||||
const commentsNotProvided = comments.trim() === '';
|
||||
const submitDisabled = reasonNotSelected || (reason === optionOther && commentsNotProvided);
|
||||
const submitDisabled = reasonNotSelected || (reasonOther && commentsNotProvided);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -71,15 +76,15 @@ function FeedbackModal(props: Props) {
|
||||
testId='FeedbackModalRadioGroup'
|
||||
values={feedbackModalOptions.map((option) => {
|
||||
return {
|
||||
value: option,
|
||||
key: option,
|
||||
testId: option,
|
||||
value: option.submissionValue,
|
||||
key: option.translatedMessage,
|
||||
testId: option.submissionValue,
|
||||
};
|
||||
})}
|
||||
value={reason}
|
||||
onChange={(e) => setReason(e.target.value)}
|
||||
/>
|
||||
{reason === optionOther &&
|
||||
{reasonOther &&
|
||||
<>
|
||||
<textarea
|
||||
data-testid={'FeedbackModal__TextInput'}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {Post} from '@mattermost/types/posts';
|
||||
import {getFileDownloadUrl, getFilePreviewUrl, getFileUrl} from 'mattermost-redux/utils/file_utils';
|
||||
import LoadingImagePreview from 'components/loading_image_preview';
|
||||
import Constants, {FileTypes, ZoomSettings} from 'utils/constants';
|
||||
import * as Keyboard from 'utils/keyboard';
|
||||
import * as Utils from 'utils/utils';
|
||||
import AudioVideoPreview from 'components/audio_video_preview';
|
||||
import CodePreview from 'components/code_preview';
|
||||
@@ -115,9 +116,9 @@ export default class FilePreviewModal extends React.PureComponent<Props, State>
|
||||
};
|
||||
|
||||
handleKeyPress = (e: KeyboardEvent) => {
|
||||
if (Utils.isKeyPressed(e, KeyCodes.RIGHT)) {
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.RIGHT)) {
|
||||
this.handleNext();
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.LEFT)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.LEFT)) {
|
||||
this.handlePrev();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import dragster from 'utils/dragster';
|
||||
import Constants from 'utils/constants';
|
||||
import DelayedAction from 'utils/delayed_action';
|
||||
import {t} from 'utils/i18n';
|
||||
import {cmdOrCtrlPressed, isKeyPressed} from 'utils/keyboard';
|
||||
import {
|
||||
isIosChrome,
|
||||
isMobileApp,
|
||||
@@ -19,8 +20,6 @@ import {
|
||||
import {getTable} from 'utils/paste';
|
||||
import {
|
||||
clearFileInput,
|
||||
cmdOrCtrlPressed,
|
||||
isKeyPressed,
|
||||
generateId,
|
||||
isFileTransfer,
|
||||
isUriDrop,
|
||||
|
||||
@@ -12,6 +12,7 @@ import Textbox, {TextboxClass, TextboxElement} from 'components/textbox';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
import {applyMarkdown, ApplyMarkdownOptions} from 'utils/markdown/apply_markdown';
|
||||
import * as Keyboard from 'utils/keyboard';
|
||||
import * as Utils from 'utils/utils';
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
@@ -77,13 +78,13 @@ const ForwardPostCommentInput = ({channelId, canForwardPost, comment, permaLinkL
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<TextboxElement>) => {
|
||||
const ctrlKeyCombo = Utils.cmdOrCtrlPressed(e) && !e.altKey && !e.shiftKey;
|
||||
const ctrlAltCombo = Utils.cmdOrCtrlPressed(e, true) && e.altKey;
|
||||
const ctrlShiftCombo = Utils.cmdOrCtrlPressed(e, true) && e.shiftKey;
|
||||
const markdownLinkKey = Utils.isKeyPressed(e, KeyCodes.K);
|
||||
const ctrlKeyCombo = Keyboard.cmdOrCtrlPressed(e) && !e.altKey && !e.shiftKey;
|
||||
const ctrlAltCombo = Keyboard.cmdOrCtrlPressed(e, true) && e.altKey;
|
||||
const ctrlShiftCombo = Keyboard.cmdOrCtrlPressed(e, true) && e.shiftKey;
|
||||
const markdownLinkKey = Keyboard.isKeyPressed(e, KeyCodes.K);
|
||||
const ctrlOrMetaKeyPressed = e.ctrlKey || e.metaKey;
|
||||
const ctrlEnterKeyCombo =
|
||||
Utils.isKeyPressed(e, KeyCodes.ENTER) && ctrlOrMetaKeyPressed;
|
||||
Keyboard.isKeyPressed(e, KeyCodes.ENTER) && ctrlOrMetaKeyPressed;
|
||||
|
||||
const {selectionStart, selectionEnd, value} =
|
||||
e.target as TextboxElement;
|
||||
@@ -98,28 +99,28 @@ const ForwardPostCommentInput = ({channelId, canForwardPost, comment, permaLinkL
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (ctrlKeyCombo && Utils.isKeyPressed(e, KeyCodes.B)) {
|
||||
} else if (ctrlKeyCombo && Keyboard.isKeyPressed(e, KeyCodes.B)) {
|
||||
applyMarkdownMode({
|
||||
markdownMode: 'bold',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (ctrlKeyCombo && Utils.isKeyPressed(e, KeyCodes.I)) {
|
||||
} else if (ctrlKeyCombo && Keyboard.isKeyPressed(e, KeyCodes.I)) {
|
||||
applyMarkdownMode({
|
||||
markdownMode: 'italic',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (ctrlShiftCombo && Utils.isKeyPressed(e, KeyCodes.X)) {
|
||||
} else if (ctrlShiftCombo && Keyboard.isKeyPressed(e, KeyCodes.X)) {
|
||||
applyMarkdownMode({
|
||||
markdownMode: 'strike',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (ctrlShiftCombo && Utils.isKeyPressed(e, KeyCodes.E)) {
|
||||
} else if (ctrlShiftCombo && Keyboard.isKeyPressed(e, KeyCodes.E)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
} else if (ctrlEnterKeyCombo && canForwardPost) {
|
||||
|
||||
@@ -16,6 +16,8 @@ import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import NotificationBox from 'components/notification_box';
|
||||
|
||||
import {getPermalinkURL} from 'selectors/urls';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||
@@ -28,7 +30,6 @@ import GenericModal from 'components/generic_modal';
|
||||
|
||||
import {PostPreviewMetadata} from '@mattermost/types/posts';
|
||||
import {getSiteURL} from '../../utils/url';
|
||||
import * as Utils from '../../utils/utils';
|
||||
|
||||
import ForwardPostChannelSelect, {ChannelOption, makeSelectedChannelOption} from './forward_post_channel_select';
|
||||
import ForwardPostCommentInput from './forward_post_comment_input';
|
||||
@@ -49,7 +50,7 @@ const ForwardPostModal = ({onExited, post, actions}: Props) => {
|
||||
const channel = useSelector((state: GlobalState) => getChannel(state, {id: post.channel_id}));
|
||||
const currentTeam = useSelector(getCurrentTeam);
|
||||
|
||||
const relativePermaLink = useSelector((state: GlobalState) => Utils.getPermalinkURL(state, currentTeam.id, post.id));
|
||||
const relativePermaLink = useSelector((state: GlobalState) => getPermalinkURL(state, currentTeam.id, post.id));
|
||||
const permaLink = `${getSiteURL()}${relativePermaLink}`;
|
||||
|
||||
const isPrivateConversation = channel.type !== Constants.OPEN_CHANNEL;
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
Constants,
|
||||
RHSStates,
|
||||
} from 'utils/constants';
|
||||
import * as Utils from 'utils/utils';
|
||||
import * as Keyboard from 'utils/keyboard';
|
||||
|
||||
const GlobalSearchNav = (): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -25,8 +25,8 @@ const GlobalSearchNav = (): JSX.Element => {
|
||||
|
||||
useEffect(() => {
|
||||
const handleShortcut = (e: KeyboardEvent) => {
|
||||
if (Utils.cmdOrCtrlPressed(e) && e.shiftKey) {
|
||||
if (Utils.isKeyPressed(e, Constants.KeyCodes.M)) {
|
||||
if (Keyboard.cmdOrCtrlPressed(e) && e.shiftKey) {
|
||||
if (Keyboard.isKeyPressed(e, Constants.KeyCodes.M)) {
|
||||
e.preventDefault();
|
||||
if (rhsState === RHSStates.MENTION) {
|
||||
dispatch(closeRightHandSide());
|
||||
|
||||
@@ -4,9 +4,8 @@ exports[`components/ProductBranding should show correct icon glyph when we are o
|
||||
<ProductBrandingContainer
|
||||
tabIndex={0}
|
||||
>
|
||||
<Icon
|
||||
glyph="product-boards"
|
||||
size={20}
|
||||
<ProductBoardsIcon
|
||||
size={24}
|
||||
/>
|
||||
<Heading
|
||||
element="h1"
|
||||
@@ -22,9 +21,8 @@ exports[`components/ProductBranding should show correct icon glyph when we are o
|
||||
<ProductBrandingContainer
|
||||
tabIndex={0}
|
||||
>
|
||||
<Icon
|
||||
glyph="product-channels"
|
||||
size={20}
|
||||
<ProductChannelsIcon
|
||||
size={24}
|
||||
/>
|
||||
<Heading
|
||||
element="h1"
|
||||
@@ -40,9 +38,8 @@ exports[`components/ProductBranding should show correct icon glyph when we are o
|
||||
<ProductBrandingContainer
|
||||
tabIndex={0}
|
||||
>
|
||||
<Icon
|
||||
glyph="product-playbooks"
|
||||
size={20}
|
||||
<ProductPlaybooksIcon
|
||||
size={24}
|
||||
/>
|
||||
<Heading
|
||||
element="h1"
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
import Icon from '@mattermost/compass-components/foundations/icon'; // eslint-disable-line no-restricted-imports
|
||||
|
||||
import {TopLevelProducts} from 'utils/constants';
|
||||
|
||||
@@ -21,7 +20,6 @@ describe('components/ProductBranding', () => {
|
||||
<ProductBranding/>,
|
||||
);
|
||||
|
||||
expect(wrapper.find(Icon).prop('glyph')).toEqual('product-channels');
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -32,7 +30,6 @@ describe('components/ProductBranding', () => {
|
||||
<ProductBranding/>,
|
||||
);
|
||||
|
||||
expect(wrapper.find(Icon).prop('glyph')).toEqual('product-playbooks');
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -44,7 +41,6 @@ describe('components/ProductBranding', () => {
|
||||
<ProductBranding/>,
|
||||
);
|
||||
|
||||
expect(wrapper.find(Icon).prop('glyph')).toEqual('product-boards');
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import Icon from '@mattermost/compass-components/foundations/icon'; // eslint-disable-line no-restricted-imports
|
||||
import glyphMap, {ProductChannelsIcon} from '@mattermost/compass-icons/components';
|
||||
import Heading from '@mattermost/compass-components/components/heading'; // eslint-disable-line no-restricted-imports
|
||||
|
||||
import {useCurrentProduct} from 'utils/products';
|
||||
@@ -20,12 +20,11 @@ const ProductBrandingContainer = styled.div`
|
||||
const ProductBranding = (): JSX.Element => {
|
||||
const currentProduct = useCurrentProduct();
|
||||
|
||||
const Icon = currentProduct?.switcherIcon ? glyphMap[currentProduct.switcherIcon] : ProductChannelsIcon;
|
||||
|
||||
return (
|
||||
<ProductBrandingContainer tabIndex={0}>
|
||||
<Icon
|
||||
size={20}
|
||||
glyph={currentProduct && typeof currentProduct.switcherIcon === 'string' ? currentProduct.switcherIcon : 'product-channels'}
|
||||
/>
|
||||
<Icon size={24}/>
|
||||
<Heading
|
||||
element='h1'
|
||||
size={200}
|
||||
|
||||
@@ -7,11 +7,11 @@ import {Link} from 'react-router-dom';
|
||||
|
||||
import styled from 'styled-components';
|
||||
|
||||
import Icon, {TIconGlyph} from '@mattermost/compass-components/foundations/icon'; // eslint-disable-line no-restricted-imports
|
||||
|
||||
import {IconGlyphTypes} from '@mattermost/compass-icons/IconGlyphs';
|
||||
import glyphMap, {CheckIcon} from '@mattermost/compass-icons/components';
|
||||
export interface ProductMenuItemProps {
|
||||
destination: string;
|
||||
icon: TIconGlyph;
|
||||
icon: IconGlyphTypes;
|
||||
text: React.ReactNode;
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
@@ -46,10 +46,6 @@ const MenuItem = styled(Link)`
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledIcon = styled(Icon)`
|
||||
color: var(--button-bg);
|
||||
`;
|
||||
|
||||
const MenuItemTextContainer = styled.div`
|
||||
margin-left: 8px;
|
||||
flex-grow: 1;
|
||||
@@ -58,27 +54,31 @@ const MenuItemTextContainer = styled.div`
|
||||
line-height: 20px;
|
||||
`;
|
||||
|
||||
const ProductMenuItem = ({icon, destination, text, active, onClick, tourTip, id}: ProductMenuItemProps): JSX.Element => (
|
||||
<MenuItem
|
||||
to={destination}
|
||||
onClick={onClick}
|
||||
id={id}
|
||||
>
|
||||
<StyledIcon
|
||||
size={20}
|
||||
glyph={icon || 'none'}
|
||||
/>
|
||||
<MenuItemTextContainer>
|
||||
{text}
|
||||
</MenuItemTextContainer>
|
||||
{active && (
|
||||
<StyledIcon
|
||||
size={16}
|
||||
glyph='check'
|
||||
const ProductMenuItem = ({icon, destination, text, active, onClick, tourTip, id}: ProductMenuItemProps): JSX.Element => {
|
||||
const ProductIcon = glyphMap[icon];
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
to={destination}
|
||||
onClick={onClick}
|
||||
id={id}
|
||||
>
|
||||
<ProductIcon
|
||||
size={24}
|
||||
color={'var(--button-bg)'}
|
||||
/>
|
||||
)}
|
||||
{tourTip || null}
|
||||
</MenuItem>
|
||||
);
|
||||
<MenuItemTextContainer>
|
||||
{text}
|
||||
</MenuItemTextContainer>
|
||||
{active && (
|
||||
<CheckIcon
|
||||
size={18}
|
||||
color={'var(--button-bg)'}
|
||||
/>
|
||||
)}
|
||||
{tourTip || null}
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductMenuItem;
|
||||
|
||||
@@ -84,9 +84,8 @@ exports[`components/global/product_switcher_menu should match snapshot with id 1
|
||||
>
|
||||
<MenuItemLink
|
||||
icon={
|
||||
<Icon
|
||||
glyph="application-cog"
|
||||
size={16}
|
||||
<ApplicationCogIcon
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
id="systemConsole"
|
||||
@@ -101,9 +100,8 @@ exports[`components/global/product_switcher_menu should match snapshot with id 1
|
||||
</Connect(SystemPermissionGate)>
|
||||
<MenuItemLink
|
||||
icon={
|
||||
<Icon
|
||||
glyph="webhook-incoming"
|
||||
size={16}
|
||||
<WebhookIncomingIcon
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
id="integrations"
|
||||
@@ -127,9 +125,8 @@ exports[`components/global/product_switcher_menu should match snapshot with id 1
|
||||
}
|
||||
disabled={false}
|
||||
icon={
|
||||
<Icon
|
||||
glyph="account-multiple-outline"
|
||||
size={16}
|
||||
<AccountMultipleOutlineIcon
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
id="userGroups"
|
||||
@@ -154,9 +151,8 @@ exports[`components/global/product_switcher_menu should match snapshot with id 1
|
||||
}
|
||||
dialogType={[Function]}
|
||||
icon={
|
||||
<Icon
|
||||
glyph="view-grid-plus-outline"
|
||||
size={16}
|
||||
<ViewGridPlusOutlineIcon
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
id="marketplaceModal"
|
||||
@@ -167,9 +163,8 @@ exports[`components/global/product_switcher_menu should match snapshot with id 1
|
||||
</Connect(TeamPermissionGate)>
|
||||
<MenuItemExternalLink
|
||||
icon={
|
||||
<Icon
|
||||
glyph="download-outline"
|
||||
size={16}
|
||||
<DownloadOutlineIcon
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
id="nativeAppLink"
|
||||
@@ -187,9 +182,8 @@ exports[`components/global/product_switcher_menu should match snapshot with id 1
|
||||
}
|
||||
}
|
||||
icon={
|
||||
<Icon
|
||||
glyph="information-outline"
|
||||
size={16}
|
||||
<InformationOutlineIcon
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
id="about"
|
||||
@@ -285,9 +279,8 @@ exports[`components/global/product_switcher_menu should match snapshot with most
|
||||
>
|
||||
<MenuItemLink
|
||||
icon={
|
||||
<Icon
|
||||
glyph="application-cog"
|
||||
size={16}
|
||||
<ApplicationCogIcon
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
id="systemConsole"
|
||||
@@ -302,9 +295,8 @@ exports[`components/global/product_switcher_menu should match snapshot with most
|
||||
</Connect(SystemPermissionGate)>
|
||||
<MenuItemLink
|
||||
icon={
|
||||
<Icon
|
||||
glyph="webhook-incoming"
|
||||
size={16}
|
||||
<WebhookIncomingIcon
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
id="integrations"
|
||||
@@ -328,9 +320,8 @@ exports[`components/global/product_switcher_menu should match snapshot with most
|
||||
}
|
||||
disabled={false}
|
||||
icon={
|
||||
<Icon
|
||||
glyph="account-multiple-outline"
|
||||
size={16}
|
||||
<AccountMultipleOutlineIcon
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
id="userGroups"
|
||||
@@ -355,9 +346,8 @@ exports[`components/global/product_switcher_menu should match snapshot with most
|
||||
}
|
||||
dialogType={[Function]}
|
||||
icon={
|
||||
<Icon
|
||||
glyph="view-grid-plus-outline"
|
||||
size={16}
|
||||
<ViewGridPlusOutlineIcon
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
id="marketplaceModal"
|
||||
@@ -368,9 +358,8 @@ exports[`components/global/product_switcher_menu should match snapshot with most
|
||||
</Connect(TeamPermissionGate)>
|
||||
<MenuItemExternalLink
|
||||
icon={
|
||||
<Icon
|
||||
glyph="download-outline"
|
||||
size={16}
|
||||
<DownloadOutlineIcon
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
id="nativeAppLink"
|
||||
@@ -388,9 +377,8 @@ exports[`components/global/product_switcher_menu should match snapshot with most
|
||||
}
|
||||
}
|
||||
icon={
|
||||
<Icon
|
||||
glyph="information-outline"
|
||||
size={16}
|
||||
<InformationOutlineIcon
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
id="about"
|
||||
@@ -419,9 +407,8 @@ exports[`components/global/product_switcher_menu should match userGroups snapsho
|
||||
}
|
||||
disabled={false}
|
||||
icon={
|
||||
<Icon
|
||||
glyph="account-multiple-outline"
|
||||
size={16}
|
||||
<AccountMultipleOutlineIcon
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
id="userGroups"
|
||||
@@ -449,9 +436,8 @@ exports[`components/global/product_switcher_menu should match userGroups snapsho
|
||||
}
|
||||
disabled={true}
|
||||
icon={
|
||||
<Icon
|
||||
glyph="account-multiple-outline"
|
||||
size={16}
|
||||
<AccountMultipleOutlineIcon
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
id="userGroups"
|
||||
@@ -492,9 +478,8 @@ exports[`components/global/product_switcher_menu should match userGroups snapsho
|
||||
}
|
||||
disabled={false}
|
||||
icon={
|
||||
<Icon
|
||||
glyph="account-multiple-outline"
|
||||
size={16}
|
||||
<AccountMultipleOutlineIcon
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
id="userGroups"
|
||||
@@ -606,9 +591,8 @@ exports[`components/global/product_switcher_menu should show integrations should
|
||||
>
|
||||
<MenuItemLink
|
||||
icon={
|
||||
<Icon
|
||||
glyph="application-cog"
|
||||
size={16}
|
||||
<ApplicationCogIcon
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
id="systemConsole"
|
||||
@@ -623,9 +607,8 @@ exports[`components/global/product_switcher_menu should show integrations should
|
||||
</Connect(SystemPermissionGate)>
|
||||
<MenuItemLink
|
||||
icon={
|
||||
<Icon
|
||||
glyph="webhook-incoming"
|
||||
size={16}
|
||||
<WebhookIncomingIcon
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
id="integrations"
|
||||
@@ -649,9 +632,8 @@ exports[`components/global/product_switcher_menu should show integrations should
|
||||
}
|
||||
disabled={false}
|
||||
icon={
|
||||
<Icon
|
||||
glyph="account-multiple-outline"
|
||||
size={16}
|
||||
<AccountMultipleOutlineIcon
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
id="userGroups"
|
||||
@@ -676,9 +658,8 @@ exports[`components/global/product_switcher_menu should show integrations should
|
||||
}
|
||||
dialogType={[Function]}
|
||||
icon={
|
||||
<Icon
|
||||
glyph="view-grid-plus-outline"
|
||||
size={16}
|
||||
<ViewGridPlusOutlineIcon
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
id="marketplaceModal"
|
||||
@@ -689,9 +670,8 @@ exports[`components/global/product_switcher_menu should show integrations should
|
||||
</Connect(TeamPermissionGate)>
|
||||
<MenuItemExternalLink
|
||||
icon={
|
||||
<Icon
|
||||
glyph="download-outline"
|
||||
size={16}
|
||||
<DownloadOutlineIcon
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
id="nativeAppLink"
|
||||
@@ -709,9 +689,8 @@ exports[`components/global/product_switcher_menu should show integrations should
|
||||
}
|
||||
}
|
||||
icon={
|
||||
<Icon
|
||||
glyph="information-outline"
|
||||
size={16}
|
||||
<InformationOutlineIcon
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
id="about"
|
||||
|
||||
@@ -3,8 +3,14 @@
|
||||
|
||||
import React, {useEffect} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import Icon from '@mattermost/compass-components/foundations/icon'; // eslint-disable-line no-restricted-imports
|
||||
import {
|
||||
AccountMultipleOutlineIcon,
|
||||
ApplicationCogIcon,
|
||||
DownloadOutlineIcon,
|
||||
InformationOutlineIcon,
|
||||
ViewGridPlusOutlineIcon,
|
||||
WebhookIncomingIcon,
|
||||
} from '@mattermost/compass-icons/components';
|
||||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
|
||||
@@ -128,12 +134,7 @@ const ProductMenuList = (props: Props): JSX.Element | null => {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
icon={
|
||||
<Icon
|
||||
size={16}
|
||||
glyph={'application-cog'}
|
||||
/>
|
||||
}
|
||||
icon={<ApplicationCogIcon size={18}/>}
|
||||
/>
|
||||
</SystemPermissionGate>
|
||||
<Menu.ItemLink
|
||||
@@ -141,12 +142,7 @@ const ProductMenuList = (props: Props): JSX.Element | null => {
|
||||
show={isMessaging && showIntegrations}
|
||||
to={'/' + teamName + '/integrations'}
|
||||
text={formatMessage({id: 'navbar_dropdown.integrations', defaultMessage: 'Integrations'})}
|
||||
icon={
|
||||
<Icon
|
||||
size={16}
|
||||
glyph={'webhook-incoming'}
|
||||
/>
|
||||
}
|
||||
icon={<WebhookIncomingIcon size={18}/>}
|
||||
/>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='userGroups'
|
||||
@@ -157,12 +153,7 @@ const ProductMenuList = (props: Props): JSX.Element | null => {
|
||||
backButtonAction: openGroupsModal,
|
||||
}}
|
||||
text={formatMessage({id: 'navbar_dropdown.userGroups', defaultMessage: 'User Groups'})}
|
||||
icon={
|
||||
<Icon
|
||||
size={16}
|
||||
glyph={'account-multiple-outline'}
|
||||
/>
|
||||
}
|
||||
icon={<AccountMultipleOutlineIcon size={18}/>}
|
||||
disabled={isStarterFree}
|
||||
sibling={(isStarterFree || isFreeTrial) && (
|
||||
<RestrictedIndicator
|
||||
@@ -215,12 +206,7 @@ const ProductMenuList = (props: Props): JSX.Element | null => {
|
||||
dialogType={MarketplaceModal}
|
||||
dialogProps={{openedFrom: 'product_menu'}}
|
||||
text={formatMessage({id: 'navbar_dropdown.marketplace', defaultMessage: 'App Marketplace'})}
|
||||
icon={
|
||||
<Icon
|
||||
size={16}
|
||||
glyph='view-grid-plus-outline'
|
||||
/>
|
||||
}
|
||||
icon={<ViewGridPlusOutlineIcon size={18}/>}
|
||||
/>
|
||||
</TeamPermissionGate>
|
||||
<Menu.ItemExternalLink
|
||||
@@ -228,24 +214,14 @@ const ProductMenuList = (props: Props): JSX.Element | null => {
|
||||
show={appDownloadLink && !UserAgent.isMobileApp()}
|
||||
url={makeUrlSafe(appDownloadLink)}
|
||||
text={formatMessage({id: 'navbar_dropdown.nativeApps', defaultMessage: 'Download Apps'})}
|
||||
icon={
|
||||
<Icon
|
||||
size={16}
|
||||
glyph={'download-outline'}
|
||||
/>
|
||||
}
|
||||
icon={<DownloadOutlineIcon size={18}/>}
|
||||
/>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='about'
|
||||
modalId={ModalIdentifiers.ABOUT}
|
||||
dialogType={AboutBuildModal}
|
||||
text={formatMessage({id: 'navbar_dropdown.about', defaultMessage: 'About {appTitle}'}, {appTitle: siteName})}
|
||||
icon={
|
||||
<Icon
|
||||
size={16}
|
||||
glyph={'information-outline'}
|
||||
/>
|
||||
}
|
||||
icon={<InformationOutlineIcon size={18}/>}
|
||||
/>
|
||||
</div>
|
||||
</Menu.Group>
|
||||
|
||||
@@ -11,7 +11,7 @@ import {GlobalState} from 'types/store';
|
||||
import {suitePluginIds} from 'utils/constants';
|
||||
|
||||
import {t} from 'utils/i18n';
|
||||
import * as Utils from 'utils/utils';
|
||||
import * as UserAgent from 'utils/user_agent';
|
||||
|
||||
import KeyboardShortcutSequence, {
|
||||
KEYBOARD_SHORTCUTS,
|
||||
@@ -91,7 +91,7 @@ const KeyboardShortcutsModal = ({onExited}: Props): JSX.Element => {
|
||||
|
||||
const handleHide = useCallback(() => setShow(false), []);
|
||||
|
||||
const isLinux = Utils.isLinux();
|
||||
const isLinux = UserAgent.isLinux();
|
||||
|
||||
const isCallsEnabled = useSelector((state: GlobalState) => {
|
||||
return Boolean(state.plugins.plugins[suitePluginIds.calls]);
|
||||
|
||||
@@ -6,7 +6,7 @@ import React, {memo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import {ShortcutKeyVariant, ShortcutKey} from 'components/shortcut_key';
|
||||
import {isMac} from 'utils/utils';
|
||||
import {isMac} from 'utils/user_agent';
|
||||
|
||||
import {isMessageDescriptor, KeyboardShortcutDescriptor} from './keyboard_shortcuts';
|
||||
|
||||
|
||||
@@ -12,8 +12,7 @@ import * as UserUtils from 'mattermost-redux/utils/user_utils';
|
||||
import FormattedMarkdownMessage from 'components/formatted_markdown_message';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
|
||||
import {isKeyPressed} from 'utils/utils';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
|
||||
type Props = {
|
||||
currentUser: UserProfile;
|
||||
|
||||
@@ -16,9 +16,10 @@ import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'
|
||||
import {getLicense, getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getCurrentUser, shouldShowTermsOfService} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {getChannelURL} from 'selectors/urls';
|
||||
|
||||
import {getHistory} from 'utils/browser_history';
|
||||
import {checkIfMFARequired} from 'utils/route';
|
||||
import {getChannelURL} from 'utils/utils';
|
||||
import {isPermalinkURL} from 'utils/url';
|
||||
|
||||
import LoggedIn from './logged_in';
|
||||
|
||||
@@ -9,7 +9,7 @@ import {Permissions} from 'mattermost-redux/constants';
|
||||
import * as GlobalActions from 'actions/global_actions';
|
||||
import {FREEMIUM_TO_ENTERPRISE_TRIAL_LENGTH_DAYS} from 'utils/cloud_utils';
|
||||
import {Constants, LicenseSkus, ModalIdentifiers, MattermostFeatures} from 'utils/constants';
|
||||
import {cmdOrCtrlPressed, isKeyPressed} from 'utils/utils';
|
||||
import {cmdOrCtrlPressed, isKeyPressed} from 'utils/keyboard';
|
||||
import {makeUrlSafe} from 'utils/url';
|
||||
import * as UserAgent from 'utils/user_agent';
|
||||
import InvitationModal from 'components/invitation_modal';
|
||||
|
||||
@@ -20,7 +20,7 @@ import {getIsMobileView} from 'selectors/views/browser';
|
||||
import {openModal, closeModal} from 'actions/views/modals';
|
||||
|
||||
import Constants, {A11yClassNames} from 'utils/constants';
|
||||
import {isKeyPressed} from 'utils/utils';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
|
||||
import CompassDesignProvider from 'components/compass_design_provider';
|
||||
import Tooltip from 'components/tooltip';
|
||||
|
||||
@@ -7,7 +7,7 @@ import MuiMenuItem from '@mui/material/MenuItem';
|
||||
import type {MenuItemProps as MuiMenuItemProps} from '@mui/material/MenuItem';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
import {isKeyPressed} from 'utils/utils';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
|
||||
export interface Props extends MuiMenuItemProps {
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import {isAnyModalOpen} from 'selectors/views/modals';
|
||||
import {openModal, closeModal} from 'actions/views/modals';
|
||||
|
||||
import Constants, {A11yClassNames} from 'utils/constants';
|
||||
import {isKeyPressed} from 'utils/utils';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
|
||||
import CompassDesignProvider from 'components/compass_design_provider';
|
||||
import GenericModal from 'components/generic_modal';
|
||||
|
||||
@@ -5,7 +5,7 @@ import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
import {isKeyPressed} from 'utils/utils';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
|
||||
import {redirectUserToDefaultTeam} from 'actions/global_actions';
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {getOptionValue} from 'react-select/src/builtins';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
import {cmdOrCtrlPressed} from 'utils/utils';
|
||||
import {cmdOrCtrlPressed} from 'utils/keyboard';
|
||||
|
||||
import LoadingScreen from 'components/loading_screen';
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import React, {memo, useEffect, useCallback} from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
import {isKeyPressed} from 'utils/utils';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
|
||||
import Toast from 'components/toast/toast';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import React, {useCallback, useEffect, useRef, useState, ReactNode} from 'react';
|
||||
import {Tabs, Tab, SelectCallback} from 'react-bootstrap';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
@@ -40,7 +40,7 @@ const MarketplaceTabs = {
|
||||
|
||||
const SEARCH_TIMEOUT_MILLISECONDS = 200;
|
||||
|
||||
const linkConsole = (msg: string) => (
|
||||
const linkConsole = (msg: string): ReactNode => (
|
||||
<Link to='/admin_console/plugins/plugin_management'>
|
||||
{msg}
|
||||
</Link>
|
||||
|
||||
@@ -6,7 +6,8 @@ import {FormattedMessage} from 'react-intl';
|
||||
import {Moment} from 'moment-timezone';
|
||||
|
||||
import GenericModal from 'components/generic_modal';
|
||||
import {isKeyPressed, localizeMessage} from 'utils/utils';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
import {localizeMessage} from 'utils/utils';
|
||||
import DateTimeInput, {getRoundedTime} from 'components/custom_status/date_time_input';
|
||||
|
||||
import {toUTCUnix} from 'utils/datetime';
|
||||
|
||||
@@ -1,36 +1,24 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useRef} from 'react';
|
||||
import {useSelector} from 'react-redux';
|
||||
import {useIntl} from 'react-intl';
|
||||
import React from 'react';
|
||||
|
||||
import {Post} from '@mattermost/types/posts';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
import {makeCreateAriaLabelForPost} from 'utils/post_utils';
|
||||
import {usePostAriaLabel} from 'utils/post_utils';
|
||||
|
||||
export type Props = React.HTMLProps<HTMLDivElement> & {
|
||||
labelPrefix?: string;
|
||||
post: Post;
|
||||
}
|
||||
|
||||
const PostAriaLabelDiv = React.forwardRef((props: Props, ref: React.Ref<HTMLDivElement>) => {
|
||||
const {
|
||||
children,
|
||||
labelPrefix,
|
||||
post,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
const createAriaLabelForPost = useRef(makeCreateAriaLabelForPost());
|
||||
let ariaLabel = useSelector<GlobalState, string>((state) => createAriaLabelForPost.current(state, post)(intl));
|
||||
if (labelPrefix) {
|
||||
ariaLabel = labelPrefix + ariaLabel;
|
||||
}
|
||||
const ariaLabel = usePostAriaLabel(post);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
|
||||
import React, {MouseEvent} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import Icon from '@mattermost/compass-components/foundations/icon'; // eslint-disable-line no-restricted-imports
|
||||
|
||||
import {getDateForTimezone} from 'mattermost-redux/utils/timezone_utils';
|
||||
import {isSameDay, isWithinLastWeek, isYesterday} from 'utils/datetime';
|
||||
|
||||
import {PencilOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
|
||||
import OverlayTrigger from '../../overlay_trigger';
|
||||
import Tooltip from '../../tooltip';
|
||||
|
||||
@@ -87,10 +88,7 @@ const PostEditedIndicator = ({postId, isMilitaryTime, timeZone, editedAt = 0, po
|
||||
data-post-id={postId}
|
||||
data-edited-at={editedAt}
|
||||
>
|
||||
<Icon
|
||||
glyph={'pencil-outline'}
|
||||
size={10}
|
||||
/>
|
||||
<PencilOutlineIcon size={12}/>
|
||||
{editedText}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -77,4 +77,18 @@ describe('LatestPostReader', () => {
|
||||
expect(span.prop('children')).toContain(author.username);
|
||||
expect(span.prop('children')).toContain('enero');
|
||||
});
|
||||
|
||||
test('should be able to handle an empty post array', () => {
|
||||
const {mountOptions} = mockStore(baseState);
|
||||
|
||||
const props = {
|
||||
...baseProps,
|
||||
postIds: [],
|
||||
};
|
||||
|
||||
const wrapper = mount(<LatestPostReader {...props}/>, mountOptions);
|
||||
const span = wrapper.childAt(0);
|
||||
|
||||
expect(span.prop('children')).toEqual('');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo, useRef} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import React, {useMemo} from 'react';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
@@ -11,27 +10,18 @@ import {Post} from '@mattermost/types/posts';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
import {getLatestPostId, makeCreateAriaLabelForPost} from 'utils/post_utils';
|
||||
import {getLatestPostId, usePostAriaLabel} from 'utils/post_utils';
|
||||
|
||||
interface Props {
|
||||
postIds?: string[];
|
||||
}
|
||||
|
||||
const LatestPostReader = (props: Props): JSX.Element => {
|
||||
const intl = useIntl();
|
||||
|
||||
const {postIds} = props;
|
||||
const latestPostId = useMemo(() => getLatestPostId(postIds || []), [postIds]);
|
||||
const latestPost = useSelector<GlobalState, Post>((state) => getPost(state, latestPostId));
|
||||
|
||||
const createAriaLabelForPost = useRef(makeCreateAriaLabelForPost());
|
||||
const ariaLabel = useSelector<GlobalState, string>((state) => {
|
||||
if (!latestPost) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return createAriaLabelForPost.current(state, latestPost)(intl);
|
||||
});
|
||||
const ariaLabel = usePostAriaLabel(latestPost);
|
||||
|
||||
return (
|
||||
<span
|
||||
|
||||
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
@@ -4,9 +4,16 @@
|
||||
import React, {ReactNode} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import styled from 'styled-components';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
|
||||
import ExternalLink from 'components/external_link';
|
||||
|
||||
import {HostedCustomerLinks} from 'utils/constants';
|
||||
|
||||
import BuildingSvg from './building.svg';
|
||||
import TadaSvg from './tada.svg';
|
||||
import BlankCardImage from './blank_card_image.svg';
|
||||
|
||||
export enum ButtonCustomiserClasses {
|
||||
grayed = 'grayed',
|
||||
@@ -38,7 +45,7 @@ type CardProps = {
|
||||
topColor: string;
|
||||
planLabel?: JSX.Element;
|
||||
plan: string;
|
||||
planSummary?: string;
|
||||
planSummary?: ReactNode;
|
||||
price?: string;
|
||||
rate?: ReactNode;
|
||||
planExtraInformation?: JSX.Element;
|
||||
@@ -48,6 +55,8 @@ type CardProps = {
|
||||
briefing: PlanBriefing;
|
||||
planAddonsInfo?: PlanAddonsInfo;
|
||||
planTrialDisclaimer?: JSX.Element;
|
||||
isCloud: boolean;
|
||||
cloudFreeDeprecated: boolean;
|
||||
}
|
||||
|
||||
type StyledProps = {
|
||||
@@ -58,28 +67,101 @@ const StyledDiv = styled.div<StyledProps>`
|
||||
background-color: ${(props) => props.bgColor};
|
||||
`;
|
||||
|
||||
export function BlankCard() {
|
||||
const {formatMessage} = useIntl();
|
||||
const [, contactSalesLink] = useOpenSalesLink();
|
||||
|
||||
return (
|
||||
<div className='BlankCard'>
|
||||
<div className='image'>
|
||||
<BlankCardImage/>
|
||||
</div>
|
||||
|
||||
<div className='description'>
|
||||
<div className='title'>
|
||||
<span className='questions'>
|
||||
{formatMessage({id: 'pricing_modal.questions', defaultMessage: 'Questions?'})}
|
||||
</span>
|
||||
<span className='contact'>
|
||||
<ExternalLink
|
||||
location='cloud_pricing_modal'
|
||||
href={contactSalesLink}
|
||||
>
|
||||
{formatMessage({id: 'pricing_modal.contact_us', defaultMessage: 'Contact us'})}
|
||||
</ExternalLink>
|
||||
</span>
|
||||
</div>
|
||||
<div className='content'>
|
||||
{formatMessage({id: 'pricing_modal.reach_out', defaultMessage: 'Reach out to us and we’ll help you decide which plan is right for you and your organization.'})}
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
<div className='self-hosted-interest'>
|
||||
<span className='interested'>
|
||||
{formatMessage({id: 'pricing_modal.interested_self_hosting', defaultMessage: 'Interested in self-hosting?'})}
|
||||
</span>
|
||||
<span className='learn'>
|
||||
<ExternalLink
|
||||
location='cloud_pricing_modal'
|
||||
href={HostedCustomerLinks.DOWNLOAD}
|
||||
>
|
||||
{formatMessage({id: 'pricing_modal.learn_more', defaultMessage: 'Learn more'})}
|
||||
</ExternalLink>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Card(props: CardProps) {
|
||||
const {formatMessage} = useIntl();
|
||||
const bottomClassName = classNames('bottom', {
|
||||
bottom__round: props.cloudFreeDeprecated && props.isCloud,
|
||||
});
|
||||
|
||||
const contactSalesCTAClassName = classNames('contact_sales_cta', {
|
||||
contact_sales_cta__reduced: props.cloudFreeDeprecated && props.isCloud,
|
||||
});
|
||||
|
||||
const planBriefingContentClassName = classNames('plan_briefing_content', {
|
||||
plan_briefing_content__reduced: props.cloudFreeDeprecated,
|
||||
});
|
||||
|
||||
const planPriceRateSectionClassName = classNames('plan_price_rate_section', {
|
||||
plan_price_rate_section__expanded: props.cloudFreeDeprecated,
|
||||
});
|
||||
|
||||
const planLimitsCtaClassName = classNames('plan_limits_cta', {
|
||||
plan_limits_cta__expanded: props.cloudFreeDeprecated,
|
||||
});
|
||||
|
||||
const buildingImgClassName = classNames('building_img', {
|
||||
building_img__expanded: props.cloudFreeDeprecated,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
id={props.id}
|
||||
className='PlanCard'
|
||||
>
|
||||
{props.planLabel}
|
||||
<StyledDiv
|
||||
className='top'
|
||||
bgColor={props.topColor}
|
||||
/>
|
||||
<div className='bottom'>
|
||||
{(!props.cloudFreeDeprecated || !props.isCloud) && (
|
||||
<StyledDiv
|
||||
className='top'
|
||||
bgColor={props.topColor}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={bottomClassName}>
|
||||
<div className='bottom_container'>
|
||||
<div className='plan_price_rate_section'>
|
||||
<div className={planPriceRateSectionClassName}>
|
||||
<h3>{props.plan}</h3>
|
||||
<p>{props.planSummary}</p>
|
||||
{props.price ? <h1>{props.price}</h1> : <BuildingSvg/>}
|
||||
<span>{props.rate}</span>
|
||||
{props.price ? <h1>{props.price}</h1> : <div className={buildingImgClassName}><BuildingSvg/></div>}
|
||||
{props.cloudFreeDeprecated ? (<span className='plan_rate'>{props.rate}</span>) : (<span>{props.rate}</span>)}
|
||||
</div>
|
||||
|
||||
<div className='plan_limits_cta'>
|
||||
<div className={planLimitsCtaClassName}>
|
||||
{props.planExtraInformation}
|
||||
</div>
|
||||
|
||||
@@ -96,7 +178,7 @@ function Card(props: CardProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='contact_sales_cta'>
|
||||
<div className={contactSalesCTAClassName}>
|
||||
{props.contactSalesCTA && (
|
||||
<div>
|
||||
<p>{formatMessage({id: 'pricing_modal.or', defaultMessage: 'or'})}</p>
|
||||
@@ -105,9 +187,9 @@ function Card(props: CardProps) {
|
||||
</div>
|
||||
|
||||
<div className='plan_briefing'>
|
||||
<hr/>
|
||||
{!props.cloudFreeDeprecated && <hr/>}
|
||||
{props.planTrialDisclaimer}
|
||||
<div className='plan_briefing_content'>
|
||||
<div className={planBriefingContentClassName}>
|
||||
<span className='title'>{props.briefing.title}</span>
|
||||
{props.briefing.items?.map((i) => {
|
||||
return (
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from 'mattermost-redux/selectors/entities/cloud';
|
||||
import {isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
import {DispatchFunc} from 'mattermost-redux/types/actions';
|
||||
import {deprecateCloudFree} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {Feedback} from '@mattermost/types/cloud';
|
||||
import useGetUsage from 'components/common/hooks/useGetUsage';
|
||||
@@ -44,7 +45,7 @@ import DowngradeTeamRemovalModal from './downgrade_team_removal_modal';
|
||||
import ContactSalesCTA from './contact_sales_cta';
|
||||
import StarterDisclaimerCTA from './starter_disclaimer_cta';
|
||||
import StartTrialCaution from './start_trial_caution';
|
||||
import Card, {ButtonCustomiserClasses} from './card';
|
||||
import Card, {BlankCard, ButtonCustomiserClasses} from './card';
|
||||
|
||||
import './content.scss';
|
||||
|
||||
@@ -68,6 +69,8 @@ function Content(props: ContentProps) {
|
||||
const subscription = useSelector(selectCloudSubscription);
|
||||
const currentProduct = useSelector(selectSubscriptionProduct);
|
||||
const products = useSelector(selectCloudProducts);
|
||||
|
||||
const cloudFreeDeprecated = useSelector(deprecateCloudFree);
|
||||
const yearlyProducts = findOnlyYearlyProducts(products || {}); // pricing modal should now only show yearly products
|
||||
|
||||
const currentSubscriptionIsMonthly = currentProduct?.recurring_interval === RecurringIntervals.MONTH;
|
||||
@@ -118,8 +121,20 @@ function Content(props: ContentProps) {
|
||||
trial_notification: isPreTrial,
|
||||
});
|
||||
|
||||
const getAdminProfessionalBtnText = () => {
|
||||
if (currentSubscriptionIsMonthlyProfessional) {
|
||||
return formatMessage({id: 'pricing_modal.btn.switch_to_annual', defaultMessage: 'Switch to annual billing'});
|
||||
}
|
||||
|
||||
if (cloudFreeDeprecated) {
|
||||
return formatMessage({id: 'pricing_modal.btn.purchase', defaultMessage: 'Purchase'});
|
||||
}
|
||||
|
||||
return formatMessage({id: 'pricing_modal.btn.upgrade', defaultMessage: 'Upgrade'});
|
||||
};
|
||||
|
||||
const freeTierText = (!isStarter && !currentSubscriptionIsMonthly) ? formatMessage({id: 'pricing_modal.btn.contactSupport', defaultMessage: 'Contact Support'}) : formatMessage({id: 'pricing_modal.btn.downgrade', defaultMessage: 'Downgrade'});
|
||||
const adminProfessionalTierText = currentSubscriptionIsMonthlyProfessional ? formatMessage({id: 'pricing_modal.btn.switch_to_annual', defaultMessage: 'Switch to annual billing'}) : formatMessage({id: 'pricing_modal.btn.upgrade', defaultMessage: 'Upgrade'});
|
||||
const adminProfessionalTierText = getAdminProfessionalBtnText();
|
||||
|
||||
const [openContactSales] = useOpenSalesLink();
|
||||
const [openContactSupport] = useOpenCloudZendeskSupportForm('Workspace downgrade', '');
|
||||
@@ -209,7 +224,7 @@ function Content(props: ContentProps) {
|
||||
action: () => openPurchaseModal('click_pricing_modal_professional_card_upgrade_button'),
|
||||
text: adminProfessionalTierText,
|
||||
disabled: isProfessionalAnnual || (isEnterprise && !isEnterpriseTrial),
|
||||
customClass: isPostTrial ? ButtonCustomiserClasses.special : ButtonCustomiserClasses.active,
|
||||
customClass: (cloudFreeDeprecated || isPostTrial) ? ButtonCustomiserClasses.special : ButtonCustomiserClasses.active,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -234,7 +249,7 @@ function Content(props: ContentProps) {
|
||||
};
|
||||
|
||||
const enterpriseBtnDetails = () => {
|
||||
if (isPostTrial && isAdmin) {
|
||||
if (cloudFreeDeprecated || (isPostTrial && isAdmin)) {
|
||||
return {
|
||||
action: () => {
|
||||
trackEvent(TELEMETRY_CATEGORIES.CLOUD_PRICING, 'click_enterprise_contact_sales');
|
||||
@@ -270,7 +285,7 @@ function Content(props: ContentProps) {
|
||||
};
|
||||
|
||||
const enterpriseCustomBtnDetails = () => {
|
||||
if (!isPostTrial && isAdmin) {
|
||||
if (!isPostTrial && isAdmin && !cloudFreeDeprecated) {
|
||||
return (
|
||||
<CloudStartTrialButton
|
||||
message={formatMessage({id: 'pricing_modal.btn.tryDays', defaultMessage: 'Try free for {days} days'}, {days: '30'})}
|
||||
@@ -311,99 +326,117 @@ function Content(props: ContentProps) {
|
||||
/>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<div className='pricing-options-container'>
|
||||
<div className='alert-option-container'>
|
||||
<div className='alert-option'>
|
||||
<span>{formatMessage({id: 'pricing_modal.lookingToSelfHost', defaultMessage: 'Looking to self-host?'})}</span>
|
||||
<ExternalLink
|
||||
onClick={() =>
|
||||
trackEvent(
|
||||
TELEMETRY_CATEGORIES.CLOUD_PURCHASING,
|
||||
'click_looking_to_self_host',
|
||||
)
|
||||
}
|
||||
href={CloudLinks.DEPLOYMENT_OPTIONS}
|
||||
location='pricing_modal_content'
|
||||
>{formatMessage({id: 'pricing_modal.reviewDeploymentOptions', defaultMessage: 'Review deployment options'})}</ExternalLink>
|
||||
{!cloudFreeDeprecated && (
|
||||
<div className='pricing-options-container'>
|
||||
<div className='alert-option-container'>
|
||||
<div className='alert-option'>
|
||||
<span>{formatMessage({id: 'pricing_modal.lookingToSelfHost', defaultMessage: 'Looking to self-host?'})}</span>
|
||||
<ExternalLink
|
||||
onClick={() =>
|
||||
trackEvent(
|
||||
TELEMETRY_CATEGORIES.CLOUD_PURCHASING,
|
||||
'click_looking_to_self_host',
|
||||
)
|
||||
}
|
||||
href={CloudLinks.DEPLOYMENT_OPTIONS}
|
||||
location='pricing_modal_content'
|
||||
>{formatMessage({id: 'pricing_modal.reviewDeploymentOptions', defaultMessage: 'Review deployment options'})}</ExternalLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='PricingModal__body'>
|
||||
<Card
|
||||
id='free'
|
||||
topColor='#339970'
|
||||
plan='Free'
|
||||
planSummary={formatMessage({id: 'pricing_modal.planSummary.free', defaultMessage: 'Increased productivity for small teams'})}
|
||||
price='$0'
|
||||
rate={formatMessage({id: 'pricing_modal.price.freeForever', defaultMessage: 'Free forever'})}
|
||||
planLabel={
|
||||
isStarter ? (
|
||||
<PlanLabel
|
||||
text={formatMessage({id: 'pricing_modal.planLabel.currentPlan', defaultMessage: 'CURRENT PLAN'})}
|
||||
color='var(--denim-status-online)'
|
||||
bgColor='var(--center-channel-bg)'
|
||||
firstSvg={<CheckMarkSvg/>}
|
||||
/>) : undefined}
|
||||
planExtraInformation={<StarterDisclaimerCTA/>}
|
||||
buttonDetails={{
|
||||
action: () => {
|
||||
if (!isStarter && !currentSubscriptionIsMonthly) {
|
||||
openContactSupport();
|
||||
return;
|
||||
}
|
||||
<div
|
||||
className='PricingModal__body'
|
||||
style={{marginTop: cloudFreeDeprecated ? '74px' : ''}}
|
||||
>
|
||||
{!cloudFreeDeprecated && (
|
||||
<Card
|
||||
id='free'
|
||||
topColor='#339970'
|
||||
plan='Free'
|
||||
planSummary={formatMessage({id: 'pricing_modal.planSummary.free', defaultMessage: 'Increased productivity for small teams'})}
|
||||
price='$0'
|
||||
rate={formatMessage({id: 'pricing_modal.price.freeForever', defaultMessage: 'Free forever'})}
|
||||
isCloud={true}
|
||||
cloudFreeDeprecated={cloudFreeDeprecated}
|
||||
planLabel={
|
||||
isStarter ? (
|
||||
<PlanLabel
|
||||
text={formatMessage({id: 'pricing_modal.planLabel.currentPlan', defaultMessage: 'CURRENT PLAN'})}
|
||||
color='var(--denim-status-online)'
|
||||
bgColor='var(--center-channel-bg)'
|
||||
firstSvg={<CheckMarkSvg/>}
|
||||
/>) : undefined}
|
||||
planExtraInformation={<StarterDisclaimerCTA/>}
|
||||
buttonDetails={{
|
||||
action: () => {
|
||||
if (!isStarter && !currentSubscriptionIsMonthly) {
|
||||
openContactSupport();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!starterProduct) {
|
||||
return;
|
||||
}
|
||||
if (!starterProduct) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (usage.teams.active > 1) {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalId: ModalIdentifiers.CLOUD_DOWNGRADE_CHOOSE_TEAM,
|
||||
dialogType: DowngradeTeamRemovalModal,
|
||||
dialogProps: {
|
||||
product_id: starterProduct?.id,
|
||||
starterProduct,
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalId: ModalIdentifiers.FEEDBACK,
|
||||
dialogType: DowngradeFeedbackModal,
|
||||
dialogProps: {
|
||||
onSubmit: handleClickDowngrade,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
text: freeTierText,
|
||||
disabled: isStarter || isEnterprise || !isAdmin,
|
||||
customClass: (isStarter || isEnterprise || !isAdmin) ? ButtonCustomiserClasses.grayed : ButtonCustomiserClasses.secondary,
|
||||
}}
|
||||
briefing={{
|
||||
title: formatMessage({id: 'pricing_modal.briefing.title', defaultMessage: 'Top features'}),
|
||||
items: hasLimits ? starterBriefing : legacyStarterBriefing,
|
||||
}}
|
||||
/>
|
||||
if (usage.teams.active > 1) {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalId: ModalIdentifiers.CLOUD_DOWNGRADE_CHOOSE_TEAM,
|
||||
dialogType: DowngradeTeamRemovalModal,
|
||||
dialogProps: {
|
||||
product_id: starterProduct?.id,
|
||||
starterProduct,
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalId: ModalIdentifiers.FEEDBACK,
|
||||
dialogType: DowngradeFeedbackModal,
|
||||
dialogProps: {
|
||||
onSubmit: handleClickDowngrade,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
text: freeTierText,
|
||||
disabled: isStarter || isEnterprise || !isAdmin,
|
||||
customClass: (isStarter || isEnterprise || !isAdmin) ? ButtonCustomiserClasses.grayed : ButtonCustomiserClasses.secondary,
|
||||
}}
|
||||
briefing={{
|
||||
title: formatMessage({id: 'pricing_modal.briefing.title', defaultMessage: 'Top features'}),
|
||||
items: hasLimits ? starterBriefing : legacyStarterBriefing,
|
||||
}}
|
||||
/>
|
||||
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
<Card
|
||||
id='professional'
|
||||
topColor='var(--denim-button-bg)'
|
||||
plan='Professional'
|
||||
planSummary={formatMessage({id: 'pricing_modal.planSummary.professional', defaultMessage: 'Scalable solutions for growing teams'})}
|
||||
planSummary={formatMessage({id: 'pricing_modal.planSummary.professional', defaultMessage: 'Scalable solutions {br} for growing teams'}, {
|
||||
br: <br/>,
|
||||
})}
|
||||
price={`$${professionalPrice}`}
|
||||
rate={formatMessage({id: 'pricing_modal.rate.seatPerMonth', defaultMessage: 'USD per seat/month {br}<b>(billed annually)</b>'}, {
|
||||
br: <br/>,
|
||||
b: (chunks: React.ReactNode | React.ReactNodeArray) => (
|
||||
<span style={{fontSize: '14px'}}>
|
||||
<b>{chunks}</b>
|
||||
<span className='billed_annually'>
|
||||
{
|
||||
cloudFreeDeprecated ? chunks : (<b>{chunks}</b>)
|
||||
}
|
||||
</span>
|
||||
),
|
||||
})}
|
||||
isCloud={true}
|
||||
cloudFreeDeprecated={cloudFreeDeprecated}
|
||||
planLabel={isProfessional ? (
|
||||
<PlanLabel
|
||||
text={professionalPlanLabelText()}
|
||||
@@ -413,7 +446,7 @@ function Content(props: ContentProps) {
|
||||
/>) : undefined}
|
||||
buttonDetails={professionalBtnDetails()}
|
||||
briefing={{
|
||||
title: formatMessage({id: 'pricing_modal.briefing.title', defaultMessage: 'Top features'}),
|
||||
title: cloudFreeDeprecated ? formatMessage({id: 'pricing_modal.briefing.title_no_limit', defaultMessage: 'No limits on your team’s usage'}) : formatMessage({id: 'pricing_modal.briefing.title', defaultMessage: 'Top features'}),
|
||||
items: [
|
||||
formatMessage({id: 'pricing_modal.briefing.professional.messageBoardsIntegrationsCalls', defaultMessage: 'Unlimited access to messages and files'}),
|
||||
formatMessage({id: 'pricing_modal.briefing.professional.unLimitedTeams', defaultMessage: 'Unlimited teams'}),
|
||||
@@ -442,6 +475,8 @@ function Content(props: ContentProps) {
|
||||
topColor='#E07315'
|
||||
plan='Enterprise'
|
||||
planSummary={formatMessage({id: 'pricing_modal.planSummary.enterprise', defaultMessage: 'Administration, security, and compliance for large teams'})}
|
||||
isCloud={true}
|
||||
cloudFreeDeprecated={cloudFreeDeprecated}
|
||||
planLabel={
|
||||
isEnterprise ? (
|
||||
<PlanLabel
|
||||
@@ -453,10 +488,10 @@ function Content(props: ContentProps) {
|
||||
/>) : undefined}
|
||||
buttonDetails={enterpriseBtnDetails()}
|
||||
customButtonDetails={enterpriseCustomBtnDetails()}
|
||||
planTrialDisclaimer={(!isPostTrial && isAdmin) ? <StartTrialCaution/> : undefined}
|
||||
contactSalesCTA={(isPostTrial || !isAdmin) ? undefined : <ContactSalesCTA/>}
|
||||
planTrialDisclaimer={(!isPostTrial && isAdmin && !cloudFreeDeprecated) ? <StartTrialCaution/> : undefined}
|
||||
contactSalesCTA={(isPostTrial || !isAdmin || cloudFreeDeprecated) ? undefined : <ContactSalesCTA/>}
|
||||
briefing={{
|
||||
title: formatMessage({id: 'pricing_modal.briefing.title', defaultMessage: 'Top features'}),
|
||||
title: cloudFreeDeprecated ? formatMessage({id: 'pricing_modal.briefing.title_large_scale', defaultMessage: 'Large scale collaboration'}) : formatMessage({id: 'pricing_modal.briefing.title', defaultMessage: 'Top features'}),
|
||||
items: [
|
||||
formatMessage({id: 'pricing_modal.briefing.enterprise.groupSync', defaultMessage: 'AD/LDAP group sync'}),
|
||||
formatMessage({id: 'pricing_modal.briefing.enterprise.rolesAndPermissions', defaultMessage: 'Advanced roles and permissions'}),
|
||||
@@ -480,6 +515,7 @@ function Content(props: ContentProps) {
|
||||
],
|
||||
}}
|
||||
/>
|
||||
{cloudFreeDeprecated && <BlankCard/>}
|
||||
</div>
|
||||
</Modal.Body>
|
||||
</div>
|
||||
|
||||
@@ -128,6 +128,74 @@
|
||||
margin-bottom: 111px;
|
||||
gap: 48px;
|
||||
|
||||
.BlankCard {
|
||||
position: relative;
|
||||
width: 280px;
|
||||
|
||||
.image {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||
}
|
||||
|
||||
.description {
|
||||
border-bottom: 1px solid rgba(var(--center-channel-color-rgb) 0.16);
|
||||
text-align: left;
|
||||
|
||||
.title {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 8px;
|
||||
font-family: 'Open Sans';
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
|
||||
.questions {
|
||||
margin-right: 2px;
|
||||
color: var(--center-channel-color);
|
||||
}
|
||||
|
||||
.contact {
|
||||
color: var(--denim-button-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-bottom: 25px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.72);
|
||||
font-family: 'Open Sans';
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.self-hosted-interest {
|
||||
padding: 8px;
|
||||
margin-top: 24px;
|
||||
background: rgba(var(--denim-button-bg-rgb), 0.08);
|
||||
border-radius: 8px;
|
||||
font-family: 'Open Sans';
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
|
||||
.interested {
|
||||
margin-right: 2px;
|
||||
color: var(--center-channel-color);
|
||||
}
|
||||
|
||||
.learn {
|
||||
color: var(--denim-button-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.PlanCard {
|
||||
position: relative;
|
||||
width: 280px;
|
||||
@@ -180,6 +248,7 @@
|
||||
text-align: center;
|
||||
|
||||
h3 {
|
||||
margin-top: 32px;
|
||||
color: var(--center-channel-color);
|
||||
font-family: 'Metropolis';
|
||||
font-size: 24px;
|
||||
@@ -210,6 +279,32 @@
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.plan_rate {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.building_img {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.building_img__expanded {
|
||||
padding-top: 14px;
|
||||
}
|
||||
|
||||
.billed_annually {
|
||||
font-family: 'Open sans', sans-serif;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.plan_price_rate_section__expanded {
|
||||
height: 244px;
|
||||
|
||||
span,
|
||||
p {
|
||||
margin-bottom: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
.contact_sales_cta {
|
||||
@@ -218,10 +313,19 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.contact_sales_cta__reduced {
|
||||
height: 32px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.plan_limits_cta {
|
||||
height: 21px;
|
||||
}
|
||||
|
||||
.plan_limits_cta__expanded {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.plan_buttons {
|
||||
height: auto;
|
||||
|
||||
@@ -306,6 +410,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.plan_briefing_content__reduced {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,6 +463,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom__round {
|
||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,6 +186,8 @@ function SelfHostedContent(props: ContentProps) {
|
||||
planSummary={formatMessage({id: 'pricing_modal.planSummary.free', defaultMessage: 'Increased productivity for small teams'})}
|
||||
price='$0'
|
||||
rate={formatMessage({id: 'pricing_modal.price.freeForever', defaultMessage: 'Free forever'})}
|
||||
isCloud={false}
|
||||
cloudFreeDeprecated={false}
|
||||
planLabel={
|
||||
isStarter ? (
|
||||
<PlanLabel
|
||||
@@ -209,16 +211,20 @@ function SelfHostedContent(props: ContentProps) {
|
||||
id='professional'
|
||||
topColor='var(--denim-button-bg)'
|
||||
plan='Professional'
|
||||
planSummary={formatMessage({id: 'pricing_modal.planSummary.professional', defaultMessage: 'Scalable solutions for growing teams'})}
|
||||
planSummary={formatMessage({id: 'pricing_modal.planSummary.professional', defaultMessage: 'Scalable solutions {br} for growing teams'}, {
|
||||
br: <br/>,
|
||||
})}
|
||||
price={professionalPrice}
|
||||
rate={formatMessage({id: 'pricing_modal.rate.seatPerMonth', defaultMessage: 'USD per seat/month {br}<b>(billed annually)</b>'}, {
|
||||
br: <br/>,
|
||||
b: (chunks: React.ReactNode | React.ReactNodeArray) => (
|
||||
<span style={{fontSize: '14px'}}>
|
||||
<span className='billed_annually'>
|
||||
<b>{chunks}</b>
|
||||
</span>
|
||||
),
|
||||
})}
|
||||
isCloud={false}
|
||||
cloudFreeDeprecated={false}
|
||||
planLabel={
|
||||
isProfessional ? (
|
||||
<PlanLabel
|
||||
@@ -277,6 +283,8 @@ function SelfHostedContent(props: ContentProps) {
|
||||
topColor='#E07315'
|
||||
plan='Enterprise'
|
||||
planSummary={formatMessage({id: 'pricing_modal.planSummary.enterprise', defaultMessage: 'Administration, security, and compliance for large teams'})}
|
||||
isCloud={false}
|
||||
cloudFreeDeprecated={false}
|
||||
planLabel={
|
||||
isEnterprise ? (
|
||||
<PlanLabel
|
||||
|
||||
@@ -3491,9 +3491,11 @@ exports[`components/ProfilePopover should show the start call button when isCall
|
||||
hide={[MockFunction]}
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
@@ -3520,6 +3522,7 @@ exports[`components/ProfilePopover should show the start call button when isCall
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
@@ -3804,9 +3807,11 @@ exports[`components/ProfilePopover should show the start call button when isCall
|
||||
hourCycle="h12"
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
@@ -3833,6 +3838,7 @@ exports[`components/ProfilePopover should show the start call button when isCall
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
@@ -4076,9 +4082,11 @@ exports[`components/ProfilePopover should show the start call button when isCall
|
||||
id="addToChannelTooltip"
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
@@ -4105,6 +4113,7 @@ exports[`components/ProfilePopover should show the start call button when isCall
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {DateTime, Duration} from 'luxon';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {UserTimezone} from '@mattermost/types/users';
|
||||
import Timestamp from 'components/timestamp';
|
||||
import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils';
|
||||
|
||||
type ProfileTimezoneProps = {
|
||||
profileUserTimezone?: UserTimezone;
|
||||
currentUserTimezone: string | undefined;
|
||||
}
|
||||
|
||||
const returnTimeDiff = (
|
||||
currentUserTimezone: string | undefined | null,
|
||||
profileUserTimezone: string,
|
||||
) => {
|
||||
if (!currentUserTimezone) {
|
||||
return null;
|
||||
}
|
||||
const currentUserDate = DateTime.local().setZone(currentUserTimezone);
|
||||
const profileUserDate = DateTime.local().setZone(profileUserTimezone);
|
||||
|
||||
const offset = Duration.fromObject({
|
||||
hours: (profileUserDate.offset - currentUserDate.offset) / 60,
|
||||
});
|
||||
|
||||
if (!offset.valueOf()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const timeOffset = offset.toHuman({unitDisplay: 'short', signDisplay: 'never'});
|
||||
|
||||
return offset.valueOf() > 0 ? (
|
||||
<FormattedMessage
|
||||
id='user_profile.account.hoursAhead'
|
||||
defaultMessage='({timeOffset} ahead)'
|
||||
values={{timeOffset}}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='user_profile.account.hoursBehind'
|
||||
defaultMessage='({timeOffset} behind)'
|
||||
values={{timeOffset}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProfileTimezone = (
|
||||
{
|
||||
currentUserTimezone,
|
||||
profileUserTimezone,
|
||||
}: ProfileTimezoneProps,
|
||||
) => {
|
||||
const profileTimezone = getUserCurrentTimezone(profileUserTimezone) || 'UTC';
|
||||
const profileTimezoneShort = profileTimezone ? DateTime.now().setZone(profileTimezone).offsetNameShort : undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
className='user-popover__time-status-container'
|
||||
>
|
||||
<span className='user-popover__subtitle'>
|
||||
{profileTimezoneShort ? (
|
||||
<FormattedMessage
|
||||
id='user_profile.account.localTimeWithTimezone'
|
||||
defaultMessage='Local Time ({timezone})'
|
||||
values={{
|
||||
timezone: profileTimezoneShort,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='user_profile.account.localTime'
|
||||
defaultMessage='Local Time'
|
||||
/>
|
||||
)}
|
||||
|
||||
</span>
|
||||
<span>
|
||||
<Timestamp
|
||||
useRelative={false}
|
||||
useDate={false}
|
||||
userTimezone={profileUserTimezone}
|
||||
useTime={{
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
}}
|
||||
/>
|
||||
{' '}
|
||||
{returnTimeDiff(currentUserTimezone, profileTimezone)}
|
||||
</span>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -20,9 +20,12 @@ import {ModalData} from 'types/actions';
|
||||
import {getHistory} from 'utils/browser_history';
|
||||
import Constants, {A11yClassNames, A11yCustomEventTypes, A11yFocusEventDetail, ModalIdentifiers, UserStatuses} from 'utils/constants';
|
||||
import {t} from 'utils/i18n';
|
||||
import * as Keyboard from 'utils/keyboard';
|
||||
import * as Utils from 'utils/utils';
|
||||
import {shouldFocusMainTextbox} from 'utils/post_utils';
|
||||
|
||||
import {ProfileTimezone} from './profile_localtime';
|
||||
|
||||
import StatusIcon from 'components/status_icon';
|
||||
import Timestamp from 'components/timestamp';
|
||||
import UserSettingsModal from 'components/user_settings/modal';
|
||||
@@ -336,7 +339,7 @@ class ProfilePopover extends React.PureComponent<ProfilePopoverProps, ProfilePop
|
||||
handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (shouldFocusMainTextbox(e, document.activeElement)) {
|
||||
this.props.hide?.();
|
||||
} else if (Utils.isKeyPressed(e, Constants.KeyCodes.ESCAPE)) {
|
||||
} else if (Keyboard.isKeyPressed(e, Constants.KeyCodes.ESCAPE)) {
|
||||
this.returnFocus();
|
||||
}
|
||||
};
|
||||
@@ -574,27 +577,11 @@ class ProfilePopover extends React.PureComponent<ProfilePopoverProps, ProfilePop
|
||||
!haveOverrideProp
|
||||
) {
|
||||
dataContent.push(
|
||||
<div
|
||||
<ProfileTimezone
|
||||
currentUserTimezone={this.props.currentUserTimezone}
|
||||
profileUserTimezone={this.props.user.timezone}
|
||||
key='user-popover-local-time'
|
||||
className='user-popover__time-status-container'
|
||||
>
|
||||
<span className='user-popover__subtitle' >
|
||||
<FormattedMessage
|
||||
id='user_profile.account.localTime'
|
||||
defaultMessage='Local Time'
|
||||
/>
|
||||
</span>
|
||||
<Timestamp
|
||||
useRelative={false}
|
||||
useDate={false}
|
||||
userTimezone={this.props.user?.timezone}
|
||||
useTime={{
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
timeZoneName: 'short',
|
||||
}}
|
||||
/>
|
||||
</div>,
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import {getCurrentChannelNameForSearchShortcut} from 'mattermost-redux/selectors
|
||||
import {isServerVersionGreaterThanOrEqualTo} from 'utils/server_version';
|
||||
import {isDesktopApp, getDesktopVersion, isMacApp} from 'utils/user_agent';
|
||||
import Constants, {searchHintOptions, RHSStates, searchFilesHintOptions} from 'utils/constants';
|
||||
import * as Utils from 'utils/utils';
|
||||
import * as Keyboard from 'utils/keyboard';
|
||||
|
||||
import HeaderIconWrapper from 'components/channel_header/components/header_icon_wrapper';
|
||||
import SearchHint from 'components/search_hint/search_hint';
|
||||
@@ -109,7 +109,7 @@ const Search: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (Utils.cmdOrCtrlPressed(e) && Utils.isKeyPressed(e, Constants.KeyCodes.F)) {
|
||||
if (Keyboard.cmdOrCtrlPressed(e) && Keyboard.isKeyPressed(e, Constants.KeyCodes.F)) {
|
||||
if (!isDesktop && !e.shiftKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import classNames from 'classnames';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
import * as Utils from 'utils/utils';
|
||||
import * as Keyboard from 'utils/keyboard';
|
||||
|
||||
import SuggestionDate from 'components/suggestion/suggestion_date';
|
||||
import SearchSuggestionList from 'components/suggestion/search_suggestion_list';
|
||||
@@ -71,27 +71,27 @@ const SearchBar: React.FunctionComponent<Props> = (props: Props): JSX.Element =>
|
||||
}, [searchTerms]);
|
||||
|
||||
const handleKeyDown = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||
if (Utils.isKeyPressed(e as any, KeyCodes.ESCAPE)) {
|
||||
if (Keyboard.isKeyPressed(e as any, KeyCodes.ESCAPE)) {
|
||||
searchRef.current?.blur();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (Utils.isKeyPressed(e as any, KeyCodes.DOWN)) {
|
||||
if (Keyboard.isKeyPressed(e as any, KeyCodes.DOWN)) {
|
||||
e.preventDefault();
|
||||
props.updateHighlightedSearchHint(1, true);
|
||||
}
|
||||
|
||||
if (Utils.isKeyPressed(e as any, KeyCodes.UP)) {
|
||||
if (Keyboard.isKeyPressed(e as any, KeyCodes.UP)) {
|
||||
e.preventDefault();
|
||||
props.updateHighlightedSearchHint(-1, true);
|
||||
}
|
||||
|
||||
if (Utils.isKeyPressed(e as any, KeyCodes.ENTER)) {
|
||||
if (Keyboard.isKeyPressed(e as any, KeyCodes.ENTER)) {
|
||||
props.handleEnterKey(e);
|
||||
}
|
||||
|
||||
if (Utils.isKeyPressed(e as any, KeyCodes.BACKSPACE) && !searchTerms) {
|
||||
if (Keyboard.isKeyPressed(e as any, KeyCodes.BACKSPACE) && !searchTerms) {
|
||||
if (props.clearSearchType) {
|
||||
props.clearSearchType();
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {FormattedMessage} from 'react-intl';
|
||||
import {SearchFilterType} from '../search/types';
|
||||
import {SearchType} from 'types/store/rhs';
|
||||
|
||||
import * as Utils from 'utils/utils';
|
||||
import * as Keyboard from 'utils/keyboard';
|
||||
import Constants from 'utils/constants';
|
||||
|
||||
import FilesFilterMenu from './files_filter_menu';
|
||||
@@ -32,7 +32,7 @@ export default function MessagesOrFilesSelector(props: Props): JSX.Element {
|
||||
<div className='buttons-container'>
|
||||
<button
|
||||
onClick={() => props.onChange('messages')}
|
||||
onKeyDown={(e: React.KeyboardEvent<HTMLSpanElement>) => Utils.isKeyPressed(e, KeyCodes.ENTER) && props.onChange('messages')}
|
||||
onKeyDown={(e: React.KeyboardEvent<HTMLSpanElement>) => Keyboard.isKeyPressed(e, KeyCodes.ENTER) && props.onChange('messages')}
|
||||
className={props.selected === 'messages' ? 'active tab messages-tab' : 'tab messages-tab'}
|
||||
>
|
||||
<FormattedMessage
|
||||
@@ -44,7 +44,7 @@ export default function MessagesOrFilesSelector(props: Props): JSX.Element {
|
||||
{props.isFileAttachmentsEnabled &&
|
||||
<button
|
||||
onClick={() => props.onChange('files')}
|
||||
onKeyDown={(e: React.KeyboardEvent<HTMLSpanElement>) => Utils.isKeyPressed(e, KeyCodes.ENTER) && props.onChange('files')}
|
||||
onKeyDown={(e: React.KeyboardEvent<HTMLSpanElement>) => Keyboard.isKeyPressed(e, KeyCodes.ENTER) && props.onChange('files')}
|
||||
className={props.selected === 'files' ? 'active tab files-tab' : 'tab files-tab'}
|
||||
>
|
||||
<FormattedMessage
|
||||
|
||||
@@ -13,12 +13,6 @@ describe('components/SearchShortcut', () => {
|
||||
return {
|
||||
...original,
|
||||
isDesktopApp: jest.fn(() => false),
|
||||
};
|
||||
});
|
||||
jest.mock('utils/utils', () => {
|
||||
const original = jest.requireActual('utils/utils');
|
||||
return {
|
||||
...original,
|
||||
isMac: jest.fn(() => false),
|
||||
};
|
||||
});
|
||||
@@ -33,12 +27,6 @@ describe('components/SearchShortcut', () => {
|
||||
return {
|
||||
...original,
|
||||
isDesktopApp: jest.fn(() => false),
|
||||
};
|
||||
});
|
||||
jest.mock('utils/utils', () => {
|
||||
const original = jest.requireActual('utils/utils');
|
||||
return {
|
||||
...original,
|
||||
isMac: jest.fn(() => true),
|
||||
};
|
||||
});
|
||||
@@ -53,13 +41,7 @@ describe('components/SearchShortcut', () => {
|
||||
return {
|
||||
...original,
|
||||
isDesktopApp: jest.fn(() => true),
|
||||
};
|
||||
});
|
||||
jest.mock('utils/utils', () => {
|
||||
const original = jest.requireActual('utils/utils');
|
||||
return {
|
||||
...original,
|
||||
isMac: jest.fn(() => false),
|
||||
isMac: jest.fn(() => true),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -73,12 +55,6 @@ describe('components/SearchShortcut', () => {
|
||||
return {
|
||||
...original,
|
||||
isDesktopApp: jest.fn(() => true),
|
||||
};
|
||||
});
|
||||
jest.mock('utils/utils', () => {
|
||||
const original = jest.requireActual('utils/utils');
|
||||
return {
|
||||
...original,
|
||||
isMac: jest.fn(() => true),
|
||||
};
|
||||
});
|
||||
|
||||
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Ссылка в новой задаче
Block a user