Entry Sku Product Label (#33847)

* Initial commit

* Linting

* Update FREE EDITION to TEAM EDITION

* Added permalink for entry limit info
This commit is contained in:
Maria A Nunez
2025-09-10 21:37:24 -04:00
committed by GitHub
parent d29b4e28cb
commit 4169cb7b65
13 changed files with 444 additions and 47 deletions
@@ -24,7 +24,7 @@ test(
// # Get license information and prepare test args
const testArgs = {page, browserName, viewport};
const license = await adminClient.getClientLicenseOld();
const editionSuffix = license.IsLicensed === 'true' ? '' : 'free edition';
const editionSuffix = license.IsLicensed === 'true' ? '' : 'team edition';
// * Verify login page appears as expected
await pw.matchSnapshot({...testInfo, title: `${testInfo.title} ${editionSuffix}`}, testArgs);
@@ -29,7 +29,7 @@ test(
// # Get license information to determine snapshot suffix
const license = await adminClient.getClientLicenseOld();
const editionSuffix = license.IsLicensed === 'true' ? '' : 'free edition';
const editionSuffix = license.IsLicensed === 'true' ? '' : 'team edition';
const testArgs = {page, browserName, viewport};
// * Verify visual appearance of signup page
@@ -148,6 +148,80 @@ exports[`components/global/product_switcher should match snapshot 1`] = `
</div>
`;
exports[`components/global/product_switcher should match snapshot for Entry license 1`] = `
<div>
<MenuWrapper
animationComponent={[Function]}
className=""
open={true}
>
<ProductMenuContainer
onClick={[Function]}
>
<ProductMenuButton
aria-controls="product-switcher-menu"
aria-expanded={true}
aria-label="Product switch menu"
style={
Object {
"backgroundColor": "rgba(var(--sidebar-text-rgb), 0.16)",
"color": "rgba(var(--sidebar-text-rgb), 0.56)",
}
}
>
<ProductsIcon
color="rgba(var(--sidebar-text-rgb), 0.56)"
size={20}
/>
<ProductBrandingFreeEdition />
</ProductMenuButton>
</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`] = `
<div>
<MenuWrapper
@@ -247,7 +321,7 @@ exports[`components/global/product_switcher should match snapshot without licens
color="rgba(var(--sidebar-text-rgb), 0.56)"
size={20}
/>
<ProductBrandingTeamEdition />
<ProductBrandingFreeEdition />
</ProductMenuButton>
</ProductMenuContainer>
<Menu
@@ -1,4 +1,4 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export {default} from './product_branding_team_edition';
export {default} from './product_branding_free_edition';
@@ -0,0 +1,122 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import ProductBrandingFreeEdition from './product_branding_free_edition';
describe('ProductBrandingFreeEdition', () => {
const baseProps = {};
test('should show ENTRY EDITION for Entry license', () => {
const state = {
entities: {
general: {
license: TestHelper.getLicenseMock({
IsLicensed: 'true',
SkuShortName: 'entry',
}),
},
},
};
const {container} = renderWithContext(
<ProductBrandingFreeEdition {...baseProps}/>,
state,
);
expect(screen.getByText('ENTRY EDITION')).toBeInTheDocument();
const logoElement = container.querySelector('svg');
expect(logoElement).toBeInTheDocument();
});
test('should show TEAM EDITION for unlicensed', () => {
const state = {
entities: {
general: {
license: TestHelper.getLicenseMock({
IsLicensed: 'false',
SkuShortName: '',
}),
},
},
};
const {container} = renderWithContext(
<ProductBrandingFreeEdition {...baseProps}/>,
state,
);
expect(screen.getByText('TEAM EDITION')).toBeInTheDocument();
const logoElement = container.querySelector('svg');
expect(logoElement).toBeInTheDocument();
});
test('should show empty badge for Professional license', () => {
const state = {
entities: {
general: {
license: TestHelper.getLicenseMock({
IsLicensed: 'true',
SkuShortName: 'professional',
}),
},
},
};
renderWithContext(
<ProductBrandingFreeEdition {...baseProps}/>,
state,
);
// Should not show any edition badge
expect(screen.queryByText('ENTRY EDITION')).not.toBeInTheDocument();
expect(screen.queryByText('TEAM EDITION')).not.toBeInTheDocument();
expect(screen.queryByText('PROFESSIONAL EDITION')).not.toBeInTheDocument();
});
test('should show empty badge for Enterprise license', () => {
const state = {
entities: {
general: {
license: TestHelper.getLicenseMock({
IsLicensed: 'true',
SkuShortName: 'enterprise',
}),
},
},
};
renderWithContext(
<ProductBrandingFreeEdition {...baseProps}/>,
state,
);
// Should not show any edition badge
expect(screen.queryByText('ENTRY EDITION')).not.toBeInTheDocument();
expect(screen.queryByText('TEAM EDITION')).not.toBeInTheDocument();
expect(screen.queryByText('ENTERPRISE EDITION')).not.toBeInTheDocument();
});
test('should show empty badge when no license object', () => {
const state = {
entities: {
general: {
license: {},
},
},
};
renderWithContext(
<ProductBrandingFreeEdition {...baseProps}/>,
state,
);
// Should not show any edition badge when license object is empty
expect(screen.queryByText('ENTRY EDITION')).not.toBeInTheDocument();
expect(screen.queryByText('TEAM EDITION')).not.toBeInTheDocument();
});
});
@@ -2,11 +2,16 @@
// See LICENSE.txt for license information.
import React from 'react';
import {useSelector} from 'react-redux';
import styled from 'styled-components';
import {getLicense} from 'mattermost-redux/selectors/entities/general';
import Logo from 'components/common/svg_images_components/logo_dark_blue_svg';
const ProductBrandingTeamEditionContainer = styled.span`
import {LicenseSkus} from 'utils/constants';
const ProductBrandingFreeEditionContainer = styled.span`
display: flex;
align-items: center;
@@ -36,16 +41,25 @@ const Badge = styled.span`
line-height: 16px;
`;
const ProductBrandingTeamEdition = (): JSX.Element => {
const ProductBrandingFreeEdition = (): JSX.Element => {
const license = useSelector(getLicense);
let badgeText = '';
if (license?.SkuShortName === LicenseSkus.Entry) {
badgeText = 'ENTRY EDITION';
} else if (license?.IsLicensed === 'false') {
badgeText = 'TEAM EDITION';
}
return (
<ProductBrandingTeamEditionContainer tabIndex={-1}>
<ProductBrandingFreeEditionContainer tabIndex={-1}>
<StyledLogo
width={116}
height={20}
/>
<Badge>{'FREE EDITION'}</Badge>
</ProductBrandingTeamEditionContainer>
<Badge>{badgeText}</Badge>
</ProductBrandingFreeEditionContainer>
);
};
export default ProductBrandingTeamEdition;
export default ProductBrandingFreeEdition;
@@ -10,6 +10,8 @@ import {TopLevelProducts} from 'utils/constants';
import * as productUtils from 'utils/products';
import {TestHelper} from 'utils/test_helper';
import ProductBranding from './product_branding';
import ProductBrandingFreeEdition from './product_branding_team_edition';
import ProductMenu, {ProductMenuButton, ProductMenuContainer} from './product_menu';
import ProductMenuItem from './product_menu_item';
import ProductMenuList from './product_menu_list';
@@ -185,4 +187,118 @@ describe('components/global/product_switcher', () => {
expect(wrapper.find(ProductMenuList)).toHaveLength(1);
expect(wrapper).toMatchSnapshot();
});
it('should render ProductBrandingFreeEdition for Entry license', () => {
const state = {
views: {
productMenu: {
switcherOpen: false,
},
},
};
const store = mockStore(state);
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue(true);
useSelectorMock.mockReturnValueOnce(true);
useSelectorMock.mockReturnValueOnce({IsLicensed: 'true', SkuShortName: 'entry'});
const wrapper = shallow(<ProductMenu/>, {
wrappingComponent: reactRedux.Provider,
wrappingComponentProps: {store},
});
expect(wrapper.find(ProductBrandingFreeEdition)).toHaveLength(1);
expect(wrapper.find(ProductBranding)).toHaveLength(0);
});
it('should render ProductBrandingFreeEdition for unlicensed', () => {
const state = {
views: {
productMenu: {
switcherOpen: false,
},
},
};
const store = mockStore(state);
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue(true);
useSelectorMock.mockReturnValueOnce(true);
useSelectorMock.mockReturnValueOnce({IsLicensed: 'false'});
const wrapper = shallow(<ProductMenu/>, {
wrappingComponent: reactRedux.Provider,
wrappingComponentProps: {store},
});
expect(wrapper.find(ProductBrandingFreeEdition)).toHaveLength(1);
expect(wrapper.find(ProductBranding)).toHaveLength(0);
});
it('should render ProductBranding for Professional license', () => {
const state = {
views: {
productMenu: {
switcherOpen: false,
},
},
};
const store = mockStore(state);
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue(true);
useSelectorMock.mockReturnValueOnce(true);
useSelectorMock.mockReturnValueOnce({IsLicensed: 'true', SkuShortName: 'professional'});
const wrapper = shallow(<ProductMenu/>, {
wrappingComponent: reactRedux.Provider,
wrappingComponentProps: {store},
});
expect(wrapper.find(ProductBranding)).toHaveLength(1);
expect(wrapper.find(ProductBrandingFreeEdition)).toHaveLength(0);
});
it('should render ProductBranding for Enterprise license', () => {
const state = {
views: {
productMenu: {
switcherOpen: false,
},
},
};
const store = mockStore(state);
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue(true);
useSelectorMock.mockReturnValueOnce(true);
useSelectorMock.mockReturnValueOnce({IsLicensed: 'true', SkuShortName: 'enterprise'});
const wrapper = shallow(<ProductMenu/>, {
wrappingComponent: reactRedux.Provider,
wrappingComponentProps: {store},
});
expect(wrapper.find(ProductBranding)).toHaveLength(1);
expect(wrapper.find(ProductBrandingFreeEdition)).toHaveLength(0);
});
it('should match snapshot for Entry license', () => {
const state = {
views: {
productMenu: {
switcherOpen: false,
},
},
};
const store = mockStore(state);
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue(true);
useSelectorMock.mockReturnValueOnce(true);
useSelectorMock.mockReturnValueOnce({IsLicensed: 'true', SkuShortName: 'entry'});
const wrapper = shallow(<ProductMenu/>, {
wrappingComponent: reactRedux.Provider,
wrappingComponentProps: {store},
});
expect(wrapper).toMatchSnapshot();
});
});
@@ -24,10 +24,11 @@ import {
import Menu from 'components/widgets/menu/menu';
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
import {LicenseSkus} from 'utils/constants';
import {useCurrentProductId, useProducts, isChannels} from 'utils/products';
import ProductBranding from './product_branding';
import ProductBrandingTeamEdition from './product_branding_team_edition';
import ProductBrandingFreeEdition from './product_branding_team_edition';
import ProductMenuItem from './product_menu_item';
import ProductMenuList from './product_menu_list';
@@ -113,6 +114,8 @@ const ProductMenu = (): JSX.Element => {
);
});
const isFreeEdition = license.IsLicensed === 'false' || license.SkuShortName === LicenseSkus.Entry;
return (
<div ref={menuRef}>
<MenuWrapper
@@ -132,8 +135,11 @@ const ProductMenu = (): JSX.Element => {
size={20}
color='rgba(var(--sidebar-text-rgb), 0.56)'
/>
{license.IsLicensed === 'false' && <ProductBrandingTeamEdition/>}
{license.IsLicensed === 'true' && <ProductBranding/>}
{isFreeEdition ? (
<ProductBrandingFreeEdition/>
) : (
<ProductBranding/>
)}
</ProductMenuButton>
</ProductMenuContainer>
<Menu
@@ -12,6 +12,7 @@ import BackButton from 'components/common/back_button';
import Logo from 'components/common/svg_images_components/logo_dark_blue_svg';
import './header.scss';
import {LicenseSkus} from 'utils/constants';
export type HeaderProps = {
alternateLink?: React.ReactElement;
@@ -27,7 +28,9 @@ const Header = ({alternateLink, backButtonURL, onBackButtonClick}: HeaderProps)
let freeBanner = null;
if (license.IsLicensed === 'false') {
freeBanner = <><Logo/><span className='freeBadge'>{'FREE EDITION'}</span></>;
freeBanner = <><Logo/><span className='freeBadge'>{'TEAM EDITION'}</span></>;
} else if (license.SkuShortName === LicenseSkus.Entry) {
freeBanner = <><Logo/><span className='freeBadge'>{'ENTRY EDITION'}</span></>;
}
let title: React.ReactNode = SiteName;
@@ -6,6 +6,7 @@ import * as reactRedux from 'react-redux';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import mockStore from 'tests/test_store';
import {TestHelper} from 'utils/test_helper';
import MenuStartTrial from './menu_start_trial';
@@ -16,24 +17,17 @@ describe('components/widgets/menu/menu_items/menu_start_trial', () => {
useDispatchMock.mockClear();
});
test('should render null there is no license currently loaded', () => {
test('should render TEAM EDITION for unlicensed', () => {
const state = {
entities: {
users: {
currentUserId: 'test_id',
profiles: {
test_id: {
id: 'test_id',
roles: 'system_user',
},
},
},
general: {
config: {},
license: {
license: TestHelper.getLicenseMock({
IsLicensed: 'false',
IsTrial: 'false',
},
SkuShortName: '',
}),
},
},
};
@@ -41,27 +35,23 @@ 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('.editionText').exists()).toBe(true);
expect(wrapper.text()).toContain('TEAM EDITION');
expect(wrapper.text()).toContain('This is the free');
});
test('should render null when there is a license currently loaded', () => {
test('should render ENTRY EDITION for Entry license', () => {
const state = {
entities: {
users: {
currentUserId: 'test_id',
profiles: {
test_id: {
id: 'test_id',
roles: 'system_user',
},
},
},
general: {
config: {},
license: {
license: TestHelper.getLicenseMock({
IsLicensed: 'true',
IsTrial: 'false',
},
SkuShortName: 'entry',
}),
},
},
};
@@ -69,6 +59,53 @@ 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('.editionText').exists()).toBe(false);
expect(wrapper.find('.editionText').exists()).toBe(true);
expect(wrapper.text()).toContain('ENTRY EDITION');
expect(wrapper.text()).toContain('Entry offers Enterprise Advance capabilities');
});
test('should return null for Professional license', () => {
const state = {
entities: {
users: {
currentUserId: 'test_id',
},
general: {
license: TestHelper.getLicenseMock({
IsLicensed: 'true',
SkuShortName: 'professional',
}),
},
},
};
const store = mockStore(state);
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
const wrapper = mountWithIntl(<reactRedux.Provider store={store}><MenuStartTrial id='startTrial'/></reactRedux.Provider>);
expect(wrapper.isEmptyRender()).toBe(true);
});
test('should return null for Enterprise license', () => {
const state = {
entities: {
users: {
currentUserId: 'test_id',
},
general: {
license: TestHelper.getLicenseMock({
IsLicensed: 'true',
SkuShortName: 'enterprise',
}),
},
},
};
const store = mockStore(state);
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
const wrapper = mountWithIntl(<reactRedux.Provider store={store}><MenuStartTrial id='startTrial'/></reactRedux.Provider>);
expect(wrapper.isEmptyRender()).toBe(true);
});
});
@@ -10,7 +10,7 @@ import {getLicense} from 'mattermost-redux/selectors/entities/general';
import ExternalLink from 'components/external_link';
import {LicenseLinks} from 'utils/constants';
import {LicenseLinks, LicenseSkus} from 'utils/constants';
import './menu_item.scss';
@@ -39,34 +39,57 @@ const MenuStartTrial = (props: Props): JSX.Element | null => {
const license = useSelector(getLicense);
const isCurrentLicensed = license?.IsLicensed;
const skuShortName = license?.SkuShortName;
if (isCurrentLicensed === 'true') {
// If licensed and NOT Entry, return null
if (isCurrentLicensed === 'true' && skuShortName !== LicenseSkus.Entry) {
return null;
}
// Determine badge text and description based on license type
const isEntryLicense = isCurrentLicensed === 'true' && skuShortName === LicenseSkus.Entry;
const badgeText = isEntryLicense ? 'ENTRY EDITION' : 'TEAM EDITION';
return (
<li
className={'MenuStartTrial'}
role='menuitem'
id={props.id}
>
<FreeVersionBadge>{'FREE EDITION'}</FreeVersionBadge>
<FreeVersionBadge>{badgeText}</FreeVersionBadge>
<div className='editionText'>
{formatMessage(
{
id: 'navbar_dropdown.versionText',
defaultMessage: 'This is the free <link>unsupported</link> edition of Mattermost.',
{isEntryLicense ? (
formatMessage({
id: 'navbar_dropdown.entryVersionText',
defaultMessage: 'Entry offers Enterprise Advance capabilities <link>with limits</link> designed to support evaluation.',
},
{
link: (msg: React.ReactNode) => (
<ExternalLink
location='menu_start_trial.unsupported-link'
href={LicenseLinks.UNSUPPORTED}
location='menu_start_trial.entry-link'
href={LicenseLinks.ENTRY_LIMITS_INFO}
>
{msg}
</ExternalLink>
),
},
})
) : (
formatMessage(
{
id: 'navbar_dropdown.versionText',
defaultMessage: 'This is the free <link>unsupported</link> edition of Mattermost.',
},
{
link: (msg: React.ReactNode) => (
<ExternalLink
location='menu_start_trial.unsupported-link'
href={LicenseLinks.UNSUPPORTED}
>
{msg}
</ExternalLink>
),
},
)
)}
</div>
</li>
+1
View File
@@ -4902,6 +4902,7 @@
"navbar_dropdown.create.modal.titleAdminPreTrial": "Try unlimited teams with a free trial",
"navbar_dropdown.create.modal.titleEndUser": "Multiple teams available in paid plans",
"navbar_dropdown.create.tooltip.cloudFreeTrial": "During your trial you are able to create multiple teams. These teams will be archived after your trial.",
"navbar_dropdown.entryVersionText": "Entry offers Enterprise Advance capabilities <link>with limits</link> designed to support evaluation.",
"navbar_dropdown.help": "Help",
"navbar_dropdown.integrations": "Integrations",
"navbar_dropdown.invitePeople": "Invite People",
+1
View File
@@ -1166,6 +1166,7 @@ export const DeveloperLinks = {
export const LicenseLinks = {
CONTACT_SALES: 'https://mattermost.com/contact-sales/',
ENTRY_LIMITS_INFO: 'https://mattermost.com/pl/entry',
TRIAL_INFO_LINK: 'https://mattermost.com/trial',
EMBARGOED_COUNTRIES: 'https://mattermost.com/pl/limitations-for-embargoed-countries',
SOFTWARE_SERVICES_LICENSE_AGREEMENT: 'https://mattermost.com/pl/software-and-services-license-agreement',