From 9e7849647c11374c9a83297ac5eb1e85c8473894 Mon Sep 17 00:00:00 2001 From: Nick Misasi Date: Thu, 26 Jun 2025 20:30:26 -0400 Subject: [PATCH] [CLD-9238] Direct preview user to proper team based on use case (#31784) * 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 * 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 * Accept use case in CWS login, redirect to proper team, with filtered content in preview modal * 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 * Fix i18n * Hiding modal will presist through refreshes * Fix linter * Add exception to notification permission bar for cloud previews * 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 * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_content.scss Co-authored-by: Matthew Birtch * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_content.scss Co-authored-by: Matthew Birtch * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_content.tsx Co-authored-by: Matthew Birtch * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_content.scss Co-authored-by: Matthew Birtch * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_content.scss Co-authored-by: Matthew Birtch * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_controller.tsx Co-authored-by: Matthew Birtch * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_controller.tsx Co-authored-by: Matthew Birtch * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_controller.scss Co-authored-by: Matthew Birtch * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_controller.tsx Co-authored-by: Matthew Birtch * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_controller.tsx Co-authored-by: Matthew Birtch * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_controller.tsx Co-authored-by: Matthew Birtch * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_controller.tsx Co-authored-by: Matthew Birtch * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_controller.scss Co-authored-by: Matthew Birtch * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_controller.scss Co-authored-by: Matthew Birtch * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_content.scss Co-authored-by: Matthew Birtch * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_content.scss Co-authored-by: Matthew Birtch * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_content.scss Co-authored-by: Matthew Birtch * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_content.scss Co-authored-by: Matthew Birtch * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_content.scss Co-authored-by: Matthew Birtch * Update webapp/channels/src/components/cloud_preview_modal/preview_modal_content.scss Co-authored-by: Matthew Birtch * 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 * feat: use getBool selector instead of get for boolean preference check - Replace getPreference with getBool to avoid explicit === 'true' comparison - Follows Harrison's review suggestion for cleaner boolean handling Co-authored-by: Nick Misasi * fix linter * Fixes for PR review * Fix linter * Fix i18n * fix linter * Changes to address Harrison's feedback * Change file name, remove index.tsx * 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 Co-authored-by: Mattermost Build Co-authored-by: Matthew Birtch --- server/channels/api4/user.go | 17 +++ .../index.test.tsx | 21 +++ .../notification_permission_bar/index.tsx | 7 + .../cloud_preview_modal_controller.test.tsx | 125 +++++++++++++++--- .../cloud_preview_modal_controller.tsx | 23 +++- .../preview_modal_content_controller.tsx | 4 +- .../preview_modal_content_data.ts | 36 ++--- 7 files changed, 192 insertions(+), 41 deletions(-) diff --git a/server/channels/api4/user.go b/server/channels/api4/user.go index e067dec0cd..e887898872 100644 --- a/server/channels/api4/user.go +++ b/server/channels/api4/user.go @@ -2040,6 +2040,12 @@ func loginCWS(c *Context, w http.ResponseWriter, r *http.Request) { "focalboard": "/boards", } + useCaseToURL := map[string]string{ + "mission-ops": "/mission-ops-hq", + "dev-sec-ops": "/dev-sec-ops-hq", + "cyber-defense": "/cyber-defense-hq", + } + if !c.App.Channels().License().IsCloud() { c.Err = model.NewAppError("loginCWS", "api.user.login_cws.license.error", nil, "", http.StatusUnauthorized) return @@ -2050,6 +2056,7 @@ func loginCWS(c *Context, w http.ResponseWriter, r *http.Request) { var loginID string var token string var campaign string + var useCase string if len(r.Form) > 0 { for key, value := range r.Form { if key == "login_id" { @@ -2061,6 +2068,9 @@ func loginCWS(c *Context, w http.ResponseWriter, r *http.Request) { if key == "utm_campaign" { campaign = value[0] } + if key == "use_case" { + useCase = value[0] + } } } @@ -2099,6 +2109,13 @@ func loginCWS(c *Context, w http.ResponseWriter, r *http.Request) { } } + // If a cloud preview, redirect to the correct use case URL + if c.App.License().IsCloudPreview() && useCase != "" { + if url, ok := useCaseToURL[useCase]; ok { + redirectURL += url + } + } + http.Redirect(w, r, redirectURL, http.StatusFound) } diff --git a/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.test.tsx b/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.test.tsx index e3b9e3a673..3521dcfebb 100644 --- a/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.test.tsx +++ b/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.test.tsx @@ -88,4 +88,25 @@ describe('NotificationPermissionBar', () => { expect(container).toBeEmptyDOMElement(); }); + + test('should not render anything in cloud preview environment', () => { + jest.spyOn(utilsNotifications, 'isNotificationAPISupported').mockReturnValue(true); + jest.spyOn(utilsNotifications, 'getNotificationPermission').mockReturnValue(utilsNotifications.NotificationPermissionNeverGranted); + + const stateWithCloudPreview = { + ...initialState, + entities: { + ...initialState.entities, + cloud: { + subscription: { + is_cloud_preview: true, + }, + }, + }, + }; + + const {container} = renderWithContext(, stateWithCloudPreview); + + expect(container).toBeEmptyDOMElement(); + }); }); diff --git a/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.tsx b/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.tsx index 17304e6f0b..8547a77840 100644 --- a/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.tsx +++ b/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.tsx @@ -4,6 +4,7 @@ import React from 'react'; import {useSelector} from 'react-redux'; +import {getCloudSubscription} from 'mattermost-redux/selectors/entities/cloud'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import NotificationPermissionNeverGrantedBar from 'components/announcement_bar/notification_permission_bar/notification_permission_never_granted_bar'; @@ -19,6 +20,7 @@ import { export default function NotificationPermissionBar() { const isLoggedIn = Boolean(useSelector(getCurrentUserId)); + const subscription = useSelector(getCloudSubscription); useDesktopAppNotificationPermission(); @@ -26,6 +28,11 @@ export default function NotificationPermissionBar() { return null; } + // Don't show the notification bar if it's a cloud preview environment + if (subscription?.is_cloud_preview) { + return null; + } + // When browser does not support notification API, we show the notification bar to update browser if (!isNotificationAPISupported()) { return ; diff --git a/webapp/channels/src/components/cloud_preview_modal/cloud_preview_modal_controller.test.tsx b/webapp/channels/src/components/cloud_preview_modal/cloud_preview_modal_controller.test.tsx index cbdd62a61a..89f0780597 100644 --- a/webapp/channels/src/components/cloud_preview_modal/cloud_preview_modal_controller.test.tsx +++ b/webapp/channels/src/components/cloud_preview_modal/cloud_preview_modal_controller.test.tsx @@ -6,6 +6,7 @@ import React from 'react'; import * as reactRedux from 'react-redux'; import type {Subscription} from '@mattermost/types/cloud'; +import type {TeamType} from '@mattermost/types/teams'; import {renderWithContext} from 'tests/react_testing_utils'; @@ -66,6 +67,30 @@ describe('CloudPreviewModal', () => { current_user_id: {roles: 'system_admin'}, }, }, + teams: { + currentTeamId: 'mission-ops-hq', + teams: { + 'mission-ops-hq': { + id: 'mission-ops-hq', + name: 'mission-ops-hq', + display_name: 'Mission Ops HQ', + type: 'O' as TeamType, + create_at: 1234567890, + update_at: 1234567890, + delete_at: 0, + allow_open_invite: true, + invite_id: 'test-invite-id', + description: 'Test team', + email: 'test@example.com', + company_name: 'Test Company', + allowed_domains: '', + scheme_id: '', + group_constrained: false, + policy_id: null, + cloud_limits_archived: false, + }, + }, + }, cloud: { subscription: baseSubscription, }, @@ -122,27 +147,26 @@ describe('CloudPreviewModal', () => { 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', - // }, - // }; + 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); + const dummyDispatch = jest.fn(); + useDispatchMock.mockReturnValue(dummyDispatch); - // renderWithContext( - // , - // state, - // ); + renderWithContext( + , + state, + ); - // expect(screen.queryByTestId('preview-modal-controller')).not.toBeInTheDocument(); - // }); + expect(screen.queryByTestId('preview-modal-controller')).not.toBeInTheDocument(); + }); it('should save preference when modal is closed', async () => { const dummyDispatch = jest.fn(); @@ -180,4 +204,69 @@ describe('CloudPreviewModal', () => { expect(screen.queryByTestId('preview-modal-controller')).not.toBeInTheDocument(); }); + + it('should not show modal when no team is present', () => { + const state = JSON.parse(JSON.stringify(initialState)); + delete state.entities.teams; + + const dummyDispatch = jest.fn(); + useDispatchMock.mockReturnValue(dummyDispatch); + + renderWithContext( + , + state, + ); + + expect(screen.queryByTestId('preview-modal-controller')).not.toBeInTheDocument(); + }); + + it('should filter content based on team use case', () => { + const dummyDispatch = jest.fn(); + useDispatchMock.mockReturnValue(dummyDispatch); + + // Test with dev-sec-ops team + const devSecOpsState = JSON.parse(JSON.stringify(initialState)); + devSecOpsState.entities.teams.currentTeamId = 'dev-sec-ops-hq'; + devSecOpsState.entities.teams.teams = { + 'dev-sec-ops-hq': { + ...initialState.entities.teams.teams['mission-ops-hq'], + id: 'dev-sec-ops-hq', + name: 'dev-sec-ops-hq', + display_name: 'DevSecOps HQ', + }, + }; + + renderWithContext( + , + devSecOpsState, + ); + + // Should show modal for dev-sec-ops team + expect(screen.getByTestId('preview-modal-controller')).toBeInTheDocument(); + }); + + it('should not show modal when team name does not match any use case', () => { + const dummyDispatch = jest.fn(); + useDispatchMock.mockReturnValue(dummyDispatch); + + // Test with unknown team + const unknownTeamState = JSON.parse(JSON.stringify(initialState)); + unknownTeamState.entities.teams.currentTeamId = 'unknown-team-hq'; + unknownTeamState.entities.teams.teams = { + 'unknown-team-hq': { + ...initialState.entities.teams.teams['mission-ops-hq'], + id: 'unknown-team-hq', + name: 'unknown-team-hq', + display_name: 'Unknown Team HQ', + }, + }; + + renderWithContext( + , + unknownTeamState, + ); + + // Should not show modal for unknown team + expect(screen.queryByTestId('preview-modal-controller')).not.toBeInTheDocument(); + }); }); diff --git a/webapp/channels/src/components/cloud_preview_modal/cloud_preview_modal_controller.tsx b/webapp/channels/src/components/cloud_preview_modal/cloud_preview_modal_controller.tsx index 7fbfb1367e..4380f2694f 100644 --- a/webapp/channels/src/components/cloud_preview_modal/cloud_preview_modal_controller.tsx +++ b/webapp/channels/src/components/cloud_preview_modal/cloud_preview_modal_controller.tsx @@ -7,10 +7,17 @@ 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 {getBool} from 'mattermost-redux/selectors/entities/preferences'; +import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {makeAsyncComponent} from 'components/async_load'; +import type {GlobalState} from 'types/store'; + +import type {PreviewModalContentData} from './preview_modal_content_data'; +import {modalContent} from './preview_modal_content_data'; + const CLOUD_PREVIEW_MODAL_SHOWN_PREF = 'cloud_preview_modal_shown'; // Lazy load the controller component and content data together @@ -24,26 +31,33 @@ const CloudPreviewModal: React.FC = () => { const subscription = useSelector(getCloudSubscription); const license = useSelector(getLicense); const currentUserId = useSelector(getCurrentUserId); + const team = useSelector(getCurrentTeam); const isCloud = license?.Cloud === 'true'; const isCloudPreview = subscription?.is_cloud_preview === true; // Check if modal has been shown before - const hasModalBeenShown = false; + const hasModalBeenShown = useSelector((state: GlobalState) => getBool(state, CLOUD_PREVIEW_MODAL_SHOWN_PREF, CLOUD_PREVIEW_MODAL_SHOWN_PREF)); const [showModal, setShowModal] = useState(false); + const filteredContentByUseCase = (content: PreviewModalContentData[]) => { + return content.filter((content) => content.useCase === team?.name.replace('-hq', '')); + }; + 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) { + // 4. There's content to display for the current team + const filteredContent = team?.name ? filteredContentByUseCase(modalContent) : []; + if (isCloud && isCloudPreview && !hasModalBeenShown && currentUserId && team?.name && filteredContent.length > 0) { setShowModal(true); } else if (hasModalBeenShown) { setShowModal(false); } - }, [isCloud, isCloudPreview, hasModalBeenShown, currentUserId]); + }, [isCloud, isCloudPreview, hasModalBeenShown, currentUserId, team?.name]); const handleClose = () => { setShowModal(false); @@ -66,10 +80,13 @@ const CloudPreviewModal: React.FC = () => { } // Only render the controller if we pass the license checks + const contentData = team?.name ? filteredContentByUseCase(modalContent) : []; + return ( ); }; diff --git a/webapp/channels/src/components/cloud_preview_modal/preview_modal_content_controller.tsx b/webapp/channels/src/components/cloud_preview_modal/preview_modal_content_controller.tsx index 19d46ff02e..88f31c5d53 100644 --- a/webapp/channels/src/components/cloud_preview_modal/preview_modal_content_controller.tsx +++ b/webapp/channels/src/components/cloud_preview_modal/preview_modal_content_controller.tsx @@ -24,8 +24,8 @@ const PreviewModalController: React.FC = ({show, onClose, contentData}) = 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'); + // Use passed contentData if it's provided otherwise use the default mission ops content + const activeContentData = contentData || modalContent.filter((content) => content.useCase === 'mission-ops'); const handlePrevious = useCallback(() => { setCurrentIndex((prev) => Math.max(0, prev - 1)); diff --git a/webapp/channels/src/components/cloud_preview_modal/preview_modal_content_data.ts b/webapp/channels/src/components/cloud_preview_modal/preview_modal_content_data.ts index 0341cdbb4f..e13faff82f 100644 --- a/webapp/channels/src/components/cloud_preview_modal/preview_modal_content_data.ts +++ b/webapp/channels/src/components/cloud_preview_modal/preview_modal_content_data.ts @@ -29,7 +29,7 @@ export const modalContent: PreviewModalContentData[] = [ }), 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', + useCase: 'mission-ops', }, { skuLabel: defineMessage({ @@ -45,7 +45,7 @@ export const modalContent: PreviewModalContentData[] = [ 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', + useCase: 'mission-ops', }, { skuLabel: defineMessage({ @@ -61,7 +61,7 @@ export const modalContent: PreviewModalContentData[] = [ 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', + useCase: 'mission-ops', }, { skuLabel: defineMessage({ @@ -77,7 +77,7 @@ export const modalContent: PreviewModalContentData[] = [ 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', + useCase: 'mission-ops', }, { skuLabel: defineMessage({ @@ -93,7 +93,7 @@ export const modalContent: PreviewModalContentData[] = [ 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', + useCase: 'mission-ops', }, // { @@ -126,7 +126,7 @@ export const modalContent: PreviewModalContentData[] = [ 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', + useCase: 'mission-ops', }, { skuLabel: defineMessage({ @@ -143,7 +143,7 @@ export const modalContent: PreviewModalContentData[] = [ }), 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', + useCase: 'dev-sec-ops', }, { skuLabel: defineMessage({ @@ -159,7 +159,7 @@ export const modalContent: PreviewModalContentData[] = [ 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', + useCase: 'dev-sec-ops', }, { skuLabel: defineMessage({ @@ -175,7 +175,7 @@ export const modalContent: PreviewModalContentData[] = [ 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', + useCase: 'dev-sec-ops', }, { skuLabel: defineMessage({ @@ -191,7 +191,7 @@ export const modalContent: PreviewModalContentData[] = [ 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', + useCase: 'dev-sec-ops', }, { skuLabel: defineMessage({ @@ -207,7 +207,7 @@ export const modalContent: PreviewModalContentData[] = [ 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', + useCase: 'dev-sec-ops', }, // { @@ -240,7 +240,7 @@ export const modalContent: PreviewModalContentData[] = [ 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', + useCase: 'dev-sec-ops', }, { skuLabel: defineMessage({ @@ -257,7 +257,7 @@ export const modalContent: PreviewModalContentData[] = [ }), 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', + useCase: 'cyber-defense', }, { skuLabel: defineMessage({ @@ -273,7 +273,7 @@ export const modalContent: PreviewModalContentData[] = [ 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', + useCase: 'cyber-defense', }, { skuLabel: defineMessage({ @@ -289,7 +289,7 @@ export const modalContent: PreviewModalContentData[] = [ 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', + useCase: 'cyber-defense', }, { skuLabel: defineMessage({ @@ -305,7 +305,7 @@ export const modalContent: PreviewModalContentData[] = [ 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', + useCase: 'cyber-defense', }, { skuLabel: defineMessage({ @@ -321,7 +321,7 @@ export const modalContent: PreviewModalContentData[] = [ 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', + useCase: 'cyber-defense', }, // { @@ -354,6 +354,6 @@ export const modalContent: PreviewModalContentData[] = [ 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', + useCase: 'cyber-defense', }, ];