Skip to content

Commit

Permalink
[Cases] Allow custom toast title and content in cases hooks (#128145)
Browse files Browse the repository at this point in the history
  • Loading branch information
academo authored Mar 23, 2022
1 parent 7aa89aa commit 55e42ce
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 37 deletions.
15 changes: 12 additions & 3 deletions x-pack/plugins/cases/public/common/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
135 changes: 118 additions & 17 deletions x-pack/plugins/cases/public/common/use_cases_toast.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -44,20 +108,57 @@ describe('Use cases toast hook', () => {
onViewCaseClick.mockReset();
});

it('renders a correct successfull message with synced alerts', () => {
const result = appMockRender.render(
<CaseToastSuccessContent syncAlerts={true} onViewCaseClick={onViewCaseClick} />
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(
<CaseToastSuccessContent content={'my content'} onViewCaseClick={onViewCaseClick} />
);
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(
<CaseToastSuccessContent syncAlerts={false} onViewCaseClick={onViewCaseClick} />
<CaseToastSuccessContent onViewCaseClick={onViewCaseClick} />
);
expect(result.queryByTestId('toaster-content-sync-text')).toBeFalsy();
expect(result.getByTestId('toaster-content-case-view-link')).toHaveTextContent('View Case');
Expand All @@ -66,7 +167,7 @@ describe('Use cases toast hook', () => {

it('Calls the onViewCaseClick when clicked', () => {
const result = appMockRender.render(
<CaseToastSuccessContent syncAlerts={false} onViewCaseClick={onViewCaseClick} />
<CaseToastSuccessContent onViewCaseClick={onViewCaseClick} />
);
userEvent.click(result.getByTestId('toaster-content-case-view-link'));
expect(onViewCaseClick).toHaveBeenCalled();
Expand Down
87 changes: 74 additions & 13 deletions x-pack/plugins/cases/public/common/use_cases_toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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(<Title>{CASE_SUCCESS_TOAST(theCase.title)}</Title>),
title: toMountPoint(<Title>{renderTitle}</Title>),
text: toMountPoint(
<CaseToastSuccessContent
syncAlerts={theCase.settings.syncAlerts}
onViewCaseClick={onViewCaseClick}
/>
<CaseToastSuccessContent content={renderContent} onViewCaseClick={onViewCaseClick} />
),
});
},
};
};

export const CaseToastSuccessContent = ({
syncAlerts,
onViewCaseClick,
content,
}: {
syncAlerts: boolean;
onViewCaseClick: () => void;
content?: string;
}) => {
return (
<>
{syncAlerts && (
{content !== undefined ? (
<EuiTextStyled size="s" data-test-subj="toaster-content-sync-text">
{CASE_SUCCESS_SYNC_TEXT}
{content}
</EuiTextStyled>
)}
) : null}
<EuiButtonEmpty
size="xs"
flush="left"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ import { CasesContextStoreActionsList } from '../../cases_context/cases_context_
import { useCasesContext } from '../../cases_context/use_cases_context';
import { useCasesAddToNewCaseFlyout } from '../../create/flyout/use_cases_add_to_new_case_flyout';

export const useCasesAddToExistingCaseModal = (props: AllCasesSelectorModalProps) => {
type AddToExistingFlyoutProps = AllCasesSelectorModalProps & {
toastTitle?: string;
toastContent?: string;
};

export const useCasesAddToExistingCaseModal = (props: AddToExistingFlyoutProps) => {
const createNewCaseFlyout = useCasesAddToNewCaseFlyout({
attachments: props.attachments,
onClose: props.onClose,
Expand All @@ -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();
Expand Down Expand Up @@ -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);
}
Expand Down
Loading

0 comments on commit 55e42ce

Please sign in to comment.