From 55e42cec93228a447b624c2b0001696712257efe Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Wed, 23 Mar 2022 20:26:00 +0100 Subject: [PATCH] [Cases] Allow custom toast title and content in cases hooks (#128145) --- .../cases/public/common/translations.ts | 15 +- .../public/common/use_cases_toast.test.tsx | 135 +++++++++++++++--- .../cases/public/common/use_cases_toast.tsx | 87 +++++++++-- .../use_cases_add_to_existing_case_modal.tsx | 16 ++- .../use_cases_add_to_new_case_flyout.tsx | 14 +- 5 files changed, 230 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/cases/public/common/translations.ts b/x-pack/plugins/cases/public/common/translations.ts index 5c349a65dd869..10005b2c87bce 100644 --- a/x-pack/plugins/cases/public/common/translations.ts +++ b/x-pack/plugins/cases/public/common/translations.ts @@ -257,13 +257,22 @@ export const LINK_APPROPRIATE_LICENSE = i18n.translate('xpack.cases.common.appro export const CASE_SUCCESS_TOAST = (title: string) => i18n.translate('xpack.cases.actions.caseSuccessToast', { + values: { title }, + defaultMessage: '{title} has been updated', + }); + +export const CASE_ALERT_SUCCESS_TOAST = (title: string) => + i18n.translate('xpack.cases.actions.caseAlertSuccessToast', { values: { title }, defaultMessage: 'An alert has been added to "{title}"', }); -export const CASE_SUCCESS_SYNC_TEXT = i18n.translate('xpack.cases.actions.caseSuccessSyncText', { - defaultMessage: 'Alerts in this case have their status synched with the case status', -}); +export const CASE_ALERT_SUCCESS_SYNC_TEXT = i18n.translate( + 'xpack.cases.actions.caseAlertSuccessSyncText', + { + defaultMessage: 'Alerts in this case have their status synched with the case status', + } +); export const VIEW_CASE = i18n.translate('xpack.cases.actions.viewCase', { defaultMessage: 'View Case', diff --git a/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx b/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx index 9bd6a6675a5c1..517d1cfdd77b1 100644 --- a/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx +++ b/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx @@ -9,33 +9,97 @@ import { renderHook } from '@testing-library/react-hooks'; import { useToasts } from '../common/lib/kibana'; import { AppMockRenderer, createAppMockRenderer, TestProviders } from '../common/mock'; import { CaseToastSuccessContent, useCasesToast } from './use_cases_toast'; -import { mockCase } from '../containers/mock'; +import { alertComment, basicComment, mockCase } from '../containers/mock'; import React from 'react'; import userEvent from '@testing-library/user-event'; +import { SupportedCaseAttachment } from '../types'; jest.mock('../common/lib/kibana'); const useToastsMock = useToasts as jest.Mock; describe('Use cases toast hook', () => { + const successMock = jest.fn(); + + function validateTitle(title: string) { + const mockParams = successMock.mock.calls[0][0]; + const el = document.createElement('div'); + mockParams.title(el); + expect(el).toHaveTextContent(title); + } + + function validateContent(content: string) { + const mockParams = successMock.mock.calls[0][0]; + const el = document.createElement('div'); + mockParams.text(el); + expect(el).toHaveTextContent(content); + } + + useToastsMock.mockImplementation(() => { + return { + addSuccess: successMock, + }; + }); + + beforeEach(() => { + successMock.mockClear(); + }); + describe('Toast hook', () => { - const successMock = jest.fn(); - useToastsMock.mockImplementation(() => { - return { - addSuccess: successMock, - }; - }); - it('should create a success tost when invoked with a case', () => { + it('should create a success toast when invoked with a case', () => { const { result } = renderHook( () => { return useCasesToast(); }, { wrapper: TestProviders } ); - result.current.showSuccessAttach(mockCase); + result.current.showSuccessAttach({ + theCase: mockCase, + }); expect(successMock).toHaveBeenCalled(); }); }); + + describe('toast title', () => { + it('should create a success toast when invoked with a case and a custom title', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ theCase: mockCase, title: 'Custom title' }); + validateTitle('Custom title'); + }); + + it('should display the alert sync title when called with an alert attachment ', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ + theCase: mockCase, + attachments: [alertComment as SupportedCaseAttachment], + }); + validateTitle('An alert has been added to "Another horrible breach!!'); + }); + + it('should display a generic title when called with a non-alert attachament', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ + theCase: mockCase, + attachments: [basicComment as SupportedCaseAttachment], + }); + validateTitle('Another horrible breach!! has been updated'); + }); + }); describe('Toast content', () => { let appMockRender: AppMockRenderer; const onViewCaseClick = jest.fn(); @@ -44,20 +108,57 @@ describe('Use cases toast hook', () => { onViewCaseClick.mockReset(); }); - it('renders a correct successfull message with synced alerts', () => { - const result = appMockRender.render( - + it('should create a success toast when invoked with a case and a custom content', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } ); - expect(result.getByTestId('toaster-content-sync-text')).toHaveTextContent( - 'Alerts in this case have their status synched with the case status' + result.current.showSuccessAttach({ theCase: mockCase, content: 'Custom content' }); + validateContent('Custom content'); + }); + + it('renders an alert-specific content when called with an alert attachment and sync on', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ + theCase: mockCase, + attachments: [alertComment as SupportedCaseAttachment], + }); + validateContent('Alerts in this case have their status synched with the case status'); + }); + + it('renders empty content when called with an alert attachment and sync off', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ + theCase: { ...mockCase, settings: { ...mockCase.settings, syncAlerts: false } }, + attachments: [alertComment as SupportedCaseAttachment], + }); + validateContent('View Case'); + }); + + it('renders a correct successful message content', () => { + const result = appMockRender.render( + ); + expect(result.getByTestId('toaster-content-sync-text')).toHaveTextContent('my content'); expect(result.getByTestId('toaster-content-case-view-link')).toHaveTextContent('View Case'); expect(onViewCaseClick).not.toHaveBeenCalled(); }); - it('renders a correct successfull message with not synced alerts', () => { + it('renders a correct successful message without content', () => { const result = appMockRender.render( - + ); expect(result.queryByTestId('toaster-content-sync-text')).toBeFalsy(); expect(result.getByTestId('toaster-content-case-view-link')).toHaveTextContent('View Case'); @@ -66,7 +167,7 @@ describe('Use cases toast hook', () => { it('Calls the onViewCaseClick when clicked', () => { const result = appMockRender.render( - + ); userEvent.click(result.getByTestId('toaster-content-case-view-link')); expect(onViewCaseClick).toHaveBeenCalled(); diff --git a/x-pack/plugins/cases/public/common/use_cases_toast.tsx b/x-pack/plugins/cases/public/common/use_cases_toast.tsx index 98cc7fa1d8faa..d02f792d601cf 100644 --- a/x-pack/plugins/cases/public/common/use_cases_toast.tsx +++ b/x-pack/plugins/cases/public/common/use_cases_toast.tsx @@ -9,10 +9,16 @@ import { EuiButtonEmpty, EuiText } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { Case } from '../../common'; +import { Case, CommentType } from '../../common'; import { useToasts } from '../common/lib/kibana'; import { useCaseViewNavigation } from '../common/navigation'; -import { CASE_SUCCESS_SYNC_TEXT, CASE_SUCCESS_TOAST, VIEW_CASE } from './translations'; +import { CaseAttachments } from '../types'; +import { + CASE_ALERT_SUCCESS_SYNC_TEXT, + CASE_ALERT_SUCCESS_TOAST, + CASE_SUCCESS_TOAST, + VIEW_CASE, +} from './translations'; const LINE_CLAMP = 3; const Title = styled.span` @@ -28,46 +34,101 @@ const EuiTextStyled = styled(EuiText)` `} `; +function getToastTitle({ + theCase, + title, + attachments, +}: { + theCase: Case; + title?: string; + attachments?: CaseAttachments; +}): string { + if (title !== undefined) { + return title; + } + if (attachments !== undefined) { + for (const attachment of attachments) { + if (attachment.type === CommentType.alert) { + return CASE_ALERT_SUCCESS_TOAST(theCase.title); + } + } + } + return CASE_SUCCESS_TOAST(theCase.title); +} + +function getToastContent({ + theCase, + content, + attachments, +}: { + theCase: Case; + content?: string; + attachments?: CaseAttachments; +}): string | undefined { + if (content !== undefined) { + return content; + } + if (attachments !== undefined) { + for (const attachment of attachments) { + if (attachment.type === CommentType.alert && theCase.settings.syncAlerts) { + return CASE_ALERT_SUCCESS_SYNC_TEXT; + } + } + } + return undefined; +} + export const useCasesToast = () => { const { navigateToCaseView } = useCaseViewNavigation(); const toasts = useToasts(); return { - showSuccessAttach: (theCase: Case) => { + showSuccessAttach: ({ + theCase, + attachments, + title, + content, + }: { + theCase: Case; + attachments?: CaseAttachments; + title?: string; + content?: string; + }) => { const onViewCaseClick = () => { navigateToCaseView({ detailName: theCase.id, }); }; + const renderTitle = getToastTitle({ theCase, title, attachments }); + const renderContent = getToastContent({ theCase, content, attachments }); + return toasts.addSuccess({ color: 'success', iconType: 'check', - title: toMountPoint({CASE_SUCCESS_TOAST(theCase.title)}), + title: toMountPoint({renderTitle}), text: toMountPoint( - + ), }); }, }; }; + export const CaseToastSuccessContent = ({ - syncAlerts, onViewCaseClick, + content, }: { - syncAlerts: boolean; onViewCaseClick: () => void; + content?: string; }) => { return ( <> - {syncAlerts && ( + {content !== undefined ? ( - {CASE_SUCCESS_SYNC_TEXT} + {content} - )} + ) : null} { +type AddToExistingFlyoutProps = AllCasesSelectorModalProps & { + toastTitle?: string; + toastContent?: string; +}; + +export const useCasesAddToExistingCaseModal = (props: AddToExistingFlyoutProps) => { const createNewCaseFlyout = useCasesAddToNewCaseFlyout({ attachments: props.attachments, onClose: props.onClose, @@ -25,6 +30,8 @@ export const useCasesAddToExistingCaseModal = (props: AllCasesSelectorModalProps return props.onRowClick(theCase); } }, + toastTitle: props.toastTitle, + toastContent: props.toastContent, }); const { dispatch } = useCasesContext(); const casesToasts = useCasesToast(); @@ -53,7 +60,12 @@ export const useCasesAddToExistingCaseModal = (props: AllCasesSelectorModalProps closeModal(); createNewCaseFlyout.open(); } else { - casesToasts.showSuccessAttach(theCase); + casesToasts.showSuccessAttach({ + theCase, + attachments: props.attachments, + title: props.toastTitle, + content: props.toastContent, + }); if (props.onRowClick) { props.onRowClick(theCase); } diff --git a/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx b/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx index 5422ab9be995d..c1c0793fe2340 100644 --- a/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx +++ b/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx @@ -12,7 +12,12 @@ import { CasesContextStoreActionsList } from '../../cases_context/cases_context_ import { useCasesContext } from '../../cases_context/use_cases_context'; import { CreateCaseFlyoutProps } from './create_case_flyout'; -export const useCasesAddToNewCaseFlyout = (props: CreateCaseFlyoutProps) => { +type AddToNewCaseFlyoutProps = CreateCaseFlyoutProps & { + toastTitle?: string; + toastContent?: string; +}; + +export const useCasesAddToNewCaseFlyout = (props: AddToNewCaseFlyoutProps) => { const { dispatch } = useCasesContext(); const casesToasts = useCasesToast(); @@ -35,7 +40,12 @@ export const useCasesAddToNewCaseFlyout = (props: CreateCaseFlyoutProps) => { }, onSuccess: async (theCase: Case) => { if (theCase) { - casesToasts.showSuccessAttach(theCase); + casesToasts.showSuccessAttach({ + theCase, + attachments: props.attachments, + title: props.toastTitle, + content: props.toastContent, + }); } if (props.onSuccess) { return props.onSuccess(theCase);