feat: Show toast when exporting course tags (#995)
Show in in-progress toast when exporting course tags
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
Layout,
|
||||
Row,
|
||||
TransitionReplace,
|
||||
Toast,
|
||||
} from '@openedx/paragon';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import {
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
SortableContext,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { LoadingSpinner } from '../generic/Loading';
|
||||
import { getProcessingNotification } from '../generic/processing-notification/data/selectors';
|
||||
@@ -52,9 +54,11 @@ import {
|
||||
} from '../generic/drag-helper/utils';
|
||||
import { useCourseOutline } from './hooks';
|
||||
import messages from './messages';
|
||||
import { getTagsExportFile } from './data/api';
|
||||
|
||||
const CourseOutline = ({ courseId }) => {
|
||||
const intl = useIntl();
|
||||
const location = useLocation();
|
||||
|
||||
const {
|
||||
courseName,
|
||||
@@ -117,6 +121,23 @@ const CourseOutline = ({ courseId }) => {
|
||||
errors,
|
||||
} = useCourseOutline({ courseId });
|
||||
|
||||
// Use `setToastMessage` to show the toast.
|
||||
const [toastMessage, setToastMessage] = useState(/** @type{null|string} */ (null));
|
||||
|
||||
useEffect(() => {
|
||||
if (location.hash === '#export-tags') {
|
||||
setToastMessage(intl.formatMessage(messages.exportTagsCreatingToastMessage));
|
||||
getTagsExportFile(courseId, courseName).then(() => {
|
||||
setToastMessage(intl.formatMessage(messages.exportTagsSuccessToastMessage));
|
||||
}).catch(() => {
|
||||
setToastMessage(intl.formatMessage(messages.exportTagsErrorToastMessage));
|
||||
});
|
||||
|
||||
// Delete `#export-tags` from location
|
||||
window.location.href = '#';
|
||||
}
|
||||
}, [location]);
|
||||
|
||||
const [sections, setSections] = useState(sectionsList);
|
||||
|
||||
const restoreSectionList = () => {
|
||||
@@ -458,6 +479,15 @@ const CourseOutline = ({ courseId }) => {
|
||||
onInternetConnectionFailed={handleInternetConnectionFailed}
|
||||
/>
|
||||
</div>
|
||||
{toastMessage && (
|
||||
<Toast
|
||||
show
|
||||
onClose={/* istanbul ignore next */ () => setToastMessage(null)}
|
||||
data-testid="taxonomy-toast"
|
||||
>
|
||||
{toastMessage}
|
||||
</Toast>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
act, render, waitFor, fireEvent, within,
|
||||
act, render, waitFor, fireEvent, within, screen,
|
||||
} from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
@@ -10,6 +10,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { closestCorners } from '@dnd-kit/core';
|
||||
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import {
|
||||
getCourseBestPracticesApiUrl,
|
||||
getCourseLaunchApiUrl,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
getCourseBlockApiUrl,
|
||||
getCourseItemApiUrl,
|
||||
getXBlockBaseApiUrl,
|
||||
exportTags,
|
||||
} from './data/api';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import {
|
||||
@@ -74,9 +76,7 @@ global.BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: () => ({
|
||||
pathname: mockPathname,
|
||||
}),
|
||||
useLocation: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../help-urls/hooks', () => ({
|
||||
@@ -135,6 +135,10 @@ describe('<CourseOutline />', () => {
|
||||
},
|
||||
});
|
||||
|
||||
useLocation.mockReturnValue({
|
||||
pathname: mockPathname,
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock
|
||||
@@ -2248,4 +2252,38 @@ describe('<CourseOutline />', () => {
|
||||
// check pasteFileNotices in store
|
||||
expect(store.getState().courseOutline.pasteFileNotices).toEqual({});
|
||||
});
|
||||
|
||||
it('should show toats on export tags', async () => {
|
||||
const expectedResponse = 'this is a test';
|
||||
axiosMock
|
||||
.onGet(exportTags(courseId))
|
||||
.reply(200, expectedResponse);
|
||||
useLocation.mockReturnValue({
|
||||
pathname: '/foo-bar',
|
||||
hash: '#export-tags',
|
||||
});
|
||||
window.URL.createObjectURL = jest.fn().mockReturnValue('http://example.com/archivo');
|
||||
window.URL.revokeObjectURL = jest.fn();
|
||||
render(<RootWrapper />);
|
||||
expect(await screen.findByText('Please wait. Creating export file for course tags...')).toBeInTheDocument();
|
||||
|
||||
const expectedRequest = axiosMock.history.get.filter(request => request.url === exportTags(courseId));
|
||||
expect(expectedRequest.length).toBe(1);
|
||||
|
||||
expect(await screen.findByText('Course tags exported successfully')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show toast on export tags error', async () => {
|
||||
axiosMock
|
||||
.onGet(exportTags(courseId))
|
||||
.reply(404);
|
||||
useLocation.mockReturnValue({
|
||||
pathname: '/foo-bar',
|
||||
hash: '#export-tags',
|
||||
});
|
||||
|
||||
render(<RootWrapper />);
|
||||
expect(await screen.findByText('Please wait. Creating export file for course tags...')).toBeInTheDocument();
|
||||
expect(await screen.findByText('An error has occurred creating the file')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,6 +29,7 @@ export const getXBlockBaseApiUrl = () => `${getApiBaseUrl()}/xblock/`;
|
||||
export const getCourseItemApiUrl = (itemId) => `${getXBlockBaseApiUrl()}${itemId}`;
|
||||
export const getXBlockApiUrl = (blockId) => `${getXBlockBaseApiUrl()}outline/${blockId}`;
|
||||
export const getClipboardUrl = () => `${getApiBaseUrl()}/api/content-staging/v1/clipboard/`;
|
||||
export const exportTags = (courseId) => `${getApiBaseUrl()}/api/content_tagging/v1/object_tags/${courseId}/export/`;
|
||||
|
||||
/**
|
||||
* @typedef {Object} courseOutline
|
||||
@@ -459,3 +460,33 @@ export async function dismissNotification(url) {
|
||||
await getAuthenticatedHttpClient()
|
||||
.delete(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the file of the exported tags
|
||||
* @param {string} courseId The ID of the content
|
||||
* @returns void
|
||||
*/
|
||||
export async function getTagsExportFile(courseId, courseName) {
|
||||
// Gets exported tags and builds the blob to download CSV file.
|
||||
// This can be done with this code:
|
||||
// `window.location.href = exportTags(contentId);`
|
||||
// but it is done in this way so we know when the operation ends to close the toast.
|
||||
const response = await getAuthenticatedHttpClient().get(exportTags(courseId), {
|
||||
responseType: 'blob',
|
||||
});
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (response.status !== 200) {
|
||||
throw response.statusText;
|
||||
}
|
||||
|
||||
const blob = new Blob([response.data], { type: 'text/csv' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${courseName}.csv`;
|
||||
a.click();
|
||||
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,21 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.course-outline.section-list.button.new-section',
|
||||
defaultMessage: 'New section',
|
||||
},
|
||||
exportTagsCreatingToastMessage: {
|
||||
id: 'course-authoring.course-outline.export-tags.toast.creating.message',
|
||||
defaultMessage: 'Please wait. Creating export file for course tags...',
|
||||
description: 'In progress message in toast when exporting tags of a course',
|
||||
},
|
||||
exportTagsSuccessToastMessage: {
|
||||
id: 'course-authoring.course-outline.export-tags.toast.success.message',
|
||||
defaultMessage: 'Course tags exported successfully',
|
||||
description: 'Success message in toast when exporting tags of a course',
|
||||
},
|
||||
exportTagsErrorToastMessage: {
|
||||
id: 'course-authoring.course-outline.export-tags.toast.error.message',
|
||||
defaultMessage: 'An error has occurred creating the file',
|
||||
description: 'Error message in toast when exporting tags of a course',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -69,7 +69,7 @@ export const getToolsMenuItems = ({ studioBaseUrl, courseId, intl }) => ([
|
||||
},
|
||||
...(getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true'
|
||||
? [{
|
||||
href: `${studioBaseUrl}/api/content_tagging/v1/object_tags/${courseId}/export/`,
|
||||
href: '#export-tags',
|
||||
title: intl.formatMessage(messages['header.links.exportTags']),
|
||||
}] : []
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user