Merge branch 'MM-50966-in-product-expansion' of github.com:mattermost/mattermost-server into MM-50966-in-product-expansion

Этот коммит содержится в:
Conor Macpherson
2023-03-31 13:39:44 -04:00
родитель 6fe45fe890 545d35e9cf
Коммит d74ff61fc8
301 изменённых файлов: 10713 добавлений и 12417 удалений

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

@@ -1,4 +0,0 @@
# Web Platform should be assigned to review all PRs that involve changing dependencies in the app, either intentional or accidental.
package.json @mattermost/web-platform
*/package.json @mattermost/web-platform
package-lock.json @mattermost/web-platform

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

@@ -161,7 +161,6 @@
"babel-plugin-typescript-to-proptypes": "1.4.2",
"bundle-loader": "0.2.0",
"copy-webpack-plugin": "11.0.0",
"eslint-plugin-mattermost": "github:mattermost/eslint-plugin-mattermost#5b0c972eacf19286e4c66221b39113bf8728a99e",
"cross-env": "7.0.3",
"css-loader": "5.2.6",
"dotenv-webpack": "8.0.1",
@@ -174,6 +173,7 @@
"eslint-plugin-formatjs": "4.3.4",
"eslint-plugin-header": "3.1.1",
"eslint-plugin-import": "2.23.4",
"eslint-plugin-mattermost": "github:mattermost/eslint-plugin-mattermost#5b0c972eacf19286e4c66221b39113bf8728a99e",
"eslint-plugin-no-only-tests": "2.6.0",
"eslint-plugin-react": "7.31.10",
"eslint-plugin-react-hooks": "4.6.0",

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

