Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: delete collection [FC-0062] #1333

Merged
merged 19 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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