[CLD-9186] Add framework for new Cloud Preview Modal (#31235)
* Remove pricing modal. Adjust everywhere to instead open mattermost.com/pricing. When air gapped, don't show buttons to view plans. * Fix lint * Further clean up of unused code. Fixes for linter * Remove onboarding tasklist for previews, add Cloud previer banner * Fixes for linter, i18n * Revert dev lines * Fix lint * When below one minute, switch to seconds * fix linter * Add scaffolding for new Cloud Preview Modal * Style updates * Fix tests * fixes for PR feedback * useExternalLink for opening pricing modal with enriched params * Fix i17n * fix style * Fix style, tests * Fix linter, types * Add file * Make types even more fixed * fix: correct test case for SKU label not provided scenario The test "should not render SKU label when not provided" was incorrectly using baseContent which includes a SKU label. Fixed by creating contentWithoutSku that explicitly sets skuLabel to undefined to properly test the scenario where no SKU label is provided. Co-authored-by: Nick Misasi <nickmisasi@users.noreply.github.com> * Fine I'll do it myself * fix linter * Refactors * Adjustments from PR review. Adjustments to video experience (poster/play button) and starting to translate * Fix i18n * Wrap translation strings with defineMessage for i18n extraction - Add import for defineMessage and MessageDescriptor from react-intl - Update type definition to use MessageDescriptor for better type safety - Wrap all skuLabel, title, and subtitle objects with defineMessage() calls - This ensures the i18n-extract tool can properly detect translation strings Co-authored-by: Nick Misasi <nickmisasi@users.noreply.github.com> * Fix i18n * Use regular modal close button * Fix pipelines * Fix i18n * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_content.scss Co-authored-by: Matthew Birtch <mattbirtch@gmail.com> * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_content.scss Co-authored-by: Matthew Birtch <mattbirtch@gmail.com> * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_content.scss Co-authored-by: Matthew Birtch <mattbirtch@gmail.com> * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_content.tsx Co-authored-by: Matthew Birtch <mattbirtch@gmail.com> * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_content.scss Co-authored-by: Matthew Birtch <mattbirtch@gmail.com> * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_content.scss Co-authored-by: Matthew Birtch <mattbirtch@gmail.com> * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_controller.tsx Co-authored-by: Matthew Birtch <mattbirtch@gmail.com> * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_controller.tsx Co-authored-by: Matthew Birtch <mattbirtch@gmail.com> * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_controller.scss Co-authored-by: Matthew Birtch <mattbirtch@gmail.com> * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_controller.tsx Co-authored-by: Matthew Birtch <mattbirtch@gmail.com> * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_controller.tsx Co-authored-by: Matthew Birtch <mattbirtch@gmail.com> * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_controller.tsx Co-authored-by: Matthew Birtch <mattbirtch@gmail.com> * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_controller.tsx Co-authored-by: Matthew Birtch <mattbirtch@gmail.com> * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_controller.scss Co-authored-by: Matthew Birtch <mattbirtch@gmail.com> * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_controller.scss Co-authored-by: Matthew Birtch <mattbirtch@gmail.com> * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_content.scss Co-authored-by: Matthew Birtch <mattbirtch@gmail.com> * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_content.scss Co-authored-by: Matthew Birtch <mattbirtch@gmail.com> * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_content.scss Co-authored-by: Matthew Birtch <mattbirtch@gmail.com> * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_content.scss Co-authored-by: Matthew Birtch <mattbirtch@gmail.com> * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_content.scss Co-authored-by: Matthew Birtch <mattbirtch@gmail.com> * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_content.scss Co-authored-by: Matthew Birtch <mattbirtch@gmail.com> * Remove unnecessary CSS properties from preview modal content Remove display: flex, height: 100%, and flex-direction: column from .preview-modal-content selector as they have no effect per code review feedback. Co-authored-by: Nick Misasi <nickmisasi@users.noreply.github.com> * Fixes for PR review * Fix linter * Fix i18n * fix linter * Changes to address Harrison's feedback * change file name, remove index.tsx * Add the new files --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Nick Misasi <nickmisasi@users.noreply.github.com> Co-authored-by: Mattermost Build <build@mattermost.com> Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>
Этот коммит содержится в:
@@ -0,0 +1,183 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {fireEvent, screen, waitFor} from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import * as reactRedux from 'react-redux';
|
||||
|
||||
import type {Subscription} from '@mattermost/types/cloud';
|
||||
|
||||
import {renderWithContext} from 'tests/react_testing_utils';
|
||||
|
||||
import CloudPreviewModal from './cloud_preview_modal_controller';
|
||||
|
||||
// Mock the async_load module to return components synchronously
|
||||
jest.mock('components/async_load', () => ({
|
||||
makeAsyncComponent: (displayName: string) => {
|
||||
// Return a component that renders immediately without suspense
|
||||
const Component = (props: any) => {
|
||||
// Mock the preview modal controller
|
||||
if (displayName === 'PreviewModalController') {
|
||||
return props.show ? (
|
||||
<div data-testid='preview-modal-controller'>
|
||||
<button onClick={props.onClose}>{'Close'}</button>
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
Component.displayName = displayName;
|
||||
return Component;
|
||||
},
|
||||
}));
|
||||
|
||||
describe('CloudPreviewModal', () => {
|
||||
const useDispatchMock = jest.spyOn(reactRedux, 'useDispatch');
|
||||
|
||||
beforeEach(() => {
|
||||
useDispatchMock.mockClear();
|
||||
});
|
||||
|
||||
const baseSubscription: Subscription = {
|
||||
id: 'test-id',
|
||||
customer_id: 'test-customer',
|
||||
product_id: 'test-product',
|
||||
add_ons: [],
|
||||
start_at: Date.now() - (24 * 60 * 60 * 1000),
|
||||
end_at: Date.now() + (2 * 60 * 60 * 1000),
|
||||
create_at: Date.now() - (24 * 60 * 60 * 1000),
|
||||
seats: 10,
|
||||
trial_end_at: 0,
|
||||
is_free_trial: 'false',
|
||||
is_cloud_preview: true,
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
entities: {
|
||||
general: {
|
||||
license: {
|
||||
IsLicensed: 'true',
|
||||
Cloud: 'true',
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: 'current_user_id',
|
||||
profiles: {
|
||||
current_user_id: {roles: 'system_admin'},
|
||||
},
|
||||
},
|
||||
cloud: {
|
||||
subscription: baseSubscription,
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('should show modal when in cloud preview and modal has not been shown before', async () => {
|
||||
const dummyDispatch = jest.fn();
|
||||
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||
|
||||
renderWithContext(
|
||||
<CloudPreviewModal/>,
|
||||
initialState,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('preview-modal-controller')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not show modal when not in cloud preview', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud.subscription = {
|
||||
...baseSubscription,
|
||||
is_cloud_preview: false,
|
||||
};
|
||||
|
||||
const dummyDispatch = jest.fn();
|
||||
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||
|
||||
renderWithContext(
|
||||
<CloudPreviewModal/>,
|
||||
state,
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('preview-modal-controller')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not show modal when not cloud', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.general.license.Cloud = 'false';
|
||||
|
||||
const dummyDispatch = jest.fn();
|
||||
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||
|
||||
renderWithContext(
|
||||
<CloudPreviewModal/>,
|
||||
state,
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('preview-modal-controller')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Skip for now.
|
||||
// it('should not show modal when modal has been shown before', () => {
|
||||
// const state = JSON.parse(JSON.stringify(initialState));
|
||||
// state.entities.preferences.myPreferences = {
|
||||
// 'cloud_preview_modal_shown--cloud_preview_modal_shown': {
|
||||
// category: 'cloud_preview_modal_shown',
|
||||
// name: 'cloud_preview_modal_shown',
|
||||
// value: 'true',
|
||||
// },
|
||||
// };
|
||||
|
||||
// const dummyDispatch = jest.fn();
|
||||
// useDispatchMock.mockReturnValue(dummyDispatch);
|
||||
|
||||
// renderWithContext(
|
||||
// <CloudPreviewModal/>,
|
||||
// state,
|
||||
// );
|
||||
|
||||
// expect(screen.queryByTestId('preview-modal-controller')).not.toBeInTheDocument();
|
||||
// });
|
||||
|
||||
it('should save preference when modal is closed', async () => {
|
||||
const dummyDispatch = jest.fn();
|
||||
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||
|
||||
renderWithContext(
|
||||
<CloudPreviewModal/>,
|
||||
initialState,
|
||||
);
|
||||
|
||||
// Wait for modal to appear
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('preview-modal-controller')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Click close button
|
||||
const closeButton = screen.getByText('Close');
|
||||
fireEvent.click(closeButton);
|
||||
|
||||
// Check that dispatch was called (savePreferences action)
|
||||
expect(dummyDispatch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not render anything when subscription is undefined', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud.subscription = undefined;
|
||||
|
||||
const dummyDispatch = jest.fn();
|
||||
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||
|
||||
renderWithContext(
|
||||
<CloudPreviewModal/>,
|
||||
state,
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('preview-modal-controller')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useState, useEffect, lazy} from 'react';
|
||||
import {useSelector, useDispatch} from 'react-redux';
|
||||
|
||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {getCloudSubscription} from 'mattermost-redux/selectors/entities/cloud';
|
||||
import {getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {makeAsyncComponent} from 'components/async_load';
|
||||
|
||||
const CLOUD_PREVIEW_MODAL_SHOWN_PREF = 'cloud_preview_modal_shown';
|
||||
|
||||
// Lazy load the controller component and content data together
|
||||
const PreviewModalController = makeAsyncComponent(
|
||||
'PreviewModalController',
|
||||
lazy(() => import('./preview_modal_content_controller')),
|
||||
);
|
||||
|
||||
const CloudPreviewModal: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const subscription = useSelector(getCloudSubscription);
|
||||
const license = useSelector(getLicense);
|
||||
const currentUserId = useSelector(getCurrentUserId);
|
||||
|
||||
const isCloud = license?.Cloud === 'true';
|
||||
const isCloudPreview = subscription?.is_cloud_preview === true;
|
||||
|
||||
// Check if modal has been shown before
|
||||
const hasModalBeenShown = false;
|
||||
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Show modal only if:
|
||||
// 1. It's a cloud preview environment
|
||||
// 2. Modal hasn't been shown before
|
||||
// 3. We have the necessary data loaded
|
||||
if (isCloud && isCloudPreview && !hasModalBeenShown && currentUserId) {
|
||||
setShowModal(true);
|
||||
} else if (hasModalBeenShown) {
|
||||
setShowModal(false);
|
||||
}
|
||||
}, [isCloud, isCloudPreview, hasModalBeenShown, currentUserId]);
|
||||
|
||||
const handleClose = () => {
|
||||
setShowModal(false);
|
||||
|
||||
// Save preference to not show modal again
|
||||
if (currentUserId) {
|
||||
const preference = {
|
||||
user_id: currentUserId,
|
||||
category: CLOUD_PREVIEW_MODAL_SHOWN_PREF,
|
||||
name: CLOUD_PREVIEW_MODAL_SHOWN_PREF,
|
||||
value: 'true',
|
||||
};
|
||||
dispatch(savePreferences(currentUserId, [preference]));
|
||||
}
|
||||
};
|
||||
|
||||
// Early return if not cloud or not cloud preview - don't load anything else
|
||||
if (!isCloud || !isCloudPreview) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only render the controller if we pass the license checks
|
||||
return (
|
||||
<PreviewModalController
|
||||
show={showModal}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CloudPreviewModal;
|
||||
@@ -0,0 +1,137 @@
|
||||
.preview-modal-content {
|
||||
|
||||
&__sku-label {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
align-items: center;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 12px;
|
||||
background: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
gap: 4px;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
|
||||
&-logo {
|
||||
width: 12px;
|
||||
height: 16px;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
fill: rgb(var(--center-channel-color-rgb));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
min-height: 60px;
|
||||
max-height: 80px;
|
||||
margin-bottom: 24px;
|
||||
overflow-y: auto;
|
||||
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__media-container {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 306px;
|
||||
flex: 0 0 306px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: solid 1px rgba(var(--center-channel-color-rgb), 0.08);
|
||||
border-radius: 4px;
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 300px;
|
||||
align-self: stretch;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(var(--center-channel-color-rgb), 0.08);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 16px rgba(var(--center-channel-color-rgb), 0.12);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-video-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
|
||||
video {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-play-button {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
transform: translate(-50%, -50%);
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translate(-50%, -50%) scale(1.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translate(-50%, -50%) scale(0.95);
|
||||
}
|
||||
|
||||
svg {
|
||||
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
|
||||
circle {
|
||||
transition: fill 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover circle {
|
||||
fill: rgba(255, 255, 255, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {screen, fireEvent} from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import {renderWithContext} from 'tests/react_testing_utils';
|
||||
|
||||
import PreviewModalContent from './preview_modal_content';
|
||||
import type {PreviewModalContentData} from './preview_modal_content_data';
|
||||
|
||||
describe('PreviewModalContent', () => {
|
||||
const baseContent: PreviewModalContentData = {
|
||||
title: {
|
||||
id: 'test.title',
|
||||
defaultMessage: 'Test Title',
|
||||
},
|
||||
subtitle: {
|
||||
id: 'test.subtitle',
|
||||
defaultMessage: 'Test subtitle with bold text',
|
||||
},
|
||||
skuLabel: {
|
||||
id: 'test.sku_label',
|
||||
defaultMessage: 'Test sku label',
|
||||
},
|
||||
videoUrl: 'https://example.com/test-video.mp4',
|
||||
useCase: 'missionops',
|
||||
};
|
||||
|
||||
const renderComponent = (content: PreviewModalContentData) => {
|
||||
return renderWithContext(
|
||||
<PreviewModalContent content={content}/>,
|
||||
);
|
||||
};
|
||||
|
||||
it('should render title and subtitle', () => {
|
||||
renderComponent(baseContent);
|
||||
|
||||
expect(screen.getByText('Test Title')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test subtitle with bold text')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render SKU label with logo when provided', () => {
|
||||
const content = {
|
||||
...baseContent,
|
||||
skuLabel: {
|
||||
id: 'test.enterprise',
|
||||
defaultMessage: 'ENTERPRISE',
|
||||
},
|
||||
};
|
||||
|
||||
renderComponent(content);
|
||||
|
||||
expect(screen.getByText('ENTERPRISE')).toBeInTheDocument();
|
||||
const skuLabelContainer = screen.getByText('ENTERPRISE').closest('.preview-modal-content__sku-label');
|
||||
expect(skuLabelContainer).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Mattermost Logo')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render SKU label when not provided', () => {
|
||||
const contentWithoutSku = {
|
||||
...baseContent,
|
||||
skuLabel: {
|
||||
id: 'test.empty',
|
||||
defaultMessage: '',
|
||||
},
|
||||
};
|
||||
|
||||
renderComponent(contentWithoutSku);
|
||||
|
||||
expect(screen.queryByText('ENTERPRISE')).not.toBeInTheDocument();
|
||||
const skuContainer = document.querySelector('.preview-modal-content__sku-label');
|
||||
expect(skuContainer).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render video when videoUrl is provided with .mp4 extension', () => {
|
||||
const content = {
|
||||
...baseContent,
|
||||
videoUrl: 'https://example.com/video.mp4',
|
||||
};
|
||||
|
||||
renderComponent(content);
|
||||
|
||||
const video = screen.getByTestId('video-element') as HTMLVideoElement;
|
||||
expect(video).toBeInTheDocument();
|
||||
expect(video.src).toBe('https://example.com/video.mp4');
|
||||
|
||||
// Just check that it's a video element
|
||||
expect(video.tagName).toBe('VIDEO');
|
||||
});
|
||||
|
||||
it('should render video with poster when videoPoster is provided', () => {
|
||||
const content = {
|
||||
...baseContent,
|
||||
videoUrl: 'https://example.com/video.mp4',
|
||||
videoPoster: 'https://example.com/poster.jpg',
|
||||
};
|
||||
|
||||
renderComponent(content);
|
||||
|
||||
const video = screen.getByTestId('video-element') as HTMLVideoElement;
|
||||
expect(video).toBeInTheDocument();
|
||||
expect(video.poster).toBe('https://example.com/poster.jpg');
|
||||
});
|
||||
|
||||
it('should render video without poster when videoPoster is not provided', () => {
|
||||
const content = {
|
||||
...baseContent,
|
||||
videoUrl: 'https://example.com/video.mp4',
|
||||
};
|
||||
|
||||
renderComponent(content);
|
||||
|
||||
const video = screen.getByTestId('video-element') as HTMLVideoElement;
|
||||
expect(video).toBeInTheDocument();
|
||||
expect(video.poster).toBe('');
|
||||
});
|
||||
|
||||
it('should show play button initially for video content', () => {
|
||||
const content = {
|
||||
...baseContent,
|
||||
videoUrl: 'https://example.com/video.mp4',
|
||||
};
|
||||
|
||||
renderComponent(content);
|
||||
|
||||
const playButton = screen.getByLabelText('Play video');
|
||||
expect(playButton).toBeInTheDocument();
|
||||
expect(playButton).toHaveClass('custom-play-button');
|
||||
});
|
||||
|
||||
it('should hide play button and show controls when play button is clicked', () => {
|
||||
const content = {
|
||||
...baseContent,
|
||||
videoUrl: 'https://example.com/video.mp4',
|
||||
};
|
||||
|
||||
// Mock video play method
|
||||
const mockPlay = jest.fn();
|
||||
HTMLVideoElement.prototype.play = mockPlay;
|
||||
|
||||
renderComponent(content);
|
||||
|
||||
const playButton = screen.getByLabelText('Play video');
|
||||
fireEvent.click(playButton);
|
||||
|
||||
expect(mockPlay).toHaveBeenCalled();
|
||||
expect(screen.queryByLabelText('Play video')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render video when videoUrl is provided with .webm extension', () => {
|
||||
const content = {
|
||||
...baseContent,
|
||||
videoUrl: 'test-video.webm',
|
||||
};
|
||||
|
||||
renderComponent(content);
|
||||
|
||||
expect(screen.getByTestId('video-element')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('video-element')).toHaveAttribute('src', 'test-video.webm');
|
||||
});
|
||||
|
||||
it('should render image when videoUrl is provided with image extension', () => {
|
||||
const content = {
|
||||
...baseContent,
|
||||
videoUrl: 'https://example.com/image.png',
|
||||
};
|
||||
|
||||
renderComponent(content);
|
||||
|
||||
const images = screen.getAllByRole('img');
|
||||
const contentImage = images.find((img) => (img as HTMLImageElement).src === 'https://example.com/image.png');
|
||||
expect(contentImage).toBeDefined();
|
||||
expect((contentImage as HTMLImageElement).alt).toBe('Test Title');
|
||||
});
|
||||
|
||||
it('should not render video container when videoUrl is not provided', () => {
|
||||
renderComponent(baseContent);
|
||||
|
||||
expect(screen.queryByRole('video')).not.toBeInTheDocument();
|
||||
|
||||
// Check that there's no content image (the logo will still be there)
|
||||
const images = screen.getAllByRole('img');
|
||||
const contentImages = images.filter((img) => {
|
||||
const ariaLabel = img.getAttribute('aria-label');
|
||||
return !ariaLabel || !ariaLabel.includes('Mattermost Logo');
|
||||
});
|
||||
expect(contentImages).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should render video when videoUrl is provided with .mov extension', () => {
|
||||
const content = {
|
||||
...baseContent,
|
||||
videoUrl: 'test-video.mov',
|
||||
};
|
||||
|
||||
renderComponent(content);
|
||||
|
||||
expect(screen.getByTestId('video-element')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('video-element')).toHaveAttribute('src', 'test-video.mov');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import MattermostLogo from 'components/widgets/icons/mattermost_logo';
|
||||
|
||||
import type {PreviewModalContentData} from './preview_modal_content_data';
|
||||
|
||||
import './preview_modal_content.scss';
|
||||
|
||||
interface Props {
|
||||
content: PreviewModalContentData;
|
||||
}
|
||||
|
||||
const PreviewModalContent: React.FC<Props> = ({content}) => {
|
||||
const intl = useIntl();
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [showControls, setShowControls] = useState(false);
|
||||
|
||||
const handlePlay = () => {
|
||||
setIsPlaying(true);
|
||||
videoRef.current?.play();
|
||||
};
|
||||
|
||||
const handleVideoClick = () => {
|
||||
if (!isPlaying) {
|
||||
handlePlay();
|
||||
}
|
||||
};
|
||||
|
||||
const handleVideoPause = () => {
|
||||
setIsPlaying(false);
|
||||
};
|
||||
|
||||
const renderVideoContent = () => {
|
||||
if (!content.videoUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle direct video files
|
||||
if (content.videoUrl.endsWith('.mp4') || content.videoUrl.endsWith('.webm') || content.videoUrl.endsWith('.mov')) {
|
||||
return (
|
||||
<div
|
||||
className='custom-video-wrapper'
|
||||
onMouseEnter={() => isPlaying && setShowControls(true)}
|
||||
onMouseLeave={() => setShowControls(false)}
|
||||
>
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={content.videoUrl}
|
||||
poster={content.videoPoster}
|
||||
controls={isPlaying && showControls}
|
||||
loop={true}
|
||||
data-testid='video-element'
|
||||
onClick={handleVideoClick}
|
||||
onPause={handleVideoPause}
|
||||
onPlay={() => setIsPlaying(true)}
|
||||
>
|
||||
<track kind='captions'/>
|
||||
</video>
|
||||
{!isPlaying && (
|
||||
<button
|
||||
className='custom-play-button'
|
||||
onClick={handlePlay}
|
||||
aria-label={intl.formatMessage({
|
||||
id: 'cloud_preview_modal.play_video',
|
||||
defaultMessage: 'Play video',
|
||||
})}
|
||||
>
|
||||
<svg
|
||||
width='48'
|
||||
height='48'
|
||||
viewBox='0 0 48 48'
|
||||
fill='none'
|
||||
>
|
||||
<circle
|
||||
cx='24'
|
||||
cy='24'
|
||||
r='24'
|
||||
fill='rgba(255,255,255,0.9)'
|
||||
/>
|
||||
<polygon
|
||||
points='20,16 34,24 20,32'
|
||||
fill='#1e3a8a'
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle images
|
||||
if (content.videoUrl.endsWith('.jpg') || content.videoUrl.endsWith('.jpeg') || content.videoUrl.endsWith('.png') || content.videoUrl.endsWith('.gif') || content.videoUrl.endsWith('.webp')) {
|
||||
return (
|
||||
<img
|
||||
src={content.videoUrl}
|
||||
alt={intl.formatMessage(content.title)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback for other URLs - treat as image
|
||||
return (
|
||||
<img
|
||||
src={content.videoUrl}
|
||||
alt={intl.formatMessage(content.title)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='preview-modal-content'>
|
||||
{content.skuLabel && content.skuLabel.defaultMessage && (
|
||||
<div className='preview-modal-content__sku-label'>
|
||||
<MattermostLogo className='preview-modal-content__sku-label-logo'/>
|
||||
<span>{intl.formatMessage(content.skuLabel)}</span>
|
||||
</div>
|
||||
)}
|
||||
<h2 className='preview-modal-content__title'>{intl.formatMessage(content.title)}</h2>
|
||||
<div className='preview-modal-content__subtitle'>
|
||||
{intl.formatMessage(content.subtitle)}
|
||||
</div>
|
||||
{content.videoUrl && (
|
||||
<div className='preview-modal-content__media-container'>
|
||||
{renderVideoContent()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PreviewModalContent;
|
||||
@@ -0,0 +1,228 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {screen, fireEvent} from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import {renderWithContext} from 'tests/react_testing_utils';
|
||||
|
||||
import PreviewModalController from './preview_modal_content_controller';
|
||||
import type {PreviewModalContentData} from './preview_modal_content_data';
|
||||
|
||||
describe('PreviewModalController', () => {
|
||||
const mockOnClose = jest.fn();
|
||||
|
||||
const contentData: PreviewModalContentData[] = [
|
||||
{
|
||||
title: {
|
||||
id: 'test.slide1.title',
|
||||
defaultMessage: 'First Slide',
|
||||
},
|
||||
subtitle: {
|
||||
id: 'test.slide1.subtitle',
|
||||
defaultMessage: 'First subtitle',
|
||||
},
|
||||
skuLabel: {
|
||||
id: 'test.slide1.sku_label',
|
||||
defaultMessage: 'First sku label',
|
||||
},
|
||||
videoUrl: 'https://example.com/test-video-1.mp4',
|
||||
useCase: 'missionops',
|
||||
},
|
||||
{
|
||||
title: {
|
||||
id: 'test.slide2.title',
|
||||
defaultMessage: 'Second Slide',
|
||||
},
|
||||
subtitle: {
|
||||
id: 'test.slide2.subtitle',
|
||||
defaultMessage: 'Second subtitle',
|
||||
},
|
||||
skuLabel: {
|
||||
id: 'test.slide2.sku_label',
|
||||
defaultMessage: 'Second sku label',
|
||||
},
|
||||
videoUrl: 'https://example.com/test-video-2.mp4',
|
||||
useCase: 'missionops',
|
||||
},
|
||||
{
|
||||
title: {
|
||||
id: 'test.slide3.title',
|
||||
defaultMessage: 'Third Slide',
|
||||
},
|
||||
subtitle: {
|
||||
id: 'test.slide3.subtitle',
|
||||
defaultMessage: 'Third subtitle',
|
||||
},
|
||||
skuLabel: {
|
||||
id: 'test.slide3.sku_label',
|
||||
defaultMessage: 'Third sku label',
|
||||
},
|
||||
videoUrl: 'https://example.com/test-video-3.mp4',
|
||||
useCase: 'missionops',
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
mockOnClose.mockClear();
|
||||
});
|
||||
|
||||
const renderComponent = (props = {}) => {
|
||||
return renderWithContext(
|
||||
<PreviewModalController
|
||||
show={true}
|
||||
onClose={mockOnClose}
|
||||
contentData={contentData}
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
};
|
||||
|
||||
it('should render when show is true', () => {
|
||||
renderComponent();
|
||||
|
||||
// Look for the modal content instead of a mocked test id
|
||||
expect(screen.getByText('First Slide')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render when show is false', () => {
|
||||
renderComponent({show: false});
|
||||
expect(screen.queryByText('First Slide')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should use default content when contentData is not provided', () => {
|
||||
renderWithContext(
|
||||
<PreviewModalController
|
||||
show={true}
|
||||
onClose={mockOnClose}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Should still render the modal with default content (first item from modalContent)
|
||||
expect(screen.getByText('Welcome to your Mattermost preview')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render first slide initially', () => {
|
||||
renderComponent();
|
||||
expect(screen.getByText('First Slide')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Second Slide')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render pagination dots', () => {
|
||||
renderComponent();
|
||||
|
||||
// Find dots by their container and check children
|
||||
const dotsContainer = screen.getByTestId('pagination-dots');
|
||||
expect(dotsContainer.children).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should render page counter', () => {
|
||||
renderComponent();
|
||||
expect(screen.getByText('1/3')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should update page counter when navigating', () => {
|
||||
renderComponent();
|
||||
|
||||
expect(screen.getByText('1/3')).toBeInTheDocument();
|
||||
|
||||
const nextButton = screen.getByText('Next');
|
||||
fireEvent.click(nextButton);
|
||||
|
||||
expect(screen.getByText('2/3')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should navigate to next slide when Next is clicked', () => {
|
||||
renderComponent();
|
||||
|
||||
const nextButton = screen.getByText('Next');
|
||||
fireEvent.click(nextButton);
|
||||
|
||||
expect(screen.getByText('Second Slide')).toBeInTheDocument();
|
||||
expect(screen.queryByText('First Slide')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should navigate to previous slide when Previous is clicked', () => {
|
||||
renderComponent();
|
||||
|
||||
// Go to second slide first
|
||||
const nextButton = screen.getByText('Next');
|
||||
fireEvent.click(nextButton);
|
||||
|
||||
// Now go back
|
||||
const previousButton = screen.getByText('Previous');
|
||||
fireEvent.click(previousButton);
|
||||
|
||||
expect(screen.getByText('First Slide')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Second Slide')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show Skip for now button only on first slide', () => {
|
||||
renderComponent();
|
||||
|
||||
// First slide should have Skip button
|
||||
expect(screen.getByText('Skip for now')).toBeInTheDocument();
|
||||
|
||||
// Go to second slide
|
||||
const nextButton = screen.getByText('Next');
|
||||
fireEvent.click(nextButton);
|
||||
|
||||
// Second slide should not have Skip button
|
||||
expect(screen.queryByText('Skip for now')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not show Previous button on first slide', () => {
|
||||
renderComponent();
|
||||
expect(screen.queryByText('Previous')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show Done button on last slide', () => {
|
||||
renderComponent();
|
||||
|
||||
// Navigate to last slide
|
||||
const nextButton = screen.getByText('Next');
|
||||
fireEvent.click(nextButton);
|
||||
fireEvent.click(nextButton);
|
||||
|
||||
expect(screen.getByText('Finish')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Next')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onClose when Skip is clicked', () => {
|
||||
renderComponent();
|
||||
|
||||
const skipButton = screen.getByText('Skip for now');
|
||||
fireEvent.click(skipButton);
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call onClose when Finish is clicked on last slide', () => {
|
||||
renderComponent();
|
||||
|
||||
// Navigate to last slide
|
||||
const nextButton = screen.getByText('Next');
|
||||
fireEvent.click(nextButton);
|
||||
fireEvent.click(nextButton);
|
||||
|
||||
const finishButton = screen.getByText('Finish');
|
||||
fireEvent.click(finishButton);
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call onClose when close button is clicked', () => {
|
||||
renderComponent();
|
||||
|
||||
// Look for the actual close button from GenericModal
|
||||
const closeButton = screen.getByLabelText('Close');
|
||||
fireEvent.click(closeButton);
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not render if contentData is empty', () => {
|
||||
renderComponent({contentData: []});
|
||||
expect(screen.queryByText('First Slide')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useState, useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import {ArrowLeftIcon, ArrowRightIcon} from '@mattermost/compass-icons/components';
|
||||
import {GenericModal} from '@mattermost/components';
|
||||
|
||||
import PreviewModalContent from './preview_modal_content';
|
||||
import {modalContent} from './preview_modal_content_data';
|
||||
import type {PreviewModalContentData} from './preview_modal_content_data';
|
||||
|
||||
import './preview_modal_controller.scss';
|
||||
|
||||
interface Props {
|
||||
show: boolean;
|
||||
onClose: () => void;
|
||||
contentData?: PreviewModalContentData[];
|
||||
}
|
||||
|
||||
const PreviewModalController: React.FC<Props> = ({show, onClose, contentData}) => {
|
||||
const intl = useIntl();
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
|
||||
// Use provided contentData or default to filtered modalContent
|
||||
// TODO: Remove hard coded filter on missionops in favour of dynamic based on selected use case
|
||||
const activeContentData = contentData || modalContent.filter((content) => content.useCase === 'missionops');
|
||||
|
||||
const handlePrevious = useCallback(() => {
|
||||
setCurrentIndex((prev) => Math.max(0, prev - 1));
|
||||
}, []);
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
setCurrentIndex((prev) => Math.min(activeContentData.length - 1, prev + 1));
|
||||
}, [activeContentData.length]);
|
||||
|
||||
const handleSkip = useCallback(() => {
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
const isFirstSlide = currentIndex === 0;
|
||||
const isLastSlide = currentIndex === activeContentData.length - 1;
|
||||
|
||||
if (activeContentData.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Custom footer content with pagination and navigation buttons
|
||||
const footerContent = (
|
||||
<div className='preview-modal-controller__footer'>
|
||||
<div className='preview-modal-controller__pagination'>
|
||||
<div
|
||||
className='preview-modal-controller__pagination-dots'
|
||||
data-testid='pagination-dots'
|
||||
>
|
||||
{activeContentData.map((_, index) => (
|
||||
<div
|
||||
key={`dot-${index}`}
|
||||
className={`preview-modal-controller__dot ${index === currentIndex ? 'preview-modal-controller__dot--active' : ''}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className='preview-modal-controller__page-counter'>
|
||||
{currentIndex + 1}{'/'}{activeContentData.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='preview-modal-controller__navigation-buttons'>
|
||||
{isFirstSlide && (
|
||||
<button
|
||||
className='btn btn-quaternary'
|
||||
onClick={handleSkip}
|
||||
>
|
||||
{intl.formatMessage({
|
||||
id: 'cloud_preview_modal.skip',
|
||||
defaultMessage: 'Skip for now',
|
||||
})}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{!isFirstSlide && (
|
||||
<button
|
||||
className='btn btn-tertiary'
|
||||
onClick={handlePrevious}
|
||||
>
|
||||
<ArrowLeftIcon size={18}/>
|
||||
{intl.formatMessage({
|
||||
id: 'cloud_preview_modal.previous',
|
||||
defaultMessage: 'Previous',
|
||||
})}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
className='btn btn-primary'
|
||||
onClick={isLastSlide ? onClose : handleNext}
|
||||
>
|
||||
{isLastSlide ? (
|
||||
intl.formatMessage({
|
||||
id: 'cloud_preview_modal.done',
|
||||
defaultMessage: 'Finish',
|
||||
})
|
||||
) : (
|
||||
<>
|
||||
{intl.formatMessage({
|
||||
id: 'cloud_preview_modal.next',
|
||||
defaultMessage: 'Next',
|
||||
})}
|
||||
<ArrowRightIcon size={18}/>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<GenericModal
|
||||
show={show}
|
||||
onHide={onClose}
|
||||
compassDesign={true}
|
||||
enforceFocus={true}
|
||||
ariaLabel={intl.formatMessage({
|
||||
id: 'cloud_preview_modal.aria_label',
|
||||
defaultMessage: 'Cloud Preview Introduction',
|
||||
})}
|
||||
showHeader={true}
|
||||
showCloseButton={true}
|
||||
bodyPadding={true}
|
||||
footerContent={footerContent}
|
||||
className='preview-modal-controller'
|
||||
>
|
||||
<PreviewModalContent content={activeContentData[currentIndex]}/>
|
||||
</GenericModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default PreviewModalController;
|
||||
@@ -0,0 +1,359 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {defineMessage} from 'react-intl';
|
||||
import type {MessageDescriptor} from 'react-intl';
|
||||
|
||||
export type PreviewModalContentData = {
|
||||
skuLabel: MessageDescriptor;
|
||||
title: MessageDescriptor;
|
||||
subtitle: MessageDescriptor;
|
||||
videoUrl: string;
|
||||
videoPoster?: string;
|
||||
useCase: string;
|
||||
};
|
||||
|
||||
export const modalContent: PreviewModalContentData[] = [
|
||||
{
|
||||
skuLabel: defineMessage({
|
||||
id: 'cloud_preview_modal.missionops.sku_label',
|
||||
defaultMessage: 'ENTERPRISE ADVANCED',
|
||||
}),
|
||||
title: defineMessage({
|
||||
id: 'cloud_preview_modal.missionops.welcome.title',
|
||||
defaultMessage: 'Welcome to your Mattermost preview',
|
||||
}),
|
||||
subtitle: defineMessage({
|
||||
id: 'cloud_preview_modal.missionops.welcome.subtitle',
|
||||
defaultMessage: 'This hands-on, 1-hour preview of Mattermost Enterprise Advanced lets your team explore secure, mission-critical collaboration. The workspace is preloaded with data to show the platform in action. Watch the 4-minute video to get started.',
|
||||
}),
|
||||
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/missionops/Mattermost_TMM_Demo_Mission+Ops_20250307.mp4',
|
||||
videoPoster: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/missionops/modal-assets/Mattermost_TMM_Demo_MissionOps_Poster.jpg',
|
||||
useCase: 'missionops',
|
||||
},
|
||||
{
|
||||
skuLabel: defineMessage({
|
||||
id: 'cloud_preview_modal.missionops.messaging.sku_label',
|
||||
defaultMessage: 'ENTERPRISE',
|
||||
}),
|
||||
title: defineMessage({
|
||||
id: 'cloud_preview_modal.missionops.messaging.title',
|
||||
defaultMessage: 'Messaging built for action, not noise',
|
||||
}),
|
||||
subtitle: defineMessage({
|
||||
id: 'cloud_preview_modal.missionops.messaging.subtitle',
|
||||
defaultMessage: 'Bring conversations and context together in one secure platform. Communicate with urgency using priority levels, persistent notifications, and acknowledgments—so critical messages are seen and acted on when every second counts.',
|
||||
}),
|
||||
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/missionops/modal-assets/priority-messages.jpg',
|
||||
useCase: 'missionops',
|
||||
},
|
||||
{
|
||||
skuLabel: defineMessage({
|
||||
id: 'cloud_preview_modal.missionops.ai.sku_label',
|
||||
defaultMessage: 'ENTERPRISE',
|
||||
}),
|
||||
title: defineMessage({
|
||||
id: 'cloud_preview_modal.missionops.ai.title',
|
||||
defaultMessage: 'Bring your own AI model with Mattermost Agents',
|
||||
}),
|
||||
subtitle: defineMessage({
|
||||
id: 'cloud_preview_modal.missionops.ai.subtitle',
|
||||
defaultMessage: 'Supercharge collaboration with Agents. Instantly summarize calls, surface action items, and find answers fast—all using the model you trust.',
|
||||
}),
|
||||
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/missionops/modal-assets/ai-search.jpg',
|
||||
useCase: 'missionops',
|
||||
},
|
||||
{
|
||||
skuLabel: defineMessage({
|
||||
id: 'cloud_preview_modal.missionops.profiles.sku_label',
|
||||
defaultMessage: 'ENTERPRISE',
|
||||
}),
|
||||
title: defineMessage({
|
||||
id: 'cloud_preview_modal.missionops.profiles.title',
|
||||
defaultMessage: 'Tailor user profiles to match your team\'s structure',
|
||||
}),
|
||||
subtitle: defineMessage({
|
||||
id: 'cloud_preview_modal.missionops.profiles.subtitle',
|
||||
defaultMessage: 'Create tailored user profiles with custom attributes like role, location, or clearance level to reflect your organization\'s structure. Help teams understand who they\'re working with and how to collaborate more effectively.',
|
||||
}),
|
||||
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/missionops/modal-assets/custom-profile-attributes.jpg',
|
||||
useCase: 'missionops',
|
||||
},
|
||||
{
|
||||
skuLabel: defineMessage({
|
||||
id: 'cloud_preview_modal.missionops.playbooks.sku_label',
|
||||
defaultMessage: 'ENTERPRISE',
|
||||
}),
|
||||
title: defineMessage({
|
||||
id: 'cloud_preview_modal.missionops.playbooks.title',
|
||||
defaultMessage: 'Build smart Playbooks for advanced workflows',
|
||||
}),
|
||||
subtitle: defineMessage({
|
||||
id: 'cloud_preview_modal.missionops.playbooks.subtitle',
|
||||
defaultMessage: 'Unlock powerful workflows tailored to real-world complexity. When conditions change, define tasks for the Playbook to evolve with your dynamic processes.',
|
||||
}),
|
||||
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/missionops/modal-assets/playbook-properties.jpg',
|
||||
useCase: 'missionops',
|
||||
},
|
||||
|
||||
// {
|
||||
// skuLabel: defineMessage({
|
||||
// id: 'cloud_preview_modal.missionops.flagging.sku_label',
|
||||
// defaultMessage: 'ENTERPRISE ADVANCED',
|
||||
// }),
|
||||
// title: defineMessage({
|
||||
// id: 'cloud_preview_modal.missionops.flagging.title',
|
||||
// defaultMessage: 'Flag sensitive content before it spreads',
|
||||
// }),
|
||||
// subtitle: defineMessage({
|
||||
// id: 'cloud_preview_modal.missionops.flagging.subtitle',
|
||||
// defaultMessage: 'Prevent accidental exposure by giving anyone the power to flag risky messages. Flagged content is instantly hidden and routed to security teams for review—helping you contain data leaks in real time.',
|
||||
// }),
|
||||
// videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/missionops/modal-assets/flag-messages.jpg',
|
||||
// useCase: 'missionops',
|
||||
// },
|
||||
{
|
||||
skuLabel: defineMessage({
|
||||
id: 'cloud_preview_modal.missionops.zero_trust.sku_label',
|
||||
defaultMessage: 'ENTERPRISE ADVANCED',
|
||||
}),
|
||||
title: defineMessage({
|
||||
id: 'cloud_preview_modal.missionops.zero_trust.title',
|
||||
defaultMessage: 'Enforce Zero Trust collaboration',
|
||||
}),
|
||||
subtitle: defineMessage({
|
||||
id: 'cloud_preview_modal.missionops.zero_trust.subtitle',
|
||||
defaultMessage: 'Define granular access to content using attribute-based policies, and display classification banners and labels to guide user behavior. Limit exposure based on role, clearance level, or operational context.',
|
||||
}),
|
||||
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/missionops/modal-assets/zero-trust.jpg',
|
||||
useCase: 'missionops',
|
||||
},
|
||||
{
|
||||
skuLabel: defineMessage({
|
||||
id: 'cloud_preview_modal.devsecops.sku_label',
|
||||
defaultMessage: 'ENTERPRISE ADVANCED',
|
||||
}),
|
||||
title: defineMessage({
|
||||
id: 'cloud_preview_modal.devsecops.welcome.title',
|
||||
defaultMessage: 'Welcome to your Mattermost preview',
|
||||
}),
|
||||
subtitle: defineMessage({
|
||||
id: 'cloud_preview_modal.devsecops.welcome.subtitle',
|
||||
defaultMessage: 'This hands-on, 1-hour preview of Mattermost Enterprise Advanced lets your DevSecOps team explore secure, collaborative development. The workspace is preloaded with data to show the platform in action. Watch the 4-minute video to get started.',
|
||||
}),
|
||||
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/devsecops/Mattermost_TMM_Demo_DevSecOps_20260610.mp4',
|
||||
videoPoster: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/devsecops/modal-assets/Mattermost_TMM_Demo_DevSecOps_Poster.jpg',
|
||||
useCase: 'devsecops',
|
||||
},
|
||||
{
|
||||
skuLabel: defineMessage({
|
||||
id: 'cloud_preview_modal.devsecops.messaging.sku_label',
|
||||
defaultMessage: 'ENTERPRISE',
|
||||
}),
|
||||
title: defineMessage({
|
||||
id: 'cloud_preview_modal.devsecops.messaging.title',
|
||||
defaultMessage: 'Messaging built for action, not noise',
|
||||
}),
|
||||
subtitle: defineMessage({
|
||||
id: 'cloud_preview_modal.devsecops.messaging.subtitle',
|
||||
defaultMessage: 'Bring conversations and context together in one secure platform. Communicate with urgency using priority levels, persistent notifications, and acknowledgments—so critical messages are seen and acted on when every second counts.',
|
||||
}),
|
||||
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/devsecops/modal-assets/priority-messages.jpg',
|
||||
useCase: 'devsecops',
|
||||
},
|
||||
{
|
||||
skuLabel: defineMessage({
|
||||
id: 'cloud_preview_modal.devsecops.ai.sku_label',
|
||||
defaultMessage: 'ENTERPRISE',
|
||||
}),
|
||||
title: defineMessage({
|
||||
id: 'cloud_preview_modal.devsecops.ai.title',
|
||||
defaultMessage: 'Bring your own AI model with Mattermost Agents',
|
||||
}),
|
||||
subtitle: defineMessage({
|
||||
id: 'cloud_preview_modal.devsecops.ai.subtitle',
|
||||
defaultMessage: 'Supercharge collaboration with Agents. Instantly summarize calls, surface action items, and find answers fast—all using the model you trust.',
|
||||
}),
|
||||
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/devsecops/modal-assets/ai-search.jpg',
|
||||
useCase: 'devsecops',
|
||||
},
|
||||
{
|
||||
skuLabel: defineMessage({
|
||||
id: 'cloud_preview_modal.devsecops.profiles.sku_label',
|
||||
defaultMessage: 'ENTERPRISE',
|
||||
}),
|
||||
title: defineMessage({
|
||||
id: 'cloud_preview_modal.devsecops.profiles.title',
|
||||
defaultMessage: 'Tailor user profiles to match your team\'s structure',
|
||||
}),
|
||||
subtitle: defineMessage({
|
||||
id: 'cloud_preview_modal.devsecops.profiles.subtitle',
|
||||
defaultMessage: 'Create tailored user profiles with custom attributes like role, location, or clearance level to reflect your organization\'s structure. Help teams understand who they\'re working with and how to collaborate more effectively.',
|
||||
}),
|
||||
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/devsecops/modal-assets/custom-profile-attributes.jpg',
|
||||
useCase: 'devsecops',
|
||||
},
|
||||
{
|
||||
skuLabel: defineMessage({
|
||||
id: 'cloud_preview_modal.devsecops.playbooks.sku_label',
|
||||
defaultMessage: 'ENTERPRISE',
|
||||
}),
|
||||
title: defineMessage({
|
||||
id: 'cloud_preview_modal.devsecops.playbooks.title',
|
||||
defaultMessage: 'Build smart Playbooks for advanced workflows',
|
||||
}),
|
||||
subtitle: defineMessage({
|
||||
id: 'cloud_preview_modal.devsecops.playbooks.subtitle',
|
||||
defaultMessage: 'Unlock powerful workflows tailored to real-world complexity. When conditions change, define tasks for the Playbook to evolve with your dynamic processes.',
|
||||
}),
|
||||
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/devsecops/modal-assets/playbook-properties.jpg',
|
||||
useCase: 'devsecops',
|
||||
},
|
||||
|
||||
// {
|
||||
// skuLabel: defineMessage({
|
||||
// id: 'cloud_preview_modal.devsecops.flagging.sku_label',
|
||||
// defaultMessage: 'ENTERPRISE ADVANCED',
|
||||
// }),
|
||||
// title: defineMessage({
|
||||
// id: 'cloud_preview_modal.devsecops.flagging.title',
|
||||
// defaultMessage: 'Flag sensitive content before it spreads',
|
||||
// }),
|
||||
// subtitle: defineMessage({
|
||||
// id: 'cloud_preview_modal.devsecops.flagging.subtitle',
|
||||
// defaultMessage: 'Prevent accidental exposure by giving anyone the power to flag risky messages. Flagged content is instantly hidden and routed to security teams for review—helping you contain data leaks in real time.',
|
||||
// }),
|
||||
// videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/devsecops/modal-assets/flag-messages.jpg',
|
||||
// useCase: 'devsecops',
|
||||
// },
|
||||
{
|
||||
skuLabel: defineMessage({
|
||||
id: 'cloud_preview_modal.devsecops.zero_trust.sku_label',
|
||||
defaultMessage: 'ENTERPRISE ADVANCED',
|
||||
}),
|
||||
title: defineMessage({
|
||||
id: 'cloud_preview_modal.devsecops.zero_trust.title',
|
||||
defaultMessage: 'Enforce Zero Trust collaboration',
|
||||
}),
|
||||
subtitle: defineMessage({
|
||||
id: 'cloud_preview_modal.devsecops.zero_trust.subtitle',
|
||||
defaultMessage: 'Define granular access to content using attribute-based policies, and display classification banners and labels to guide user behavior. Limit exposure based on role, clearance level, or operational context.',
|
||||
}),
|
||||
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/devsecops/modal-assets/zero-trust.jpg',
|
||||
useCase: 'devsecops',
|
||||
},
|
||||
{
|
||||
skuLabel: defineMessage({
|
||||
id: 'cloud_preview_modal.cyberdefense.sku_label',
|
||||
defaultMessage: 'ENTERPRISE ADVANCED',
|
||||
}),
|
||||
title: defineMessage({
|
||||
id: 'cloud_preview_modal.cyberdefense.welcome.title',
|
||||
defaultMessage: 'Welcome to your Mattermost preview',
|
||||
}),
|
||||
subtitle: defineMessage({
|
||||
id: 'cloud_preview_modal.cyberdefense.welcome.subtitle',
|
||||
defaultMessage: 'This hands-on, 1-hour preview of Mattermost Enterprise Advanced lets your cyber defense team explore secure, threat-aware collaboration. The workspace is preloaded with data to show the platform in action. Watch the 4-minute video to get started.',
|
||||
}),
|
||||
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/cyberdefense/modal-assets/Mattermost_TMM_Demo_Cyber_Defense_20250417.mp4',
|
||||
videoPoster: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/cyberdefense/modal-assets/Mattermost_TMM_Demo_Cyber_Defense_Poster.jpg',
|
||||
useCase: 'cyberdefense',
|
||||
},
|
||||
{
|
||||
skuLabel: defineMessage({
|
||||
id: 'cloud_preview_modal.cyberdefense.messaging.sku_label',
|
||||
defaultMessage: 'ENTERPRISE',
|
||||
}),
|
||||
title: defineMessage({
|
||||
id: 'cloud_preview_modal.cyberdefense.messaging.title',
|
||||
defaultMessage: 'Messaging built for action, not noise',
|
||||
}),
|
||||
subtitle: defineMessage({
|
||||
id: 'cloud_preview_modal.cyberdefense.messaging.subtitle',
|
||||
defaultMessage: 'Bring conversations and context together in one secure platform. Communicate with urgency using priority levels, persistent notifications, and acknowledgments—so critical messages are seen and acted on when every second counts.',
|
||||
}),
|
||||
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/cyberdefense/modal-assets/priority-messages.jpg',
|
||||
useCase: 'cyberdefense',
|
||||
},
|
||||
{
|
||||
skuLabel: defineMessage({
|
||||
id: 'cloud_preview_modal.cyberdefense.ai.sku_label',
|
||||
defaultMessage: 'ENTERPRISE',
|
||||
}),
|
||||
title: defineMessage({
|
||||
id: 'cloud_preview_modal.cyberdefense.ai.title',
|
||||
defaultMessage: 'Bring your own AI model with Mattermost Agents',
|
||||
}),
|
||||
subtitle: defineMessage({
|
||||
id: 'cloud_preview_modal.cyberdefense.ai.subtitle',
|
||||
defaultMessage: 'Supercharge collaboration with Agents. Instantly summarize calls, surface action items, and find answers fast—all using the model you trust.',
|
||||
}),
|
||||
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/cyberdefense/modal-assets/ai-search.jpg',
|
||||
useCase: 'cyberdefense',
|
||||
},
|
||||
{
|
||||
skuLabel: defineMessage({
|
||||
id: 'cloud_preview_modal.cyberdefense.profiles.sku_label',
|
||||
defaultMessage: 'ENTERPRISE',
|
||||
}),
|
||||
title: defineMessage({
|
||||
id: 'cloud_preview_modal.cyberdefense.profiles.title',
|
||||
defaultMessage: 'Tailor user profiles to match your team\'s structure',
|
||||
}),
|
||||
subtitle: defineMessage({
|
||||
id: 'cloud_preview_modal.cyberdefense.profiles.subtitle',
|
||||
defaultMessage: 'Create tailored user profiles with custom attributes like role, location, or clearance level to reflect your organization\'s structure. Help teams understand who they\'re working with and how to collaborate more effectively.',
|
||||
}),
|
||||
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/cyberdefense/modal-assets/custom-profile-attributes.jpg',
|
||||
useCase: 'cyberdefense',
|
||||
},
|
||||
{
|
||||
skuLabel: defineMessage({
|
||||
id: 'cloud_preview_modal.cyberdefense.playbooks.sku_label',
|
||||
defaultMessage: 'ENTERPRISE',
|
||||
}),
|
||||
title: defineMessage({
|
||||
id: 'cloud_preview_modal.cyberdefense.playbooks.title',
|
||||
defaultMessage: 'Build smart Playbooks for advanced workflows',
|
||||
}),
|
||||
subtitle: defineMessage({
|
||||
id: 'cloud_preview_modal.cyberdefense.playbooks.subtitle',
|
||||
defaultMessage: 'Unlock powerful workflows tailored to real-world complexity. When conditions change, define tasks for the Playbook to evolve with your dynamic processes.',
|
||||
}),
|
||||
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/cyberdefense/modal-assets/playbook-properties.jpg',
|
||||
useCase: 'cyberdefense',
|
||||
},
|
||||
|
||||
// {
|
||||
// skuLabel: defineMessage({
|
||||
// id: 'cloud_preview_modal.cyberdefense.flagging.sku_label',
|
||||
// defaultMessage: 'ENTERPRISE ADVANCED',
|
||||
// }),
|
||||
// title: defineMessage({
|
||||
// id: 'cloud_preview_modal.cyberdefense.flagging.title',
|
||||
// defaultMessage: 'Flag sensitive content before it spreads',
|
||||
// }),
|
||||
// subtitle: defineMessage({
|
||||
// id: 'cloud_preview_modal.cyberdefense.flagging.subtitle',
|
||||
// defaultMessage: 'Prevent accidental exposure by giving anyone the power to flag risky messages. Flagged content is instantly hidden and routed to security teams for review—helping you contain data leaks in real time.',
|
||||
// }),
|
||||
// videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/cyberdefense/modal-assets/flag-messages.jpg',
|
||||
// useCase: 'cyberdefense',
|
||||
// },
|
||||
{
|
||||
skuLabel: defineMessage({
|
||||
id: 'cloud_preview_modal.cyberdefense.zero_trust.sku_label',
|
||||
defaultMessage: 'ENTERPRISE ADVANCED',
|
||||
}),
|
||||
title: defineMessage({
|
||||
id: 'cloud_preview_modal.cyberdefense.zero_trust.title',
|
||||
defaultMessage: 'Enforce Zero Trust collaboration',
|
||||
}),
|
||||
subtitle: defineMessage({
|
||||
id: 'cloud_preview_modal.cyberdefense.zero_trust.subtitle',
|
||||
defaultMessage: 'Define granular access to content using attribute-based policies, and display classification banners and labels to guide user behavior. Limit exposure based on role, clearance level, or operational context.',
|
||||
}),
|
||||
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/cyberdefense/modal-assets/zero-trust.jpg',
|
||||
useCase: 'cyberdefense',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,65 @@
|
||||
.preview-modal-controller {
|
||||
&.modal-dialog {
|
||||
&.GenericModal {
|
||||
margin-top: calc(50vh - 307px);
|
||||
}
|
||||
width: 640px;
|
||||
height: 614px;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&__pagination-dots {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: rgba(var(--center-channel-color-rgb), 0.16);
|
||||
transition: background 0.2s ease;
|
||||
|
||||
&--active {
|
||||
background: var(--button-bg);
|
||||
}
|
||||
}
|
||||
|
||||
&__page-counter {
|
||||
color: rgb(var(--center-channel-color-rgb));
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&__navigation-buttons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
||||
.modal-content {
|
||||
.modal-header {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
.GenericModal__body {
|
||||
&.padding {
|
||||
padding: 8px 48px !important;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {Route} from 'react-router-dom';
|
||||
import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {makeAsyncComponent} from 'components/async_load';
|
||||
import CloudPreviewModalController from 'components/cloud_preview_modal/cloud_preview_modal_controller';
|
||||
import CompassThemeProvider from 'components/compass_theme_provider/compass_theme_provider';
|
||||
import LoggedIn from 'components/logged_in';
|
||||
|
||||
@@ -30,6 +31,7 @@ export default function LoggedInRoute(props: Props) {
|
||||
{theme && (
|
||||
<CompassThemeProvider theme={theme}>
|
||||
<OnBoardingTaskList/>
|
||||
<CloudPreviewModalController/>
|
||||
</CompassThemeProvider>
|
||||
)}
|
||||
<Component {...(routeProps)}/>
|
||||
|
||||
@@ -3730,6 +3730,66 @@
|
||||
"cloud_archived.error.access": "Permalink belongs to a message that has been archived because of {planName} limits. Upgrade to access message again.",
|
||||
"cloud_archived.error.title": "Message Archived",
|
||||
"cloud_billing_history_modal.title": "Invoice(s)",
|
||||
"cloud_preview_modal.aria_label": "Cloud Preview Introduction",
|
||||
"cloud_preview_modal.cyberdefense.ai.sku_label": "ENTERPRISE",
|
||||
"cloud_preview_modal.cyberdefense.ai.subtitle": "Supercharge collaboration with Agents. Instantly summarize calls, surface action items, and find answers fast—all using the model you trust.",
|
||||
"cloud_preview_modal.cyberdefense.ai.title": "Bring your own AI model with Mattermost Agents",
|
||||
"cloud_preview_modal.cyberdefense.messaging.sku_label": "ENTERPRISE",
|
||||
"cloud_preview_modal.cyberdefense.messaging.subtitle": "Bring conversations and context together in one secure platform. Communicate with urgency using priority levels, persistent notifications, and acknowledgments—so critical messages are seen and acted on when every second counts.",
|
||||
"cloud_preview_modal.cyberdefense.messaging.title": "Messaging built for action, not noise",
|
||||
"cloud_preview_modal.cyberdefense.playbooks.sku_label": "ENTERPRISE",
|
||||
"cloud_preview_modal.cyberdefense.playbooks.subtitle": "Unlock powerful workflows tailored to real-world complexity. When conditions change, define tasks for the Playbook to evolve with your dynamic processes.",
|
||||
"cloud_preview_modal.cyberdefense.playbooks.title": "Build smart Playbooks for advanced workflows",
|
||||
"cloud_preview_modal.cyberdefense.profiles.sku_label": "ENTERPRISE",
|
||||
"cloud_preview_modal.cyberdefense.profiles.subtitle": "Create tailored user profiles with custom attributes like role, location, or clearance level to reflect your organization's structure. Help teams understand who they're working with and how to collaborate more effectively.",
|
||||
"cloud_preview_modal.cyberdefense.profiles.title": "Tailor user profiles to match your team's structure",
|
||||
"cloud_preview_modal.cyberdefense.sku_label": "ENTERPRISE ADVANCED",
|
||||
"cloud_preview_modal.cyberdefense.welcome.subtitle": "This hands-on, 1-hour preview of Mattermost Enterprise Advanced lets your cyber defense team explore secure, threat-aware collaboration. The workspace is preloaded with data to show the platform in action. Watch the 4-minute video to get started.",
|
||||
"cloud_preview_modal.cyberdefense.welcome.title": "Welcome to your Mattermost preview",
|
||||
"cloud_preview_modal.cyberdefense.zero_trust.sku_label": "ENTERPRISE ADVANCED",
|
||||
"cloud_preview_modal.cyberdefense.zero_trust.subtitle": "Define granular access to content using attribute-based policies, and display classification banners and labels to guide user behavior. Limit exposure based on role, clearance level, or operational context.",
|
||||
"cloud_preview_modal.cyberdefense.zero_trust.title": "Enforce Zero Trust collaboration",
|
||||
"cloud_preview_modal.devsecops.ai.sku_label": "ENTERPRISE",
|
||||
"cloud_preview_modal.devsecops.ai.subtitle": "Supercharge collaboration with Agents. Instantly summarize calls, surface action items, and find answers fast—all using the model you trust.",
|
||||
"cloud_preview_modal.devsecops.ai.title": "Bring your own AI model with Mattermost Agents",
|
||||
"cloud_preview_modal.devsecops.messaging.sku_label": "ENTERPRISE",
|
||||
"cloud_preview_modal.devsecops.messaging.subtitle": "Bring conversations and context together in one secure platform. Communicate with urgency using priority levels, persistent notifications, and acknowledgments—so critical messages are seen and acted on when every second counts.",
|
||||
"cloud_preview_modal.devsecops.messaging.title": "Messaging built for action, not noise",
|
||||
"cloud_preview_modal.devsecops.playbooks.sku_label": "ENTERPRISE",
|
||||
"cloud_preview_modal.devsecops.playbooks.subtitle": "Unlock powerful workflows tailored to real-world complexity. When conditions change, define tasks for the Playbook to evolve with your dynamic processes.",
|
||||
"cloud_preview_modal.devsecops.playbooks.title": "Build smart Playbooks for advanced workflows",
|
||||
"cloud_preview_modal.devsecops.profiles.sku_label": "ENTERPRISE",
|
||||
"cloud_preview_modal.devsecops.profiles.subtitle": "Create tailored user profiles with custom attributes like role, location, or clearance level to reflect your organization's structure. Help teams understand who they're working with and how to collaborate more effectively.",
|
||||
"cloud_preview_modal.devsecops.profiles.title": "Tailor user profiles to match your team's structure",
|
||||
"cloud_preview_modal.devsecops.sku_label": "ENTERPRISE ADVANCED",
|
||||
"cloud_preview_modal.devsecops.welcome.subtitle": "This hands-on, 1-hour preview of Mattermost Enterprise Advanced lets your DevSecOps team explore secure, collaborative development. The workspace is preloaded with data to show the platform in action. Watch the 4-minute video to get started.",
|
||||
"cloud_preview_modal.devsecops.welcome.title": "Welcome to your Mattermost preview",
|
||||
"cloud_preview_modal.devsecops.zero_trust.sku_label": "ENTERPRISE ADVANCED",
|
||||
"cloud_preview_modal.devsecops.zero_trust.subtitle": "Define granular access to content using attribute-based policies, and display classification banners and labels to guide user behavior. Limit exposure based on role, clearance level, or operational context.",
|
||||
"cloud_preview_modal.devsecops.zero_trust.title": "Enforce Zero Trust collaboration",
|
||||
"cloud_preview_modal.done": "Finish",
|
||||
"cloud_preview_modal.missionops.ai.sku_label": "ENTERPRISE",
|
||||
"cloud_preview_modal.missionops.ai.subtitle": "Supercharge collaboration with Agents. Instantly summarize calls, surface action items, and find answers fast—all using the model you trust.",
|
||||
"cloud_preview_modal.missionops.ai.title": "Bring your own AI model with Mattermost Agents",
|
||||
"cloud_preview_modal.missionops.messaging.sku_label": "ENTERPRISE",
|
||||
"cloud_preview_modal.missionops.messaging.subtitle": "Bring conversations and context together in one secure platform. Communicate with urgency using priority levels, persistent notifications, and acknowledgments—so critical messages are seen and acted on when every second counts.",
|
||||
"cloud_preview_modal.missionops.messaging.title": "Messaging built for action, not noise",
|
||||
"cloud_preview_modal.missionops.playbooks.sku_label": "ENTERPRISE",
|
||||
"cloud_preview_modal.missionops.playbooks.subtitle": "Unlock powerful workflows tailored to real-world complexity. When conditions change, define tasks for the Playbook to evolve with your dynamic processes.",
|
||||
"cloud_preview_modal.missionops.playbooks.title": "Build smart Playbooks for advanced workflows",
|
||||
"cloud_preview_modal.missionops.profiles.sku_label": "ENTERPRISE",
|
||||
"cloud_preview_modal.missionops.profiles.subtitle": "Create tailored user profiles with custom attributes like role, location, or clearance level to reflect your organization's structure. Help teams understand who they're working with and how to collaborate more effectively.",
|
||||
"cloud_preview_modal.missionops.profiles.title": "Tailor user profiles to match your team's structure",
|
||||
"cloud_preview_modal.missionops.sku_label": "ENTERPRISE ADVANCED",
|
||||
"cloud_preview_modal.missionops.welcome.subtitle": "This hands-on, 1-hour preview of Mattermost Enterprise Advanced lets your team explore secure, mission-critical collaboration. The workspace is preloaded with data to show the platform in action. Watch the 4-minute video to get started.",
|
||||
"cloud_preview_modal.missionops.welcome.title": "Welcome to your Mattermost preview",
|
||||
"cloud_preview_modal.missionops.zero_trust.sku_label": "ENTERPRISE ADVANCED",
|
||||
"cloud_preview_modal.missionops.zero_trust.subtitle": "Define granular access to content using attribute-based policies, and display classification banners and labels to guide user behavior. Limit exposure based on role, clearance level, or operational context.",
|
||||
"cloud_preview_modal.missionops.zero_trust.title": "Enforce Zero Trust collaboration",
|
||||
"cloud_preview_modal.next": "Next",
|
||||
"cloud_preview_modal.play_video": "Play video",
|
||||
"cloud_preview_modal.previous": "Previous",
|
||||
"cloud_preview_modal.skip": "Skip for now",
|
||||
"cloud_signup.signup_consequences": "Your credit card will be charged today. <a>See how billing works.</a>",
|
||||
"cloud_upgrade.error_min_seats": "Minimum of 10 seats required",
|
||||
"cloud.fetch_error": "Error fetching billing data. Please try again later.",
|
||||
|
||||
Ссылка в новой задаче
Block a user