Adding more visibility to the free edition in the interface (#26698)

* Initial version of make more evident the mattermost edition used

* Updating tests

* Adding i18n strings

* Fixing css property order

* tweaks to badge and use proper MM logo svg

* update snapshot

* update language, tweaks to menu trial block

* fix style lint issue

* tweak letter-spacing

* fix lint

* update language

* Removing trial related info from the main menu, and simplifying showing the license

* Applying proposed changes

* Fixing linter errors

* Fixing tests

* Updating i18n strings

* fixing some styles and a bug

* Fix linter error

* updated login css and fixed lint issue

* Moving to styled components the Free edition badge

* Fixing linter error

* Addressing PR review comments

* Removing old snapshot

* Addressing PR review comments

* Fixing linter errors

---------

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>
Этот коммит содержится в:
Jesús Espino
2024-05-06 19:19:10 +02:00
коммит произвёл GitHub
родитель 31015a971e
Коммит 708d046c1b
15 изменённых файлов: 466 добавлений и 146 удалений

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

@@ -2,14 +2,23 @@
// See LICENSE.txt for license information.
import React from 'react';
import styled from 'styled-components';
type Props = {
width?: number;
height?: number;
className?: string;
}
const Svg = styled.svg.attrs({
version: '1.1',
xmlns: 'http://www.w3.org/2000/svg',
xmlnsXlink: 'http://www.w3.org/1999/xlink',
})``;
export default (props: Props) => (
<svg
<Svg
className={props.className}
width={props.width ? props.width.toString() : '182'}
height={props.height ? props.height.toString() : '30'}
viewBox='0 0 182 30'
@@ -28,5 +37,5 @@ export default (props: Props) => (
d='M20.1373 12.2575C20.1373 12.2575 20.1838 14.3044 18.7644 15.695C17.3448 17.0856 15.6012 16.9585 14.467 16.5745C13.3326 16.1905 11.8704 15.2323 11.5873 13.2655C11.3045 11.2986 12.5848 9.70069 12.5848 9.70069L15.3747 6.23118L16.9996 4.24963L18.3944 2.52282C18.3944 2.52282 19.0345 1.66544 19.2169 1.48849C19.2531 1.45342 19.2901 1.43042 19.3265 1.4127L19.3529 1.39919L19.3577 1.39736C19.4344 1.36431 19.5228 1.35719 19.6081 1.38604C19.6915 1.41435 19.7562 1.47169 19.7971 1.54236L19.8056 1.55605L19.8131 1.57176C19.833 1.60938 19.8496 1.65229 19.8575 1.70525C19.8948 1.95671 19.8825 3.02665 19.8825 3.02665L19.9415 5.24543L20.0286 7.8066L20.1373 12.2575ZM23.7488 2.86301C28.9088 6.61849 31.2753 13.4339 29.1235 19.8082C26.4751 27.653 17.9863 31.8597 10.1633 29.2041C2.34031 26.5484 -1.85455 18.0361 0.79367 10.1914C2.9491 3.80658 8.97324 -0.167855 15.3585 0.00544665L13.3118 2.43039C9.52375 3.11721 6.25309 5.71126 4.96759 9.51933C3.05491 15.185 6.26074 21.3926 12.1281 23.3842C17.9952 25.376 24.3022 22.3977 26.2149 16.7321C27.4962 12.9366 26.4802 8.89825 23.9063 6.04672L23.7488 2.86301Z'
fill='#1E325C'
/>
</svg>
</Svg>
);

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

@@ -54,45 +54,78 @@ exports[`components/global/product_switcher should have an active button state w
isMessaging={true}
onClick={[Function]}
/>
<Memo(MenuGroup)>
<MenuStartTrial
id="startTrial"
/>
</Memo(MenuGroup)>
</Menu>
</MenuWrapper>
</div>
`;
exports[`components/global/product_switcher 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,
},
}
}
>
<ProductMenu />
</ContextProvider>
<div>
<MenuWrapper
animationComponent={[Function]}
className=""
open={true}
>
<ProductMenuContainer
onClick={[Function]}
>
<ProductMenuButton
active={true}
aria-controls="product-switcher-menu"
aria-expanded={true}
aria-label="Product switch menu"
/>
<ProductBranding />
</ProductMenuContainer>
<Menu
ariaLabel="switcherOpen"
className="product-switcher-menu"
id="product-switcher-menu"
listId="product-switcher-menu-dropdown"
>
<ProductMenuItem
active={true}
destination="/"
icon="product-channels"
onClick={[Function]}
text="Channels"
/>
<ProductMenuItem
active={false}
destination=""
icon="product-boards"
id="product-menu-item-Boards"
key="Boards"
onClick={[Function]}
text="Boards"
/>
<ProductMenuItem
active={false}
destination=""
icon="product-playbooks"
id="product-menu-item-Playbooks"
key="Playbooks"
onClick={[Function]}
text="Playbooks"
/>
<Connect(ProductMenuList)
handleVisitConsoleClick={[Function]}
isMessaging={true}
onClick={[Function]}
/>
<Memo(MenuGroup)>
<MenuStartTrial
id="startTrial"
/>
</Memo(MenuGroup)>
</Menu>
</MenuWrapper>
</div>
`;
exports[`components/global/product_switcher should match snapshot with product switcher menu 1`] = `
@@ -149,6 +182,75 @@ exports[`components/global/product_switcher should match snapshot with product s
isMessaging={true}
onClick={[Function]}
/>
<Memo(MenuGroup)>
<MenuStartTrial
id="startTrial"
/>
</Memo(MenuGroup)>
</Menu>
</MenuWrapper>
</div>
`;
exports[`components/global/product_switcher should match snapshot without license 1`] = `
<div>
<MenuWrapper
animationComponent={[Function]}
className=""
open={true}
>
<ProductMenuContainer
onClick={[Function]}
>
<ProductMenuButton
active={true}
aria-controls="product-switcher-menu"
aria-expanded={true}
aria-label="Product switch menu"
/>
<ProductBrandingTeamEdition />
</ProductMenuContainer>
<Menu
ariaLabel="switcherOpen"
className="product-switcher-menu"
id="product-switcher-menu"
listId="product-switcher-menu-dropdown"
>
<ProductMenuItem
active={true}
destination="/"
icon="product-channels"
onClick={[Function]}
text="Channels"
/>
<ProductMenuItem
active={false}
destination=""
icon="product-boards"
id="product-menu-item-Boards"
key="Boards"
onClick={[Function]}
text="Boards"
/>
<ProductMenuItem
active={false}
destination=""
icon="product-playbooks"
id="product-menu-item-Playbooks"
key="Playbooks"
onClick={[Function]}
text="Playbooks"
/>
<Connect(ProductMenuList)
handleVisitConsoleClick={[Function]}
isMessaging={true}
onClick={[Function]}
/>
<Memo(MenuGroup)>
<MenuStartTrial
id="startTrial"
/>
</Memo(MenuGroup)>
</Menu>
</MenuWrapper>
</div>
@@ -208,6 +310,11 @@ exports[`components/global/product_switcher should render once when there are no
isMessaging={true}
onClick={[Function]}
/>
<Memo(MenuGroup)>
<MenuStartTrial
id="startTrial"
/>
</Memo(MenuGroup)>
</Menu>
</MenuWrapper>
</div>
@@ -267,6 +374,11 @@ exports[`components/global/product_switcher should render the correct amount of
isMessaging={true}
onClick={[Function]}
/>
<Memo(MenuGroup)>
<MenuStartTrial
id="startTrial"
/>
</Memo(MenuGroup)>
</Menu>
</MenuWrapper>
</div>

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

@@ -0,0 +1,4 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export {default} from './product_branding_team_edition';

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

@@ -0,0 +1,53 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import styled from 'styled-components';
import Logo from 'components/common/svg_images_components/logo_dark_blue_svg';
const ProductBrandingTeamEditionContainer = styled.div`
display: flex;
align-items: center;
> * + * {
margin-left: 8px;
}
`;
const StyledLogo = styled(Logo)`
path {
fill: rgba(var(--sidebar-text-rgb), 0.75);
}
`;
const Badge = styled.div`
display: flex;
align-self: center;
padding: 2px 6px;
position: relative;
top: 1px;
border-radius: var(--radius-s);
margin-left: 12px;
background: rgba(var(--sidebar-text-rgb), 0.08);
color: rgba(var(--sidebar-text-rgb), 0.75);
font-family: 'Open Sans', sans-serif;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.025em;
line-height: 16px;
`;
const ProductBrandingTeamEdition = (): JSX.Element => {
return (
<ProductBrandingTeamEditionContainer tabIndex={0}>
<StyledLogo
width={116}
height={20}
/>
<Badge>{'FREE EDITION'}</Badge>
</ProductBrandingTeamEditionContainer>
);
};
export default ProductBrandingTeamEdition;

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

@@ -40,10 +40,38 @@ describe('components/global/product_switcher', () => {
},
},
};
useSelectorMock.mockReturnValue(true);
useSelectorMock.mockReturnValueOnce(true);
useSelectorMock.mockReturnValueOnce({IsLicensed: 'true'});
const store = mockStore(state);
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
const wrapper = shallow(<reactRedux.Provider store={store}><ProductMenu/></reactRedux.Provider>);
const wrapper = shallow(<ProductMenu/>, {
wrappingComponent: reactRedux.Provider,
wrappingComponentProps: {store},
});
expect(wrapper).toMatchSnapshot();
});
it('should match snapshot without license', () => {
const state = {
views: {
productMenu: {
switcherOpen: false,
},
},
};
useSelectorMock.mockReturnValue(true);
useSelectorMock.mockReturnValueOnce(true);
useSelectorMock.mockReturnValueOnce({IsLicensed: 'false'});
const store = mockStore(state);
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
const wrapper = shallow(<ProductMenu/>, {
wrappingComponent: reactRedux.Provider,
wrappingComponentProps: {store},
});
expect(wrapper).toMatchSnapshot();
});
@@ -63,6 +91,8 @@ describe('components/global/product_switcher', () => {
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue(true);
useSelectorMock.mockReturnValueOnce(true);
useSelectorMock.mockReturnValueOnce({IsLicensed: 'true'});
const wrapper = shallow(<ProductMenu/>, {
wrappingComponent: reactRedux.Provider,
wrappingComponentProps: {store},
@@ -87,6 +117,8 @@ describe('components/global/product_switcher', () => {
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue(true);
useSelectorMock.mockReturnValueOnce(true);
useSelectorMock.mockReturnValueOnce({IsLicensed: 'true'});
const wrapper = shallow(<ProductMenu/>, {
wrappingComponent: reactRedux.Provider,
wrappingComponentProps: {store},
@@ -115,6 +147,8 @@ describe('components/global/product_switcher', () => {
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue(true);
useSelectorMock.mockReturnValueOnce(true);
useSelectorMock.mockReturnValueOnce({IsLicensed: 'true'});
const wrapper = shallow(<ProductMenu/>, {
wrappingComponent: reactRedux.Provider,
wrappingComponentProps: {store},
@@ -141,6 +175,8 @@ describe('components/global/product_switcher', () => {
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue(true);
useSelectorMock.mockReturnValueOnce(true);
useSelectorMock.mockReturnValueOnce({IsLicensed: 'true'});
const wrapper = shallow(<ProductMenu/>, {
wrappingComponent: reactRedux.Provider,
wrappingComponentProps: {store},

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

@@ -8,7 +8,7 @@ import styled from 'styled-components';
import IconButton from '@mattermost/compass-components/components/icon-button'; // eslint-disable-line no-restricted-imports
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {getInt} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
@@ -34,6 +34,7 @@ import {useCurrentProductId, useProducts, isChannels} from 'utils/products';
import type {GlobalState} from 'types/store';
import ProductBranding from './product_branding';
import ProductBrandingTeamEdition from './product_branding_team_edition';
import ProductMenuItem from './product_menu_item';
import ProductMenuList from './product_menu_list';
@@ -74,6 +75,7 @@ const ProductMenu = (): JSX.Element => {
const switcherOpen = useSelector(isSwitcherOpen);
const menuRef = useRef<HTMLDivElement>(null);
const currentProductID = useCurrentProductId();
const license = useSelector(getLicense);
const enableTutorial = useSelector(getConfig).EnableTutorial === 'true';
const currentUserId = useSelector(getCurrentUserId);
@@ -137,7 +139,8 @@ const ProductMenu = (): JSX.Element => {
aria-label={formatMessage({id: 'global_header.productSwitchMenu', defaultMessage: 'Product switch menu'})}
aria-controls='product-switcher-menu'
/>
<ProductBranding/>
{license.IsLicensed === 'false' && <ProductBrandingTeamEdition/>}
{license.IsLicensed === 'true' && <ProductBranding/>}
</ProductMenuContainer>
<Menu
listId={'product-switcher-menu-dropdown'}
@@ -158,6 +161,11 @@ const ProductMenu = (): JSX.Element => {
onClick={handleClick}
handleVisitConsoleClick={handleVisitConsoleClick}
/>
<Menu.Group>
<Menu.StartTrial
id='startTrial'
/>
</Menu.Group>
</Menu>
</MenuWrapper>
</div>

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

@@ -11,17 +11,6 @@ exports[`components/global/product_switcher_menu should match snapshot with id 1
<MenuItemCloudLimit
id="menuItemCloudLimit"
/>
<Memo(SystemPermissionGate)
permissions={
Array [
"sysconsole_write_about_edition_and_license",
]
}
>
<MenuStartTrial
id="startTrial"
/>
</Memo(SystemPermissionGate)>
<Memo(SystemPermissionGate)
permissions={
Array [
@@ -209,17 +198,6 @@ exports[`components/global/product_switcher_menu should match snapshot with most
<MenuItemCloudLimit
id="menuItemCloudLimit"
/>
<Memo(SystemPermissionGate)
permissions={
Array [
"sysconsole_write_about_edition_and_license",
]
}
>
<MenuStartTrial
id="startTrial"
/>
</Memo(SystemPermissionGate)>
<Memo(SystemPermissionGate)
permissions={
Array [
@@ -533,17 +511,6 @@ exports[`components/global/product_switcher_menu should show integrations should
<MenuItemCloudLimit
id="menuItemCloudLimit"
/>
<Memo(SystemPermissionGate)
permissions={
Array [
"sysconsole_write_about_edition_and_license",
]
}
>
<MenuStartTrial
id="startTrial"
/>
</Memo(SystemPermissionGate)>
<Memo(SystemPermissionGate)
permissions={
Array [

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

@@ -112,13 +112,6 @@ const ProductMenuList = (props: Props): JSX.Element | null => {
<div onClick={onClick}>
<Menu.CloudTrial id='menuCloudTrial'/>
<Menu.ItemCloudLimit id='menuItemCloudLimit'/>
<SystemPermissionGate
permissions={[Permissions.SYSCONSOLE_WRITE_ABOUT_EDITION_AND_LICENSE]}
>
<Menu.StartTrial
id='startTrial'
/>
</SystemPermissionGate>
<SystemPermissionGate permissions={Permissions.SYSCONSOLE_READ_PERMISSIONS}>
<Menu.ItemLink
id='systemConsole'

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

@@ -24,17 +24,39 @@
@include link;
display: flex;
color: var(--center-channel-color);
margin-top: 12px;
color: rgba(var(--center-channel-color-rgb), 0.75);
font-family: 'Metropolis';
font-size: 20px;
font-weight: 600;
font-weight: 500;
line-height: 28px;
text-decoration: none;
&:hover, &:active, &:visited, &:link {
text-decoration: none;
}
svg {
width: 170px;
fill: var(--center-channel-color);
}
.freeBadge {
position: relative;
top: 1px;
display: flex;
align-self: center;
padding: 2px 6px;
border-radius: var(--radius-s);
margin-left: 12px;
background: rgba(var(--center-channel-color-rgb), 0.08);
color: rgba(var(--center-channel-color-rgb), 0.75);
font-family: 'Open Sans', sans-serif;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.025em;
line-height: 16px;
}
}
}

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

@@ -5,7 +5,7 @@ import React from 'react';
import {useSelector} from 'react-redux';
import {Link} from 'react-router-dom';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import BackButton from 'components/common/back_button';
import Logo from 'components/common/svg_images_components/logo_dark_blue_svg';
@@ -19,20 +19,48 @@ export type HeaderProps = {
}
const Header = ({alternateLink, backButtonURL, onBackButtonClick}: HeaderProps) => {
const {EnableCustomBrand, SiteName} = useSelector(getConfig);
const {SiteName} = useSelector(getConfig);
const license = useSelector(getLicense);
const ariaLabel = EnableCustomBrand === 'true' && SiteName ? SiteName : 'Mattermost';
const ariaLabel = SiteName || 'Mattermost';
let freeBanner = null;
if (license.IsLicensed === 'false') {
freeBanner = <><Logo/><span className='freeBadge'>{'FREE EDITION'}</span></>;
}
let title: React.ReactNode = SiteName;
if (title === 'Mattermost') {
if (freeBanner) {
title = '';
} else {
title = <Logo/>;
}
}
return (
<div className='hfroute-header'>
<div className='header-main'>
<Link
className='header-logo-link'
to='/'
aria-label={ariaLabel}
>
{EnableCustomBrand === 'true' || SiteName !== 'Mattermost' ? SiteName : <Logo/>}
</Link>
<div>
{freeBanner &&
<Link
className='header-logo-link'
to='/'
aria-label={ariaLabel}
>
{freeBanner}
</Link>
}
{title &&
<Link
className='header-logo-link'
to='/'
aria-label={ariaLabel}
>
{title}
</Link>
}
</div>
{alternateLink}
</div>
{onBackButtonClick && (

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

@@ -6,6 +6,7 @@
.login-body {
display: flex;
flex-direction: column;
padding-bottom: 32px;
margin: auto;
.login-body-action {
@@ -43,7 +44,7 @@
align-self: flex-start;
.login-body-message-title {
padding-right: 48px;
padding-right: 24px;
color: var(--title-color-indigo-500);
font-family: Metropolis;
font-size: 80px;
@@ -229,7 +230,7 @@
}
.login-body-message .login-body-message-title {
padding-right: 110px;
padding-right: 80px;
margin-top: 48px;
}
}
@@ -254,7 +255,7 @@
padding: 24px;
.login-body-message-title {
padding-right: 180px;
padding-right: 80px;
font-size: 64px;
line-height: 76px;
}
@@ -305,8 +306,13 @@
.login-body-card-content {
.login-body-card-title {
display: block;
margin-bottom: 24px;
font-size: 32px;
line-height: 40px;
&:has(+ .login-body-custom-branding-markdown){
margin-bottom: 12px;
}
}
.login-body-custom-branding-markdown,
@@ -339,7 +345,6 @@
padding: 24px;
.login-body-message-title {
max-width: 271px;
padding-right: 0;
font-size: 45px;
line-height: 56px;

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

@@ -313,12 +313,11 @@
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 8px;
padding-left: 16px;
padding: 8px 20px;
border-radius: 4px;
margin: 8px;
margin-top: 0;
background-color: rgba(var(--button-bg-rgb), 0.04);
font-size: 12px;
line-height: 16px;
button {
padding: 3px 0;
@@ -327,16 +326,21 @@
color: var(--link-color);
font-style: normal;
font-weight: 600;
}
button:hover {
background-color: transparent;
&:hover {
background: transparent;
text-decoration: underline;
}
}
.start_trial_content {
position: relative;
width: 100%;
}
.editionName {
text-transform: capitalize;
}
}
}

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

@@ -4,6 +4,8 @@
import React from 'react';
import * as reactRedux from 'react-redux';
import {Permissions} from 'mattermost-redux/constants';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import mockStore from 'tests/test_store';
@@ -21,10 +23,17 @@ describe('components/widgets/menu/menu_items/menu_start_trial', () => {
entities: {
users: {
currentUserId: 'test_id',
profiles: {
test_id: {
id: 'test_id',
roles: 'system_user',
},
},
},
general: {
config: {
EnableTutorial: 'true',
BuildEnterpriseReady: 'true',
},
license: {
IsLicensed: 'false',
@@ -35,6 +44,13 @@ describe('components/widgets/menu/menu_items/menu_start_trial', () => {
IsLicensed: 'false',
},
},
roles: {
roles: {
system_user: {
permissions: [Permissions.SYSCONSOLE_WRITE_ABOUT_EDITION_AND_LICENSE],
},
},
},
preferences: {
myPreferences: {
'tutorial_step-test_id': {
@@ -59,10 +75,17 @@ describe('components/widgets/menu/menu_items/menu_start_trial', () => {
entities: {
users: {
currentUserId: 'test_id',
profiles: {
test_id: {
id: 'test_id',
roles: 'system_user',
},
},
},
general: {
config: {
EnableTutorial: 'true',
BuildEnterpriseReady: 'true',
},
license: {
IsLicensed: 'false',
@@ -74,6 +97,13 @@ describe('components/widgets/menu/menu_items/menu_start_trial', () => {
IsLicensed: 'true',
},
},
roles: {
roles: {
system_user: {
permissions: [Permissions.SYSCONSOLE_WRITE_ABOUT_EDITION_AND_LICENSE],
},
},
},
preferences: {
myPreferences: {
'tutorial_step-test_id': {
@@ -98,10 +128,17 @@ describe('components/widgets/menu/menu_items/menu_start_trial', () => {
entities: {
users: {
currentUserId: 'test_id',
profiles: {
test_id: {
id: 'test_id',
roles: 'system_user',
},
},
},
general: {
config: {
EnableTutorial: 'true',
BuildEnterpriseReady: 'true',
},
license: {
IsLicensed: 'true',
@@ -113,6 +150,13 @@ describe('components/widgets/menu/menu_items/menu_start_trial', () => {
IsLicensed: 'false',
},
},
roles: {
roles: {
system_user: {
permissions: [Permissions.SYSCONSOLE_WRITE_ABOUT_EDITION_AND_LICENSE],
},
},
},
preferences: {
myPreferences: {
'tutorial_step-test_id': {
@@ -132,15 +176,22 @@ describe('components/widgets/menu/menu_items/menu_start_trial', () => {
expect(wrapper.find('button').exists()).toEqual(false);
});
test('should render menu option that open the start trial benefits modal when is current licensed but is trial', () => {
test('should render null when is current licensed but is trial and you have no permissions to load the trial', () => {
const state = {
entities: {
users: {
currentUserId: 'test_id',
profiles: {
test_id: {
id: 'test_id',
roles: 'system_user',
},
},
},
general: {
config: {
EnableTutorial: 'true',
BuildEnterpriseReady: 'true',
},
license: {
IsLicensed: 'true',
@@ -152,6 +203,13 @@ describe('components/widgets/menu/menu_items/menu_start_trial', () => {
IsLicensed: 'false',
},
},
roles: {
roles: {
system_user: {
permissions: [],
},
},
},
preferences: {
myPreferences: {
'tutorial_step-test_id': {
@@ -168,19 +226,25 @@ describe('components/widgets/menu/menu_items/menu_start_trial', () => {
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
const wrapper = mountWithIntl(<reactRedux.Provider store={store}><MenuStartTrial id='startTrial'/></reactRedux.Provider>);
expect(wrapper.find('button').exists()).toEqual(true);
expect(wrapper.find('button').text()).toEqual('Learn More');
expect(wrapper.find('button').exists()).toEqual(false);
});
test('should render menu option that open the start trial modal when has no license and no previous license', () => {
test('should render menu option that open the start trial modal when has no license and no previous license and you have permissions to load the trial', () => {
const state = {
entities: {
users: {
currentUserId: 'test_id',
profiles: {
test_id: {
id: 'test_id',
roles: 'system_user',
},
},
},
general: {
config: {
EnableTutorial: 'true',
BuildEnterpriseReady: 'true',
},
license: {
IsLicensed: 'false',
@@ -192,6 +256,13 @@ describe('components/widgets/menu/menu_items/menu_start_trial', () => {
IsLicensed: 'false',
},
},
roles: {
roles: {
system_user: {
permissions: [Permissions.SYSCONSOLE_WRITE_ABOUT_EDITION_AND_LICENSE],
},
},
},
preferences: {
myPreferences: {
'tutorial_step-test_id': {
@@ -209,7 +280,7 @@ describe('components/widgets/menu/menu_items/menu_start_trial', () => {
useDispatchMock.mockReturnValue(dummyDispatch);
const wrapper = mountWithIntl(<reactRedux.Provider store={store}><MenuStartTrial id='startTrial'/></reactRedux.Provider>);
expect(wrapper.find('button').exists()).toEqual(true);
expect(wrapper.find('div.start_trial_content').text()).toEqual('Try Enterprise for free now!');
expect(wrapper.find('button').text()).toEqual('Learn More');
expect(wrapper.find('div.start_trial_content').text()).toEqual('This is the free edition of Mattermost, ideal for evaluation and small teams.');
expect(wrapper.find('button').text()).toEqual('Start an Enterprise trial');
});
});

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

@@ -4,11 +4,14 @@
import React, {useEffect} from 'react';
import {useIntl} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
import styled from 'styled-components';
import type {GlobalState} from '@mattermost/types/store';
import {getPrevTrialLicense} from 'mattermost-redux/actions/admin';
import {getLicense} from 'mattermost-redux/selectors/entities/general';
import {Permissions} from 'mattermost-redux/constants';
import {getLicense, getConfig} from 'mattermost-redux/selectors/entities/general';
import {haveISystemPermission} from 'mattermost-redux/selectors/entities/roles';
import {trackEvent} from 'actions/telemetry_actions';
import {openModal} from 'actions/views/modals';
@@ -16,13 +19,27 @@ import {openModal} from 'actions/views/modals';
import {makeAsyncComponent} from 'components/async_load';
import {ModalIdentifiers, TELEMETRY_CATEGORIES} from 'utils/constants';
import {isTrialLicense} from 'utils/license_utils';
import './menu_item.scss';
const TrialBenefitsModal = makeAsyncComponent('TrialBenefitsModal', React.lazy(() => import('components/trial_benefits_modal/trial_benefits_modal')));
const LearnMoreTrialModal = makeAsyncComponent('LearnMoreTrialModal', React.lazy(() => import('components/learn_more_trial_modal/learn_more_trial_modal')));
const FreeVersionBadge = styled.div`
position: relative;
top: 1px;
display: flex;
padding: 2px 6px;
border-radius: var(--radius-s);
margin-bottom: 6px;
background: rgba(var(--center-channel-color-rgb), 0.08);
color: rgba(var(--center-channel-color-rgb), 0.75);
font-family: 'Open Sans', sans-serif;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.025em;
line-height: 16px;
`;
type Props = {
id: string;
}
@@ -35,6 +52,10 @@ const MenuStartTrial = (props: Props): JSX.Element | null => {
dispatch(getPrevTrialLicense());
}, []);
const config = useSelector(getConfig);
const isE0 = config.BuildEnterpriseReady === 'true';
const hasPermissionForStartTrial = useSelector((state: GlobalState) => haveISystemPermission(state, {permission: Permissions.SYSCONSOLE_WRITE_ABOUT_EDITION_AND_LICENSE}));
const openLearnMoreTrialModal = () => {
trackEvent(
TELEMETRY_CATEGORIES.SELF_HOSTED_START_TRIAL_MODAL,
@@ -45,54 +66,42 @@ const MenuStartTrial = (props: Props): JSX.Element | null => {
dialogType: LearnMoreTrialModal,
}));
};
const openTrialBenefitsModal = () => {
trackEvent(
TELEMETRY_CATEGORIES.SELF_HOSTED_START_TRIAL_MODAL,
'open_trial_benefits_modal_from_menu',
);
dispatch(openModal({
modalId: ModalIdentifiers.TRIAL_BENEFITS_MODAL,
dialogType: TrialBenefitsModal,
}));
};
const prevTrialLicense = useSelector((state: GlobalState) => state.entities.admin.prevTrialLicense);
const license = useSelector(getLicense);
const isPrevLicensed = prevTrialLicense?.IsLicensed;
const isCurrentLicensed = license?.IsLicensed;
const isCurrentLicenseTrial = isTrialLicense(license);
// Show this CTA if the instance is currently not licensed and has never had a trial license loaded before
const show = (isCurrentLicensed === 'false' && isPrevLicensed === 'false') || isCurrentLicenseTrial;
if (!show) {
if (isCurrentLicensed === 'true') {
return null;
}
const showTrialButton = isCurrentLicensed === 'false' && isPrevLicensed === 'false' && hasPermissionForStartTrial && isE0;
return (
<li
className={'MenuStartTrial'}
role='menuitem'
id={props.id}
>
{isCurrentLicenseTrial ? <>
<div style={{display: 'inline'}}>
<span>
{formatMessage({id: 'navbar_dropdown.reviewTrialBenefits', defaultMessage: 'Review the features you get with Enterprise. '})}
</span>
<button onClick={openTrialBenefitsModal}>
{formatMessage({id: 'navbar_dropdown.learnMoreTrialBenefits', defaultMessage: 'Learn More'})}
</button>
</div>
</> : <>
<FreeVersionBadge>{'FREE EDITION'}</FreeVersionBadge>
{isE0 &&
<div className='start_trial_content'>
{formatMessage({id: 'navbar_dropdown.tryTrialNow', defaultMessage: 'Try Enterprise for free now!'})}
</div>
{formatMessage({
id: 'navbar_dropdown.versionTextE0',
defaultMessage: 'This is the free edition of Mattermost, ideal for evaluation and small teams.',
})}
</div>}
{!isE0 &&
<div className='start_trial_content'>
{formatMessage({
id: 'navbar_dropdown.versionTextTeamEdition',
defaultMessage: 'This is the free open source edition of Mattermost, ideal for evaluation and small teams.',
})}
</div>}
{showTrialButton &&
<button onClick={openLearnMoreTrialModal}>
{formatMessage({id: 'navbar_dropdown.learnMore', defaultMessage: 'Learn More'})}
</button>
</>
}
{formatMessage({id: 'navbar_dropdown.startAnEnterpriseTrial', defaultMessage: 'Start an Enterprise trial'})}
</button>}
</li>
);
};

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

@@ -4206,8 +4206,6 @@
"navbar_dropdown.invitePeople": "Invite People",
"navbar_dropdown.invitePeopleExtraText": "Add people to the team",
"navbar_dropdown.join": "Join Another Team",
"navbar_dropdown.learnMore": "Learn More",
"navbar_dropdown.learnMoreTrialBenefits": "Learn More",
"navbar_dropdown.leave": "Leave Team",
"navbar_dropdown.leave.icon": "Leave Team Icon",
"navbar_dropdown.logout": "Log Out",
@@ -4218,10 +4216,9 @@
"navbar_dropdown.nativeApps": "Download Apps",
"navbar_dropdown.profileSettings": "Profile",
"navbar_dropdown.report": "Report a Problem",
"navbar_dropdown.reviewTrialBenefits": "Review the features you get with Enterprise. ",
"navbar_dropdown.startAnEnterpriseTrial": "Start an Enterprise trial",
"navbar_dropdown.switchTo": "Switch to ",
"navbar_dropdown.teamSettings": "Team Settings",
"navbar_dropdown.tryTrialNow": "Try Enterprise for free now!",
"navbar_dropdown.userGroups": "User Groups",
"navbar_dropdown.userGroups.modal.messageAdminPostTrial": "User groups are a way to organize users and apply actions to all users within that group. Upgrade to the Professional plan to create unlimited user groups.",
"navbar_dropdown.userGroups.modal.messageAdminPreTrial": "Create unlimited user groups with one of our paid plans. Get the full experience of Enterprise when you start a free, {trialLength} day trial.",
@@ -4230,6 +4227,8 @@
"navbar_dropdown.userGroups.modal.titleAdminPreTrial": "Try unlimited user groups with a free trial",
"navbar_dropdown.userGroups.modal.titleEndUser": "User groups available in paid plans",
"navbar_dropdown.userGroups.tooltip.cloudFreeTrial": "During your trial you are able to create user groups. These user groups will be archived after your trial.",
"navbar_dropdown.versionTextE0": "This is the free edition of Mattermost, ideal for evaluation and small teams.",
"navbar_dropdown.versionTextTeamEdition": "This is the free open source edition of Mattermost, ideal for evaluation and small teams.",
"navbar_dropdown.viewMembers": "View Members",
"navbar.addGroups": "Add Groups",
"navbar.addMembers": "Add Members",