Skip to content

Commit

Permalink
feat: delete collection [FC-0062] (#1333)
Browse files Browse the repository at this point in the history
* feat: delete collection

* feat: update button status on delete

* test: add tests for collection delete
  • Loading branch information
navinkarkera authored Oct 8, 2024
1 parent 75f937e commit 434fea3
Show file tree
Hide file tree
Showing 14 changed files with 406 additions and 108 deletions.
27 changes: 16 additions & 11 deletions src/course-outline/CourseOutline.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ describe('<CourseOutline />', () => {
});

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

axiosMock
.onPost(getCourseBlockApiUrl(courseId), {
Expand All @@ -235,7 +235,7 @@ describe('<CourseOutline />', () => {
},
})
.reply(500);
const optionDropdown = await findByLabelText(statusBarMessages.videoSharingTitle.defaultMessage);
const optionDropdown = await screen.findByLabelText(statusBarMessages.videoSharingTitle.defaultMessage);
await act(
async () => fireEvent.change(optionDropdown, { target: { value: VIDEO_SHARING_OPTIONS.allOff } }),
);
Expand All @@ -247,8 +247,10 @@ describe('<CourseOutline />', () => {
},
}));

const alertElement = queryByRole('alert');
expect(alertElement).toHaveTextContent(
const alertElements = screen.queryAllByRole('alert');
expect(alertElements.find(
(el) => el.classList.contains('alert-content'),
)).toHaveTextContent(
pageAlertMessages.alertFailedGeneric.defaultMessage,
);
});
Expand Down Expand Up @@ -511,9 +513,10 @@ describe('<CourseOutline />', () => {
notificationDismissUrl: '/some/url',
});

const { findByRole } = render(<RootWrapper />);
expect(await findByRole('alert')).toBeInTheDocument();
const dismissBtn = await findByRole('button', { name: 'Dismiss' });
render(<RootWrapper />);
const alert = await screen.findByText(pageAlertMessages.configurationErrorTitle.defaultMessage);
expect(alert).toBeInTheDocument();
const dismissBtn = await screen.findByRole('button', { name: 'Dismiss' });
axiosMock
.onDelete('/some/url')
.reply(204);
Expand Down Expand Up @@ -2160,10 +2163,10 @@ describe('<CourseOutline />', () => {
});

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

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

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

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

// check alerts for errorFiles
Expand Down
16 changes: 10 additions & 6 deletions src/course-unit/CourseUnit.test.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import {
act, render, waitFor, fireEvent, within,
act, render, waitFor, fireEvent, within, screen,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@edx/frontend-platform/i18n';
Expand Down Expand Up @@ -525,17 +525,19 @@ describe('<CourseUnit />', () => {
});

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

await waitFor(() => {
const unpublishedAlert = getByRole('alert', { class: 'course-unit-unpublished-alert' });
const unpublishedAlert = screen.getAllByRole('alert').find(
(el) => el.classList.contains('alert-content'),
);
expect(unpublishedAlert).toHaveTextContent(messages.alertUnpublishedVersion.defaultMessage);
expect(unpublishedAlert).toHaveClass('alert-warning');
});
});

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

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

await waitFor(() => {
const unpublishedAlert = queryByRole('alert', { class: 'course-unit-unpublished-alert' });
expect(unpublishedAlert).toBeNull();
const alert = screen.queryAllByRole('alert').find(
(el) => el.classList.contains('alert-content'),
);
expect(alert).toBeUndefined();
});
});

Expand Down
29 changes: 24 additions & 5 deletions src/generic/delete-modal/DeleteModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ActionRow,
Button,
AlertModal,
StatefulButton,
} from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';

Expand All @@ -15,6 +16,8 @@ const DeleteModal = ({
onDeleteSubmit,
title,
description,
variant,
btnState,
}) => {
const intl = useIntl();

Expand All @@ -26,20 +29,32 @@ const DeleteModal = ({
title={modalTitle}
isOpen={isOpen}
onClose={close}
variant={variant}
footerNode={(
<ActionRow>
<Button variant="tertiary" onClick={close}>
<Button
variant="tertiary"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
close();
}}
>
{intl.formatMessage(messages.cancelButton)}
</Button>
<Button
<StatefulButton
data-testid="delete-confirm-button"
state={btnState}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onDeleteSubmit();
}}
>
{intl.formatMessage(messages.deleteButton, { category })}
</Button>
labels={{
default: intl.formatMessage(messages.deleteButton),
pending: intl.formatMessage(messages.pendingDeleteButton),
}}
/>
</ActionRow>
)}
>
Expand All @@ -52,6 +67,8 @@ DeleteModal.defaultProps = {
category: '',
title: '',
description: '',
variant: 'default',
btnState: 'default',
};

