Skip to content

Commit 434fea3

Browse files
authored
feat: delete collection [FC-0062] (#1333)
* feat: delete collection * feat: update button status on delete * test: add tests for collection delete
1 parent 75f937e commit 434fea3

14 files changed

Lines changed: 406 additions & 108 deletions

File tree

src/course-outline/CourseOutline.test.jsx

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ describe('<CourseOutline />', () => {
226226
});
227227

228228
it('check video sharing option shows error on failure', async () => {
229-
const { findByLabelText, queryByRole } = render(<RootWrapper />);
229+
render(<RootWrapper />);
230230

231231
axiosMock
232232
.onPost(getCourseBlockApiUrl(courseId), {
@@ -235,7 +235,7 @@ describe('<CourseOutline />', () => {
235235
},
236236
})
237237
.reply(500);
238-
const optionDropdown = await findByLabelText(statusBarMessages.videoSharingTitle.defaultMessage);
238+
const optionDropdown = await screen.findByLabelText(statusBarMessages.videoSharingTitle.defaultMessage);
239239
await act(
240240
async () => fireEvent.change(optionDropdown, { target: { value: VIDEO_SHARING_OPTIONS.allOff } }),
241241
);
@@ -247,8 +247,10 @@ describe('<CourseOutline />', () => {
247247
},
248248
}));
249249

250-
const alertElement = queryByRole('alert');
251-
expect(alertElement).toHaveTextContent(
250+
const alertElements = screen.queryAllByRole('alert');
251+
expect(alertElements.find(
252+
(el) => el.classList.contains('alert-content'),
253+
)).toHaveTextContent(
252254
pageAlertMessages.alertFailedGeneric.defaultMessage,
253255
);
254256
});
@@ -511,9 +513,10 @@ describe('<CourseOutline />', () => {
511513
notificationDismissUrl: '/some/url',
512514
});
513515

514-
const { findByRole } = render(<RootWrapper />);
515-
expect(await findByRole('alert')).toBeInTheDocument();
516-
const dismissBtn = await findByRole('button', { name: 'Dismiss' });
516+
render(<RootWrapper />);
517+
const alert = await screen.findByText(pageAlertMessages.configurationErrorTitle.defaultMessage);
518+
expect(alert).toBeInTheDocument();
519+
const dismissBtn = await screen.findByRole('button', { name: 'Dismiss' });
517520
axiosMock
518521
.onDelete('/some/url')
519522
.reply(204);
@@ -2160,10 +2163,10 @@ describe('<CourseOutline />', () => {
21602163
});
21612164

21622165
it('check whether unit copy & paste option works correctly', async () => {
2163-
const { findAllByTestId, queryByTestId, findAllByRole } = render(<RootWrapper />);
2166+
render(<RootWrapper />);
21642167
// get first section -> first subsection -> first unit element
21652168
const [section] = courseOutlineIndexMock.courseStructure.childInfo.children;
2166-
const [sectionElement] = await findAllByTestId('section-card');
2169+
const [sectionElement] = await screen.findAllByTestId('section-card');
21672170
const [subsection] = section.childInfo.children;
21682171
axiosMock
21692172
.onGet(getXBlockApiUrl(section.id))
@@ -2202,7 +2205,7 @@ describe('<CourseOutline />', () => {
22022205
await act(async () => fireEvent.mouseOver(clipboardLabel));
22032206

22042207
// find clipboard content popover link
2205-
const popoverContent = queryByTestId('popover-content');
2208+
const popoverContent = screen.queryByTestId('popover-content');
22062209
expect(popoverContent.tagName).toBe('A');
22072210
expect(popoverContent).toHaveAttribute('href', `${getConfig().STUDIO_BASE_URL}${unit.studioUrl}`);
22082211

@@ -2233,8 +2236,10 @@ describe('<CourseOutline />', () => {
22332236
errorFiles: ['error.css'],
22342237
});
22352238

2239+
let alerts = await screen.findAllByRole('alert');
2240+
// Exclude processing notification toast
2241+
alerts = alerts.filter((el) => !el.classList.contains('toast-container'));
22362242
// 3 alerts should be present
2237-
const alerts = await findAllByRole('alert');
22382243
expect(alerts.length).toEqual(3);
22392244

22402245
// check alerts for errorFiles

src/course-unit/CourseUnit.test.jsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import MockAdapter from 'axios-mock-adapter';
22
import {
3-
act, render, waitFor, fireEvent, within,
3+
act, render, waitFor, fireEvent, within, screen,
44
} from '@testing-library/react';
55
import userEvent from '@testing-library/user-event';
66
import { IntlProvider } from '@edx/frontend-platform/i18n';
@@ -525,17 +525,19 @@ describe('<CourseUnit />', () => {
525525
});
526526

527527
it('should display a warning alert for unpublished course unit version', async () => {
528-
const { getByRole } = render(<RootWrapper />);
528+
render(<RootWrapper />);
529529

530530
await waitFor(() => {
531-
const unpublishedAlert = getByRole('alert', { class: 'course-unit-unpublished-alert' });
531+
const unpublishedAlert = screen.getAllByRole('alert').find(
532+
(el) => el.classList.contains('alert-content'),
533+
);
532534
expect(unpublishedAlert).toHaveTextContent(messages.alertUnpublishedVersion.defaultMessage);
533535
expect(unpublishedAlert).toHaveClass('alert-warning');
534536
});
535537
});
536538

537539
it('should not display an unpublished alert for a course unit with explicit staff lock and unpublished status', async () => {
538-
const { queryByRole } = render(<RootWrapper />);
540+
render(<RootWrapper />);
539541

540542
axiosMock
541543
.onGet(getCourseUnitApiUrl(courseId))
@@ -547,8 +549,10 @@ describe('<CourseUnit />', () => {
547549
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
548550

549551
await waitFor(() => {
550-
const unpublishedAlert = queryByRole('alert', { class: 'course-unit-unpublished-alert' });
551-
expect(unpublishedAlert).toBeNull();
552+
const alert = screen.queryAllByRole('alert').find(
553+
(el) => el.classList.contains('alert-content'),
554+
);
555+
expect(alert).toBeUndefined();
552556
});
553557
});
554558

src/generic/delete-modal/DeleteModal.jsx

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
ActionRow,
44
Button,
55
AlertModal,
6+
StatefulButton,
67
} from '@openedx/paragon';
78
import { useIntl } from '@edx/frontend-platform/i18n';
89

@@ -15,6 +16,8 @@ const DeleteModal = ({
1516
onDeleteSubmit,
1617
title,
1718
description,
19+
variant,
20+
btnState,
1821
}) => {
1922
const intl = useIntl();
2023

@@ -26,20 +29,32 @@ const DeleteModal = ({
2629
title={modalTitle}
2730
isOpen={isOpen}
2831
onClose={close}
32+
variant={variant}
2933
footerNode={(
3034
<ActionRow>
31-
<Button variant="tertiary" onClick={close}>
35+
<Button
36+
variant="tertiary"
37+
onClick={(e) => {
38+
e.preventDefault();
39+
e.stopPropagation();
40+
close();
41+
}}
42+
>
3243
{intl.formatMessage(messages.cancelButton)}
3344
</Button>
34-
<Button
45+
<StatefulButton
3546
data-testid="delete-confirm-button"
47+
state={btnState}
3648
onClick={(e) => {
3749
e.preventDefault();
50+
e.stopPropagation();
3851
onDeleteSubmit();
3952
}}
40-
>
41-
{intl.formatMessage(messages.deleteButton, { category })}
42-
</Button>
53+
labels={{
54+
default: intl.formatMessage(messages.deleteButton),
55+
pending: intl.formatMessage(messages.pendingDeleteButton),
56+
}}
57+
/>
4358
</ActionRow>
4459
)}
4560
>
@@ -52,6 +67,8 @@ DeleteModal.defaultProps = {
5267
category: '',
5368
title: '',
5469
description: '',
70+
variant: 'default',
71+
btnState: 'default',
5572
};
5673

5774
DeleteModal.propTypes = {
@@ -61,6 +78,8 @@ DeleteModal.propTypes = {
6178
onDeleteSubmit: PropTypes.func.isRequired,
6279
title: PropTypes.string,
6380
description: PropTypes.string,
81+
variant: PropTypes.string,
82+
btnState: PropTypes.string,
6483
};
6584

6685
export default DeleteModal;

src/generic/delete-modal/messages.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ const messages = defineMessages({
1313
id: 'course-authoring.course-outline.delete-modal.button.delete',
1414
defaultMessage: 'Delete',
1515
},
16+
pendingDeleteButton: {
17+
id: 'course-authoring.course-outline.delete-modal.button.pending-delete',
18+
defaultMessage: 'Deleting',
19+
},
1620
cancelButton: {
1721
id: 'course-authoring.course-outline.delete-modal.button.cancel',
1822
defaultMessage: 'Cancel',
Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,15 @@
1-
.processing-notification {
2-
display: flex;
3-
position: fixed;
4-
bottom: -13rem;
5-
transition: bottom 1s;
6-
right: 1.25rem;
7-
padding: .625rem 1.25rem;
8-
z-index: $zindex-popover;
9-
10-
&.is-show {
11-
bottom: .625rem;
12-
}
1+
.processing-notification-icon {
2+
animation: rotate 1s linear infinite;
3+
}
134

14-
.processing-notification-icon {
15-
margin-right: .625rem;
16-
animation: rotate 1s linear infinite;
5+
.processing-notification-hide-close-button {
6+
.btn-icon {
7+
display: none;
178
}
9+
}
1810

19-
.processing-notification-title {
20-
font-size: 1rem;
21-
line-height: 1.5rem;
22-
color: $white;
23-
margin-bottom: 0;
24-
}
11+
.toast-container {
12+
right: 1.25rem;
13+
left: unset;
14+
z-index: $zindex-popover;
2515
}
Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,37 @@
1-
import React from 'react';
2-
import { render } from '@testing-library/react';
31
import { capitalize } from 'lodash';
2+
import userEvent from '@testing-library/user-event';
3+
import { initializeMocks, render, screen } from '../../testUtils';
44
import { NOTIFICATION_MESSAGES } from '../../constants';
55
import ProcessingNotification from '.';
66

7+
const mockUndo = jest.fn();
8+
79
const props = {
810
title: NOTIFICATION_MESSAGES.saving,
911
isShow: true,
12+
action: {
13+
label: 'Undo',
14+
onClick: mockUndo,
15+
},
1016
};
1117

1218
describe('<ProcessingNotification />', () => {
19+
beforeEach(() => {
20+
initializeMocks();
21+
});
22+
1323
it('renders successfully', () => {
14-
const { getByText } = render(<ProcessingNotification {...props} />);
15-
expect(getByText(capitalize(props.title))).toBeInTheDocument();
24+
render(<ProcessingNotification {...props} close={() => {}} />);
25+
expect(screen.getByText(capitalize(props.title))).toBeInTheDocument();
26+
expect(screen.getByText('Undo')).toBeInTheDocument();
27+
expect(screen.getByRole('alert').querySelector('.processing-notification-hide-close-button')).not.toBeInTheDocument();
28+
userEvent.click(screen.getByText('Undo'));
29+
expect(mockUndo).toBeCalled();
30+
});
31+
32+
it('add hide-close-button class if no close action is passed', () => {
33+
render(<ProcessingNotification {...props} />);
34+
expect(screen.getByText(capitalize(props.title))).toBeInTheDocument();
35+
expect(screen.getByRole('alert').querySelector('.processing-notification-hide-close-button')).toBeInTheDocument();
1636
});
1737
});
Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,40 @@
1-
import React from 'react';
21
import PropTypes from 'prop-types';
3-
import classNames from 'classnames';
4-
import { Badge, Icon } from '@openedx/paragon';
2+
import {
3+
Icon, Toast,
4+
} from '@openedx/paragon';
55
import { Settings as IconSettings } from '@openedx/paragon/icons';
66
import { capitalize } from 'lodash';
7+
import classNames from 'classnames';
78

8-
const ProcessingNotification = ({ isShow, title }) => (
9-
<Badge
10-
className={classNames('processing-notification', {
11-
'is-show': isShow,
12-
})}
13-
variant="secondary"
9+
const ProcessingNotification = ({
10+
isShow, title, action, close,
11+
}) => (
12+
<Toast
13+
className={classNames({ 'processing-notification-hide-close-button': !close })}
14+
show={isShow}
1415
aria-hidden={isShow}
16+
action={action && { ...action }}
17+
onClose={close || (() => {})}
1518
>
16-
<Icon className="processing-notification-icon" src={IconSettings} />
17-
<h2 className="processing-notification-title">
18-
{capitalize(title)}
19-
</h2>
20-
</Badge>
19+
<span className="d-flex align-items-center">
20+
<Icon className="processing-notification-icon mb-0 mr-2" src={IconSettings} />
21+
<span className="font-weight-bold h4 mb-0 text-white">{capitalize(title)}</span>
22+
</span>
23+
</Toast>
2124
);
2225

26+
ProcessingNotification.defaultProps = {
27+
close: null,
28+
};
29+
2330
ProcessingNotification.propTypes = {
2431
isShow: PropTypes.bool.isRequired,
2532
title: PropTypes.string.isRequired,
33+
action: PropTypes.shape({
34+
label: PropTypes.string.isRequired,
35+
onClick: PropTypes.func,
36+
}),
37+
close: PropTypes.func,
2638
};
2739

2840
export default ProcessingNotification;

0 commit comments

Comments
 (0)