@@ -451,12 +451,12 @@ export function ping() {
};
}
export function requestTrialLicense(users, termsAccepted, receiveEmailsAccepted, page) {
export function requestTrialLicense(requestLicenseBody, page) {
return async () => {
try {
trackEvent('api', 'api_request_trial_license', {from_page: page});
const response = await Client4.requestTrialLicense({users, terms_accepted: termsAccepted, receive_emails_accepted: receiveEmailsAccepted});
const response = await Client4.requestTrialLicense(requestLicenseBody);
return {data: response};
} catch (e) {
// In the event that the status code returned is 451, this request has been blocked because it originated from an embargoed country_dropdown

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

@@ -65,7 +65,7 @@ export function emitChannelClickEvent(channel: Channel) {
const currentChannelId = getCurrentChannelId(state);
const previousRhsState = getPreviousRhsState(state);
dispatch(getChannelStats(chan.id));
dispatch(getChannelStats(chan.id, true));
const penultimate = LocalStorageStore.getPreviousChannelName(userId, teamId);
const penultimateType = LocalStorageStore.getPreviousViewedType(userId, teamId);

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

@@ -6,6 +6,7 @@ exports[`components/ColorInput should match snapshot, click on picker 1`] = `
>
<input
className="form-control"
data-testid="color-inputColorValue"
id="sidebarBg-inputColorValue"
maxLength={7}
onBlur={[Function]}
@@ -51,6 +52,7 @@ exports[`components/ColorInput should match snapshot, init 1`] = `
>
<input
className="form-control"
data-testid="color-inputColorValue"
id="sidebarBg-inputColorValue"
maxLength={7}
onBlur={[Function]}
@@ -84,6 +86,7 @@ exports[`components/ColorInput should match snapshot, opened 1`] = `
>
<input
className="form-control"
data-testid="color-inputColorValue"
id="sidebarBg-inputColorValue"
maxLength={7}
onBlur={[Function]}
@@ -129,6 +132,7 @@ exports[`components/ColorInput should match snapshot, toggle picker 1`] = `
>
<input
className="form-control"
data-testid="color-inputColorValue"
id="sidebarBg-inputColorValue"
maxLength={7}
onBlur={[Function]}

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

@@ -282,13 +282,6 @@ exports[`components/AboutBuildModal should match snapshot for enterprise edition
/>
<Nbsp />
0123456789abcdef
<br />
<MemoizedFormattedMessage
defaultMessage="Webapp Build Hash:"
id="about.hashwebapp"
/>
<Nbsp />
0a1b2c3d4f
</p>
<p>
<MemoizedFormattedMessage
@@ -513,13 +506,6 @@ exports[`components/AboutBuildModal should match snapshot for team edition 1`] =
id="about.hashee"
/>
<Nbsp />
<br />
<MemoizedFormattedMessage
defaultMessage="Webapp Build Hash:"
id="about.hashwebapp"
/>
<Nbsp />
0a1b2c3d4f
</p>
<p>
<MemoizedFormattedMessage
@@ -755,13 +741,6 @@ exports[`components/AboutBuildModal should show ci if a ci build 1`] = `
id="about.hashee"
/>
<Nbsp />
<br />
<MemoizedFormattedMessage
defaultMessage="Webapp Build Hash:"
id="about.hashwebapp"
/>
<Nbsp />
0a1b2c3d4f
</p>
<p>
<MemoizedFormattedMessage
@@ -986,13 +965,6 @@ exports[`components/AboutBuildModal should show dev if this is a dev build 1`] =
id="about.hashee"
/>
<Nbsp />
<br />
<MemoizedFormattedMessage
defaultMessage="Webapp Build Hash:"
id="about.hashwebapp"
/>
<Nbsp />
0a1b2c3d4f
</p>
<p>
<MemoizedFormattedMessage

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

@@ -153,7 +153,6 @@ describe('components/AboutBuildModal', () => {
<AboutBuildModal
config={config}
license={license}
webappBuildHash='0a1b2c3d4f'
onExited={onExited}
/>
</Provider>,
@@ -211,7 +210,6 @@ describe('components/AboutBuildModal', () => {
const allProps = {
show,
onExited,
webappBuildHash: '0a1b2c3d4f',
config,
license,
...props,

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

@@ -32,11 +32,6 @@ type Props = {
* Global license object
*/
license: ClientLicense;
/**
* Webapp build hash override. By default, webpack sets this (so it must be overridden in tests).
*/
webappBuildHash?: string;
};
type State = {
@@ -330,17 +325,6 @@ export default class AboutBuildModal extends React.PureComponent<Props, State> {
/>
<Nbsp/>
{config.BuildHashEnterprise}
<br/>
<FormattedMessage
id='about.hashwebapp'
defaultMessage='Webapp Build Hash:'
/>
<Nbsp/>
{
/* global COMMIT_HASH */ this.props.
webappBuildHash ||
(typeof COMMIT_HASH === 'undefined' ? '' : COMMIT_HASH)
}
</p>
<p>
<FormattedMessage

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

@@ -21,9 +21,6 @@ type Props = {
doHide: () => void;
};
// Webpack global var
declare const COMMIT_HASH: string;
export default function AboutBuildModalCloud(props: Props) {
const config = props.config;
const license = props.license;
@@ -162,12 +159,6 @@ export default function AboutBuildModalCloud(props: Props) {
defaultMessage='EE Build Hash:'
/>
{config.BuildHashEnterprise}
<br/>
<FormattedMessage
id='about.hashwebapp'
defaultMessage='Webapp Build Hash:'
/>
{typeof COMMIT_HASH === 'undefined' ? '' : COMMIT_HASH}
</p>
<p>
<FormattedMessage

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

@@ -1,13 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/AdminConsole should generate the routes 1`] = `
<Redirect
to="/team-id/channels/town-square"
/>
`;
exports[`components/AdminConsole should redirect to town-square when not system admin 1`] = `
<Redirect
to="/team-id/channels/town-square"
/>
`;

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

@@ -1,59 +1,180 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/ColorSetting should match snapshot, all 1`] = `
<Settings
helpText="helptext"
inputId="id"
label="label"
>
<ColorInput
id="id"
isDisabled={false}
onChange={[Function]}
value="#fff"
/>
</Settings>
<div>
<div
class="form-group"
data-testid="id"
>
<label
class="control-label col-sm-4"
for="id"
>
label
</label>
<div
class="col-sm-8"
>
<div
class="color-input input-group"
>
<input
class="form-control"
data-testid="color-inputColorValue"
id="id-inputColorValue"
maxlength="7"
type="text"
value="#fff"
/>
<span
class="input-group-addon color-pad"
id="id-squareColorIcon"
>
<i
class="color-icon"
id="id-squareColorIconValue"
style="background-color: rgb(255, 255, 255);"
/>
</span>
</div>
<div
class="help-text"
data-testid="idhelp-text"
>
helptext
</div>
</div>
</div>
</div>
`;
exports[`components/ColorSetting should match snapshot, clicked on color setting 1`] = `
<Settings
helpText="helptext"
inputId="id"
label="label"
>
<ColorInput
id="id"
isDisabled={false}
onChange={[Function]}
value="#fff"
/>
</Settings>
<div>
<div
class="form-group"
data-testid="id"
>
<label
class="control-label col-sm-4"
for="id"
>
label
</label>
<div
class="col-sm-8"
>
<div
class="color-input input-group"
>
<input
class="form-control"
data-testid="color-inputColorValue"
id="id-inputColorValue"
maxlength="7"
type="text"
value="#fff"
/>
<span
class="input-group-addon color-pad"
id="id-squareColorIcon"
>
<i
class="color-icon"
id="id-squareColorIconValue"
style="background-color: rgb(255, 255, 255);"
/>
</span>
</div>
<div
class="help-text"
data-testid="idhelp-text"
>
helptext
</div>
</div>
</div>
</div>
`;
exports[`components/ColorSetting should match snapshot, disabled 1`] = `
<Settings
inputId="id"
label="label"
>
<ColorInput
id="id"
isDisabled={true}
onChange={[Function]}
value="#fff"
/>
</Settings>
<div>
<div
class="form-group"
data-testid="id"
>
<label
class="control-label col-sm-4"
for="id"
>
label
</label>
<div
class="col-sm-8"
>
<div
class="color-input input-group"
>
<input
class="form-control"
data-testid="color-inputColorValue"
disabled=""
id="id-inputColorValue"
maxlength="7"
type="text"
value="#fff"
/>
</div>
<div
class="help-text"
data-testid="idhelp-text"
/>
</div>
</div>
</div>
`;
exports[`components/ColorSetting should match snapshot, no help text 1`] = `
<Settings
inputId="id"
label="label"
>
<ColorInput
id="id"
isDisabled={false}
onChange={[Function]}
value="#fff"
/>
</Settings>
<div>
<div
class="form-group"
data-testid="id"
>
<label
class="control-label col-sm-4"
for="id"
>
label
</label>
<div
class="col-sm-8"
>
<div
class="color-input input-group"
>
<input
class="form-control"
data-testid="color-inputColorValue"
id="id-inputColorValue"
maxlength="7"
type="text"
value="#fff"
/>
<span
class="input-group-addon color-pad"
id="id-squareColorIcon"
>
<i
class="color-icon"
id="id-squareColorIconValue"
style="background-color: rgb(255, 255, 255);"
/>
</span>
</div>
<div
class="help-text"
data-testid="idhelp-text"
/>
</div>
</div>
</div>
`;

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

@@ -1,110 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import {Team} from '@mattermost/types/teams';
import {SelfHostedSignupProgress} from '@mattermost/types/cloud';
import {AdminConfig, ExperimentalSettings} from '@mattermost/types/config';
import {Theme} from 'mattermost-redux/selectors/entities/preferences';
import AdminDefinition from 'components/admin_console/admin_definition';
import {TestHelper} from 'utils/test_helper';
import * as Utils from 'utils/utils';
import AdminConsole from './admin_console';
import type {Props} from './admin_console';
describe('components/AdminConsole', () => {
const baseProps: Props = {
config: {
TestField: true,
ExperimentalSettings: {
RestrictSystemAdmin: false,
} as ExperimentalSettings,
} as Partial<AdminConfig>,
adminDefinition: AdminDefinition,
environmentConfig: {},
unauthorizedRoute: '/',
consoleAccess: {
read: {},
write: {},
},
team: {} as Team,
license: {},
cloud: {
limits: {
limits: {},
limitsLoaded: false,
},
errors: {},
selfHostedSignup: {
progress: SelfHostedSignupProgress.START,
},
},
buildEnterpriseReady: true,
match: {
url: '',
},
roles: {
channel_admin: TestHelper.getRoleMock(),
channel_user: TestHelper.getRoleMock(),
team_admin: TestHelper.getRoleMock(),
team_user: TestHelper.getRoleMock(),
system_admin: TestHelper.getRoleMock(),
system_user: TestHelper.getRoleMock(),
},
showNavigationPrompt: false,
isCurrentUserSystemAdmin: false,
currentUserHasAnAdminRole: false,
currentTheme: {} as Theme,
actions: {
getConfig: jest.fn(),
getEnvironmentConfig: jest.fn(),
setNavigationBlocked: jest.fn(),
confirmNavigation: jest.fn(),
cancelNavigation: jest.fn(),
loadRolesIfNeeded: jest.fn(),
editRole: jest.fn(),
selectLhsItem: jest.fn(),
selectTeam: jest.fn(),
},
};
beforeEach(() => {
jest.spyOn(Utils, 'applyTheme').mockImplementation(() => {});
jest.spyOn(Utils, 'resetTheme').mockImplementation(() => {});
});
test('should redirect to town-square when not system admin', () => {
const props = {
...baseProps,
unauthorizedRoute: '/team-id/channels/town-square',
isCurrentUserSystemAdmin: false,
currentUserHasAnAdminRole: false,
consoleAccess: {read: {}, write: {}},
team: {name: 'development'} as Team,
};
const wrapper = shallow(
<AdminConsole {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should generate the routes', () => {
const props = {
...baseProps,
unauthorizedRoute: '/team-id/channels/town-square',
isCurrentUserSystemAdmin: true,
currentUserHasAnAdminRole: false,
consoleAccess: {read: {}, write: {}},
team: {name: 'development'} as Team,
};
const wrapper = shallow(
<AdminConsole {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
});

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

@@ -2497,11 +2497,7 @@ const AdminDefinition = {
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.NOTIFICATIONS)),
it.stateIsFalse('EmailSettings.SendEmailNotifications'),
),
// MM-50952
// If the setting is hidden, then it is not being set in state so there is
// nothing to validate, and validation would fail anyways and prevent saving
validate: it.configIsFalse('ExperimentalSettings', 'RestrictSystemAdmin') && validators.isRequired(t('admin.environment.notifications.feedbackEmail.required'), '"Notification From Address" is required'),
validate: validators.isRequired(t('admin.environment.notifications.feedbackEmail.required'), '"Notification From Address" is required'),
},
{
type: Constants.SettingsTypes.TYPE_TEXT,

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

@@ -2,25 +2,29 @@
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import {screen} from '@testing-library/react';
import {MenuItemBlockableLinkImpl} from './menu_item_blockable_link';
import {renderWithIntl} from 'tests/react_testing_utils';
import {Provider} from 'react-redux';
import store from 'stores/redux_store';
import {BrowserRouter} from 'react-router-dom';
describe('components/MenuItemBlockableLink', () => {
test('should match snapshot', () => {
const wrapper = shallow(
<MenuItemBlockableLinkImpl
to='/wherever'
text='Whatever'
/>,
test('should render my link', () => {
renderWithIntl(
<BrowserRouter>
<Provider store={store}>
<MenuItemBlockableLinkImpl
to='/wherever'
text='Whatever'
/>
</Provider>
</BrowserRouter>,
);
expect(wrapper).toMatchInlineSnapshot(`
<Connect(BlockableLink)
to="/wherever"
>
Whatever
</Connect(BlockableLink)>
`);
screen.getByText('Whatever');
expect((screen.getByRole('link') as HTMLAnchorElement).href).toContain('/wherever');
});
});

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

@@ -1,349 +1,232 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/admin_console/admin_user_card/admin_user_card should match default snapshot 1`] = `
<div
className="AdminUserCard"
>
<div>
<div
className="AdminUserCard__header"
class="AdminUserCard"
>
<ProfilePicture
hasMention={false}
isEmoji={false}
isRHS={false}
popoverPlacement="right"
size="xxl"
src="/api/v4/users/1234/image"
userId="1234"
wrapperClass="admin-user-card"
<div
class="AdminUserCard__header"
>
<span
class="status-wrapper admin-user-card"
>
<button
class="RoundButton-dvlhqG gnYSKj style--none"
>
<span
class="profile-icon "
>
<img
alt="user profile image"
class="Avatar Avatar-xxl"
loading="lazy"
src="/api/v4/users/1234/image"
tabindex="-1"
/>
</span>
</button>
</span>
<div
class="AdminUserCard__user-info"
>
<span>
Jim
Halpert
</span>
<span>
</span>
<span
class="AdminUserCard__user-nickname"
>
Big Tuna
</span>
</div>
<div
class="AdminUserCard__user-id"
>
User ID:
1234
</div>
</div>
<div
class="AdminUserCard__body"
/>
<div
className="AdminUserCard__user-info"
>
<span>
Jim
Halpert
</span>
<Bullet
user={
Object {
"auth_service": "",
"bot_description": "",
"create_at": 0,
"delete_at": 0,
"email": "",
"first_name": "Jim",
"id": "1234",
"is_bot": false,
"last_activity_at": 0,
"last_name": "Halpert",
"last_password_update": 0,
"last_picture_update": 0,
"locale": "",
"mfa_active": false,
"nickname": "Big Tuna",
"notify_props": Object {
"channel": "false",
"comments": "never",
"desktop": "default",
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
"push_status": "offline",
},
"password": "",
"position": "",
"props": Object {},
"roles": "",
"terms_of_service_create_at": 0,
"terms_of_service_id": "",
"update_at": 0,
"username": "some-user",
}
}
/>
<span
className="AdminUserCard__user-nickname"
>
Big Tuna
</span>
</div>
<div
className="AdminUserCard__user-id"
>
User ID:
1234
</div>
class="AdminUserCard__footer"
/>
</div>
<div
className="AdminUserCard__body"
/>
<div
className="AdminUserCard__footer"
/>
</div>
`;
exports[`components/admin_console/admin_user_card/admin_user_card should match snapshot if no first/last name is defined 1`] = `
<div
className="AdminUserCard"
>
<div>
<div
className="AdminUserCard__header"
class="AdminUserCard"
>
<ProfilePicture
hasMention={false}
isEmoji={false}
isRHS={false}
popoverPlacement="right"
size="xxl"
src="/api/v4/users/1234/image"
userId="1234"
wrapperClass="admin-user-card"
<div
class="AdminUserCard__header"
>
<span
class="status-wrapper admin-user-card"
>
<button
class="RoundButton-dvlhqG gnYSKj style--none"
>
<span
class="profile-icon "
>
<img
alt="user profile image"
class="Avatar Avatar-xxl"
loading="lazy"
src="/api/v4/users/1234/image"
tabindex="-1"
/>
</span>
</button>
</span>
<div
class="AdminUserCard__user-info"
>
<span>
</span>
<span
class="AdminUserCard__user-nickname"
>
Big Tuna
</span>
</div>
<div
class="AdminUserCard__user-id"
>
User ID:
1234
</div>
</div>
<div
class="AdminUserCard__body"
/>
<div
className="AdminUserCard__user-info"
>
<span>
</span>
<Bullet
user={
Object {
"auth_service": "",
"bot_description": "",
"create_at": 0,
"delete_at": 0,
"email": "",
"first_name": null,
"id": "1234",
"is_bot": false,
"last_activity_at": 0,
"last_name": null,
"last_password_update": 0,
"last_picture_update": 0,
"locale": "",
"mfa_active": false,
"nickname": "Big Tuna",
"notify_props": Object {
"channel": "false",
"comments": "never",
"desktop": "default",
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
"push_status": "offline",
},
"password": "",
"position": "",
"props": Object {},
"roles": "",
"terms_of_service_create_at": 0,
"terms_of_service_id": "",
"update_at": 0,
"username": "some-user",
}
}
/>
<span
className="AdminUserCard__user-nickname"
>
Big Tuna
</span>
</div>
<div
className="AdminUserCard__user-id"
>
User ID:
1234
</div>
class="AdminUserCard__footer"
/>
</div>
<div
className="AdminUserCard__body"
/>
<div
className="AdminUserCard__footer"
/>
</div>
`;
exports[`components/admin_console/admin_user_card/admin_user_card should match snapshot if no first/last name or nickname is defined 1`] = `
<div
className="AdminUserCard"
>
<div>
<div
className="AdminUserCard__header"
class="AdminUserCard"
>
<ProfilePicture
hasMention={false}
isEmoji={false}
isRHS={false}
popoverPlacement="right"
size="xxl"
src="/api/v4/users/1234/image"
userId="1234"
wrapperClass="admin-user-card"
<div
class="AdminUserCard__header"
>
<span
class="status-wrapper admin-user-card"
>
<button
class="RoundButton-dvlhqG gnYSKj style--none"
>
<span
class="profile-icon "
>
<img
alt="user profile image"
class="Avatar Avatar-xxl"
loading="lazy"
src="/api/v4/users/1234/image"
tabindex="-1"
/>
</span>
</button>
</span>
<div
class="AdminUserCard__user-info"
>
<span>
</span>
<span
class="AdminUserCard__user-nickname"
/>
</div>
<div
class="AdminUserCard__user-id"
>
User ID:
1234
</div>
</div>
<div
class="AdminUserCard__body"
/>
<div
className="AdminUserCard__user-info"
>
<span>
</span>
<Bullet
user={
Object {
"auth_service": "",
"bot_description": "",
"create_at": 0,
"delete_at": 0,
"email": "",
"first_name": null,
"id": "1234",
"is_bot": false,
"last_activity_at": 0,
"last_name": null,
"last_password_update": 0,
"last_picture_update": 0,
"locale": "",
"mfa_active": false,
"nickname": null,
"notify_props": Object {
"channel": "false",
"comments": "never",
"desktop": "default",
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
"push_status": "offline",
},
"password": "",
"position": "",
"props": Object {},
"roles": "",
"terms_of_service_create_at": 0,
"terms_of_service_id": "",
"update_at": 0,
"username": "some-user",
}
}
/>
<span
className="AdminUserCard__user-nickname"
/>
</div>
<div
className="AdminUserCard__user-id"
>
User ID:
1234
</div>
class="AdminUserCard__footer"
/>
</div>
<div
className="AdminUserCard__body"
/>
<div
className="AdminUserCard__footer"
/>
</div>
`;
exports[`components/admin_console/admin_user_card/admin_user_card should match snapshot if no nickname is defined 1`] = `
<div
className="AdminUserCard"
>
<div>
<div
className="AdminUserCard__header"
class="AdminUserCard"
>
<ProfilePicture
hasMention={false}
isEmoji={false}
isRHS={false}
popoverPlacement="right"
size="xxl"
src="/api/v4/users/1234/image"
userId="1234"
wrapperClass="admin-user-card"
<div
class="AdminUserCard__header"
>
<span
class="status-wrapper admin-user-card"
>
<button
class="RoundButton-dvlhqG gnYSKj style--none"
>
<span
class="profile-icon "
>
<img
alt="user profile image"
class="Avatar Avatar-xxl"
loading="lazy"
src="/api/v4/users/1234/image"
tabindex="-1"
/>
</span>
</button>
</span>
<div
class="AdminUserCard__user-info"
>
<span>
Jim
Halpert
</span>
<span
class="AdminUserCard__user-nickname"
/>
</div>
<div
class="AdminUserCard__user-id"
>
User ID:
1234
</div>
</div>
<div
class="AdminUserCard__body"
/>
<div
className="AdminUserCard__user-info"
>
<span>
Jim
Halpert
</span>
<Bullet
user={
Object {
"auth_service": "",
"bot_description": "",
"create_at": 0,
"delete_at": 0,
"email": "",
"first_name": "Jim",
"id": "1234",
"is_bot": false,
"last_activity_at": 0,
"last_name": "Halpert",
"last_password_update": 0,
"last_picture_update": 0,
"locale": "",
"mfa_active": false,
"nickname": null,
"notify_props": Object {
"channel": "false",
"comments": "never",
"desktop": "default",
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
"push_status": "offline",
},
"password": "",
"position": "",
"props": Object {},
"roles": "",
"terms_of_service_create_at": 0,
"terms_of_service_id": "",
"update_at": 0,
"username": "some-user",
}
}
/>
<span
className="AdminUserCard__user-nickname"
/>
</div>
<div
className="AdminUserCard__user-id"
>
User ID:
1234
</div>
class="AdminUserCard__footer"
/>
</div>
<div
className="AdminUserCard__body"
/>
<div
className="AdminUserCard__footer"
/>
</div>
`;

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

@@ -2,11 +2,13 @@
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import {screen} from '@testing-library/react';
import {TestHelper} from 'utils/test_helper';
import AdminUserCard from 'components/admin_console/admin_user_card/admin_user_card';
import {renderWithIntl} from 'tests/react_testing_utils';
describe('components/admin_console/admin_user_card/admin_user_card', () => {
const user = TestHelper.getUserMock({
@@ -22,8 +24,12 @@ describe('components/admin_console/admin_user_card/admin_user_card', () => {
test('should match default snapshot', () => {
const props = defaultProps;
const wrapper = shallow(<AdminUserCard {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithIntl(<AdminUserCard {...props}/>);
screen.getByText(props.user.first_name, {exact: false});
screen.getByText(props.user.last_name, {exact: false});
screen.getByText(props.user.nickname, {exact: false});
expect(container).toMatchSnapshot();
});
test('should match snapshot if no nickname is defined', () => {
@@ -34,8 +40,12 @@ describe('components/admin_console/admin_user_card/admin_user_card', () => {
nickname: null,
},
};
const wrapper = shallow(<AdminUserCard {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithIntl(<AdminUserCard {...props}/>);
screen.getByText(props.user.first_name, {exact: false});
screen.getByText(props.user.last_name, {exact: false});
expect(screen.queryByText(defaultProps.user.nickname)).not.toBeInTheDocument();
expect(container).toMatchSnapshot();
});
test('should match snapshot if no first/last name is defined', () => {
@@ -47,8 +57,12 @@ describe('components/admin_console/admin_user_card/admin_user_card', () => {
last_name: null,
},
};
const wrapper = shallow(<AdminUserCard {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithIntl(<AdminUserCard {...props}/>);
expect(screen.queryByText(defaultProps.user.first_name)).not.toBeInTheDocument();
expect(screen.queryByText(defaultProps.user.last_name)).not.toBeInTheDocument();
screen.getByText(props.user.nickname, {exact: false});
expect(container).toMatchSnapshot();
});
test('should match snapshot if no first/last name or nickname is defined', () => {
@@ -61,7 +75,12 @@ describe('components/admin_console/admin_user_card/admin_user_card', () => {
nickname: null,
},
};
const wrapper = shallow(<AdminUserCard {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithIntl(<AdminUserCard {...props}/>);
expect(screen.queryByText(defaultProps.user.first_name)).not.toBeInTheDocument();
expect(screen.queryByText(defaultProps.user.last_name)).not.toBeInTheDocument();
expect(screen.queryByText(defaultProps.user.nickname)).not.toBeInTheDocument();
screen.getByText(props.user.id, {exact: false});
expect(container).toMatchSnapshot();
});
});

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

@@ -6,11 +6,14 @@ 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 mockStore from 'tests/test_store';
import BillingHistory from './billing_history';
import {CloudLinks, HostedCustomerLinks} from 'utils/constants';
import BillingHistory, {NoBillingHistorySection} from './billing_history';
const NO_INVOICES_LEGEND = 'All of your invoices will be shown here';
@@ -214,3 +217,17 @@ describe('BillingHistory -- self-hosted', () => {
expect(invoiceTableRows.length).toBe(2);
});
});
describe('NoBillingHistorySection', () => {
const state = {entities: {users: {}, general: {config: {}, license: {}}}} as any;
test('goes to cloud docs on cloud', () => {
renderWithIntlAndStore(<NoBillingHistorySection selfHosted={false}/>, state);
expect((screen.getByRole('link') as HTMLAnchorElement).href).toContain(CloudLinks.BILLING_DOCS);
});
test('goes to self-hosted docs on self-hosted', () => {
renderWithIntlAndStore(<NoBillingHistorySection selfHosted={true}/>, state);
expect((screen.getByRole('link') as HTMLAnchorElement).href).toContain(HostedCustomerLinks.SELF_HOSTED_BILLING);
});
});

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

@@ -16,14 +16,17 @@ import LoadingSpinner from 'components/widgets/loading/loading_spinner';
import FormattedAdminHeader from 'components/widgets/admin_console/formatted_admin_header';
import EmptyBillingHistorySvg from 'components/common/svg_images_components/empty_billing_history_svg';
import {CloudLinks} from 'utils/constants';
import {CloudLinks, HostedCustomerLinks} from 'utils/constants';
import ExternalLink from 'components/external_link';
import BillingHistoryTable from './billing_history_table';
import './billing_history.scss';
const noBillingHistorySection = (
interface NoBillingHistorySectionProps {
selfHosted: boolean;
}
export const NoBillingHistorySection = (props: NoBillingHistorySectionProps) => (
<div className='BillingHistory__noHistory'>
<EmptyBillingHistorySvg
width={300}
@@ -37,7 +40,7 @@ const noBillingHistorySection = (
</div>
<ExternalLink
location='billing_history'
href={CloudLinks.BILLING_DOCS}
href={props.selfHosted ? HostedCustomerLinks.SELF_HOSTED_BILLING : CloudLinks.BILLING_DOCS}
className='BillingHistory__noHistory-link'
onClick={() => trackEvent('cloud_admin', 'click_billing_history', {screen: 'billing'})}
>
@@ -92,9 +95,7 @@ const BillingHistory = () => {
<div className='BillingHistory__cardBody'>
{invoices != null && (
<>
{areInvoicesEmpty ? noBillingHistorySection : billingHistoryTable}
</>
areInvoicesEmpty ? <NoBillingHistorySection selfHosted={!isCloud}/> : billingHistoryTable
)}
{invoices == null && (
<div className='BillingHistory__spinner'>

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

@@ -3,3 +3,77 @@
transform: translateY(50%);
}
}
.ResultModal__small {
.modal-header {
.close {
&:hover,
&:active,
&:focus,
&:active:focus {
background-color: rgba(var(--center-channel-color-rgb), 0.08);
color: rgba(var(--center-channel-color-rgb), 0.72);
opacity: 1;
}
top: 26px;
right: 26px;
width: 24px;
height: 24px;
border-radius: 4px;
color: rgba(var(--center-channel-color-rgb), 0.56) !important;
font-family:
'Open Sans',
sans-serif;
font-size: 32px;
font-weight: 400;
}
}
.modal-dialog {
position: absolute;
top: 50%;
left: 50%;
width: 600px;
height: 360px;
border: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
margin: auto;
border-radius: 8px;
transform: translate(-50%, -50%) !important;
.modal-header {
border: none;
background: var(--center-channel-bg) !important;
}
.modal-content {
height: 100%;
border-radius: 8px;
.IconMessage__svg-wrapper {
margin-left: 0;
}
.IconMessage-h3 {
margin-top: 0;
}
.IconMessage-sub {
max-width: 504px;
margin-top: 0;
}
.IconMessage-buttons {
justify-content: right;
padding-right: 24px;
margin-top: 60px;
.btn {
font-family: 'Open sans', sans-serif;
font-size: 12px;
font-weight: 600;
}
}
}
}
}

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

@@ -12,10 +12,12 @@ import {useOpenCloudZendeskSupportForm} from 'components/common/hooks/useOpenZen
import {closeModal} from 'actions/views/modals';
import {isModalOpen} from 'selectors/views/modals';
import {GlobalState} from 'types/store';
import {Modal} from 'react-bootstrap';
import './result_modal.scss';
type Props = {
type?: string;
onHide?: () => void;
icon: JSX.Element;
title: JSX.Element;
@@ -28,47 +30,77 @@ type Props = {
ignoreExit: boolean;
};
export default function ResultModal(props: Props) {
export default function ResultModal({type, icon, title, subtitle, primaryButtonText, primaryButtonHandler, identifier, contactSupportButtonVisible, resultType, ignoreExit, onHide}: Props) {
const dispatch = useDispatch();
const [openContactSupport] = useOpenCloudZendeskSupportForm('Delete workspace', '');
const isResultModalOpen = useSelector((state: GlobalState) =>
isModalOpen(state, props.identifier),
isModalOpen(state, identifier),
);
const onHide = () => {
dispatch(closeModal(props.identifier));
if (typeof props.onHide === 'function') {
props.onHide();
}
const handleHide = () => {
dispatch(closeModal(identifier));
onHide?.();
};
const modalType = `delete-workspace-result_modal__${props.resultType}`;
const modalType = `delete-workspace-result_modal__${resultType}`;
if (type === 'small') {
return (
<Modal
className='ResultModal__small'
show={isResultModalOpen}
onHide={handleHide}
>
<Modal.Header closeButton={true}/>
<div className={modalType}>
<IconMessage
formattedTitle={title}
formattedSubtitle={subtitle}
error={false}
icon={icon}
formattedButtonText={primaryButtonText}
buttonHandler={primaryButtonHandler}
className={'success'}
formattedTertiaryButonText={
contactSupportButtonVisible ?
<FormattedMessage
id={'admin.billing.deleteWorkspace.resultModal.ContactSupport'}
defaultMessage={'Contact Support'}
/> :
undefined
}
tertiaryButtonHandler={contactSupportButtonVisible ? openContactSupport : undefined}
/>
</div>
</Modal>
);
}
return (
<FullScreenModal
show={isResultModalOpen}
onClose={onHide}
ignoreExit={props.ignoreExit}
onClose={handleHide}
ignoreExit={ignoreExit}
>
<div className={modalType}>
<IconMessage
formattedTitle={props.title}
formattedSubtitle={props.subtitle}
formattedTitle={title}
formattedSubtitle={subtitle}
error={false}
icon={props.icon}
formattedButtonText={props.primaryButtonText}
buttonHandler={props.primaryButtonHandler}
icon={icon}
formattedButtonText={primaryButtonText}
buttonHandler={primaryButtonHandler}
className={'success'}
formattedTertiaryButonText={
props.contactSupportButtonVisible ? (
contactSupportButtonVisible ? (
<FormattedMessage
id={'admin.billing.deleteWorkspace.resultModal.ContactSupport'}
defaultMessage={'Contact Support'}
/>) : undefined
}
tertiaryButtonHandler={props.contactSupportButtonVisible ? openContactSupport : undefined}
tertiaryButtonHandler={contactSupportButtonVisible ? openContactSupport : undefined}
/>
</div>
</FullScreenModal>

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

@@ -2,15 +2,16 @@
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import ColorSetting from 'components/admin_console/color_setting';
import {renderWithIntl} from 'tests/react_testing_utils';
import {screen} from '@testing-library/react';
describe('components/ColorSetting', () => {
test('should match snapshot, all', () => {
function emptyFunction() {} //eslint-disable-line no-empty-function
const wrapper = shallow(
const {container} = renderWithIntl(
<ColorSetting
id='id'
label='label'
@@ -20,13 +21,16 @@ describe('components/ColorSetting', () => {
disabled={false}
/>,
);
expect(wrapper).toMatchSnapshot();
expect(screen.getByText('helptext')).toBeInTheDocument();
expect(screen.getByTestId('color-inputColorValue')).not.toBeDisabled();
expect(container).toMatchSnapshot();
});
test('should match snapshot, no help text', () => {
function emptyFunction() {} //eslint-disable-line no-empty-function
const wrapper = shallow(
const {container} = renderWithIntl(
<ColorSetting
id='id'
label='label'
@@ -35,13 +39,15 @@ describe('components/ColorSetting', () => {
disabled={false}
/>,
);
expect(wrapper).toMatchSnapshot();
expect(screen.queryByText('helptext')).not.toBeInTheDocument();
expect(container).toMatchSnapshot();
});
test('should match snapshot, disabled', () => {
function emptyFunction() {} //eslint-disable-line no-empty-function
const wrapper = shallow(
const {container} = renderWithIntl(
<ColorSetting
id='id'
label='label'
@@ -50,13 +56,16 @@ describe('components/ColorSetting', () => {
disabled={true}
/>,
);
expect(wrapper).toMatchSnapshot();
expect(screen.getByTestId('color-inputColorValue')).toBeDisabled();
expect(screen.queryByText('helptext')).not.toBeInTheDocument();
expect(container).toMatchSnapshot();
});
test('should match snapshot, clicked on color setting', () => {
function emptyFunction() {} //eslint-disable-line no-empty-function
const wrapper = shallow(
const {container} = renderWithIntl(
<ColorSetting
id='id'
label='label'
@@ -66,7 +75,9 @@ describe('components/ColorSetting', () => {
disabled={false}
/>,
);
expect(screen.getByTestId('color-inputColorValue')).not.toBeDisabled();
expect(screen.queryByText('helptext')).toBeInTheDocument();
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
});

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

@@ -208,117 +208,6 @@ exports[`components/admin_console/license_settings/LicenseSettings load screen w
</div>
`;
exports[`components/admin_console/license_settings/LicenseSettings should match snapshot after starting trial and removing license 1`] = `
<div
className="wrapper--fixed"
>
<FormattedAdminHeader
defaultMessage="Edition and License"
id="admin.license.title"
values={Object {}}
/>
<div
className="admin-console__wrapper"
>
<div
className="admin-console__content"
>
<div
className="admin-console__banner_section"
>
<TrialBanner
enterpriseReady={true}
gettingTrial={false}
gettingTrialError={null}
handleRestart={[Function]}
handleUpgrade={[Function]}
isDisabled={false}
openEEModal={[Function]}
requestLicense={[Function]}
restartError={null}
restarting={false}
upgradeError={null}
upgradingPercentage={0}
/>
</div>
<div
className="top-wrapper"
>
<div
className="left-panel"
>
<div
className="panel-card"
>
<Memo(StarterLeftPanel)
currentPlan={
<div
className="current-plan-legend"
>
<i
className="icon-check-circle"
/>
Current Plan
</div>
}
fileInputRef={
Object {
"current": null,
}
}
handleChange={[Function]}
openEELicenseModal={[Function]}
upgradedFromTE={false}
/>
</div>
<div
className="terms-and-policy"
>
See also
<ExternalLink
href="https://mattermost.com/terms-of-use/"
id="privacyLink"
location="license_settings"
>
Enterprise Edition Terms of Use
</ExternalLink>
and
<ExternalLink
href="https://mattermost.com/privacy-policy/"
id="privacyLink"
location="license_settings"
>
Privacy Policy
</ExternalLink>
</div>
</div>
<div
className="right-panel"
>
<div
className="panel-card"
>
<Memo(StarterRightPanel) />
</div>
<div
className="compare-plans-text"
>
Curious about upgrading?
<ExternalLink
href="https://mattermost.com/pricing/"
id="privacyLink"
location="license_settings"
>
Compare Plans
</ExternalLink>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`components/admin_console/license_settings/LicenseSettings should match snapshot enterprise build with E10 license 1`] = `
<div
className="wrapper--fixed"
@@ -1187,7 +1076,7 @@ exports[`components/admin_console/license_settings/LicenseSettings should match
<div
className="admin-console__banner_section"
>
<TrialBanner
<Component
enterpriseReady={true}
gettingTrial={false}
gettingTrialError={null}
@@ -1196,7 +1085,6 @@ exports[`components/admin_console/license_settings/LicenseSettings should match
handleUpgrade={[Function]}
isDisabled={false}
openEEModal={[Function]}
requestLicense={[Function]}
restartError={null}
restarting={false}
upgradeError={null}
@@ -1299,7 +1187,7 @@ exports[`components/admin_console/license_settings/LicenseSettings should match
<div
className="admin-console__banner_section"
>
<TrialBanner
<Component
enterpriseReady={true}
gettingTrial={false}
gettingTrialError={null}
@@ -1308,7 +1196,6 @@ exports[`components/admin_console/license_settings/LicenseSettings should match
handleUpgrade={[Function]}
isDisabled={false}
openEEModal={[Function]}
requestLicense={[Function]}
restartError={null}
restarting={false}
upgradeError={null}
@@ -1515,7 +1402,7 @@ exports[`components/admin_console/license_settings/LicenseSettings should match
<div
className="admin-console__banner_section"
>
<TrialBanner
<Component
enterpriseReady={false}
gettingTrial={false}
gettingTrialError={null}
@@ -1524,7 +1411,6 @@ exports[`components/admin_console/license_settings/LicenseSettings should match
handleUpgrade={[Function]}
isDisabled={false}
openEEModal={[Function]}
requestLicense={[Function]}
restartError={null}
restarting={false}
upgradeError={null}

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

@@ -167,34 +167,6 @@ describe('components/admin_console/license_settings/LicenseSettings', () => {
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot after starting trial and removing license', async () => {
const actions = {
...defaultProps.actions,
getLicenseConfig: jest.fn(),
upgradeToE0: jest.fn(),
upgradeToE0Status: jest.fn().mockImplementation(() => Promise.resolve({percentage: 0, error: null})),
};
const props = {...defaultProps, license: {IsLicensed: 'false'}, prevTrialLicense: {IsLicensed: 'false'}, actions};
const wrapper = shallow<LicenseSettings>(<LicenseSettings {...props}/>);
const instance = wrapper.instance();
// First start trial
actions.requestTrialLicense = jest.fn().mockImplementation(() => Promise.resolve({percentage: 1, error: null}));
actions.getLicenseConfig = jest.fn().mockImplementation(() => Promise.resolve({}));
await instance.requestLicense({preventDefault: jest.fn()} as unknown as React.MouseEvent<HTMLButtonElement>);
expect(wrapper.state('gettingTrial')).toBe(false);
// Then remove license
actions.removeLicense = jest.fn().mockImplementation(() => Promise.resolve({percentage: 1, error: null}));
actions.getPrevTrialLicense = jest.fn().mockImplementation(() => Promise.resolve({}));
await instance.handleRemove({preventDefault: jest.fn()} as unknown as React.MouseEvent<HTMLButtonElement>);
expect(wrapper.state('removing')).toBe(false);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot enterprise build with E20 license', () => {
const props = {...defaultProps, license: {...defaultProps.license, SkuShortName: LicenseSkus.E20}};
const wrapper = shallow<LicenseSettings>(<LicenseSettings {...props}/>);

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

@@ -202,23 +202,6 @@ export default class LicenseSettings extends React.PureComponent<Props, State> {
}
}
requestLicense = async (e?: React.MouseEvent<HTMLButtonElement>) => {
if (e) {
e.preventDefault();
}
if (this.state.gettingTrial) {
return;
}
this.setState({gettingTrial: true, gettingTrialError: null});
const requestedUsers = Math.max(this.props.totalUsers, 30) || 30;
const {error, data} = await this.props.actions.requestTrialLicense(requestedUsers, true, true, 'license');
if (error) {
this.setState({gettingTrialError: error});
}
this.setState({gettingTrial: false, gettingTrialResponseCode: data?.status});
await this.props.actions.getLicenseConfig();
}
checkRestarted = () => {
this.props.actions.ping().then(() => {
window.location.reload();
@@ -363,7 +346,6 @@ export default class LicenseSettings extends React.PureComponent<Props, State> {
isDisabled={isDisabled}
gettingTrialResponseCode={this.state.gettingTrialResponseCode}
gettingTrialError={this.state.gettingTrialError}
requestLicense={this.requestLicense}
gettingTrial={this.state.gettingTrial}
enterpriseReady={this.props.enterpriseReady}
upgradingPercentage={this.state.upgradingPercentage}

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

@@ -13,6 +13,8 @@ import {makeGetCategory} from 'mattermost-redux/selectors/entities/preferences';
import AlertBanner from 'components/alert_banner';
import LoadingWrapper from 'components/widgets/loading/loading_wrapper';
import FormattedMarkdownMessage from 'components/formatted_markdown_message';
import withOpenStartTrialFormModal from 'components/common/hocs/cloud/with_open_start_trial_form_modal';
import {TelemetryProps} from 'components/common/hooks/useOpenPricingModal';
import {format} from 'utils/markdown';
@@ -26,13 +28,13 @@ interface TrialBannerProps {
isDisabled: boolean;
gettingTrialError: string | null;
gettingTrialResponseCode: number | null;
requestLicense: (e?: React.MouseEvent<HTMLButtonElement>, reload?: boolean) => Promise<void>;
gettingTrial: boolean;
enterpriseReady: boolean;
upgradingPercentage: number;
handleUpgrade: () => Promise<void>;
upgradeError: string | null;
restartError: string | null;
openTrialForm?: (telemetryProps?: TelemetryProps) => void;
handleRestart: () => Promise<void>;
@@ -72,7 +74,6 @@ const TrialBanner = ({
isDisabled,
gettingTrialError,
gettingTrialResponseCode,
requestLicense,
gettingTrial,
enterpriseReady,
upgradingPercentage,
@@ -82,6 +83,7 @@ const TrialBanner = ({
handleRestart,
restarting,
openEEModal,
openTrialForm,
}: TrialBannerProps) => {
let trialButton;
let upgradeTermsMessage;
@@ -119,6 +121,12 @@ const TrialBanner = ({
}
};
const handleRequestLicense = () => {
if (openTrialForm) {
openTrialForm({trackingLocation: 'license_settings.trial_banner'});
}
};
useEffect(() => {
async function savePrefsAndRequestTrial() {
await savePrefsRestartedAfterUpgrade();
@@ -150,7 +158,7 @@ const TrialBanner = ({
const clickedBtn = Unique.CLICKED_UPGRADE_AND_TRIAL_BTN;
dispatch(savePreferences(userId, [{category, name: reqLicense, user_id: userId, value: ''}, {category, name: clickedBtn, user_id: userId, value: ''}]));
requestLicense();
handleRequestLicense();
}
}, [restartedAfterUpgradePrefs, clickedUpgradeAndTrialBtn]);
@@ -213,7 +221,7 @@ const TrialBanner = ({
<button
type='button'
className='btn btn-primary'
onClick={requestLicense}
onClick={handleRequestLicense}
disabled={isDisabled || gettingTrialError !== null || gettingTrialResponseCode === 451}
>
{btnText(status)}
@@ -372,4 +380,4 @@ const TrialBanner = ({
);
};
export default TrialBanner;
export default withOpenStartTrialFormModal(TrialBanner);

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

@@ -1126,6 +1126,15 @@ export default class SchemaAdminSettings extends React.PureComponent {
}
if (setting.validate) {
if (setting.isHidden?.(this.props.config)) {
// MM-50952
// If the setting is hidden, then it is not being set in state so there is
// nothing to validate, and validation would fail anyways and prevent saving
// In practice, this only happens in custom cloud setup environments like RFQA
// where it sets things in the config file directly instead of in the environment
// (like cloud Mattermost does)
continue;
}
const result = setting.validate(this.state[setting.key]);
if (!result.isValid()) {
return false;

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

@@ -2,14 +2,15 @@
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import AdminTextSetting from './text_setting';
import {renderWithIntl} from 'tests/react_testing_utils';
import {screen} from '@testing-library/react';
describe('components/admin_console/TextSetting', () => {
test('render component with required props', () => {
const onChange = jest.fn();
const wrapper = shallow(
renderWithIntl(
<AdminTextSetting
id='string.id'
label='some label'
@@ -23,19 +24,9 @@ describe('components/admin_console/TextSetting', () => {
type='input'
/>,
);
expect(wrapper).toMatchInlineSnapshot(`
<TextSetting
disabled={false}
id="string.id"
inputClassName="col-sm-8"
label="some label"
labelClassName="col-sm-4"
maxLength={-1}
onChange={[MockFunction]}
resizable={true}
type="input"
value="some value"
/>
`);
screen.getByText('some label', {exact: false});
expect(screen.getByTestId('string.idinput')).toHaveProperty('id', 'string.id');
expect(screen.getByTestId('string.idinput')).toHaveValue('some value');
});
});

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

@@ -13,8 +13,8 @@ import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general
import {makeGetCategory} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUser, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
import {savePreferences} from 'mattermost-redux/actions/preferences';
import useOpenStartTrialFormModal from 'components/common/hooks/useOpenStartTrialFormModal';
import {openModal} from 'actions/views/modals';
import {GlobalState} from 'types/store';
import {
@@ -24,13 +24,12 @@ import {
ModalIdentifiers,
} from 'utils/constants';
import StartTrialModal from 'components/start_trial_modal';
import {trackEvent} from 'actions/telemetry_actions';
import {isModalOpen} from 'selectors/views/modals';
const ShowStartTrialModal = () => {
const isUserAdmin = useSelector((state: GlobalState) => isCurrentUserSystemAdmin(state));
const openStartTrialFormModal = useOpenStartTrialFormModal();
const dispatch = useDispatch<DispatchFunc>();
const getCategory = makeGetCategory();
@@ -81,11 +80,7 @@ const ShowStartTrialModal = () => {
const hasEnvMoreThan10Users = Number(totalUsers) > userThreshold;
const hadAdminDismissedModal = preferences.some((pref: PreferenceType) => pref.name === Constants.TRIAL_MODAL_AUTO_SHOWN && pref.value === TRUE);
if (isUserAdmin && !isBenefitsModalOpened && hasEnvMoreThan10Users && hasEnvMoreThan6Hours && !hadAdminDismissedModal && !isLicensedOrPreviousLicensed) {
dispatch(openModal({
modalId: ModalIdentifiers.START_TRIAL_MODAL,
dialogType: StartTrialModal,
dialogProps: {onClose: handleOnClose},
}));
openStartTrialFormModal({trackingLocation: 'show_start_trial_modal'}, handleOnClose);
trackEvent(
TELEMETRY_CATEGORIES.SELF_HOSTED_START_TRIAL_AUTO_MODAL,
'trigger_start_trial_auto_modal',

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

@@ -3,8 +3,11 @@
import React from 'react';
import {Channel, ChannelStats} from '@mattermost/types/channels';
import {act} from '@testing-library/react';
import {renderWithIntl} from 'tests/react_testing_utils';
import {Channel, ChannelStats} from '@mattermost/types/channels';
import {UserProfile} from '@mattermost/types/users';
import {Team} from '@mattermost/types/teams';
@@ -40,6 +43,7 @@ describe('channel_info_rhs', () => {
showChannelFiles: jest.fn(),
showPinnedPosts: jest.fn(),
showChannelMembers: jest.fn(),
getChannelStats: jest.fn().mockImplementation(() => Promise.resolve({data: {}})),
},
};
let props = {...OriginalProps};
@@ -49,20 +53,24 @@ describe('channel_info_rhs', () => {
});
describe('about area', () => {
test('should be editable', () => {
test('should be editable', async () => {
renderWithIntl(
<ChannelInfoRHS
{...props}
/>,
);
await act(async () => {
props.actions.getChannelStats();
});
expect(mockAboutArea).toHaveBeenCalledWith(
expect.objectContaining({
canEditChannelProperties: true,
}),
);
});
test('should not be editable in archived channel', () => {
test('should not be editable in archived channel', async () => {
props.isArchived = true;
renderWithIntl(
@@ -71,6 +79,10 @@ describe('channel_info_rhs', () => {
/>,
);
await act(async () => {
props.actions.getChannelStats();
});
expect(mockAboutArea).toHaveBeenCalledWith(
expect.objectContaining({
canEditChannelProperties: false,

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

@@ -64,6 +64,7 @@ export interface Props {
showChannelFiles: (channelId: string) => void;
showPinnedPosts: (channelId: string | undefined) => void;
showChannelMembers: (channelId: string) => void;
getChannelStats: (channelId: string) => Promise<{data: ChannelStats}>;
};
}
@@ -192,6 +193,7 @@ const ChannelInfoRhs = ({
showChannelFiles: actions.showChannelFiles,
showPinnedPosts: actions.showPinnedPosts,
showChannelMembers: actions.showChannelMembers,
getChannelStats: actions.getChannelStats,
}}
/>
</div>

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

@@ -16,7 +16,7 @@ import {Constants, ModalIdentifiers} from 'utils/constants';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/common';
import {getIsMobileView} from 'selectors/views/browser';
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
import {unfavoriteChannel, favoriteChannel} from 'mattermost-redux/actions/channels';
import {unfavoriteChannel, favoriteChannel, getChannelStats} from 'mattermost-redux/actions/channels';
import {muteChannel, unmuteChannel} from 'actions/channel_actions';
import {openModal} from 'actions/views/modals';
import {getDisplayNameByUser, getUserIdFromChannelId} from 'utils/utils';
@@ -92,6 +92,7 @@ function mapDispatchToProps(dispatch: Dispatch<AnyAction>) {
showChannelFiles,
showPinnedPosts,
showChannelMembers,
getChannelStats,
}, dispatch),
};
}

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

@@ -2,12 +2,13 @@
// See LICENSE.txt for license information.
import React from 'react';
import {fireEvent, screen} from '@testing-library/react';
import {act, fireEvent, screen} from '@testing-library/react';
import {Channel, ChannelStats} from '@mattermost/types/channels';
import {renderWithIntl} from 'tests/react_testing_utils';
import Constants from 'utils/constants';
import {Channel, ChannelStats} from '@mattermost/types/channels';
import Menu from './menu';
describe('channel_info_rhs/menu', () => {
@@ -20,6 +21,7 @@ describe('channel_info_rhs/menu', () => {
showChannelFiles: jest.fn(),
showPinnedPosts: jest.fn(),
showChannelMembers: jest.fn(),
getChannelStats: jest.fn().mockImplementation(() => Promise.resolve({data: {files_count: 3, pinnedpost_count: 12, member_count: 32}})),
},
};
@@ -29,10 +31,11 @@ describe('channel_info_rhs/menu', () => {
showChannelFiles: jest.fn(),
showPinnedPosts: jest.fn(),
showChannelMembers: jest.fn(),
getChannelStats: jest.fn().mockImplementation(() => Promise.resolve({data: {files_count: 3, pinnedpost_count: 12, member_count: 32}})),
};
});
test('should display notifications preferences', () => {
test('should display notifications preferences', async () => {
const props = {...defaultProps};
props.actions.openNotificationSettings = jest.fn();
@@ -42,13 +45,17 @@ describe('channel_info_rhs/menu', () => {
/>,
);
await act(async () => {
props.actions.getChannelStats();
});
expect(screen.getByText('Notification Preferences')).toBeInTheDocument();
fireEvent.click(screen.getByText('Notification Preferences'));
expect(props.actions.openNotificationSettings).toHaveBeenCalled();
});
test('should NOT display notifications preferences in a DM', () => {
test('should NOT display notifications preferences in a DM', async () => {
const props = {
...defaultProps,
channel: {type: Constants.DM_CHANNEL} as Channel,
@@ -60,10 +67,14 @@ describe('channel_info_rhs/menu', () => {
/>,
);
await act(async () => {
props.actions.getChannelStats();
});
expect(screen.queryByText('Notification Preferences')).not.toBeInTheDocument();
});
test('should NOT display notifications preferences in an archived channel', () => {
test('should NOT display notifications preferences in an archived channel', async () => {
const props = {
...defaultProps,
isArchived: true,
@@ -75,10 +86,14 @@ describe('channel_info_rhs/menu', () => {
/>,
);
await act(async () => {
props.actions.getChannelStats();
});
expect(screen.queryByText('Notification Preferences')).not.toBeInTheDocument();
});
test('should display the number of files', () => {
test('should display the number of files', async () => {
const props = {...defaultProps};
props.actions.showChannelFiles = jest.fn();
@@ -88,6 +103,10 @@ describe('channel_info_rhs/menu', () => {
/>,
);
await act(async () => {
props.actions.getChannelStats();
});
const fileItem = screen.getByText('Files');
expect(fileItem).toBeInTheDocument();
expect(fileItem.parentElement).toHaveTextContent('3');
@@ -96,7 +115,7 @@ describe('channel_info_rhs/menu', () => {
expect(props.actions.showChannelFiles).toHaveBeenCalled();
});
test('should display the pinned messages', () => {
test('should display the pinned messages', async () => {
const props = {...defaultProps};
props.actions.showPinnedPosts = jest.fn();
@@ -106,6 +125,10 @@ describe('channel_info_rhs/menu', () => {
/>,
);
await act(async () => {
props.actions.getChannelStats();
});
const fileItem = screen.getByText('Pinned Messages');
expect(fileItem).toBeInTheDocument();
expect(fileItem.parentElement).toHaveTextContent('12');
@@ -114,7 +137,7 @@ describe('channel_info_rhs/menu', () => {
expect(props.actions.showPinnedPosts).toHaveBeenCalled();
});
test('should display members', () => {
test('should display members', async () => {
const props = {...defaultProps};
props.actions.showChannelMembers = jest.fn();
@@ -124,6 +147,10 @@ describe('channel_info_rhs/menu', () => {
/>,
);
await act(async () => {
props.actions.getChannelStats();
});
const membersItem = screen.getByText('Members');
expect(membersItem).toBeInTheDocument();
expect(membersItem.parentElement).toHaveTextContent('32');
@@ -132,7 +159,7 @@ describe('channel_info_rhs/menu', () => {
expect(props.actions.showChannelMembers).toHaveBeenCalled();
});
test('should NOT display members in DM', () => {
test('should NOT display members in DM', async () => {
const props = {
...defaultProps,
channel: {type: Constants.DM_CHANNEL} as Channel,
@@ -144,6 +171,10 @@ describe('channel_info_rhs/menu', () => {
/>,
);
await act(async () => {
props.actions.getChannelStats();
});
const membersItem = screen.queryByText('Members');
expect(membersItem).not.toBeInTheDocument();
});

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

@@ -1,12 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import React, {useEffect, useState} from 'react';
import styled from 'styled-components';
import {useIntl} from 'react-intl';
import {Constants} from 'utils/constants';
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
import {Channel, ChannelStats} from '@mattermost/types/channels';
const MenuItemContainer = styled.div`
@@ -32,6 +34,9 @@ const RightSide = styled.div`
const Badge = styled.div`
font-size: 12px;
line-height: 18px;
width: 20px;
display: flex;
place-content: center;
`;
interface MenuItemProps {
@@ -39,7 +44,7 @@ interface MenuItemProps {
icon: JSX.Element;
text: string;
opensSubpanel?: boolean;
badge?: string|number;
badge?: string|number|JSX.Element;
onClick: () => void;
}
@@ -94,14 +99,26 @@ interface MenuProps {
showChannelFiles: (channelId: string) => void;
showPinnedPosts: (channelId: string | undefined) => void;
showChannelMembers: (channelId: string) => void;
getChannelStats: (channelId: string) => Promise<{data: ChannelStats}>;
};
}
const Menu = ({channel, channelStats, isArchived, className, actions}: MenuProps) => {
const {formatMessage} = useIntl();
const [loadingStats, setLoadingStats] = useState(true);
const showNotificationPreferences = channel.type !== Constants.DM_CHANNEL && !isArchived;
const showMembers = channel.type !== Constants.DM_CHANNEL;
const fileCount = channelStats?.files_count >= 0 ? channelStats?.files_count : 0;
useEffect(() => {
actions.getChannelStats(channel.id).then(() => {
setLoadingStats(false);
});
return () => {
setLoadingStats(true);
};
}, [channel.id]);
return (
<div
@@ -135,7 +152,7 @@ const Menu = ({channel, channelStats, isArchived, className, actions}: MenuProps
icon={<i className='icon icon-file-text-outline'/>}
text={formatMessage({id: 'channel_info_rhs.menu.files', defaultMessage: 'Files'})}
opensSubpanel={true}
badge={channelStats?.files_count}
badge={loadingStats ? <LoadingSpinner/> : fileCount}
onClick={() => actions.showChannelFiles(channel.id)}
/>
</div>

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

@@ -146,6 +146,7 @@ export default class ColorInput extends React.PureComponent<Props, State> {
onKeyDown={this.onKeyDown}
maxLength={7}
disabled={this.props.isDisabled}
data-testid='color-inputColorValue'
/>
{!this.props.isDisabled &&

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

@@ -0,0 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {ComponentType} from 'react';
import useOpenStartTrialFormModal from 'components/common/hooks/useOpenStartTrialFormModal';
export default function withOpenStartTrialFormModal<T>(WrappedComponent: ComponentType<T>) {
return (props: T) => {
const openTrialForm = useOpenStartTrialFormModal();
return (
<WrappedComponent
openTrialForm={openTrialForm}
{...props}
/>
);
};
}

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

@@ -8,14 +8,16 @@ import {Client4} from 'mattermost-redux/client';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
export default function useCWSAvailabilityCheck() {
const [canReachCWS, setCanReachCWS] = useState(false);
const [canReachCWS, setCanReachCWS] = useState<boolean | undefined>(undefined);
const config = useSelector(getConfig);
const isEnterpriseReady = config.BuildEnterpriseReady === 'true';
useEffect(() => {
if (!isEnterpriseReady) {
return;
}
Client4.cwsAvailabilityCheck().then(() => setCanReachCWS(true));
Client4.cwsAvailabilityCheck().then(() => {
setCanReachCWS(true);
}).catch(() => setCanReachCWS(false));
}, [isEnterpriseReady]);
return canReachCWS;

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

@@ -0,0 +1,23 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {useDispatch} from 'react-redux';
import {openModal} from 'actions/views/modals';
import {ModalIdentifiers} from 'utils/constants';
import StartTrialFormModal from 'components/start_trial_form_modal';
import {TelemetryProps} from './useOpenPricingModal';
export default function useOpenStartTrialFormModal() {
const dispatch = useDispatch();
return (telemetryProps?: TelemetryProps, onClose?: () => void) => {
dispatch(openModal({
modalId: ModalIdentifiers.START_TRIAL_FORM_MODAL,
dialogType: StartTrialFormModal,
dialogProps: {
page: telemetryProps?.trackingLocation,
onClose,
},
}));
};
}

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

@@ -0,0 +1,621 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
type SvgProps = {
height?: number;
width?: number;
}
const AirgappedTrialActivationConfirmSvg = ({height = 474, width = 578}: SvgProps) => (
<svg
width={width.toString()}
height={height.toString()}
viewBox='0 0 223 159'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<g clipPath='url(#clip0_754_57919)'>
<path
d='M17.1671 115.779H174.208C175.364 115.763 176.466 115.281 177.273 114.439C178.081 113.597 178.528 112.464 178.516 111.287V4.46575C178.528 3.28926 178.081 2.15606 177.273 1.3142C176.466 0.47233 175.364 -0.00961028 174.208 -0.026123H17.1671C16.0121 -0.00797133 14.9109 0.474468 14.1039 1.31597C13.297 2.15748 12.8496 3.28974 12.8595 4.46575V111.294C12.8513 112.468 13.2993 113.599 14.1061 114.439C14.9129 115.279 16.0131 115.761 17.1671 115.779Z'
fill='#3F4350'
/>
<path
d='M-0.15625 131.141C-0.15625 136.459 4.17094 141.776 9.41922 141.776H181.963C186.909 141.776 191.532 136.471 191.532 131.141H-0.15625Z'
fill='#767D93'
/>
<path
d='M175.559 115.779H15.8173L-0.15625 131.141H191.532L175.559 115.779Z'
fill='#D1D4DB'
/>
<path
d='M170.782 116.961H20.5884L15.2266 123.46H176.15L170.782 116.961Z'
fill='#AFB3C0'
/>
<path
d='M112.155 125.823H79.2272L76.7559 129.369H114.62L112.155 125.823Z'
fill='#24262E'
/>
<g clipPath='url(#clip1_754_57919)'>
<rect
width='146.725'
height='94.5349'
transform='translate(22.3262 10.0183)'
fill='white'
/>
<rect
x='22.3262'
y='10.0183'
width='43.7808'
height='95.1258'
fill='#1E325C'
/>
<ellipse
opacity='0.32'
cx='29.4256'
cy='17.6994'
rx='4.14142'
ry='4.1359'
fill='white'
/>
<rect
opacity='0.32'
x='36.5254'
y='16.5176'
width='19.5238'
height='1.77253'
rx='0.886265'
fill='white'
/>
<rect
opacity='0.32'
x='26.4678'
y='27.1528'
width='13.0159'
height='1.77253'
rx='0.886265'
fill='white'
/>
<rect
opacity='0.32'
x='26.4678'
y='83.8738'
width='13.0159'
height='1.77253'
rx='0.886265'
fill='white'
/>
<ellipse
opacity='0.32'
cx='28.5385'
cy='34.5382'
rx='2.07071'
ry='2.06795'
fill='white'
/>
<rect
opacity='0.32'
x='32.9756'
y='33.6519'
width='24.5527'
height='1.69648'
rx='0.848242'
fill='white'
/>
<ellipse
opacity='0.32'
cx='28.5385'
cy='55.8087'
rx='2.07071'
ry='2.06795'
fill='white'
/>
<rect
opacity='0.32'
x='32.9756'
y='54.9224'
width='24.2569'
height='1.77253'
rx='0.886265'
fill='white'
/>
<ellipse
opacity='0.32'
cx='28.5385'
cy='41.6285'
rx='2.07071'
ry='2.06795'
fill='white'
/>
<rect
opacity='0.32'
x='32.9756'
y='40.7422'
width='21.2987'
height='1.77253'
rx='0.886264'
fill='white'
/>
<ellipse
opacity='0.32'
cx='28.5385'
cy='62.8988'
rx='2.07071'
ry='2.06795'
fill='white'
/>
<rect
opacity='0.32'
x='32.9756'
y='62.0125'
width='21.2987'
height='1.77253'
rx='0.886265'
fill='white'
/>
<ellipse
opacity='0.32'
cx='28.5385'
cy='48.7186'
rx='2.07071'
ry='2.06795'
fill='white'
/>
<rect
opacity='0.32'
x='32.9756'
y='47.8323'
width='27.8067'
height='1.77253'
rx='0.886265'
fill='white'
/>
<ellipse
opacity='0.32'
cx='28.5385'
cy='69.9888'
rx='2.07071'
ry='2.06795'
fill='white'
/>
<rect
opacity='0.32'
x='32.9756'
y='69.1025'
width='27.8067'
height='1.77253'
rx='0.886265'
fill='white'
/>
<ellipse
opacity='0.32'
cx='28.5385'
cy='91.2594'
rx='2.07071'
ry='2.06795'
fill='white'
/>
<rect
opacity='0.32'
x='32.9756'
y='90.373'
width='27.8067'
height='1.77253'
rx='0.886265'
fill='white'
/>
<ellipse
opacity='0.32'
cx='28.5385'
cy='77.0789'
rx='2.07071'
ry='2.06795'
fill='white'
/>
<rect
opacity='0.32'
x='32.9756'
y='76.1926'
width='18.3406'
height='1.77253'
rx='0.886265'
fill='white'
/>
<ellipse
cx='74.3896'
cy='34.8336'
rx='3.54979'
ry='3.54506'
fill='#3F4350'
fillOpacity='0.24'
/>
<rect
x='81.4893'
y='31.8794'
width='20.1155'
height='1.77253'
rx='0.886265'
fill='#3F4350'
fillOpacity='0.24'
/>
<rect
x='81.4893'
y='37.197'
width='60.9381'
height='1.18169'
rx='0.590843'
fill='#3F4350'
fillOpacity='0.24'
/>
<rect
x='81.4893'
y='41.9238'
width='68.6293'
height='1.18169'
rx='0.590843'
fill='#3F4350'
fillOpacity='0.24'
/>
<ellipse
cx='74.3896'
cy='59.0582'
rx='3.54979'
ry='3.54506'
fill='#3F4350'
fillOpacity='0.24'
/>
<rect
x='81.4893'
y='56.104'
width='20.1155'
height='1.77253'
rx='0.886265'
fill='#3F4350'
fillOpacity='0.24'
/>
<rect
x='81.4893'
y='61.4216'
width='60.9381'
height='1.18169'
rx='0.590843'
fill='#3F4350'
fillOpacity='0.24'
/>
<rect
x='81.4893'
y='66.1484'
width='68.6293'
height='1.18169'
rx='0.590843'
fill='#3F4350'
fillOpacity='0.24'
/>
<ellipse
cx='74.3896'
cy='81.5104'
rx='3.54979'
ry='3.54506'
fill='#3F4350'
fillOpacity='0.24'
/>
<rect
x='81.4893'
y='78.5562'
width='20.1155'
height='1.77253'
rx='0.886265'
fill='#3F4350'
fillOpacity='0.24'
/>
<rect
x='81.4893'
y='83.8738'
width='60.9381'
height='1.18169'
rx='0.590843'
fill='#3F4350'
fillOpacity='0.24'
/>
<rect
x='81.4893'
y='88.6006'
width='68.6293'
height='1.18169'
rx='0.590843'
fill='#3F4350'
fillOpacity='0.24'
/>
<ellipse
cx='92.73'
cy='17.6993'
rx='1.77489'
ry='1.77253'
fill='#3F4350'
fillOpacity='0.24'
/>
<ellipse
cx='124.086'
cy='17.6993'
rx='1.77489'
ry='1.77253'
fill='#3F4350'
fillOpacity='0.24'
/>
<ellipse
cx='154.26'
cy='17.6993'
rx='1.77489'
ry='1.77253'
fill='#3F4350'
fillOpacity='0.24'
/>
<ellipse
cx='159.584'
cy='17.6993'
rx='1.77489'
ry='1.77253'
fill='#3F4350'
fillOpacity='0.24'
/>
<ellipse
cx='97.4634'
cy='17.6993'
rx='1.77489'
ry='1.77253'
fill='#3F4350'
fillOpacity='0.24'
/>
<rect
x='70.8398'
y='17.1084'
width='18.3406'
height='1.18169'
rx='0.590843'
fill='#3F4350'
fillOpacity='0.24'
/>
<rect
x='127.637'
y='16.5176'
width='23.0736'
height='2.36337'
rx='1.18169'
fill='#3F4350'
fillOpacity='0.24'
/>
</g>
<path
d='M95.6878 2.92798C96.1559 2.92798 96.6134 3.06659 97.0026 3.32629C97.3918 3.58598 97.6951 3.95508 97.8742 4.38693C98.0533 4.81878 98.1002 5.29397 98.0089 5.75242C97.9175 6.21087 97.6922 6.63199 97.3612 6.96251C97.0303 7.29303 96.6086 7.51813 96.1495 7.60932C95.6905 7.70051 95.2146 7.65369 94.7822 7.47481C94.3497 7.29593 93.9801 6.99303 93.7201 6.60438C93.4601 6.21572 93.3213 5.75878 93.3213 5.29135C93.3213 4.66455 93.5707 4.06341 94.0145 3.62019C94.4583 3.17697 95.0602 2.92798 95.6878 2.92798Z'
fill='#989DAE'
/>
<path
d='M109.322 138.231H81.4444C80.4823 138.231 77.9395 138.231 77.9395 134.686H112.846C112.846 138.231 110.228 138.231 109.322 138.231Z'
fill='#3F4350'
/>
</g>
<g clipPath='url(#clip2_754_57919)'>
<rect
x='184.015'
y='147.833'
width='14.3123'
height='2.45026'
rx='1.22513'
fill='#8D93A5'
/>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M219.65 155.56C218.049 157.196 215.767 158.45 213.264 158.45H169.486C166.983 158.45 164.701 157.196 163.1 155.56C161.496 153.92 160.297 151.613 160.297 149.117V47.721C160.297 45.2239 161.494 42.9173 163.098 41.2769C164.698 39.6401 166.979 38.3875 169.482 38.3875H213.264C215.766 38.3875 218.048 39.6398 219.65 41.2762C221.254 42.9162 222.453 45.223 222.453 47.721V149.117C222.453 151.613 221.254 153.92 219.65 155.56ZM213.264 156C216.632 156 219.999 152.554 219.999 149.117V47.721C219.999 44.2793 216.632 40.8377 213.264 40.8377H169.482C166.114 40.8377 162.75 44.2793 162.75 47.721V149.117C162.75 152.554 166.118 156 169.486 156H213.264Z'
fill='#363A45'
/>
<path
d='M220 149.117C220 152.554 216.632 156 213.265 156H169.486C166.119 156 162.751 152.554 162.751 149.117V47.7209C162.751 44.2793 166.115 40.8376 169.482 40.8376H213.265C216.632 40.8376 220 44.2793 220 47.7209V149.117Z'
fill='#3F4350'
/>
<path
d='M196.283 46.7579C196.283 47.0974 196.068 47.3719 195.796 47.3719H186.959C186.691 47.3719 186.469 47.0889 186.469 46.7579C186.469 46.4268 186.691 46.1467 186.959 46.1467H195.781C196.053 46.1467 196.283 46.4212 196.283 46.7579Z'
fill='#8D93A5'
/>
<path
d='M217.546 53.4976H165.204V143.749H217.546V53.4976Z'
fill='white'
/>
<ellipse
cx='171.516'
cy='71.2071'
rx='3.53454'
ry='3.52983'
fill='#3F4350'
fillOpacity='0.24'
/>
<ellipse
cx='171.516'
cy='92.386'
rx='3.53454'
ry='3.52983'
fill='#3F4350'
fillOpacity='0.24'
/>
<ellipse
cx='171.516'
cy='113.565'
rx='3.53454'
ry='3.52983'
fill='#3F4350'
fillOpacity='0.24'
/>
<rect
x='176.465'
y='69.0891'
width='24.0349'
height='1.41193'
rx='0.705966'
fill='#3F4350'
fillOpacity='0.24'
/>
<rect
x='176.465'
y='90.2681'
width='24.0349'
height='1.41193'
rx='0.705967'
fill='#3F4350'
fillOpacity='0.24'
/>
<rect
x='176.465'
y='111.447'
width='24.0349'
height='1.41193'
rx='0.705967'
fill='#3F4350'
fillOpacity='0.24'
/>
<rect
x='176.465'
y='77.5608'
width='33.9316'
height='1.41193'
rx='0.705966'
fill='#3F4350'
fillOpacity='0.24'
/>
<rect
x='176.465'
y='98.7397'
width='33.9316'
height='1.41193'
rx='0.705965'
fill='#3F4350'
fillOpacity='0.24'
/>
<rect
x='176.465'
y='119.919'
width='33.9316'
height='1.41193'
rx='0.705963'
fill='#3F4350'
fillOpacity='0.24'
/>
<rect
x='176.465'
y='81.7966'
width='16.9658'
height='1.41193'
rx='0.705966'
fill='#3F4350'
fillOpacity='0.24'
/>
<rect
x='176.465'
y='102.976'
width='16.9658'
height='1.41193'
rx='0.705965'
fill='#3F4350'
fillOpacity='0.24'
/>
<rect
x='176.465'
y='124.155'
width='16.9658'
height='1.41193'
rx='0.705967'
fill='#3F4350'
fillOpacity='0.24'
/>
<rect
x='176.465'
y='73.325'
width='36.7592'
height='1.41193'
rx='0.705966'
fill='#3F4350'
fillOpacity='0.24'
/>
<rect
x='176.465'
y='94.5039'
width='36.7592'
height='1.41193'
rx='0.705965'
fill='#3F4350'
fillOpacity='0.24'
/>
<rect
x='176.465'
y='115.683'
width='36.7592'
height='1.41193'
rx='0.705967'
fill='#3F4350'
fillOpacity='0.24'
/>
<rect
x='165.204'
y='53.4976'
width='52.342'
height='9.80105'
fill='#1E325C'
/>
<rect
opacity='0.4'
x='173.791'
y='57.9897'
width='17.1747'
height='1.22513'
rx='0.612565'
fill='white'
/>
<rect
opacity='0.4'
x='210.595'
y='56.3562'
width='4.08922'
height='4.08377'
rx='2'
fill='white'
/>
<rect
opacity='0.4'
x='168.066'
y='56.3562'
width='4.08922'
height='4.08377'
rx='2'
fill='white'
/>
</g>
<defs>
<clipPath id='clip0_754_57919'>
<rect
width='179'
height='142'
fill='white'
/>
</clipPath>
<clipPath id='clip1_754_57919'>
<rect
width='146.725'
height='94.5349'
fill='white'
transform='translate(22.3262 10.0183)'
/>
</clipPath>
<clipPath id='clip2_754_57919'>
<rect
width='63'
height='121'
fill='white'
transform='translate(160 38)'
/>
</clipPath>
</defs>
</svg>
);
export default AirgappedTrialActivationConfirmSvg;

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

@@ -0,0 +1,76 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
type SvgProps = {
width?: number;
height?: number;
}
const Svg = (props: SvgProps) => (
<svg
width={props.width?.toString() || '170'}
height={props.height?.toString() || '129'}
viewBox='0 0 170 129'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M23.6258 81.7032H134.554C135.371 81.6915 136.149 81.3515 136.719 80.7574C137.29 80.1633 137.605 79.3636 137.597 78.5334V3.15128C137.605 2.32105 137.29 1.52137 136.719 0.927285C136.149 0.333195 135.371 -0.00690192 134.554 -0.0185547H23.6258C22.81 -0.00574534 22.0321 0.334704 21.4621 0.928539C20.8921 1.52237 20.5761 2.32139 20.5831 3.15128V78.5378C20.5773 79.3669 20.8938 80.1648 21.4637 80.7577C22.0336 81.3505 22.8107 81.6904 23.6258 81.7032Z'
fill='#3F4350'
/>
<path
d='M11.3887 92.5439C11.3887 96.2965 14.4453 100.049 18.1524 100.049H140.031C143.525 100.049 146.791 96.3052 146.791 92.5439H11.3887Z'
fill='#767D93'
/>
<path
d='M135.508 81.7031H22.6718L11.3887 92.5438H146.791L135.508 81.7031Z'
fill='#D1D4DB'
/>
<path
d='M132.133 82.5371H26.0413L22.2539 87.1235H135.925L132.133 82.5371Z'
fill='#AFB3C0'
/>
<path
d='M90.7217 88.7915H67.4624L65.7168 91.2932H92.4629L90.7217 88.7915Z'
fill='#24262E'
/>
<rect
width='103.641'
height='66.7116'
transform='translate(27.2695 7.06982)'
fill='white'
/>
<rect
x='27.2695'
y='7.06982'
width='104'
height='67'
fill='#3F4350'
fillOpacity='0.16'
/>
<path
d='M79.0896 2.06641C79.4202 2.06641 79.7434 2.16423 80.0183 2.34748C80.2932 2.53074 80.5075 2.79121 80.634 3.09596C80.7605 3.40071 80.7936 3.73605 80.7291 4.05957C80.6646 4.38309 80.5054 4.68026 80.2717 4.9135C80.0379 5.14675 79.74 5.30559 79.4157 5.36994C79.0915 5.4343 78.7553 5.40125 78.4499 5.27502C78.1444 5.14879 77.8834 4.93504 77.6997 4.66077C77.516 4.38651 77.418 4.06405 77.418 3.7342C77.418 3.29187 77.5941 2.86766 77.9076 2.55489C78.2211 2.24212 78.6463 2.06641 79.0896 2.06641Z'
fill='#989DAE'
/>
<path
d='M88.7203 97.5471H69.0285C68.3489 97.5471 66.5527 97.5471 66.5527 95.0454H91.2093C91.2093 97.5471 89.3602 97.5471 88.7203 97.5471Z'
fill='#3F4350'
/>
<path
d='M60.1793 63.0923C57.6677 63.0923 56.5297 61.286 57.6504 59.0783L77.9502 19.2945C79.0997 17.0926 80.9101 17.0926 82.0366 19.2945L102.331 59.0783C103.48 61.2803 102.331 63.0923 99.8019 63.0923H60.1793Z'
fill='#FFBC1F'
/>
<path
d='M76.8061 34.0084L78.8924 47.9941C78.9125 48.2716 79.0372 48.5312 79.2414 48.7207C79.4456 48.9103 79.7141 49.0156 79.993 49.0156C80.2719 49.0156 80.5404 48.9103 80.7446 48.7207C80.9488 48.5312 81.0736 48.2716 81.0937 47.9941L83.18 34.0084C83.5593 28.5552 76.421 28.5552 76.8061 34.0084Z'
fill='#2D3039'
/>
<path
d='M79.9903 50.523C80.6221 50.5241 81.2393 50.712 81.7641 51.063C82.2888 51.4141 82.6976 51.9124 82.9385 52.4951C83.1795 53.0777 83.242 53.7186 83.118 54.3367C82.9941 54.9547 82.6893 55.5223 82.2421 55.9676C81.795 56.4129 81.2256 56.716 80.6059 56.8385C79.9862 56.9611 79.344 56.8976 78.7604 56.6561C78.1768 56.4147 77.6781 56.006 77.3272 55.4818C76.9763 54.9577 76.7891 54.3415 76.7891 53.7112C76.7891 53.292 76.8718 52.8769 77.0328 52.4898C77.1937 52.1026 77.4297 51.7509 77.727 51.4547C78.0244 51.1586 78.3773 50.9239 78.7657 50.764C79.1541 50.6041 79.5702 50.5222 79.9903 50.523Z'
fill='#2D3039'
/>
</svg>
);
export default Svg;

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

@@ -132,7 +132,7 @@ const FeatureRestrictedModal = ({
trialBtn = (
<CloudStartTrialButton
extraClass='button-trial'
message={formatMessage({id: 'trial_btn.free.tryFreeFor30Days', defaultMessage: 'Try free for 30 days'})}
message={formatMessage({id: 'trial_btn.free.tryFreeFor30Days', defaultMessage: 'Start trial'})}
telemetryId={'start_cloud_trial_after_team_creation_restricted'}
onClick={dismissAction}
/>
@@ -140,7 +140,8 @@ const FeatureRestrictedModal = ({
} else {
trialBtn = (
<StartTrialBtn
message={formatMessage({id: 'trial_btn.free.tryFreeFor30Days', defaultMessage: 'Try free for 30 days'})}
message={formatMessage({id: 'trial_btn.free.tryFreeFor30Days', defaultMessage: 'Start trial'})}
onClick={dismissAction}
telemetryId='start_self_hosted_trial_after_team_creation_restricted'
btnClass='btn btn-primary'
renderAsButton={true}

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

@@ -69,7 +69,7 @@ const LearnMoreTrialModal = (
// no need to check if is cloud trial or if it have had prev cloud trial because the button that show this modal takes care of that
if (isCloud) {
startTrialBtnMsg = formatMessage({id: 'trial_btn.free.tryFreeFor30Days', defaultMessage: 'Try free for 30 days'});
startTrialBtnMsg = formatMessage({id: 'trial_btn.free.tryFreeFor30Days', defaultMessage: 'Start trial'});
startTrialBtn = (
<CloudStartTrialButton
message={startTrialBtnMsg}

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

@@ -12,12 +12,8 @@ import {act} from 'react-dom/test-utils';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import mockStore from 'tests/test_store';
import {trackEvent} from 'actions/telemetry_actions.jsx';
import StartTrialBtn from 'components/learn_more_trial_modal/start_trial_btn';
import {TELEMETRY_CATEGORIES} from 'utils/constants';
jest.mock('actions/telemetry_actions.jsx', () => {
const original = jest.requireActual('actions/telemetry_actions.jsx');
return {
@@ -116,8 +112,6 @@ describe('components/learn_more_trial_modal/start_trial_btn', () => {
});
expect(mockOnClick).toHaveBeenCalled();
expect(trackEvent).toHaveBeenCalledWith(TELEMETRY_CATEGORIES.SELF_HOSTED_START_TRIAL_MODAL, 'test_telemetry_id');
});
test('should handle on click when rendered as button', async () => {
@@ -143,33 +137,31 @@ describe('components/learn_more_trial_modal/start_trial_btn', () => {
});
expect(mockOnClick).toHaveBeenCalled();
expect(trackEvent).toHaveBeenCalledWith(TELEMETRY_CATEGORIES.SELF_HOSTED_START_TRIAL_MODAL, 'test_telemetry_id');
});
test('does not show success for embargoed countries', async () => {
const mockOnClick = jest.fn();
// test('does not show success for embargoed countries', async () => {
// const mockOnClick = jest.fn();
let wrapper: ReactWrapper<any>;
const clonedState = JSON.parse(JSON.stringify(state));
clonedState.entities.admin.analytics.TOTAL_USERS = 451;
// let wrapper: ReactWrapper<any>;
// const clonedState = JSON.parse(JSON.stringify(state));
// clonedState.entities.admin.analytics.TOTAL_USERS = 451;
// Mount the component
await act(async () => {
wrapper = mountWithIntl(
<Provider store={mockStore(clonedState)}>
<StartTrialBtn
{...props}
onClick={mockOnClick}
/>
</Provider>,
);
});
// // Mount the component
// await act(async () => {
// wrapper = mountWithIntl(
// <Provider store={mockStore(clonedState)}>
// <StartTrialBtn
// {...props}
// onClick={mockOnClick}
// />
// </Provider>,
// );
// });
await act(async () => {
wrapper.find('.start-trial-btn').simulate('click');
});
// await act(async () => {
// wrapper.find('.start-trial-btn').simulate('click');
// });
expect(mockOnClick).not.toHaveBeenCalled();
});
// expect(mockOnClick).not.toHaveBeenCalled();
// });
});

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

@@ -1,27 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useState} from 'react';
import React from 'react';
import {useIntl} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
import {EmbargoedEntityTrialError} from 'components/admin_console/license_settings/trial_banner/trial_banner';
import {DispatchFunc} from 'mattermost-redux/types/actions';
import {getLicenseConfig} from 'mattermost-redux/actions/general';
import {GlobalState} from 'types/store';
import {requestTrialLicense} from 'actions/admin_actions';
import {trackEvent} from 'actions/telemetry_actions';
import {openModal} from 'actions/views/modals';
import TrialBenefitsModal from 'components/trial_benefits_modal/trial_benefits_modal';
import {ModalIdentifiers, TELEMETRY_CATEGORIES} from 'utils/constants';
import useOpenStartTrialFormModal from 'components/common/hooks/useOpenStartTrialFormModal';
import './start_trial_btn.scss';
@@ -36,116 +19,31 @@ export type StartTrialBtnProps = {
trackingPage?: string;
};
enum TrialLoadStatus {
NotStarted = 'NOT_STARTED',
Started = 'STARTED',
Success = 'SUCCESS',
Failed = 'FAILED',
Embargoed = 'EMBARGOED',
}
const StartTrialBtn = ({
message,
btnClass,
telemetryId,
onClick,
handleEmbargoError,
disabled = false,
renderAsButton = false,
trackingPage = 'licensing',
}: StartTrialBtnProps) => {
const {formatMessage} = useIntl();
const dispatch = useDispatch<DispatchFunc>();
const stats = useSelector((state: GlobalState) => state.entities.admin.analytics);
const [status, setLoadStatus] = useState(TrialLoadStatus.NotStarted);
const requestLicense = async (): Promise<TrialLoadStatus> => {
setLoadStatus(TrialLoadStatus.Started);
let users = 0;
if (stats && (typeof stats.TOTAL_USERS === 'number')) {
users = stats.TOTAL_USERS;
}
const requestedUsers = Math.max(users, 30);
const {error, data} = await dispatch(requestTrialLicense(requestedUsers, true, true, trackingPage));
if (error) {
if (typeof data?.status !== 'undefined' && data.status === 451) {
setLoadStatus(TrialLoadStatus.Embargoed);
if (typeof handleEmbargoError === 'function') {
handleEmbargoError();
}
return TrialLoadStatus.Embargoed;
}
setLoadStatus(TrialLoadStatus.Failed);
return TrialLoadStatus.Failed;
}
await dispatch(getLicenseConfig());
setLoadStatus(TrialLoadStatus.Success);
return TrialLoadStatus.Success;
};
const openTrialBenefitsModal = async (status: TrialLoadStatus) => {
// Only open the benefits modal if the trial request succeeded
if (status !== TrialLoadStatus.Success) {
return;
}
await dispatch(openModal({
modalId: ModalIdentifiers.TRIAL_BENEFITS_MODAL,
dialogType: TrialBenefitsModal,
dialogProps: {trialJustStarted: true},
}));
};
const btnText = (status: TrialLoadStatus): string => {
switch (status) {
case TrialLoadStatus.Started:
return formatMessage({id: 'start_trial.modal.gettingTrial', defaultMessage: 'Getting Trial...'});
case TrialLoadStatus.Success:
return formatMessage({id: 'start_trial.modal.loaded', defaultMessage: 'Loaded!'});
case TrialLoadStatus.Failed:
return formatMessage({id: 'start_trial.modal.failed', defaultMessage: 'Failed'});
case TrialLoadStatus.Embargoed:
return formatMessage({id: 'admin.license.trial-request.embargoed'});
default:
return message;
}
};
const openTrialForm = useOpenStartTrialFormModal();
const startTrial = async () => {
// reading status from here instead of normal flow because
// by the time the function needs the updated value from requestLicense,
// it will be too late to wait for the render cycle to happen again
// to close over the updated value
const updatedStatus = await requestLicense();
if (updatedStatus !== TrialLoadStatus.Success) {
return;
}
trackEvent(
TELEMETRY_CATEGORIES.SELF_HOSTED_START_TRIAL_MODAL,
telemetryId,
);
openTrialForm();
// on click will execute whatever action is sent from the invoking place, if nothing is sent, open the trial benefits modal
if (onClick) {
onClick();
return;
}
await openTrialBenefitsModal(updatedStatus);
};
if (status === TrialLoadStatus.Embargoed) {
return (
<div className='StartTrialBtn embargoed'>
<EmbargoedEntityTrialError/>
</div>
);
}
const id = 'start_trial_btn';
const btnText = formatMessage({id: 'admin.ldap_feature_discovery.call_to_action.primary', defaultMessage: 'Start trial'});
return renderAsButton ? (
<button
id={id}
@@ -153,7 +51,7 @@ const StartTrialBtn = ({
onClick={startTrial}
disabled={disabled}
>
{btnText(status)}
{btnText}
</button>
) : (
<a
@@ -161,7 +59,7 @@ const StartTrialBtn = ({
className='StartTrialBtn start-trial-btn'
onClick={startTrial}
>
{btnText(status)}
{btnText}
</a>
);
};

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

@@ -0,0 +1,91 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/onboarding_tasklist/onboarding_tasklist_completed.tsx should match snapshot 1`] = `
<Fragment>
<CSSTransition
classNames="fade"
in={true}
timeout={150}
>
<CompletedWrapper>
<img
alt="completed tasks image"
src={null}
/>
<h2>
<MemoizedFormattedMessage
defaultMessage="Well done. Youve completed all of the tasks!"
id="onboardingTask.checklist.completed_title"
/>
</h2>
<span
className="completed-subtitle"
>
<MemoizedFormattedMessage
defaultMessage="We hope Mattermost is more familiar now."
id="onboardingTask.checklist.completed_subtitle"
/>
</span>
<span
className="start-trial-text"
>
<MemoizedFormattedMessage
defaultMessage="Interested in our higher-security features?"
id="onboardingTask.checklist.higher_security_features"
/>
<br />
<MemoizedFormattedMessage
defaultMessage="Start your free Enterprise trial now!"
id="onboardingTask.checklist.start_enterprise_now"
/>
</span>
<StartTrialBtn
message="Start free 30-day trial"
onClick={[MockFunction]}
telemetryId="start_trial_from_onboarding_completed_task"
/>
<button
className="no-thanks-link style-link"
onClick={[MockFunction]}
>
<MemoizedFormattedMessage
defaultMessage="No, thanks"
id="onboardingTask.checklist.no_tanks"
/>
</button>
<div
className="download-apps"
>
<span>
<MemoizedFormattedMessage
defaultMessage="Now that youre all set up, <link>download our apps.</link>!"
id="onboardingTask.checklist.downloads"
values={
Object {
"link": [Function],
}
}
/>
</span>
</div>
<div
className="disclaimer"
>
<span>
<MemoizedFormattedMessage
defaultMessage="By clicking “Start trial”, I agree to the <linkEvaluation>Mattermost Software and Services License Agreement</linkEvaluation>, <linkPrivacy>privacy policy</linkPrivacy> and receiving product emails."
id="onboardingTask.checklist.disclaimer"
values={
Object {
"linkEvaluation": [Function],
"linkPrivacy": [Function],
}
}
/>
</span>
</div>
</CompletedWrapper>
</CSSTransition>
</Fragment>
`;

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

@@ -0,0 +1,73 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import Completed from './onboarding_tasklist_completed';
import {shallow} from 'enzyme';
let mockState: any;
const mockDispatch = jest.fn();
const dismissMockFn = jest.fn();
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux') as typeof import('react-redux'),
useSelector: (selector: (state: typeof mockState) => unknown) => selector(mockState),
useDispatch: () => mockDispatch,
}));
describe('components/onboarding_tasklist/onboarding_tasklist_completed.tsx', () => {
const props = {
dismissAction: dismissMockFn,
isCurrentUserSystemAdmin: true,
isFirstAdmin: true,
};
beforeEach(() => {
mockState = {
entities: {
admin: {
prevTrialLicense: {
IsLicensed: 'false',
},
},
general: {
license: {
IsLicensed: 'false',
},
},
cloud: {
subscription: {
product_id: 'prod_professional',
is_free_trial: 'false',
trial_end_at: 1,
},
},
},
};
});
afterEach(() => {
jest.restoreAllMocks();
});
test('should match snapshot', () => {
const wrapper = shallow(<Completed {...props}/>);
expect(wrapper).toMatchSnapshot();
});
test('finds the completed subtitle', () => {
const wrapper = shallow(<Completed {...props}/>);
expect(wrapper.find('.completed-subtitle')).toHaveLength(1);
});
test('displays the no thanks option to close the onboarding list', () => {
const wrapper = shallow(<Completed {...props}/>);
const noThanksLink = wrapper.find('.no-thanks-link');
expect(noThanksLink).toHaveLength(1);
// calls the dissmiss function on click
noThanksLink.simulate('click');
expect(dismissMockFn).toHaveBeenCalledTimes(1);
});
});

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

@@ -49,7 +49,7 @@ const CompletedWrapper = styled.div`
&.fade-exit-done {
transform: scale(1);
}
.start-trial-btn, button {
.start-trial-btn {
padding: 13px 20px;
background: var(--button-bg);
border-radius: 4px;
@@ -101,6 +101,24 @@ const CompletedWrapper = styled.div`
width: 200px;
font-size: 12px;
}
.style-link {
border: none;
background: none !important;
color: var(--button-bg) !important;
}
.no-thanks-link {
display: inline-block;
min-width: fit-content;
margin-top: 18px;
font-weight: 600;
font-size: 14px;
line-height: 20px;
&:hover {
text-decoration: underline;
}
}
`;
interface Props {
@@ -181,7 +199,7 @@ const Completed = (props: Props): JSX.Element => {
</span>
{isCloud ? (
<CloudStartTrialButton
message={formatMessage({id: 'trial_btn.free.tryFreeFor30Days', defaultMessage: 'Try free for 30 days'})}
message={formatMessage({id: 'trial_btn.free.tryFreeFor30Days', defaultMessage: 'Start trial'})}
telemetryId={'start_cloud_trial_after_completing_steps'}
extraClass={'btn btn-primary'}
afterTrialRequest={dismissAction}
@@ -193,6 +211,15 @@ const Completed = (props: Props): JSX.Element => {
onClick={dismissAction}
/>
)}
<button
onClick={dismissAction}
className={'no-thanks-link style-link'}
>
<FormattedMessage
id={'onboardingTask.checklist.no_tanks'}
defaultMessage='No, thanks'
/>
</button>
</>
) : (

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

@@ -10,6 +10,8 @@ import {getFileDownloadUrl} from 'mattermost-redux/utils/file_utils';
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
import FileInfoPreview from 'components/file_info_preview';
import {getSiteURL} from 'utils/url';
const INITIAL_RENDERED_PAGES = 3;
export default class PDFPreview extends React.PureComponent {
@@ -149,7 +151,11 @@ export default class PDFPreview extends React.PureComponent {
const worker = await import('pdfjs-dist/build/pdf.worker.entry.js');
PDFJS.GlobalWorkerOptions.workerSrc = worker;
const pdf = await PDFJS.getDocument(this.props.fileUrl).promise;
const pdf = await PDFJS.getDocument({
url: this.props.fileUrl,
cMapUrl: getSiteURL() + '/static/cmaps/',
cMapPacked: true,
}).promise;
this.onDocumentLoad(pdf);
} catch (err) {
this.onDocumentLoadError(err);

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

@@ -58,6 +58,7 @@ exports[`components/signup/Signup should match snapshot for all signup options e
disabled={false}
inputSize="large"
name="email"
onBlur={[Function]}
onChange={[Function]}
placeholder="Email address"
type="text"
@@ -75,6 +76,7 @@ exports[`components/signup/Signup should match snapshot for all signup options e
disabled={false}
inputSize="large"
name="name"
onBlur={[Function]}
onChange={[Function]}
placeholder="Choose a Username"
type="text"
@@ -87,6 +89,7 @@ exports[`components/signup/Signup should match snapshot for all signup options e
error=""
info="Must be 5-64 characters long."
inputSize="large"
onBlur={[Function]}
onChange={[Function]}
value=""
/>
@@ -206,6 +209,7 @@ exports[`components/signup/Signup should match snapshot for all signup options e
disabled={false}
inputSize="large"
name="email"
onBlur={[Function]}
onChange={[Function]}
placeholder="Email address"
type="text"
@@ -223,6 +227,7 @@ exports[`components/signup/Signup should match snapshot for all signup options e
disabled={false}
inputSize="large"
name="name"
onBlur={[Function]}
onChange={[Function]}
placeholder="Choose a Username"
type="text"
@@ -235,6 +240,7 @@ exports[`components/signup/Signup should match snapshot for all signup options e
error=""
info="Must be 5-64 characters long."
inputSize="large"
onBlur={[Function]}
onChange={[Function]}
value=""
/>

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

@@ -1,7 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useState, useEffect, useRef, useCallback} from 'react';
import React, {useState, useEffect, useRef, useCallback, FocusEvent} from 'react';
import {useIntl} from 'react-intl';
import {useLocation, useHistory} from 'react-router-dom';
import {useSelector, useDispatch} from 'react-redux';
@@ -455,16 +456,25 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
}
};
function sendSignUpTelemetryEvents(telemetryId: string, props?: any) {
trackEvent('signup', telemetryId, props);
}
type TelemetryErrorList = {errors: Array<{field: string; rule: string}>; success: boolean};
const isUserValid = () => {
let isValid = true;
const providedEmail = emailInput.current?.value.trim();
const telemetryEvents: TelemetryErrorList = {errors: [], success: true};
if (!providedEmail) {
setEmailError(formatMessage({id: 'signup_user_completed.required', defaultMessage: 'This field is required'}));
telemetryEvents.errors.push({field: 'email', rule: 'not_provided'});
isValid = false;
} else if (!isEmail(providedEmail)) {
setEmailError(formatMessage({id: 'signup_user_completed.validEmail', defaultMessage: 'Please enter a valid email address'}));
telemetryEvents.errors.push({field: 'email', rule: 'invalid_email'});
isValid = false;
}
@@ -474,10 +484,11 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
const usernameError = isValidUsername(providedUsername);
if (usernameError) {
setNameError(usernameError.id === ValidationErrors.RESERVED_NAME ? (
formatMessage({id: 'signup_user_completed.reserved', defaultMessage: 'This username is reserved, please choose a new one.'})
) : (
formatMessage(
let nameError = '';
if (usernameError.id === ValidationErrors.RESERVED_NAME) {
nameError = formatMessage({id: 'signup_user_completed.reserved', defaultMessage: 'This username is reserved, please choose a new one.'});
} else {
nameError = formatMessage(
{
id: 'signup_user_completed.usernameLength',
defaultMessage: 'Usernames have to begin with a lowercase letter and be {min}-{max} characters long. You can use lowercase letters, numbers, periods, dashes, and underscores.',
@@ -486,23 +497,33 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
min: Constants.MIN_USERNAME_LENGTH,
max: Constants.MAX_USERNAME_LENGTH,
},
)
));
);
}
telemetryEvents.errors.push({field: 'username', rule: usernameError.id.toLowerCase()});
setNameError(nameError);
isValid = false;
}
} else {
setNameError(formatMessage({id: 'signup_user_completed.required', defaultMessage: 'This field is required'}));
telemetryEvents.errors.push({field: 'username', rule: 'not_provided'});
isValid = false;
}
const providedPassword = passwordInput.current?.value ?? '';
const {error} = isValidPassword(providedPassword, getPasswordConfig(config), intl);
const {error, telemetryErrorIds} = isValidPassword(providedPassword, getPasswordConfig(config), intl);
if (error) {
setPasswordError(error as string);
telemetryEvents.errors = [...telemetryEvents.errors, ...telemetryErrorIds];
isValid = false;
}
if (telemetryEvents.errors.length) {
telemetryEvents.success = false;
}
sendSignUpTelemetryEvents('validate_user', telemetryEvents);
return isValid;
};
@@ -512,7 +533,7 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
const handleSubmit = async (e: React.MouseEvent | React.KeyboardEvent) => {
e.preventDefault();
trackEvent('signup_email', 'click_create_account', getRoleFromTrackFlow());
sendSignUpTelemetryEvents('click_create_account', getRoleFromTrackFlow());
setIsWaiting(true);
if (isUserValid()) {
@@ -550,6 +571,14 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
const handleReturnButtonOnClick = () => history.replace('/');
const handleOnBlur = (e: FocusEvent<HTMLInputElement>, inputId: string) => {
const text = e.target.value;
if (!text) {
return;
}
sendSignUpTelemetryEvents(`typed_input_${inputId}`);
};
const getContent = () => {
if (!enableSignUpWithEmail && !enableExternalSignup) {
return (
@@ -671,6 +700,7 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
disabled={isWaiting || Boolean(parsedEmail)}
autoFocus={true}
customMessage={emailCustomLabelForInput}
onBlur={(e) => handleOnBlur(e, 'email')}
/>
<Input
ref={nameInput}
@@ -692,6 +722,7 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
value: formatMessage({id: 'signup_user_completed.userHelp', defaultMessage: 'You can use lowercase letters, numbers, periods, dashes, and underscores.'}),
}
}
onBlur={(e) => handleOnBlur(e, 'username')}
/>
<PasswordInput
ref={passwordInput}
@@ -703,6 +734,7 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
createMode={true}
info={passwordInfo as string}
error={passwordError}
onBlur={(e) => handleOnBlur(e, 'password')}
/>
<SaveButton
extraClasses='signup-body-card-form-button-submit large'

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

@@ -0,0 +1,392 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/start_trial_form_modal/start_trial_form_modal should match snapshot 1`] = `
Object {
"asFragment": [Function],
"baseElement": <body
class="modal-open"
style="padding-right: 0px;"
>
<div
aria-hidden="true"
/>
<div
role="dialog"
>
<div
class="fade modal-backdrop in"
/>
<div
class="fade StartTrialFormModal in modal"
id="StartTrialFormModal"
role="dialog"
style="display: block; padding-right: 0px;"
tabindex="-1"
>
<div
class="a11y__modal modal-dialog"
>
<div
class="modal-content"
role="document"
>
<div
class="modal-header"
>
<button
aria-label="Close"
class="close"
type="button"
>
<span
aria-hidden="true"
>
×
</span>
<span
class="sr-only"
>
Close
</span>
</button>
<div
class="title"
>
Start Trial
</div>
<div
class="description"
>
Just a few quick items to help us tailor your trial experience.
</div>
</div>
<div
class="modal-body"
>
<div
class="Input_container"
>
<fieldset
class="Input_fieldset name_input"
>
<legend
class="Input_legend"
/>
<div
class="Input_wrapper"
>
<input
class="Input form-control large"
id="input_name"
name="name"
placeholder="Name"
type="text"
value=""
/>
</div>
</fieldset>
</div>
<div
class="Input_container"
>
<fieldset
class="Input_fieldset email_input Input_fieldset___error Input_fieldset___legend"
>
<legend
class="Input_legend Input_legend___focus"
>
Business Email
</legend>
<div
class="Input_wrapper"
>
<input
class="Input form-control large Input__focus"
id="input_email"
name="email"
placeholder="Business Email"
type="text"
value="test@mattermost.com"
/>
</div>
</fieldset>
<div
class="Input___customMessage Input___error"
>
<i
class="icon error icon-alert-circle-outline"
/>
<span>
Please enter a valid business email address.
</span>
</div>
</div>
<div
class="Input_container"
>
<fieldset
class="Input_fieldset company_name_input"
>
<legend
class="Input_legend"
/>
<div
class="Input_wrapper"
>
<input
class="Input form-control large"
id="input_company_name"
name="company_name"
placeholder="Company Name"
type="text"
value=""
/>
</div>
</fieldset>
</div>
<div
class="DropdownInput Input_container"
data-testid=""
>
<fieldset
class="Input_fieldset company_size_dropdown"
>
<legend
class="Input_legend"
/>
<div
class="Input_wrapper"
>
<div
class="Input company_size_dropdown css-2b097c-container"
id="DropdownInput_company_size_dropdown"
>
<div
class="DropdownInput__controlContainer"
>
<div
class="DropDown__control css-1nr98fh-control"
>
<div
class="DropDown__value-container css-1hwfws3"
>
<div
class="DropDown__placeholder css-1wa3eu0-placeholder"
>
Company Size
</div>
<div
class="css-1ed09z3-Input"
>
<div
class="DropDown__input"
style="display: inline-block;"
>
<input
aria-autocomplete="list"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
id="react-select-2-input"
spellcheck="false"
style="box-sizing: content-box; width: 2px; border: 0px; opacity: 1; outline: 0; padding: 0px;"
tabindex="0"
type="text"
value=""
/>
<div
style="position: absolute; top: 0px; left: 0px; visibility: hidden; height: 0px; overflow: scroll; white-space: pre; font-family: -webkit-small-control; letter-spacing: normal; text-transform: none;"
/>
</div>
</div>
</div>
<div
class="DropdownInput__indicatorsContainer"
>
<div
class="DropDown__indicators css-1hb7zxy-IndicatorsContainer"
>
<i
class="icon icon-chevron-down"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</fieldset>
</div>
<div
class="countries-section"
>
<div
class="DropdownInput Input_container"
data-testid=""
>
<fieldset
class="Input_fieldset"
>
<legend
class="Input_legend"
/>
<div
class="Input_wrapper"
>
<div
class="Input css-2b097c-container"
id="DropdownInput_country_dropdown"
>
<div
class="DropdownInput__controlContainer"
>
<div
class="DropDown__control css-1nr98fh-control"
>
<div
class="DropDown__value-container css-1hwfws3"
>
<div
class="DropDown__placeholder css-1wa3eu0-placeholder"
>
Country
</div>
<div
class="css-1ed09z3-Input"
>
<div
class="DropDown__input"
style="display: inline-block;"
>
<input
aria-autocomplete="list"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
id="react-select-3-input"
spellcheck="false"
style="box-sizing: content-box; width: 2px; border: 0px; opacity: 1; outline: 0; padding: 0px;"
tabindex="0"
type="text"
value=""
/>
<div
style="position: absolute; top: 0px; left: 0px; visibility: hidden; height: 0px; overflow: scroll; white-space: pre; font-family: -webkit-small-control; letter-spacing: normal; text-transform: none;"
/>
</div>
</div>
</div>
<div
class="DropdownInput__indicatorsContainer"
>
<div
class="DropDown__indicators css-1hb7zxy-IndicatorsContainer"
>
<i
class="icon icon-chevron-down"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</fieldset>
</div>
</div>
<div
class="disclaimer"
>
By selecting Start trial, I agree to the
<a
href="https://mattermost.com/software-evaluation-agreement/?utm_source=mattermost&utm_medium=in-product&utm_content=start_trial_form_modal&uid=user1&sid=test123"
location="start_trial_form_modal"
rel="noopener noreferrer"
target="_blank"
>
Mattermost Software Evaluation Agreement
</a>
,
<a
href="https://mattermost.com/privacy-policy/?utm_source=mattermost&utm_medium=in-product&utm_content=start_trial_form_modal&uid=user1&sid=test123"
location="start_trial_form_modal"
rel="noopener noreferrer"
target="_blank"
>
Privacy Policy
</a>
, and receiving product emails.
</div>
<div
class="buttons"
>
<button
class="confirm-btn btn btn-default"
disabled=""
type="button"
>
Start trial
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</body>,
"container": <div
aria-hidden="true"
/>,
"debug": [Function],
"findAllByAltText": [Function],
"findAllByDisplayValue": [Function],
"findAllByLabelText": [Function],
"findAllByPlaceholderText": [Function],
"findAllByRole": [Function],
"findAllByTestId": [Function],
"findAllByText": [Function],
"findAllByTitle": [Function],
"findByAltText": [Function],
"findByDisplayValue": [Function],
"findByLabelText": [Function],
"findByPlaceholderText": [Function],
"findByRole": [Function],
"findByTestId": [Function],
"findByText": [Function],
"findByTitle": [Function],
"getAllByAltText": [Function],
"getAllByDisplayValue": [Function],
"getAllByLabelText": [Function],
"getAllByPlaceholderText": [Function],
"getAllByRole": [Function],
"getAllByTestId": [Function],
"getAllByText": [Function],
"getAllByTitle": [Function],
"getByAltText": [Function],
"getByDisplayValue": [Function],
"getByLabelText": [Function],
"getByPlaceholderText": [Function],
"getByRole": [Function],
"getByTestId": [Function],
"getByText": [Function],
"getByTitle": [Function],
"queryAllByAltText": [Function],
"queryAllByDisplayValue": [Function],
"queryAllByLabelText": [Function],
"queryAllByPlaceholderText": [Function],
"queryAllByRole": [Function],
"queryAllByTestId": [Function],
"queryAllByText": [Function],
"queryAllByTitle": [Function],
"queryByAltText": [Function],
"queryByDisplayValue": [Function],
"queryByLabelText": [Function],
"queryByPlaceholderText": [Function],
"queryByRole": [Function],
"queryByTestId": [Function],
"queryByText": [Function],
"queryByTitle": [Function],
"rerender": [Function],
"unmount": [Function],
}
`;

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

@@ -0,0 +1,117 @@
.AirGappedModal {
.modal-dialog {
position: absolute;
top: 50%;
left: 50%;
width: 512px;
height: 660px;
border: 1 px solid rgba(var(--center-channel-color-rgb), 0.08);
margin: auto;
border-radius: 12px;
transform: translate(-50%, -50%) !important;
}
.modal-content {
border-radius: 12px;
}
.modal-header {
.close {
&:hover,
&:active,
&:focus,
&:active:focus {
background-color: rgba(var(--center-channel-color-rgb), 0.08);
color: rgba(var(--center-channel-color-rgb), 0.72);
opacity: 1;
}
top: 26px;
right: 26px;
width: 24px;
height: 24px;
border-radius: 4px;
color: rgba(var(--center-channel-color-rgb), 0.56) !important;
font-family: 'Open Sans', sans-serif;
font-size: 32px;
font-weight: 400;
}
.title {
font-family: Metropolis, sans-serif;
font-size: 22px;
font-style: normal;
font-weight: 600;
line-height: 28px;
text-align: left;
}
display: block;
min-height: 48px;
padding-top: 26px;
padding-bottom: 0;
padding-left: 32px;
border: 0;
background: var(--center-channel-bg) !important;
border-radius: 12px;
color: var(--center-channel-color);
}
.modal-body {
display: flex;
overflow: hidden;
width: 100%;
flex-direction: column;
padding-top: 24px;
padding-right: 32px;
padding-bottom: 26px;
padding-left: 32px;
.description {
margin: 0 auto;
margin-top: 8px;
color: rgb(var(--center-channel-color-rgb));
font-family: 'Open Sans';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
text-align: left;
}
.icon {
width: 100%;
align-items: center;
margin-top: 32px;
text-align: center;
}
.buttons {
margin-top: 32px;
text-align: right;
.confirm-btn {
&:hover,
&:active,
&:focus,
&:active:focus {
background:
linear-gradient(0deg, rgba(0, 0, 0, 0.16), rgba(0, 0, 0, 0.16)),
var(--button-bg);
}
height: 40px;
flex: none;
padding: 12px 20px;
border: none;
margin-left: 8px;
background: var(--button-bg);
border-radius: 4px;
color: var(--sys-button-color);
font-size: 14px;
font-weight: 600;
line-height: 14px;
}
}
}
}

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

@@ -0,0 +1,75 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Button, Modal} from 'react-bootstrap';
import {useIntl} from 'react-intl';
import AirgappedTrialActivationConfirmSvg from 'components/common/svg_images_components/airgapped_trial_activation_confirm_svg';
import './air_gapped_modal.scss';
import ExternalLink from 'components/external_link';
type Props = {
onClose?: () => void;
}
function AirGappedModal({onClose}: Props) {
const {formatMessage} = useIntl();
const airGappedLink = (
<ExternalLink
location='start_trial_air_gapped_modal'
href='https://mattermost.com/trial/'
>
{'https://mattermost.com/trial/'}
</ExternalLink>
);
return (
<Modal
className={'AirGappedModal'}
dialogClassName={'AirGappedModal__dialog'}
show={true}
id='airGappedModal'
role='dialog'
onHide={() => onClose?.()}
>
<Modal.Header closeButton={true}>
<div className='title'>
{formatMessage({id: 'air_gapped_modal.title', defaultMessage: 'Request a trial key'})}
</div>
</Modal.Header>
<Modal.Body>
<div className='body'>
<div className='description'>
{
formatMessage(
{
id: 'air_gapped_modal.description',
defaultMessage: 'To start your trial, please visit {link} and request a trial key.',
},
{
link: airGappedLink,
},
)
}
</div>
<div className='icon'>
<AirgappedTrialActivationConfirmSvg
width={256}
height={200}
/>
</div>
</div>
<div className='buttons'>
<Button
className='confirm-btn'
onClick={() => onClose?.()}
>
{formatMessage({id: 'air_gapped_modal.close', defaultMessage: 'Close'})}
</Button>
</div>
</Modal.Body>
</Modal>
);
}
export default AirGappedModal;

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

@@ -0,0 +1,77 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {useDispatch} from 'react-redux';
import {closeModal} from 'actions/views/modals';
import {ModalIdentifiers} from 'utils/constants';
import LaptopAlertSvg from 'components/common/svg_images_components/laptop_with_warning_symbol_svg';
import ResultModal from 'components/admin_console/billing/delete_workspace/result_modal';
type Props = {
onTryAgain?: () => void;
title?: JSX.Element;
subtitle?: JSX.Element;
buttonText?: JSX.Element;
}
export default function StartTrialFormModalResult(props: Props) {
const dispatch = useDispatch();
const handleButtonClick = () => {
props.onTryAgain?.();
dispatch(closeModal(ModalIdentifiers.START_TRIAL_FORM_MODAL_RESULT));
};
const title = props.title || (
<FormattedMessage
defaultMessage={'Please try again'}
id={'start_trial_form_modal.failureModal.title'}
/>
);
const subtitle = (
<>
<FormattedMessage
id={'start_trial_form_modal.failureModal.subtitle'}
defaultMessage={'There was an issue processing your trial request.'}
/>
<br/>
<FormattedMessage
id={'start_trial_form_modal.failureModal.subtitle2'}
defaultMessage={'Please try again or contact support.'}
/>
</>
);
const buttonText = props.buttonText || (
<FormattedMessage
id='admin.billing.deleteWorkspace.failureModal.buttonText'
defaultMessage={'Try Again'}
/>
);
return (
<ResultModal
primaryButtonText={buttonText}
primaryButtonHandler={handleButtonClick}
onHide={handleButtonClick}
identifier={ModalIdentifiers.START_TRIAL_FORM_MODAL_RESULT}
subtitle={subtitle}
title={title}
ignoreExit={false}
type='small'
resultType='failure'
icon={
<LaptopAlertSvg
width={135}
height={100}
/>
}
contactSupportButtonVisible={true}
/>
);
}

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

@@ -0,0 +1,361 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useEffect, useState} from 'react';
import {useSelector, useDispatch} from 'react-redux';
import {Modal, Button} from 'react-bootstrap';
import {FormattedMessage, useIntl} from 'react-intl';
import classNames from 'classnames';
import {isModalOpen} from 'selectors/views/modals';
import {GlobalState} from 'types/store';
import {closeModal, openModal} from 'actions/views/modals';
import {requestTrialLicense} from 'actions/admin_actions';
import {validateBusinessEmail} from 'actions/cloud';
import {getLicenseConfig} from 'mattermost-redux/actions/general';
import {DispatchFunc} from 'mattermost-redux/types/actions';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/common';
import {makeAsyncComponent} from 'components/async_load';
import useGetTotalUsersNoBots from 'components/common/hooks/useGetTotalUsersNoBots';
import {COUNTRIES} from 'utils/countries';
import {LicenseLinks, ModalIdentifiers, TELEMETRY_CATEGORIES} from 'utils/constants';
import Input, {SIZE, CustomMessageInputType} from 'components/widgets/inputs/input/input';
import DropdownInput from 'components/dropdown_input';
import StartTrialFormModalResult from './failure_modal';
import ExternalLink from 'components/external_link';
import {trackEvent} from 'actions/telemetry_actions';
import './start_trial_form_modal.scss';
import useCWSAvailabilityCheck from 'components/common/hooks/useCWSAvailabilityCheck';
import AirGappedModal from './air_gapped_modal';
// TODO: Handle embargoed entities explicitly https://mattermost.atlassian.net/browse/MM-51470
const TrialBenefitsModal = makeAsyncComponent('TrialBenefitsModal', React.lazy(() => import('components/trial_benefits_modal/trial_benefits_modal')));
enum TrialLoadStatus {
NotStarted = 'NOT_STARTED',
Started = 'STARTED',
Success = 'SUCCESS',
Failed = 'FAILED'
}
export enum OrgSize {
ONE_TO_50 = '1-50',
FIFTY_TO_100 = '51-100',
ONE_HUNDRED_TO_500 = '101-500',
}
type Props = {
onClose?: () => void;
page?: string;
}
function StartTrialFormModal(props: Props): JSX.Element | null {
const [status, setLoadStatus] = useState(TrialLoadStatus.NotStarted);
const dispatch = useDispatch<DispatchFunc>();
const currentUser = useSelector(getCurrentUser);
const [name, setName] = useState('');
const [email, setEmail] = useState(currentUser.email);
const [companyName, setCompanyName] = useState('');
const [orgSize, setOrgSize] = useState<OrgSize | undefined>();
const [country, setCountry] = useState('');
const [businessEmailError, setBusinessEmailError] = useState<CustomMessageInputType | undefined>(undefined);
const {formatMessage} = useIntl();
const canReachCWS = useCWSAvailabilityCheck();
const show = useSelector((state: GlobalState) => isModalOpen(state, ModalIdentifiers.START_TRIAL_FORM_MODAL));
const totalUsers = useGetTotalUsersNoBots(true) || 0;
const [didOnce, setDidOnce] = useState(false);
const handleValidateBusinessEmail = async (email: string) => {
setDidOnce(true);
if (!email) {
setBusinessEmailError(undefined);
return;
}
const isBusinessEmail = await validateBusinessEmail(email)();
if (isBusinessEmail) {
setBusinessEmailError(undefined);
return;
}
setBusinessEmailError({
type: 'error',
value: formatMessage({id: 'start_trial_form.invalid_business_email', defaultMessage: 'Please enter a valid business email address.'},
),
});
};
useEffect(() => {
trackEvent(TELEMETRY_CATEGORIES.SELF_HOSTED_START_TRIAL_MODAL, 'form_opened');
if (email && !didOnce) {
handleValidateBusinessEmail(email);
}
}, [email, didOnce]);
if (!show) {
return null;
}
const openTrialBenefitsModal = () => {
dispatch(openModal({
modalId: ModalIdentifiers.TRIAL_BENEFITS_MODAL,
dialogType: TrialBenefitsModal,
dialogProps: {trialJustStarted: true},
}));
};
// Reset the TrialLoadStatus so the user can re-submit the form
const handleErrorModalTryAgain = () => {
dispatch(closeModal(ModalIdentifiers.START_TRIAL_FORM_MODAL_RESULT));
setLoadStatus(TrialLoadStatus.NotStarted);
};
const requestLicense = async () => {
setLoadStatus(TrialLoadStatus.Started);
const requestedUsers = Math.max(totalUsers, 30);
const trialRequestBody = {
users: requestedUsers,
terms_accepted: true,
receive_emails_accepted: true,
contact_name: name,
contact_email: email,
company_name: companyName,
company_country: country,
company_size: orgSize,
};
const error = await dispatch(requestTrialLicense(trialRequestBody, props.page || 'license'));
if (error) {
setLoadStatus(TrialLoadStatus.Failed);
let title;
let subtitle;
let buttonText;
let onTryAgain = handleErrorModalTryAgain;
if (error?.data.status === 422) {
title = (<></>);
subtitle = (
<FormattedMessage
id='admin.license.trial-request.embargoed'
defaultMessage='We were unable to process the request due to limitations for embargoed countries. <link>Learn more in our documentation</link>, or reach out to legal@mattermost.com for questions around export limitations.'
values={{
link: (text: string) => (
<ExternalLink
location='trial_banner'
href={LicenseLinks.EMBARGOED_COUNTRIES}
>
{text}
</ExternalLink>
),
}}
/>
);
buttonText = (
<FormattedMessage
id='admin.license.trial-request.embargoed.button'
defaultMessage='Close'
/>
);
onTryAgain = handleOnClose;
}
dispatch(openModal({
modalId: ModalIdentifiers.START_TRIAL_FORM_MODAL_RESULT,
dialogType: StartTrialFormModalResult,
dialogProps: {
onTryAgain,
title,
subtitle,
buttonText,
},
}));
return;
}
setLoadStatus(TrialLoadStatus.Success);
await dispatch(getLicenseConfig());
dispatch(closeModal(ModalIdentifiers.START_TRIAL_FORM_MODAL));
openTrialBenefitsModal();
};
const btnText = (status: TrialLoadStatus): string => {
switch (status) {
case TrialLoadStatus.Started:
return formatMessage({id: 'start_trial.modal.loading', defaultMessage: 'Loading...'});
case TrialLoadStatus.Success:
return formatMessage({id: 'start_trial.modal.loaded', defaultMessage: 'Loaded!'});
case TrialLoadStatus.Failed:
return formatMessage({id: 'start_trial.modal.failed', defaultMessage: 'Failed'});
default:
return formatMessage({id: 'start_trial_form.modal_btn.start', defaultMessage: 'Start trial'});
}
};
const handleOnClose = () => {
if (props.onClose) {
props.onClose();
}
trackEvent(TELEMETRY_CATEGORIES.SELF_HOSTED_START_TRIAL_MODAL, 'form_closed');
dispatch(closeModal(ModalIdentifiers.START_TRIAL_FORM_MODAL));
};
const getOrgSizeDropdownValue = () => {
if (typeof orgSize === 'undefined') {
return orgSize;
}
return {
value: orgSize,
label: OrgSize[orgSize as unknown as keyof typeof OrgSize],
};
};
const isSubmitDisabled = (
!name ||
!email ||
!companyName ||
!orgSize ||
!country ||
Boolean(businessEmailError) ||
status === TrialLoadStatus.Started ||
status === TrialLoadStatus.Success
);
if (typeof canReachCWS !== 'undefined' && !canReachCWS) {
return (
<AirGappedModal
onClose={handleOnClose}
/>
);
}
return (
<Modal
className={classNames('StartTrialFormModal', {error: TrialLoadStatus.Failed === status})}
dialogClassName='a11y__modal'
show={show}
id='StartTrialFormModal'
role='dialog'
onHide={handleOnClose}
>
<Modal.Header closeButton={true}>
<div className='title'>
<FormattedMessage
id='start_trial_form.modal_title'
defaultMessage='Start Trial'
/>
</div>
<div className='description'>
<FormattedMessage
id='start_trial_form.modal_body'
defaultMessage='Just a few quick items to help us tailor your trial experience.'
/>
</div>
</Modal.Header>
<Modal.Body>
<Input
className={'name_input'}
name='name'
type='text'
value={name}
inputSize={SIZE.LARGE}
onChange={(e) => setName(e.target.value)}
required={true}
placeholder={formatMessage({id: 'start_trial_form.name', defaultMessage: 'Name'})}
/>
<Input
className={'email_input'}
onBlur={(e) => handleValidateBusinessEmail(e.target.value)}
name='email'
type='text'
value={email}
inputSize={SIZE.LARGE}
onChange={(e) => setEmail(e.target.value)}
required={true}
placeholder={formatMessage({id: 'start_trial_form.email', defaultMessage: 'Business Email'})}
customMessage={businessEmailError}
/>
<Input
className={'company_name_input'}
name='company_name'
type='text'
inputSize={SIZE.LARGE}
value={companyName}
onChange={(e) => setCompanyName(e.target.value)}
required={true}
placeholder={formatMessage({id: 'start_trial_form.company_name', defaultMessage: 'Company Name'})}
/>
<DropdownInput
className={'company_size_dropdown'}
onChange={(e) => {
setOrgSize(e.value as OrgSize);
}}
value={getOrgSizeDropdownValue()}
options={Object.entries(OrgSize).map(([value, label]) => ({value, label}))}
legend={formatMessage({id: 'start_trial_form.company_size', defaultMessage: 'Company Size'})}
placeholder={formatMessage({id: 'start_trial_form.company_size', defaultMessage: 'Company Size'})}
name='company_size_dropdown'
/>
<div className='countries-section'>
<DropdownInput
onChange={(e) => setCountry(e.value)}
value={
country ? {value: country, label: country} : undefined
}
options={COUNTRIES.map((country) => ({
value: country.name,
label: country.name,
}))}
legend={formatMessage({
id: 'payment_form.country',
defaultMessage: 'Country',
})}
placeholder={formatMessage({
id: 'payment_form.country',
defaultMessage: 'Country',
})}
name={'country_dropdown'}
/>
</div>
<div className='disclaimer'>
<FormattedMessage
id='start_trial_form.disclaimer'
defaultMessage='By selecting Start trial, I agree to the <agreement>Mattermost Software Evaluation Agreement</agreement>, <privacypolicy>Privacy Policy</privacypolicy>, and receiving product emails.'
values={{
agreement: (msg: React.ReactNode) => (
<ExternalLink
href='https://mattermost.com/software-evaluation-agreement/'
location='start_trial_form_modal'
>
{msg}
</ExternalLink>
),
privacypolicy: (msg: React.ReactNode) => (
<ExternalLink
href='https://mattermost.com/privacy-policy/'
location='start_trial_form_modal'
>
{msg}
</ExternalLink>
),
}}
/>
</div>
<div className='buttons'>
<Button
disabled={isSubmitDisabled}
className='confirm-btn'
onClick={requestLicense}
>
{btnText(status)}
</Button>
</div>
</Modal.Body>
</Modal>
);
}
export default StartTrialFormModal;

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

@@ -1,10 +1,14 @@
.StartTrialModal {
.StartTrialFormModal {
&.error {
display: none !important;
}
.modal-dialog {
position: absolute;
top: 50%;
left: 50%;
width: 512px;
height: 496px;
height: 660px;
border: 1 px solid rgba(var(--center-channel-color-rgb), 0.08);
margin: auto;
border-radius: 8px;
@@ -26,10 +30,10 @@
opacity: 1;
}
top: 6px;
right: 4px;
width: 4rem;
height: 4rem;
top: 26px;
right: 26px;
width: 24px;
height: 24px;
border-radius: 4px;
color: rgba(var(--center-channel-color-rgb), 0.56) !important;
font-family:
@@ -39,9 +43,32 @@
font-weight: 400;
}
height: 48px;
.title {
font-family: Metropolis, sans-serif;
font-size: 22px;
font-style: normal;
font-weight: 600;
line-height: 28px;
text-align: left;
}
.description {
margin: 0 auto;
margin-top: 8px;
color: rgba(var(--center-channel-color-rgb), 0.72);
font-family: 'Open Sans';
font-size: 14px;
font-style: normal;
font-weight: normal;
line-height: 16px;
text-align: left;
}
display: block;
min-height: 48px;
padding: 0;
padding-top: 26px;
padding-bottom: 0;
padding-left: 32px;
border: 0;
background: var(--center-channel-bg) !important;
border-radius: 8px;
@@ -53,75 +80,69 @@
overflow: hidden;
width: 100%;
flex-direction: column;
padding: 0;
.upgrade-image {
display: block;
margin: auto;
}
.title {
width: 80%;
margin: 0 auto;
margin-top: 16px;
font-family: Metropolis, sans-serif;
font-size: 22px;
font-style: normal;
font-weight: 600;
line-height: 28px;
text-align: center;
}
.description {
width: 80%;
margin: 0 auto;
margin-top: 8px;
font-family: 'Open Sans';
font-size: 14px;
font-style: normal;
font-weight: normal;
line-height: 16px;
text-align: center;
}
padding-top: 32px;
padding-right: 32px;
padding-bottom: 26px;
padding-left: 32px;
.disclaimer {
width: 80%;
width: 100%;
height: 32px;
margin: 10px auto;
margin: 0;
margin-top: 36px;
margin-bottom: 32px;
color: rgba(var(--center-channel-color-rgb), 0.72);
font-family: 'Open Sans';
font-size: 12px;
font-style: normal;
font-weight: normal;
line-height: 16px;
text-align: center;
text-align: left;
}
.email_input {
margin-top: 36px;
}
.company_name_input {
margin-top: 36px;
}
.DropdownInput {
margin-top: 36px;
.DropDown__menu {
z-index: 999;
max-height: 124px;
}
.DropDown__menu-list {
z-index: 999;
max-height: 124px;
}
}
.Input___error {
height: 100%;
font-weight: 600;
line-height: 16px;
.icon.error.icon-alert-circle-outline {
width: 16px;
height: 16px;
margin-right: 4px;
font-size: 16px;
}
}
.countries-section {
z-index: 999;
max-height: 164px;
}
.buttons {
margin-top: 32px;
margin-bottom: 32px;
text-align: center;
.dismiss-btn {
&:hover,
&:active,
&:focus,
&:active:focus {
border: none;
background: rgba(var(--button-bg-rgb), 0.08);
border-radius: 4px;
}
height: 40px;
align-items: center;
align-self: center;
border: none;
background: var(--center-channel-bg);
color: var(--button-bg);
font-size: 14px;
font-weight: 600;
line-height: 14px;
}
text-align: right;
.confirm-btn {
&:hover,

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

@@ -0,0 +1,108 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {act, RenderResult, screen} from '@testing-library/react';
import React from 'react';
import {Provider} from 'react-redux';
import mockStore from 'tests/test_store';
import {ModalIdentifiers} from 'utils/constants';
import StartTrialFormModal from '.';
import {BrowserRouter} from 'react-router-dom';
import {renderWithIntl} from 'tests/react_testing_utils';
import {trackEvent} from 'actions/telemetry_actions';
jest.mock('actions/telemetry_actions.jsx', () => {
const original = jest.requireActual('actions/telemetry_actions.jsx');
return {
...original,
trackEvent: jest.fn(),
};
});
describe('components/start_trial_form_modal/start_trial_form_modal', () => {
const state = {
entities: {
users: {
currentUserId: 'user1',
profiles: {
user1: {
id: 'user1',
roles: '',
email: 'test@mattermost.com',
},
},
},
general: {
license: {
IsLicensed: 'true',
Cloud: 'false',
},
config: {
TelemetryId: 'test123',
},
},
},
views: {
modals: {
modalState: {
[ModalIdentifiers.START_TRIAL_FORM_MODAL]: {
open: 'true',
},
},
},
admin: {
navigationBlock: {
blocked: true,
},
},
},
};
const handleOnClose = jest.fn();
const props = {
onClose: handleOnClose,
page: 'some_modal',
};
test('should match snapshot', async () => {
const store = await mockStore(state);
let wrapper: RenderResult | HTMLElement | null;
await act(async () => {
wrapper = await renderWithIntl(
<Provider store={store}>
<BrowserRouter>
<StartTrialFormModal {...props}/>
</BrowserRouter>
</Provider>);
});
expect(wrapper!).toMatchSnapshot();
});
test('should pre-fill email, fire trackEvent', async () => {
const store = await mockStore(state);
await act(async () => {
await renderWithIntl(
<Provider store={store}>
<BrowserRouter>
<StartTrialFormModal {...props}/>
</BrowserRouter>
</Provider>);
});
expect(screen.getByDisplayValue('test@mattermost.com')).toBeInTheDocument();
expect(trackEvent).toHaveBeenCalled();
});
test('Start trial button should be disabled on load', async () => {
const store = await mockStore(state);
await act(async () => {
await renderWithIntl(
<Provider store={store}>
<BrowserRouter>
<StartTrialFormModal {...props}/>
</BrowserRouter>
</Provider>);
});
expect(screen.getByRole('button', {name: 'Start trial'})).toBeDisabled();
});
});

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

@@ -1,165 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useState} from 'react';
import {useSelector, useDispatch} from 'react-redux';
import {Modal, Button} from 'react-bootstrap';
import {FormattedMessage, useIntl} from 'react-intl';
import {isModalOpen} from 'selectors/views/modals';
import {GlobalState} from 'types/store';
import {closeModal, openModal} from 'actions/views/modals';
import {requestTrialLicense} from 'actions/admin_actions';
import {getLicenseConfig} from 'mattermost-redux/actions/general';
import {DispatchFunc} from 'mattermost-redux/types/actions';
import {makeAsyncComponent} from 'components/async_load';
import useGetTotalUsersNoBots from 'components/common/hooks/useGetTotalUsersNoBots';
import ExternalLink from 'components/external_link';
import {AboutLinks, LicenseLinks, ModalIdentifiers} from 'utils/constants';
import StartTrialModalSvg from './start_trial_modal_svg';
const TrialBenefitsModal = makeAsyncComponent('TrialBenefisModal', React.lazy(() => import('components/trial_benefits_modal/trial_benefits_modal')));
import './start_trial_modal.scss';
enum TrialLoadStatus {
NotStarted = 'NOT_STARTED',
Started = 'STARTED',
Success = 'SUCCESS',
Failed = 'FAILED'
}
type Props = {
onClose?: () => void;
}
function StartTrialModal(props: Props): JSX.Element | null {
const [status, setLoadStatus] = useState(TrialLoadStatus.NotStarted);
const dispatch = useDispatch<DispatchFunc>();
const {formatMessage} = useIntl();
const show = useSelector((state: GlobalState) => isModalOpen(state, ModalIdentifiers.START_TRIAL_MODAL));
const totalUsers = useGetTotalUsersNoBots(true) || 0;
const openTrialBenefitsModal = async () => {
await dispatch(openModal({
modalId: ModalIdentifiers.TRIAL_BENEFITS_MODAL,
dialogType: TrialBenefitsModal,
dialogProps: {trialJustStarted: true},
}));
};
const requestLicense = async () => {
setLoadStatus(TrialLoadStatus.Started);
const requestedUsers = Math.max(totalUsers, 30);
const {error} = await dispatch(requestTrialLicense(requestedUsers, true, true, 'license'));
if (error) {
setLoadStatus(TrialLoadStatus.Failed);
}
setLoadStatus(TrialLoadStatus.Success);
await dispatch(getLicenseConfig());
await dispatch(closeModal(ModalIdentifiers.START_TRIAL_MODAL));
openTrialBenefitsModal();
};
const btnText = (status: TrialLoadStatus): string => {
switch (status) {
case TrialLoadStatus.Started:
return formatMessage({id: 'start_trial.modal.loading', defaultMessage: 'Loading...'});
case TrialLoadStatus.Success:
return formatMessage({id: 'start_trial.modal.loaded', defaultMessage: 'Loaded!'});
case TrialLoadStatus.Failed:
return formatMessage({id: 'start_trial.modal.failed', defaultMessage: 'Failed'});
default:
return formatMessage({id: 'start_trial.modal_btn.start', defaultMessage: 'Start 30-day trial'});
}
};
if (!show) {
return null;
}
const handleOnClose = () => {
if (props.onClose) {
props.onClose();
}
dispatch(closeModal(ModalIdentifiers.START_TRIAL_MODAL));
};
return (
<Modal
className='StartTrialModal'
dialogClassName='a11y__modal'
show={show}
id='startTrialModal'
role='dialog'
onHide={handleOnClose}
>
<Modal.Header closeButton={true}/>
<Modal.Body>
<StartTrialModalSvg/>
<div className='title'>
<FormattedMessage
id='start_trial.modal_title'
defaultMessage='Start your free Enterprise trial now'
/>
</div>
<div className='description'>
<FormattedMessage
id='start_trial.modal_body'
defaultMessage='Access all platform features including advanced security and enterprise compliance.'
/>
</div>
<div className='buttons'>
<Button
className='dismiss-btn'
onClick={handleOnClose}
>
<FormattedMessage
id='start_trial.modal_btn.nottnow'
defaultMessage='Not now'
/>
</Button>
<Button
className='confirm-btn'
onClick={requestLicense}
>
{btnText(status)}
</Button>
</div>
<div className='disclaimer'>
<span>
<FormattedMessage
id='start_trial.modal.disclaimer'
defaultMessage='By clicking “Start free 30-day trial”, I agree to the <linkEvaluation>Mattermost Software and Services License Agreement</linkEvaluation>, <linkPrivacy>privacy policy</linkPrivacy> and receiving product emails.'
values={{
linkEvaluation: (msg: React.ReactNode) => (
<ExternalLink
href={LicenseLinks.SOFTWARE_SERVICES_LICENSE_AGREEMENT}
location='start_trial_modal'
>
{msg}
</ExternalLink>
),
linkPrivacy: (msg: React.ReactNode) => (
<ExternalLink
href={AboutLinks.PRIVACY_POLICY}
location='start_trial_modal'
>
{msg}
</ExternalLink>
),
}}
/>
</span>
</div>
</Modal.Body>
</Modal>
);
}
export default StartTrialModal;

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

@@ -1,308 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {FC} from 'react';
const StartTrialModalSvg: FC = () => (
<svg
width='512'
height='156'
viewBox='0 0 512 156'
display='inline-block'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<g
clipPath='url(#clip0)'
style={{transform: 'translate(135px, 10px)'}}
>
<path
d='M117.504 138.565C156.52 138.565 188.158 107.604 188.158 69.4C188.158 31.1964 156.52 0.235336 117.504 0.235336C78.4886 0.235336 46.851 31.1964 46.851 69.4C46.851 107.604 78.4886 138.565 117.504 138.565Z'
fill='#E8E9ED'
stroke='#E8E9ED'
strokeWidth='0.5'
/>
<path
d='M97.7842 49.8971C100.351 52.3506 101.639 55.4564 101.639 59.2294C101.639 63.0023 100.351 66.1155 97.7842 68.5616C95.2171 71.0151 92.0367 72.2974 88.2504 72.4309H17.4471C12.4417 72.2974 8.28436 70.6148 4.96759 67.3682C1.65839 64.129 0 60.0596 0 55.1599C0 50.2603 1.65839 46.1909 4.96759 42.9443C8.27679 39.7051 12.4417 38.015 17.4471 37.8816C17.5834 31.3957 19.8476 25.9995 24.2473 21.7003C28.6394 17.4011 34.1522 15.1848 40.7781 15.0514C46.3212 15.044 51.1525 16.6302 55.2796 19.8101C59.4066 22.9901 62.1403 27.0891 63.4958 32.1222C65.7902 30.5359 68.4331 29.7354 71.4091 29.7354C75.3316 29.8688 78.6106 31.2253 81.2458 33.8048C83.8811 36.3843 85.2668 39.6013 85.4031 43.4335C85.4031 44.4935 85.2668 45.4867 84.9942 46.4133C86.0771 46.1465 87.1524 46.013 88.2428 46.013C92.0291 46.1539 95.2096 47.4436 97.7842 49.8971Z'
fill='#FFBC1F'
/>
<path
d='M101.639 59.2301C101.639 55.4572 100.351 52.3514 97.7842 49.8979C95.2171 47.4518 92.0367 46.1621 88.2504 46.0286C87.1675 46.0286 86.0847 46.1621 85.0018 46.4289C85.2744 45.5024 85.4107 44.5091 85.4107 43.4491C85.2744 39.6095 83.8886 36.3999 81.2534 33.8204C78.6181 31.2409 75.3316 29.8696 71.4091 29.7361C68.4331 29.7361 65.7978 30.5293 63.4958 32.1229C62.1403 27.0899 59.4066 22.9908 55.2796 19.8109C51.1525 16.631 46.3212 15.0447 40.7781 15.0447C34.1522 15.1782 28.6394 17.3945 24.2473 21.6937C19.8476 26.0003 17.5834 31.3965 17.4471 37.875C12.4417 38.0084 8.28436 39.6984 4.96759 42.9377C1.65839 46.1917 0 50.2611 0 55.1607C0 55.1607 0.21203 42.6412 18.5906 39.6169C19.2797 22.4572 34.5687 17.7503 40.9296 17.7503C47.2905 17.7503 57.7028 20.7745 63.0338 34.3986C74.2563 27.505 86.804 35.9033 83.3661 48.5266C98.9958 45.1614 101.639 59.2301 101.639 59.2301Z'
fill='#F5AB00'
/>
<path
d='M225.894 63.9064C227.346 65.2949 228.075 67.0526 228.075 69.1878C228.075 71.323 227.346 73.0848 225.894 74.4691C224.441 75.8576 222.641 76.5834 220.498 76.6589H180.429C177.596 76.5834 175.243 75.6311 173.366 73.7938C171.493 71.9606 170.555 69.6576 170.555 66.8848C170.555 64.112 171.493 61.809 173.366 59.9716C175.239 58.1385 177.596 57.182 180.429 57.1065C180.506 53.436 181.787 50.3821 184.277 47.9491C186.763 45.5161 189.882 44.2618 193.632 44.1863C196.769 44.1821 199.503 45.0798 201.839 46.8794C204.175 48.679 205.722 50.9988 206.489 53.8471C207.787 52.9494 209.283 52.4963 210.967 52.4963C213.187 52.5718 215.043 53.3395 216.534 54.7993C218.025 56.2591 218.81 58.0797 218.887 60.2485C218.887 60.8484 218.81 61.4105 218.655 61.9348C219.268 61.7838 219.877 61.7083 220.494 61.7083C222.637 61.788 224.436 62.5179 225.894 63.9064Z'
fill='#FFBC1F'
/>
<path
d='M228.075 69.189C228.075 67.0538 227.346 65.2961 225.894 63.9076C224.441 62.5233 222.641 61.7934 220.498 61.7179C219.885 61.7179 219.272 61.7934 218.66 61.9444C218.814 61.42 218.891 60.8579 218.891 60.2581C218.814 58.0851 218.03 56.2687 216.538 54.8089C215.047 53.3491 213.187 52.573 210.967 52.4975C209.283 52.4975 207.792 52.9464 206.489 53.8483C205.722 50.9999 204.175 48.6802 201.839 46.8806C199.503 45.081 196.769 44.1833 193.632 44.1833C189.882 44.2588 186.763 45.513 184.277 47.9461C181.787 50.3833 180.506 53.4372 180.429 57.1035C177.596 57.179 175.243 58.1354 173.366 59.9686C171.493 61.8102 170.555 64.1132 170.555 66.886C170.555 66.886 170.675 59.8008 181.076 58.0893C181.466 48.3781 190.118 45.7144 193.718 45.7144C197.318 45.7144 203.21 47.4259 206.227 55.1361C212.579 51.2349 219.68 55.9877 217.734 63.1316C226.579 61.2271 228.075 69.189 228.075 69.189Z'
fill='#F5AB00'
/>
<path
d='M199.061 63.0908L190.387 73.9565H194.064V92.2097H203.809V73.9702V73.9565H207.74L199.061 63.0908Z'
fill='#386FE5'
/>
<path
d='M49.3268 40.2811L28.7566 65.8203H37.4771V108.225H60.5857V65.8523V65.8203H69.9079L49.3268 40.2811Z'
fill='#386FE5'
/>
<mask
id='mask0'
mask-type='alpha'
maskUnits='userSpaceOnUse'
x='47'
y='0'
width='141'
height='139'
>
<path
d='M117.504 138.315C156.387 138.315 187.908 107.46 187.908 69.4C187.908 31.3395 156.387 0.485336 117.504 0.485336C78.6216 0.485336 47.101 31.3395 47.101 69.4C47.101 107.46 78.6216 138.315 117.504 138.315Z'
fill='#E8E9ED'
/>
</mask>
<g mask='url(#mask0)'>
<path
d='M54.6396 94.6503C55.2591 91.4217 56.4956 88.343 57.7274 85.2854C59.0477 82.0077 60.3799 78.7019 62.2527 75.6864C66.6201 68.6627 73.7333 63.594 81.3871 60.2062C89.0408 56.8185 96.3645 54.9034 104.554 53.0679C101.87 55.8961 101.052 59.4968 99.4475 63.0227C95.702 71.2498 92.4826 79.6945 89.2656 88.1323C84.4358 100.792 79.6084 113.45 74.7833 126.107C64.3096 120.069 56.785 109.348 54.7855 97.6119L54.6396 94.6503Z'
fill='white'
/>
<path
d='M134.964 53.3651C139.63 54.8775 146.923 57.5418 146.923 57.5418C146.923 57.5418 163.665 104.783 167.375 127.544C167.485 128.214 167.583 128.949 167.268 129.544C166.806 130.419 165.689 130.686 164.778 131.105C162.69 132.061 161.369 134.102 159.561 135.5C158.002 136.701 156.107 137.401 154.244 138.075C137.659 144.026 119.647 149.004 102.522 144.759C101.438 144.49 100.319 144.162 99.4936 143.422C98.776 142.769 98.3383 141.861 97.9437 140.974C92.7272 129.267 82.4831 105.083 82.5835 87.973C82.7007 68.2037 93.6456 56.1534 93.825 55.8983C94.2531 55.2919 110.271 51.1947 120.135 50.9817C120.144 50.9864 131.041 52.0914 134.964 53.3651Z'
fill='white'
/>
<mask
id='mask1'
mask-type='alpha'
maskUnits='userSpaceOnUse'
x='82'
y='50'
width='86'
height='97'
>
<path
d='M134.964 53.3651C139.63 54.8775 146.923 57.5418 146.923 57.5418C146.923 57.5418 163.665 104.783 167.375 127.544C167.485 128.214 167.583 128.949 167.268 129.544C166.806 130.419 165.689 130.686 164.778 131.105C162.69 132.061 161.369 134.102 159.561 135.5C158.002 136.701 156.107 137.401 154.244 138.075C137.659 144.026 119.647 149.004 102.522 144.759C101.438 144.49 100.319 144.162 99.4936 143.422C98.776 142.769 98.3383 141.861 97.9437 140.974C92.7272 129.267 82.4831 105.083 82.5835 87.973C82.7007 68.2037 93.6456 56.1534 93.825 55.8983C94.2531 55.2919 110.271 51.1947 120.135 50.9817C120.144 50.9864 131.041 52.0914 134.964 53.3651Z'
fill='white'
/>
</mask>
<g mask='url(#mask1)'>
<rect
x='82.3802'
y='50.9582'
width='85.148'
height='82.6451'
fill='url(#pattern0)'
/>
<rect
x='82.3802'
y='50.9582'
width='74.6241'
height='95.5218'
fill='url(#pattern1)'
/>
</g>
<path
d='M84.3219 100.889C83.7064 98.6605 83.0902 96.4317 82.4731 94.2028C82.4443 94.1173 82.3831 94.0459 82.3021 94.0033C82.221 93.9607 82.1264 93.9502 82.0377 93.974C81.949 93.9977 81.873 94.0539 81.8254 94.1309C81.7778 94.2079 81.7622 94.2998 81.7818 94.3878C82.3989 96.6166 83.0152 98.8462 83.6307 101.077C83.6582 101.164 83.7195 101.237 83.8013 101.281C83.8832 101.325 83.9793 101.336 84.0692 101.311C84.1591 101.287 84.2358 101.229 84.283 101.151C84.3302 101.072 84.3441 100.978 84.3219 100.889Z'
fill='#1D1400'
/>
<path
d='M81.4944 92.3346L81.3222 91.5573C81.2934 91.4718 81.2321 91.4004 81.1511 91.3578C81.0701 91.3152 80.9755 91.3047 80.8868 91.3284C80.7981 91.3522 80.7221 91.4083 80.6745 91.4853C80.6268 91.5623 80.6113 91.6543 80.6309 91.7422L80.8031 92.5219C80.8294 92.6108 80.89 92.6863 80.9722 92.7322C81.0544 92.7781 81.1516 92.7908 81.2432 92.7677C81.3317 92.7386 81.4057 92.6777 81.4501 92.5973C81.4945 92.5169 81.506 92.423 81.4824 92.3346H81.4944Z'
fill='#1D1400'
/>
</g>
<path
d='M145.561 57.0946C150.823 58.7334 168.01 64.4273 172.009 68.5853C186.157 83.2952 191.928 122.918 192.376 126.608C193.021 131.789 191.558 143.465 186.157 146.742C183.198 148.538 174.437 142.762 169.893 138.08C163.562 131.555 160.483 114.845 159.213 105.932C156.884 89.586 145.561 57.0946 145.561 57.0946Z'
fill='white'
/>
<path
d='M122.065 51.2391L151.268 49.5137C151.268 49.5137 152.225 45.5547 149.439 40.9612C147.829 38.3062 144.949 36.8617 143.06 33.2C141.247 29.6882 141.479 25.6262 140.752 22.0277C139.199 14.3392 133.5 8.19112 130.639 6.88004C123.782 3.73578 122.065 51.2391 122.065 51.2391Z'
fill='#1D1400'
/>
<path
d='M112.684 49.5534C108.962 52.611 111.155 68.55 119.529 68.55C128.702 68.55 128.075 46.3084 128.737 46.175C131.78 45.592 134.904 43.9953 136.843 36.9014C137.121 35.89 137.126 35.2626 137.305 34.305C137.398 33.8157 138.111 27.1572 137.912 19.8667C137.824 16.5515 134.456 6.36955 122.289 8.95894C107.077 12.1945 109.696 26.6352 111.177 29.7326C112.045 31.547 113.275 36.0258 113.48 38.4255C113.731 41.317 113.1 49.2092 112.684 49.5534Z'
fill='#CB8E00'
/>
<path
opacity='0.3'
d='M128.711 46.154C128.045 46.2766 127.369 46.3393 126.69 46.3413C124.979 46.3388 123.29 45.9587 121.749 45.2292H121.73C121.694 45.2131 121.657 45.1991 121.62 45.1871C120.955 44.9857 120.61 45.9573 121.24 46.2476C122.483 46.8343 123.629 47.5997 124.641 48.5186C125.678 49.4739 126.494 50.6364 127.032 51.9274C127.373 52.5704 127.651 53.2429 127.865 53.9362C128.403 49.7407 128.468 46.2593 128.726 46.1868L128.711 46.154Z'
fill='black'
/>
<path
d='M111.636 7.62455C117.415 2.56985 125.549 4.52243 129.096 6.05827C135.188 8.69917 137.037 14.8332 137.037 14.8332C135.626 16.5446 136.236 18.0828 134.667 20.1828C132.835 22.6388 130.584 20.5855 126.838 23.5565C125.306 24.7888 124.061 26.3279 123.186 28.0704C123.186 28.0704 121.926 25.6004 120.395 25.6215C118.642 25.6473 117.585 27.2604 118.07 28.8992C118.422 30.0815 119.804 32.8114 119.804 32.8114L114.32 46.5544C113.754 47.9729 112.686 49.1455 111.311 49.8587C109.936 50.5719 108.346 50.7785 106.829 50.4408L74.2858 40.2167C77.3545 33.3382 82.4706 32.4743 86.3811 29.1872C90.208 25.961 91.1839 22.6622 94.3817 20.1992C98.1536 17.2914 104.042 15.6011 105.697 13.9856C110.402 9.39217 107.977 10.825 111.636 7.62455Z'
fill='#1D1400'
/>
<mask
id='mask2'
mask-type='alpha'
maskUnits='userSpaceOnUse'
x='74'
y='4'
width='64'
height='47'
>
<path
d='M111.636 7.62455C117.415 2.56985 125.549 4.52243 129.096 6.05827C135.188 8.69917 137.037 14.8332 137.037 14.8332C135.626 16.5446 136.236 18.0828 134.667 20.1828C132.835 22.6388 130.584 20.5855 126.838 23.5565C125.306 24.7888 124.061 26.3279 123.186 28.0704C123.186 28.0704 121.926 25.6004 120.395 25.6215C118.642 25.6473 117.585 27.2604 118.07 28.8992C118.422 30.0815 119.804 32.8114 119.804 32.8114L114.32 46.5544C113.754 47.9729 112.686 49.1455 111.311 49.8587C109.936 50.5719 108.346 50.7785 106.829 50.4408L74.2858 40.2167C77.3545 33.3382 82.4706 32.4743 86.3811 29.1872C90.208 25.961 91.1839 22.6622 94.3817 20.1992C98.1536 17.2914 104.042 15.6011 105.697 13.9856C110.402 9.39217 107.977 10.825 111.636 7.62455Z'
fill='#1D1400'
/>
</mask>
<g mask='url(#mask2)'>
<rect
x='74.2472'
y='4.36786'
width='53.3371'
height='46.3562'
fill='url(#pattern2)'
/>
</g>
<path
d='M132.08 67.6581C130.776 64.1323 129.927 60.5385 131.051 56.9494C132.534 52.171 134.321 51.9509 138.868 49.6916C143.414 47.4323 144.18 46.1517 146.954 41.3288C147.607 40.1909 148.289 40.4321 148.425 41.2937C148.545 42.0522 148.425 43.6349 148.425 43.6349C148.425 43.6349 150.339 37.7818 151.056 37.5477C152.436 37.0958 152.783 39.9545 152.97 40.5913C153.792 43.4125 153.209 44.8055 151.295 49.9562C150.049 53.3065 147.325 57.3357 144.228 59.2555C143.032 59.9953 141.597 60.8733 141.513 62.7673C141.482 63.4931 142.018 64.1229 142.446 64.7152C144.94 68.1545 164.998 91.3138 167.923 94.0601C177.452 103.008 185.67 109.709 189.564 122.061C191.274 127.49 189.803 141.493 188.368 144.537C185.02 151.631 172.822 142.196 167.799 135.874C156.636 121.822 136.519 79.6803 132.08 67.6581Z'
fill='#CB8E00'
/>
<mask
id='mask3'
mask-type='alpha'
maskUnits='userSpaceOnUse'
x='130'
y='37'
width='61'
height='111'
>
<path
d='M132.08 67.6581C130.776 64.1323 129.927 60.5385 131.051 56.9494C132.534 52.171 134.321 51.9509 138.868 49.6916C143.414 47.4323 144.18 46.1517 146.954 41.3288C147.607 40.1909 148.289 40.4321 148.425 41.2937C148.545 42.0522 148.425 43.6349 148.425 43.6349C148.425 43.6349 150.339 37.7818 151.056 37.5477C152.436 37.0958 152.783 39.9545 152.97 40.5913C153.792 43.4125 153.209 44.8055 151.295 49.9562C150.049 53.3065 147.325 57.3357 144.228 59.2555C143.032 59.9953 141.597 60.8733 141.513 62.7673C141.482 63.4931 142.018 64.1229 142.446 64.7152C144.94 68.1545 164.998 91.3138 167.923 94.0601C177.452 103.008 185.67 109.709 189.564 122.061C191.274 127.49 189.803 141.493 188.368 144.537C185.02 151.631 172.822 142.196 167.799 135.874C156.636 121.822 136.519 79.6803 132.08 67.6581Z'
fill='#674600'
/>
</mask>
<g mask='url(#mask3)'>
<rect
x='130.456'
y='37.3792'
width='37.5512'
height='76.0897'
fill='url(#pattern3)'
/>
</g>
<path
d='M190.041 122.059C186.147 109.706 177.451 103.006 167.922 94.0577C167.343 93.5146 166.099 92.1848 164.454 90.3539C164.353 90.464 164.255 90.588 164.155 90.684C160.768 94.3644 156.587 97.0053 151.885 98.7542C150.256 99.3606 147.247 99.9553 145.561 100.18C152.669 114.298 161.043 129.515 166.762 136.703C171.785 143.024 185.499 151.619 188.845 144.525C190.28 141.491 191.751 127.488 190.041 122.059Z'
fill='white'
/>
<path
d='M95.0299 134.636C97.426 135.825 99.7521 137.144 101.997 138.588C105.286 140.749 114.824 140.187 114.824 140.187C114.824 140.187 119.692 150.238 120.194 146.827C120.455 145.05 119.677 143.999 119.445 142.226C120.436 143.502 121.036 144.186 121.526 145.715C122.017 147.244 121.84 148.62 121.619 150.163C121.452 151.322 122.045 151.844 122.665 151.898C123.861 151.999 124.1 150.664 124.698 150.475C129.802 148.836 128.749 146.981 129.201 142.409C129.163 147.976 128.845 147.508 126.532 150.709C126.068 151.35 126.932 152.113 129.163 150.863C130.916 149.849 132.103 148.756 132.911 146.181C133.787 143.51 133.582 140.612 132.337 138.085C129.57 132.543 127.367 130.698 121.11 129.949C114.853 129.2 107.63 128.65 103.172 130.183C101.44 130.78 95.0299 134.636 95.0299 134.636Z'
fill='#CB8E00'
/>
<path
d='M182.266 107.644C176.867 101.505 171.325 95.4961 165.639 89.6165C165.572 89.5567 165.484 89.524 165.393 89.525C165.303 89.5259 165.216 89.5603 165.15 89.6214C165.084 89.6825 165.045 89.7657 165.039 89.8542C165.033 89.9428 165.062 90.0301 165.12 90.0988C170.804 95.9846 176.347 101.994 181.747 108.126C182.049 108.47 182.568 107.986 182.266 107.644Z'
fill='#1D1400'
/>
<path
d='M160.311 126.231L160.472 126.388C160.54 126.452 160.631 126.487 160.725 126.487C160.82 126.487 160.911 126.452 160.979 126.388C161.045 126.321 161.083 126.232 161.083 126.139C161.083 126.047 161.045 125.957 160.979 125.891L160.819 125.734C160.786 125.7 160.746 125.672 160.702 125.653C160.658 125.634 160.611 125.623 160.562 125.623C160.514 125.622 160.466 125.63 160.422 125.648C160.377 125.666 160.336 125.692 160.302 125.725C160.268 125.759 160.241 125.799 160.223 125.842C160.205 125.886 160.196 125.933 160.197 125.98C160.198 126.027 160.209 126.074 160.228 126.117C160.248 126.16 160.276 126.199 160.311 126.231Z'
fill='white'
/>
<path
d='M160.381 126.746C155.218 118.275 150.381 109.625 145.872 100.796C145.666 100.393 145.047 100.746 145.253 101.152C149.759 109.973 154.595 118.623 159.761 127.102C159.809 127.182 159.888 127.24 159.981 127.264C160.073 127.287 160.171 127.274 160.253 127.227C160.335 127.18 160.395 127.102 160.419 127.012C160.443 126.922 160.429 126.826 160.381 126.746Z'
fill='#1D1400'
/>
<path
d='M148.121 43.8619C147.681 45.0332 147.098 46.148 146.385 47.1818C146.126 47.5564 146.748 47.9099 147.004 47.5353C147.748 46.4482 148.355 45.2778 148.815 44.0492C148.972 43.6231 148.281 43.4405 148.121 43.8619Z'
fill='black'
/>
<path
d='M145.975 48.7621H146.095C146.19 48.7621 146.281 48.7251 146.348 48.6593C146.416 48.5934 146.453 48.5041 146.453 48.4109C146.453 48.3178 146.416 48.2285 146.348 48.1626C146.281 48.0968 146.19 48.0598 146.095 48.0598H145.975C145.88 48.0598 145.789 48.0968 145.721 48.1626C145.654 48.2285 145.616 48.3178 145.616 48.4109C145.616 48.5041 145.654 48.5934 145.721 48.6593C145.789 48.7251 145.88 48.7621 145.975 48.7621Z'
fill='black'
/>
<path
d='M151.529 42.2161C151.112 44.3131 150.312 46.319 149.166 48.137C148.927 48.5233 149.544 48.8745 149.785 48.4906C150.964 46.6214 151.789 44.5593 152.22 42.4034C152.311 41.9609 151.617 41.7713 151.529 42.2161Z'
fill='black'
/>
<path
d='M148.965 49.4646C149.06 49.4646 149.151 49.4276 149.219 49.3617C149.286 49.2959 149.324 49.2065 149.324 49.1134C149.324 49.0203 149.286 48.9309 149.219 48.8651C149.151 48.7992 149.06 48.7622 148.965 48.7622C148.87 48.7622 148.779 48.7992 148.711 48.8651C148.644 48.9309 148.606 49.0203 148.606 49.1134C148.606 49.2065 148.644 49.2959 148.711 49.3617C148.779 49.4276 148.87 49.4646 148.965 49.4646Z'
fill='black'
/>
<path
d='M119.709 139.555L119.039 138.129C118.845 137.719 118.226 138.075 118.42 138.482C118.641 138.951 118.865 139.426 119.089 139.908C119.283 140.318 119.9 139.962 119.709 139.555Z'
fill='#1D1400'
/>
<path
d='M127.446 136.165C128.286 138.026 128.609 140.021 128.661 142.043C128.697 142.846 128.683 143.649 128.621 144.45C128.503 145.635 127.774 146.384 126.946 147.178C126.614 147.494 127.123 147.99 127.453 147.674C128.113 147.049 128.81 146.379 129.128 145.515C129.482 144.579 129.398 143.446 129.388 142.472C129.364 140.177 129.02 137.932 128.068 135.82C127.881 135.408 127.264 135.764 127.449 136.174L127.446 136.165Z'
fill='#1D1400'
/>
<path
d='M124.781 142.397C125.072 143.671 125.139 144.985 124.982 146.281C124.93 146.728 125.647 146.726 125.7 146.281C125.86 144.921 125.783 143.544 125.473 142.21C125.372 141.77 124.679 141.957 124.781 142.397Z'
fill='#1D1400'
/>
<path
d='M124.59 139.15C124.398 138.299 124.309 137.429 124.325 136.558C124.325 136.106 123.607 136.106 123.607 136.558C123.592 137.492 123.69 138.425 123.899 139.337C124 139.777 124.691 139.59 124.59 139.15Z'
fill='#1D1400'
/>
<path
d='M128.768 27.8247H128.701C128.445 27.8247 128.198 27.9246 128.016 28.1024C127.835 28.2802 127.733 28.5214 127.733 28.7729C127.733 29.0243 127.835 29.2655 128.016 29.4433C128.198 29.6212 128.445 29.7211 128.701 29.7211H128.768C129.025 29.7211 129.272 29.6212 129.453 29.4433C129.635 29.2655 129.737 29.0243 129.737 28.7729C129.737 28.5214 129.635 28.2802 129.453 28.1024C129.272 27.9246 129.025 27.8247 128.768 27.8247Z'
fill='black'
/>
<path
d='M135.585 29.4471H135.516C135.259 29.4471 135.013 29.5469 134.832 29.7245C134.65 29.9021 134.548 30.143 134.548 30.3942C134.548 30.6453 134.65 30.8862 134.832 31.0638C135.013 31.2414 135.259 31.3412 135.516 31.3412H135.585C135.842 31.3412 136.088 31.2414 136.269 31.0638C136.451 30.8862 136.553 30.6453 136.553 30.3942C136.553 30.143 136.451 29.9021 136.269 29.7245C136.088 29.5469 135.842 29.4471 135.585 29.4471Z'
fill='black'
/>
<path
d='M126.41 37.379C127.381 38.4247 128.504 39.3255 129.742 40.0527C129.117 40.8768 127.752 40.9237 126.943 40.2728C126.135 39.6219 125.876 38.4607 126.14 37.4633'
fill='black'
/>
<path
d='M131.811 36.8033C132.529 36.6465 134.153 36.6231 134.267 35.6514C134.346 34.9772 133.631 34.0805 133.411 33.4788C133.007 32.4336 132.778 31.3316 132.732 30.2151C132.732 29.6954 131.895 29.693 131.904 30.2151C131.963 31.669 132.303 33.0988 132.904 34.4293C132.997 34.6424 133.289 35.0287 133.301 35.2628C133.332 35.8622 133.244 35.5578 132.823 35.7451C132.462 35.9066 131.976 35.9301 131.586 36.0143C131.065 36.1267 131.287 36.911 131.806 36.7963L131.811 36.8033Z'
fill='black'
/>
<path
opacity='0.3'
d='M142.445 64.7129C139.192 62.5027 138.618 59.0939 142.445 57.0945C146.272 55.0951 150.671 51.4241 150.671 51.4241C150.671 51.4241 148.568 57.043 144.227 59.2485C139.886 61.4539 142.445 64.7129 142.445 64.7129Z'
fill='black'
/>
</g>
<defs>
<pattern
id='pattern0'
patternContentUnits='objectBoundingBox'
width='1'
height='1'
>
<use
transform='scale(0.00280899)'
/>
</pattern>
<pattern
id='pattern1'
patternContentUnits='objectBoundingBox'
width='1'
height='1'
>
<use
transform='scale(0.00320513 0.00245098)'
/>
</pattern>
<pattern
id='pattern2'
patternContentUnits='objectBoundingBox'
width='1'
height='1'
>
<use
transform='scale(0.0044843 0.00505051)'
/>
</pattern>
<pattern
id='pattern3'
patternContentUnits='objectBoundingBox'
width='1'
height='1'
>
<use
transform='scale(0.00636943 0.00307692)'
/>
</pattern>
<clipPath id='clip0'>
<rect
width='236'
height='156'
fill='white'
/>
</clipPath>
</defs>
</svg>
);
export default StartTrialModalSvg;

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

@@ -4,6 +4,8 @@
import React from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {useMeasurePunchouts} from '@mattermost/components';
import OnboardingWorkTemplateTourTip from './worktemplate_explore_tour_tip';
import {useShowTourTip} from './useShowTourTip';
@@ -11,6 +13,7 @@ export const BoardsTourTip = (): JSX.Element | null => {
const {formatMessage} = useIntl();
const {playbooksCount, boardsCount, showBoardsTour} = useShowTourTip();
const overlayPunchOut = useMeasurePunchouts(['sidebar-right'], []);
if (!showBoardsTour) {
return null;
@@ -50,11 +53,11 @@ export const BoardsTourTip = (): JSX.Element | null => {
return (
<OnboardingWorkTemplateTourTip
pulsatingDotPlacement={'left'}
pulsatingDotTranslate={{x: 10, y: -140}}
pulsatingDotTranslate={{x: 10, y: -350}}
title={title}
screen={screen}
overlayPunchOut={overlayPunchOut}
singleTip={playbooksCount === 0}
overlayPunchOut={null}
placement='left-start'
showOptOut={false}
/>

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

@@ -4,12 +4,15 @@
import React from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {useMeasurePunchouts} from '@mattermost/components';
import {useShowTourTip} from './useShowTourTip';
import OnboardingWorkTemplateTourTip from './worktemplate_explore_tour_tip';
export const PlaybooksTourTip = (): JSX.Element | null => {
const {formatMessage} = useIntl();
const {playbooksCount, boardsCount, showPlaybooksTour} = useShowTourTip();
const overlayPunchOut = useMeasurePunchouts(['sidebar-right'], []);
if (!showPlaybooksTour) {
return null;
@@ -53,7 +56,7 @@ export const PlaybooksTourTip = (): JSX.Element | null => {
title={title}
screen={screen}
singleTip={boardsCount === 0}
overlayPunchOut={null}
overlayPunchOut={overlayPunchOut}
placement='left-start'
showOptOut={false}
/>

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

@@ -3,7 +3,7 @@
import {useSelector} from 'react-redux';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/common';
import {getCurrentChannelId, getCurrentUserId} from 'mattermost-redux/selectors/entities/common';
import {getConfig, getWorkTemplatesLinkedProducts} from 'mattermost-redux/selectors/entities/general';
import {getInt} from 'mattermost-redux/selectors/entities/preferences';
@@ -17,18 +17,22 @@ export const useShowTourTip = () => {
const activeRhsComponent = useSelector(getActiveRhsComponent);
const pluginId = activeRhsComponent?.pluginId || '';
const currentChannelId = useSelector(getCurrentChannelId);
const currentUserId = useSelector(getCurrentUserId);
const enableTutorial = useSelector(getConfig).EnableTutorial === 'true';
const tutorialStep = useSelector((state: GlobalState) => getInt(state, TutorialTourName.WORK_TEMPLATE_TUTORIAL, currentUserId, 0));
const workTemplateTourTipShown = tutorialStep === WorkTemplateTourSteps.FINISHED;
const showProductTour = !workTemplateTourTipShown && enableTutorial;
const channelLinkedItems = useSelector(getWorkTemplatesLinkedProducts);
const boardsCount = channelLinkedItems?.boards || 0;
const playbooksCount = channelLinkedItems?.playbooks || 0;
const channelId = channelLinkedItems?.channelId || null;
const showProductTour = channelId && channelId === currentChannelId && !workTemplateTourTipShown && enableTutorial;
const showBoardsTour = showProductTour && pluginId === suitePluginIds.boards && boardsCount > 0;
const showPlaybooksTour = showProductTour && pluginId === suitePluginIds.playbooks && playbooksCount > 0;
@@ -38,5 +42,6 @@ export const useShowTourTip = () => {
showPlaybooksTour,
boardsCount,
playbooksCount,
showProductTour,
};
};

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

@@ -72,7 +72,7 @@ describe('component/user_groups_modal/ad_ldap_upsell_banner', () => {
);
expect(wrapper.find('#ad_ldap_upsell_banner')).toHaveLength(1);
expect(wrapper.find('.ad-ldap-banner-btn').text()).toEqual('Try free for 30 days');
expect(wrapper.find('.ad-ldap-banner-btn').text()).toEqual('Start trial');
});
test('should display for admin users on professional with option to start trial if no cloud trial before', async () => {
@@ -108,7 +108,7 @@ describe('component/user_groups_modal/ad_ldap_upsell_banner', () => {
await actImmediate(wrapper);
expect(wrapper.find('#ad_ldap_upsell_banner')).toHaveLength(1);
expect(wrapper.find('.ad-ldap-banner-btn').text()).toEqual('Try free for 30 days');
expect(wrapper.find('.ad-ldap-banner-btn').text()).toEqual('Start trial');
});
test('should display for admin users on professional with option to contact sales if self-hosted trialed before', () => {

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

@@ -93,7 +93,7 @@ function ADLDAPUpsellBanner() {
let btn = (
<StartTrialBtn
btnClass='ad-ldap-banner-btn'
message={formatMessage({id: 'adldap_upsell_banner.trial_btn', defaultMessage: 'Try free for 30 days'})}
message={formatMessage({id: 'adldap_upsell_banner.trial_btn', defaultMessage: 'Start trial'})}
telemetryId={'start_self-hosted_trial_from_adldap_upsell_banner'}
renderAsButton={true}
onClick={() => setConfirmed(true)}
@@ -103,7 +103,7 @@ function ADLDAPUpsellBanner() {
btn = (
<CloudStartTrialButton
extraClass='ad-ldap-banner-btn'
message={formatMessage({id: 'adldap_upsell_banner.trial_btn', defaultMessage: 'Try free for 30 days'})}
message={formatMessage({id: 'adldap_upsell_banner.trial_btn', defaultMessage: 'Start trial'})}
telemetryId={'start_cloud_trial_from_adldap_upsell_banner'}
onClick={() => setConfirmed(true)}
/>

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

@@ -8,7 +8,7 @@ import {useDispatch, useSelector} from 'react-redux';
import styled from 'styled-components';
import LocalizedIcon from 'components/localized_icon';
import {TTNameMapToATStatusKey, TutorialTourName, WorkTemplateTourSteps} from 'components/tours/constant';
import {TTNameMapToATStatusKey, TutorialTourName} from 'components/tours/constant';
import {closeModal as closeModalAction} from 'actions/views/modals';
import {trackEvent} from 'actions/telemetry_actions';
@@ -195,18 +195,15 @@ const WorkTemplateModal = () => {
* Creates the necessary data in the global store as long storing in DB preferences the tourtip information
* @param template current used worktempplate
*/
const tourTipActions = async (template: WorkTemplate) => {
const linkedProductsCount = getContentCount(template, playbookTemplates);
const tourTipActions = async (template: WorkTemplate, firstChannelId: string) => {
const linkedProductsCount = getContentCount(template, playbookTemplates, firstChannelId);
// stepValue and pluginId are used for showing the tourtip for the used template
let stepValue = 0;
let pluginId;
if (linkedProductsCount.playbooks) {
pluginId = rhsPluggableIds.get(suitePluginIds.playbooks);
stepValue = WorkTemplateTourSteps.PLAYBOOKS_TOUR_TIP;
} else {
pluginId = rhsPluggableIds.get(suitePluginIds.boards);
stepValue = WorkTemplateTourSteps.BOARDS_TOUR_TIP;
}
if (!pluginId) {
@@ -219,17 +216,8 @@ const WorkTemplateModal = () => {
// store the required preferences for the tourtip
const tourCategory = TutorialTourName.WORK_TEMPLATE_TUTORIAL;
const preferences = [
// here reset the step value to be able to show the tour again (if we dedide to show the tour only once, this must be removed)
{
user_id: currentUserId,
category: tourCategory,
name: currentUserId,
value: stepValue.toString(),
},
// this one is for defining the auto tour start for the tour tip
{
user_id: currentUserId,
category: tourCategory,
@@ -237,7 +225,6 @@ const WorkTemplateModal = () => {
value: String(AutoTourStatus.ENABLED),
},
];
await dispatch(savePreferences(currentUserId, preferences));
dispatch(showRHSPlugin(pluginId));
@@ -285,7 +272,7 @@ const WorkTemplateModal = () => {
dispatch(loadIfNecessaryAndSwitchToChannelById(firstChannelId));
}
await tourTipActions(template);
await tourTipActions(template, firstChannelId);
setIsCreating(false);
closeModal();

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

@@ -31,10 +31,11 @@ export function getTemplateDefaultIllustration(template: WorkTemplate): string {
return '';
}
export const getContentCount = (template: WorkTemplate, playbookTemplates: PlaybookTemplateType[]) => {
export const getContentCount = (template: WorkTemplate, playbookTemplates: PlaybookTemplateType[], channelId: string) => {
const res = {
playbooks: 0,
boards: 0,
channelId,
};
for (const item of template.content) {
if (item.playbook) {

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

@@ -11,7 +11,6 @@
"about.enterpriseEditionSt": "Modern communication from behind your firewall.",
"about.hash": "Build Hash:",
"about.hashee": "EE Build Hash:",
"about.hashwebapp": "Webapp Build Hash:",
"about.licensed": "Licensed to:",
"about.notice": "Mattermost is made possible by the open source software used in our <linkServer>server</linkServer>, <linkDesktop>desktop</linkDesktop> and <linkMobile>mobile</linkMobile> apps.",
"about.privacy": "Privacy Policy",
@@ -1342,6 +1341,7 @@
"admin.license.Trial": "Trial",
"admin.license.trial-request.accept-terms": "By clicking <strong>Start trial</strong>, I agree to the <linkEvaluation>Mattermost Software and Services License Agreement</linkEvaluation>, <linkPrivacy>Privacy Policy</linkPrivacy>, and receiving product emails.",
"admin.license.trial-request.embargoed": "We were unable to process the request due to limitations for embargoed countries. <link>Learn more in our documentation</link>, or reach out to legal@mattermost.com for questions around export limitations.",
"admin.license.trial-request.embargoed.button": "Close",
"admin.license.trial-request.startTrial": "Start trial",
"admin.license.trial-request.title": "Experience Mattermost Enterprise Edition for free for the next 30 days. No obligation to buy or credit card required. ",
"admin.license.trialCard.contactSales": "Contact sales",
@@ -2564,6 +2564,9 @@
"admin.webserverModeTitle": "Webserver Mode:",
"admin.webserverModeUncompressed": "Uncompressed",
"admin.webserverModeUncompressedDescription": "The Mattermost server will serve static files uncompressed.",
"air_gapped_modal.close": "Close",
"air_gapped_modal.description": "To start your trial, please visit {link} and request a trial key.",
"air_gapped_modal.title": "Request a trial key",
"alert_banner.tooltipCloseBtn": "Close",
"analytics.chart.loading": "Loading...",
"analytics.chart.meaningful": "Not enough data for a meaningful representation.",
@@ -4306,6 +4309,7 @@
"onboardingTask.checklist.downloads": "Now that youre all set up, <link>download our apps.</link>",
"onboardingTask.checklist.higher_security_features": "Interested in our higher-security features?",
"onboardingTask.checklist.main_subtitle": "Let's get up and running.",
"onboardingTask.checklist.no_tanks": "No, thanks",
"onboardingTask.checklist.start_enterprise_now": "Start your free Enterprise trial now!",
"onboardingTask.checklist.task_complete_your_profile": "Complete your profile.",
"onboardingTask.checklist.task_create_from_work_template": "Create from a template - set up a channel with linked boards and playbooks.",
@@ -4989,11 +4993,19 @@
"start_cloud_trial.modal.failed": "Failed",
"start_cloud_trial.modal.gettingTrial": "Getting Trial...",
"start_cloud_trial.modal.loaded": "Loaded!",
"start_trial.modal_body": "Access all platform features including advanced security and enterprise compliance.",
"start_trial.modal_btn.nottnow": "Not now",
"start_trial.modal_btn.start": "Start free 30-day trial",
"start_trial_form_modal.failureModal.subtitle": "There was an issue processing your trial request.",
"start_trial_form_modal.failureModal.subtitle2": "Please try again or contact support.",
"start_trial_form_modal.failureModal.title": "Please try again",
"start_trial_form.company_name": "Company Name",
"start_trial_form.company_size": "Company Size",
"start_trial_form.disclaimer": "By selecting Start trial, I agree to the <agreement>Mattermost Software Evaluation Agreement</agreement>, <privacypolicy>Privacy Policy</privacypolicy>, and receiving product emails.",
"start_trial_form.email": "Business Email",
"start_trial_form.invalid_business_email": "Please enter a valid business email address.",
"start_trial_form.modal_body": "Just a few quick items to help us tailor your trial experience",
"start_trial_form.modal_btn.start": "Start trial",
"start_trial_form.modal_title": "Start Trial",
"start_trial_form.name": "Name",
"start_trial.modal_btn.start_free_trial": "Start free 30-day trial",
"start_trial.modal_title": "Start your free Enterprise trial now",
"start_trial.modal.disclaimer": "By clicking “Start free 30-day trial”, I agree to the <linkEvaluation>Mattermost Software Evaluation Agreement</linkEvaluation>, <linkPrivacy>privacy policy</linkPrivacy> and receiving product emails.",
"start_trial.modal.failed": "Failed",
"start_trial.modal.gettingTrial": "Getting Trial...",

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

@@ -8,7 +8,6 @@ import {ChannelTypes, PreferenceTypes, UserTypes} from 'mattermost-redux/action_
import {Client4} from 'mattermost-redux/client';
import {General, Preferences} from '../constants';
import {CategoryTypes} from 'mattermost-redux/constants/channel_categories';
import {MarkUnread} from 'mattermost-redux/constants/channels';
@@ -25,12 +24,15 @@ import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {ActionFunc, ActionResult, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions';
import {getChannelsIdForTeam, getChannelByName} from 'mattermost-redux/utils/channel_utils';
import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
import {Channel, ChannelNotifyProps, ChannelMembership, ChannelModerationPatch, ChannelsWithTotalCount, ChannelSearchOpts} from '@mattermost/types/channels';
import {PreferenceType} from '@mattermost/types/preferences';
import {getChannelsIdForTeam, getChannelByName} from 'mattermost-redux/utils/channel_utils';
import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
import {General, Preferences} from '../constants';
import {addChannelToInitialCategory, addChannelToCategory} from './channel_categories';
import {logError} from './errors';
@@ -1074,11 +1076,11 @@ export function searchGroupChannels(term: string): ActionFunc {
});
}
export function getChannelStats(channelId: string): ActionFunc {
export function getChannelStats(channelId: string, excludeFilesCount?: boolean): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
let stat;
try {
stat = await Client4.getChannelStats(channelId);
stat = await Client4.getChannelStats(channelId, excludeFilesCount);
} catch (error) {
forceLogoutIfNecessary(error, dispatch, getState);
dispatch(logError(error));

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

@@ -47,7 +47,7 @@ export function clearWorkTemplates(): ActionFunc {
}
// stores the linked product information in the state so it can be used to show the tourtip
export function onExecuteSuccess(data: Record<string, number>): ActionFunc {
export function onExecuteSuccess(data: Record<string, string | number>): ActionFunc {
return async (dispatch) => {
dispatch({type: WorkTemplatesType.EXECUTE_SUCCESS, data});
return [];

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

@@ -388,7 +388,7 @@ export function makeGetPostsForThread(): (state: GlobalState, rootId: string) =>
);
}
// The selector below filters current user if it exists. Excluding currentUser is just for convinience
// The selector below filters current user if it exists. Excluding currentUser is just for convenience
export function makeGetProfilesForThread(): (state: GlobalState, rootId: string) => UserProfile[] {
const getPostsForThread = makeGetPostsForThread();
return createSelector(
@@ -398,7 +398,7 @@ export function makeGetProfilesForThread(): (state: GlobalState, rootId: string)
getPostsForThread,
getUserStatuses,
(allUsers, currentUserId, posts, userStatuses) => {
const profileIds = posts.map((post) => post.user_id);
const profileIds = posts.map((post) => post.user_id).filter(Boolean);
const uniqueIds = [...new Set(profileIds)];
return uniqueIds.reduce((acc: UserProfile[], id: string) => {
const profile: UserProfile = userStatuses ? {...allUsers[id], status: userStatuses[id]} : {...allUsers[id]};

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

@@ -442,7 +442,7 @@ export function makeSearchProfilesStartingWithTerm(): (state: GlobalState, term:
(state: GlobalState, term: string, skipCurrent?: boolean) => skipCurrent || false,
(stateGlobalState, term: string, skipCurrent?: boolean, filters?: Filters) => filters,
(users, currentUserId, term, skipCurrent, filters) => {
const profiles = filterProfilesStartingWithTerm(Object.values(users), term);
const profiles = filterProfilesMatchingWithTerm(Object.values(users), term);
return filterFromProfiles(currentUserId, profiles, skipCurrent, filters);
},
);
@@ -509,7 +509,7 @@ export function searchProfilesInCurrentTeam(state: GlobalState, term: string, sk
}
export function searchProfilesInTeam(state: GlobalState, teamId: Team['id'], term: string, skipCurrent = false, filters?: Filters): UserProfile[] {
const profiles = filterProfilesStartingWithTerm(getProfilesInTeam(state, teamId, filters), term);
const profiles = filterProfilesMatchingWithTerm(getProfilesInTeam(state, teamId, filters), term);
if (skipCurrent) {
removeCurrentUserFromList(profiles, getCurrentUserId(state));
}

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

@@ -21,6 +21,7 @@ import PurchaseModal from 'components/purchase_modal';
import {useNotifyAdmin} from 'components/notify_admin_cta/notify_admin_cta';
import Timestamp from 'components/timestamp';
import Avatar from 'components/widgets/users/avatar';
import StartTrialFormModal from 'components/start_trial_form_modal';
import {openPricingModal} from '../components/global_header/right_controls/plan_upgrade_button';
@@ -70,6 +71,7 @@ window.Components = {
Avatar,
imageURLForUser,
BotBadge: BotTag,
StartTrialFormModal,
};
// This is a prototype of the Product API for use by internal plugins only while we transition to the proper architecture

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

@@ -463,6 +463,8 @@ export const ModalIdentifiers = {
CONFIRM_SWITCH_TO_YEARLY: 'confirm_switch_to_yearly',
EXPANSION_IN_PROGRESS: 'expansion_in_progress',
SELF_HOSTED_EXPANSION: 'self_hosted_expansion',
START_TRIAL_FORM_MODAL: 'start_trial_form_modal',
START_TRIAL_FORM_MODAL_RESULT: 'start_trial_form_modal_result',
};
export const UserStatuses = {
@@ -1076,6 +1078,7 @@ export const CloudLinks = {
export const HostedCustomerLinks = {
BILLING_DOCS: 'https://mattermost.com/pl/how-self-hosted-billing-works',
SELF_HOSTED_BILLING: 'https://docs.mattermost.com/manage/self-hosted-billing.html',
TERMS_AND_CONDITIONS: 'https://mattermost.com/enterprise-edition-terms/',
};

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

@@ -7,7 +7,13 @@ import EmojiMap from 'utils/emoji_map';
import {getEmojiMap} from 'selectors/emojis';
import store from 'stores/redux_store.jsx';
import {formatText, autolinkAtMentions, highlightSearchTerms, handleUnicodeEmoji, parseSearchTerms} from 'utils/text_formatting';
import {
formatText,
autolinkAtMentions,
highlightSearchTerms,
handleUnicodeEmoji,
parseSearchTerms, autolinkChannelMentions, ChannelNamesMap,
} from 'utils/text_formatting';
import LinkOnlyRenderer from 'utils/markdown/link_only_renderer';
const emptyEmojiMap = new EmojiMap(new Map());
@@ -176,6 +182,26 @@ describe('highlightSearchTerms', () => {
});
});
describe('autolink channel mentions', () => {
test('link a channel mention', () => {
const mention = '~test-channel';
const leadingText = 'pre blah blah ';
const trailingText = ' post blah blah';
const text = `${leadingText}${mention}${trailingText}`;
const tokens = new Map();
const channelNamesMap: ChannelNamesMap = {
'test-channel': {
team_name: 'ad-1',
display_name: 'Test Channel',
},
};
const output = autolinkChannelMentions(text, tokens, channelNamesMap);
expect(output).toBe(`${leadingText}$MM_CHANNELMENTION0$${trailingText}`);
expect(tokens.get('$MM_CHANNELMENTION0$').value).toBe('<a class="mention-link" href="/ad-1/channels/test-channel" data-channel-mention-team="ad-1" data-channel-mention="test-channel">~Test Channel</a>');
});
});
describe('handleUnicodeEmoji', () => {
const emojiMap = getEmojiMap(store.getState());
const UNICODE_EMOJI_REGEX = emojiRegex();

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

@@ -481,7 +481,7 @@ export function allAtMentions(text: string): RegExpMatchArray {
return text.match(Constants.SPECIAL_MENTIONS_REGEX && AT_MENTION_PATTERN) || [];
}
function autolinkChannelMentions(
export function autolinkChannelMentions(
text: string,
tokens: Tokens,
channelNamesMap: ChannelNamesMap,
@@ -498,7 +498,7 @@ function autolinkChannelMentions(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
href = ((window as any).basename || '') + '/' + teamName + '/channels/' + channelName;
tokens.set(alias, {
value: `<a class="mention-link" href="${href}" data-channel-mention-team="${teamName}" "data-channel-mention="${channelName}">~${displayName}</a>`,
value: `<a class="mention-link" href="${href}" data-channel-mention-team="${teamName}" data-channel-mention="${channelName}">~${displayName}</a>`,
originalText: mention,
});
} else if (team) {

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

@@ -1366,11 +1366,13 @@ export function getPasswordConfig(config: Partial<ClientConfig>) {
export function isValidPassword(password: string, passwordConfig: ReturnType<typeof getPasswordConfig>, intl?: IntlShape) {
let errorId = t('user.settings.security.passwordError');
const telemetryErrorIds = [];
let valid = true;
const minimumLength = passwordConfig.minimumLength || Constants.MIN_PASSWORD_LENGTH;
if (password.length < minimumLength || password.length > Constants.MAX_PASSWORD_LENGTH) {
valid = false;
telemetryErrorIds.push({field: 'password', rule: 'error_length'});
}
if (passwordConfig.requireLowercase) {
@@ -1379,6 +1381,7 @@ export function isValidPassword(password: string, passwordConfig: ReturnType<typ
}
errorId += 'Lowercase';
telemetryErrorIds.push({field: 'password', rule: 'lowercase'});
}
if (passwordConfig.requireUppercase) {
@@ -1387,6 +1390,7 @@ export function isValidPassword(password: string, passwordConfig: ReturnType<typ
}
errorId += 'Uppercase';
telemetryErrorIds.push({field: 'password', rule: 'uppercase'});
}
if (passwordConfig.requireNumber) {
@@ -1395,6 +1399,7 @@ export function isValidPassword(password: string, passwordConfig: ReturnType<typ
}
errorId += 'Number';
telemetryErrorIds.push({field: 'password', rule: 'number'});
}
if (passwordConfig.requireSymbol) {
@@ -1403,6 +1408,7 @@ export function isValidPassword(password: string, passwordConfig: ReturnType<typ
}
errorId += 'Symbol';
telemetryErrorIds.push({field: 'password', rule: 'symbol'});
}
let error;
@@ -1430,7 +1436,7 @@ export function isValidPassword(password: string, passwordConfig: ReturnType<typ
);
}
return {valid, error};
return {valid, error, telemetryErrorIds};
}
function isChannelOrPermalink(link: string) {
@@ -1828,9 +1834,14 @@ export function getRoleForTrackFlow() {
return {started_by_role: startedByRole};
}
export function getRoleFromTrackFlow() {
export function getSbr() {
const params = new URLSearchParams(window.location.search);
const sbr = params.get('sbr') ?? '';
return sbr;
}
export function getRoleFromTrackFlow() {
const sbr = getSbr();
const startedByRole = TrackFlowRoles[sbr] ?? '';
return {started_by_role: startedByRole};

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

@@ -171,9 +171,6 @@ var config = {
new webpack.ProvidePlugin({
process: 'process/browser',
}),
new webpack.DefinePlugin({
COMMIT_HASH: JSON.stringify(childProcess.execSync('git rev-parse HEAD || echo dev').toString()),
}),
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
chunkFilename: '[name].[contenthash].css',
@@ -220,6 +217,7 @@ var config = {
{from: 'src/images/cloud-laptop-error.png', to: 'images'},
{from: 'src/images/cloud-laptop-warning.png', to: 'images'},
{from: 'src/images/cloud-upgrade-person-hand-to-face.png', to: 'images'},
{from: '../node_modules/pdfjs-dist/cmaps', to: 'cmaps'},
],
}),