DeleteModal.propTypes = {
Expand All @@ -61,6 +78,8 @@ DeleteModal.propTypes = {
onDeleteSubmit: PropTypes.func.isRequired,
title: PropTypes.string,
description: PropTypes.string,
variant: PropTypes.string,
btnState: PropTypes.string,
};

export default DeleteModal;
4 changes: 4 additions & 0 deletions src/generic/delete-modal/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ const messages = defineMessages({
id: 'course-authoring.course-outline.delete-modal.button.delete',
defaultMessage: 'Delete',
},
pendingDeleteButton: {
id: 'course-authoring.course-outline.delete-modal.button.pending-delete',
defaultMessage: 'Deleting',
},
cancelButton: {
id: 'course-authoring.course-outline.delete-modal.button.cancel',
defaultMessage: 'Cancel',
Expand Down
32 changes: 11 additions & 21 deletions src/generic/processing-notification/ProccessingNotification.scss
Original file line number Diff line number Diff line change
@@ -1,25 +1,15 @@
.processing-notification {
display: flex;
position: fixed;
bottom: -13rem;
transition: bottom 1s;
right: 1.25rem;
padding: .625rem 1.25rem;
z-index: $zindex-popover;

&.is-show {
bottom: .625rem;
}
.processing-notification-icon {
animation: rotate 1s linear infinite;
}

.processing-notification-icon {
margin-right: .625rem;
animation: rotate 1s linear infinite;
.processing-notification-hide-close-button {
.btn-icon {
display: none;
}
}

.processing-notification-title {
font-size: 1rem;
line-height: 1.5rem;
color: $white;
margin-bottom: 0;
}
.toast-container {
right: 1.25rem;
left: unset;
z-index: $zindex-popover;
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,37 @@
import React from 'react';
import { render } from '@testing-library/react';
import { capitalize } from 'lodash';
import userEvent from '@testing-library/user-event';
import { initializeMocks, render, screen } from '../../testUtils';
import { NOTIFICATION_MESSAGES } from '../../constants';
import ProcessingNotification from '.';

const mockUndo = jest.fn();

const props = {
title: NOTIFICATION_MESSAGES.saving,
isShow: true,
action: {
label: 'Undo',
onClick: mockUndo,
},
};

describe('<ProcessingNotification />', () => {
beforeEach(() => {
initializeMocks();
});

it('renders successfully', () => {
const { getByText } = render(<ProcessingNotification {...props} />);
expect(getByText(capitalize(props.title))).toBeInTheDocument();
render(<ProcessingNotification {...props} close={() => {}} />);
expect(screen.getByText(capitalize(props.title))).toBeInTheDocument();
expect(screen.getByText('Undo')).toBeInTheDocument();
expect(screen.getByRole('alert').querySelector('.processing-notification-hide-close-button')).not.toBeInTheDocument();
userEvent.click(screen.getByText('Undo'));
expect(mockUndo).toBeCalled();
});

it('add hide-close-button class if no close action is passed', () => {
render(<ProcessingNotification {...props} />);
expect(screen.getByText(capitalize(props.title))).toBeInTheDocument();
expect(screen.getByRole('alert').querySelector('.processing-notification-hide-close-button')).toBeInTheDocument();
});
});
40 changes: 26 additions & 14 deletions src/generic/processing-notification/index.jsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,40 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Badge, Icon } from '@openedx/paragon';
import {
Icon, Toast,
} from '@openedx/paragon';
import { Settings as IconSettings } from '@openedx/paragon/icons';
import { capitalize } from 'lodash';
import classNames from 'classnames';

const ProcessingNotification = ({ isShow, title }) => (
<Badge
className={classNames('processing-notification', {
'is-show': isShow,
})}
variant="secondary"
const ProcessingNotification = ({
isShow, title, action, close,
}) => (
<Toast
className={classNames({ 'processing-notification-hide-close-button': !close })}
show={isShow}
aria-hidden={isShow}
action={action && { ...action }}
onClose={close || (() => {})}
>
<Icon className="processing-notification-icon" src={IconSettings} />
<h2 className="processing-notification-title">
{capitalize(title)}
</h2>
</Badge>
<span className="d-flex align-items-center">
<Icon className="processing-notification-icon mb-0 mr-2" src={IconSettings} />
<span className="font-weight-bold h4 mb-0 text-white">{capitalize(title)}</span>
</span>
</Toast>
);

ProcessingNotification.defaultProps = {
close: null,
};

ProcessingNotification.propTypes = {
isShow: PropTypes.bool.isRequired,
title: PropTypes.string.isRequired,
action: PropTypes.shape({
label: PropTypes.string.isRequired,
onClick: PropTypes.func,
}),
close: PropTypes.func,
};

export default ProcessingNotification;
Loading

0 comments on commit 434fea3

Please sign in to comment.