Skip to content

Commit c7da735

Browse files
authored
feat: extract toasts and add a CTA for refresh COMPASS-7457 (#5159)
* chore: extract toasts and add a CTA for refresh * chore: test bulk action toasts * chore: fix linting issues * chore: linting 🔥 * chore: support undefined counts on toasts * chore: add e2e test to toasts * chore: remove unnecessary loop * chore: remove outdated comment
1 parent a1c61e5 commit c7da735

File tree

11 files changed

+510
-120
lines changed

11 files changed

+510
-120
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React from 'react';
2+
import { Link } from './leafygreen';
3+
import { css } from '@leafygreen-ui/emotion';
4+
5+
const toastBodyFlexStyles = css({
6+
display: 'flex',
7+
flexDirection: 'row',
8+
});
9+
10+
const toastBodyTextStyles = css({
11+
flexGrow: 1,
12+
});
13+
14+
const toastActionStyles = css({
15+
textTransform: 'uppercase',
16+
flexGrow: 0,
17+
alignSelf: 'center',
18+
19+
// Remove button styles.
20+
border: 'none',
21+
padding: 0,
22+
margin: 0,
23+
background: 'none',
24+
});
25+
26+
export function ToastBody({
27+
statusMessage,
28+
actionHandler,
29+
actionText,
30+
}: {
31+
statusMessage: string;
32+
actionHandler?: () => void;
33+
actionText?: string;
34+
}) {
35+
return (
36+
<div className={toastBodyFlexStyles}>
37+
<p className={toastBodyTextStyles}>{statusMessage}</p>
38+
{!!actionHandler && (
39+
<Link
40+
as="button"
41+
data-testid={`toast-action-${actionText ?? 'none'}`}
42+
onClick={actionHandler}
43+
hideExternalIcon
44+
className={toastActionStyles}
45+
>
46+
{actionText}
47+
</Link>
48+
)}
49+
</div>
50+
);
51+
}

packages/compass-components/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,4 @@ export { usePersistedState } from './hooks/use-persisted-state';
183183
export { GuideCue, GuideCueProvider } from './components/guide-cue/guide-cue';
184184
export type { Cue, GroupCue } from './components/guide-cue/guide-cue';
185185
export { PerformanceSignals } from './components/signals';
186+
export { ToastBody } from './components/toast-body';
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import React from 'react';
2+
import { ToastArea } from '@mongodb-js/compass-components';
3+
import { render, screen, cleanup, waitFor } from '@testing-library/react';
4+
import userEvent from '@testing-library/user-event';
5+
import {
6+
openBulkDeleteSuccessToast,
7+
openBulkDeleteProgressToast,
8+
openBulkDeleteFailureToast,
9+
openBulkUpdateSuccessToast,
10+
openBulkUpdateProgressToast,
11+
openBulkUpdateFailureToast,
12+
} from './bulk-actions-toasts';
13+
import { expect } from 'chai';
14+
import sinon from 'sinon';
15+
16+
function renderToastPortal() {
17+
return render(<ToastArea></ToastArea>);
18+
}
19+
20+
describe('Bulk Action Toasts', function () {
21+
beforeEach(function () {
22+
renderToastPortal();
23+
});
24+
25+
afterEach(function () {
26+
cleanup();
27+
});
28+
29+
describe('delete toasts', function () {
30+
describe('text for documents affected', function () {
31+
const USE_CASES = [
32+
{
33+
modal: openBulkDeleteSuccessToast,
34+
affected: undefined,
35+
expected: 'The delete operation finished successfully.',
36+
},
37+
{
38+
modal: openBulkDeleteSuccessToast,
39+
affected: 1,
40+
expected: '1 document has been deleted.',
41+
},
42+
{
43+
modal: openBulkDeleteSuccessToast,
44+
affected: 2,
45+
expected: '2 documents have been deleted.',
46+
},
47+
{
48+
modal: openBulkDeleteProgressToast,
49+
affected: undefined,
50+
expected: 'The delete operation is in progress.',
51+
},
52+
{
53+
modal: openBulkDeleteProgressToast,
54+
affected: 1,
55+
expected: '1 document is being deleted.',
56+
},
57+
{
58+
modal: openBulkDeleteProgressToast,
59+
affected: 2,
60+
expected: '2 documents are being deleted.',
61+
},
62+
{
63+
modal: openBulkDeleteFailureToast,
64+
affected: undefined,
65+
expected: 'The delete operation failed.',
66+
},
67+
{
68+
modal: openBulkDeleteFailureToast,
69+
affected: 1,
70+
expected: '1 document could not been deleted.',
71+
},
72+
{
73+
modal: openBulkDeleteFailureToast,
74+
affected: 2,
75+
expected: '2 documents could not been deleted.',
76+
},
77+
];
78+
79+
for (const useCase of USE_CASES) {
80+
it(`${useCase.modal.name} shows the text '${useCase.expected}' when affected document/s is/are '${useCase.affected}'`, async function () {
81+
useCase.modal({
82+
affectedDocuments: useCase.affected,
83+
onRefresh: () => {},
84+
});
85+
86+
await waitFor(async function () {
87+
const node = await screen.findByText(useCase.expected);
88+
expect(node).to.exist;
89+
});
90+
});
91+
}
92+
});
93+
94+
describe('action for successful toasts', function () {
95+
let onRefreshSpy: sinon.SinonSpy;
96+
97+
beforeEach(function () {
98+
onRefreshSpy = sinon.spy();
99+
});
100+
101+
it('calls the refresh action', async function () {
102+
openBulkDeleteSuccessToast({
103+
affectedDocuments: 42,
104+
onRefresh: onRefreshSpy,
105+
});
106+
107+
await waitFor(async function () {
108+
const node = await screen.findByText(/REFRESH/i);
109+
userEvent.click(node);
110+
});
111+
112+
expect(onRefreshSpy).to.have.been.called;
113+
});
114+
});
115+
});
116+
117+
describe('update toasts', function () {
118+
describe('text for documents affected', function () {
119+
const USE_CASES = [
120+
{
121+
modal: openBulkUpdateSuccessToast,
122+
affected: undefined,
123+
expected: 'The update operation finished successfully.',
124+
},
125+
{
126+
modal: openBulkUpdateSuccessToast,
127+
affected: 1,
128+
expected: '1 document has been updated.',
129+
},
130+
{
131+
modal: openBulkUpdateSuccessToast,
132+
affected: 2,
133+
expected: '2 documents have been updated.',
134+
},
135+
{
136+
modal: openBulkUpdateProgressToast,
137+
affected: undefined,
138+
expected: 'The update operation is in progress.',
139+
},
140+
{
141+
modal: openBulkUpdateProgressToast,
142+
affected: 1,
143+
expected: '1 document is being updated.',
144+
},
145+
{
146+
modal: openBulkUpdateProgressToast,
147+
affected: 2,
148+
expected: '2 documents are being updated.',
149+
},
150+
{
151+
modal: openBulkUpdateFailureToast,
152+
affected: undefined,
153+
expected: 'The update operation failed.',
154+
},
155+
{
156+
modal: openBulkUpdateFailureToast,
157+
affected: 1,
158+
expected: '1 document could not been updated.',
159+
},
160+
{
161+
modal: openBulkUpdateFailureToast,
162+
affected: 2,
163+
expected: '2 documents could not been updated.',
164+
},
165+
];
166+
167+
for (const useCase of USE_CASES) {
168+
it(`${useCase.modal.name} shows the text '${useCase.expected}' when ${useCase.affected} document/s affected`, async function () {
169+
useCase.modal({
170+
affectedDocuments: useCase.affected,
171+
onRefresh: () => {},
172+
});
173+
174+
await waitFor(async function () {
175+
const node = await screen.findByText(useCase.expected);
176+
expect(node).to.exist;
177+
});
178+
});
179+
}
180+
});
181+
182+
describe('action for successful toasts', function () {
183+
let onRefreshSpy: sinon.SinonSpy;
184+
185+
beforeEach(function () {
186+
onRefreshSpy = sinon.spy();
187+
});
188+
189+
it('calls the refresh action', async function () {
190+
openBulkUpdateSuccessToast({
191+
affectedDocuments: 42,
192+
onRefresh: onRefreshSpy,
193+
});
194+
195+
await waitFor(async function () {
196+
const node = await screen.findByText(/REFRESH/i);
197+
userEvent.click(node);
198+
});
199+
200+
expect(onRefreshSpy).to.have.been.called;
201+
});
202+
});
203+
});
204+
});

0 commit comments

Comments
 (0)