diff --git a/src/components/catalogSearchResults/CatalogSearchResults.jsx b/src/components/catalogSearchResults/CatalogSearchResults.jsx
index 5ba01f53..4fe2cb0e 100644
--- a/src/components/catalogSearchResults/CatalogSearchResults.jsx
+++ b/src/components/catalogSearchResults/CatalogSearchResults.jsx
@@ -427,13 +427,15 @@ export const BaseCatalogSearchResults = ({
/>
)}
{preview && isCourseType && searchResults?.nbHits !== 0 && (
-
-
-
+
diff --git a/src/components/catalogSearchResults/associatedComponents/downloadCsvButton/DownloadCsvButton.jsx b/src/components/catalogSearchResults/associatedComponents/downloadCsvButton/DownloadCsvButton.jsx
index f3340d6a..55f55a54 100644
--- a/src/components/catalogSearchResults/associatedComponents/downloadCsvButton/DownloadCsvButton.jsx
+++ b/src/components/catalogSearchResults/associatedComponents/downloadCsvButton/DownloadCsvButton.jsx
@@ -1,14 +1,21 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
-import { Toast, Button, useToggle } from '@openedx/paragon';
-import { Download } from '@openedx/paragon/icons';
+import {
+ Icon, Toast, useToggle, StatefulButton, Spinner,
+} from '@openedx/paragon';
+import { Check, Close, Download } from '@openedx/paragon/icons';
+import { saveAs } from 'file-saver';
+import { useIntl } from '@edx/frontend-platform/i18n';
import EnterpriseCatalogApiService from '../../../../data/services/EnterpriseCatalogAPIService';
const DownloadCsvButton = ({ facets, query }) => {
const [isOpen, open, close] = useToggle(false);
const [filters, setFilters] = useState();
+ const [buttonState, setButtonState] = useState('default');
+
+ const intl = useIntl();
const formatFilterText = (filterObject) => {
let filterString = '';
@@ -23,13 +30,25 @@ const DownloadCsvButton = ({ facets, query }) => {
const handleClick = () => {
formatFilterText(facets);
open();
- const downloadUrl = EnterpriseCatalogApiService.generateCsvDownloadLink(
+ setButtonState('pending');
+ EnterpriseCatalogApiService.generateCsvDownloadLink(
facets,
query,
- );
- global.location.href = downloadUrl;
+ ).then(response => {
+ const blob = new Blob([response.data], {
+ type: response.headers['content-type'],
+ });
+ const timestamp = new Date().toISOString();
+ saveAs(blob, `Enterprise-Catalog-Export-${timestamp}.xlsx`);
+ setButtonState('complete');
+ }).catch(() => setButtonState('error'));
};
- const toastText = `Downloaded with filters: ${filters}. Check website for the most up-to-date information on courses.`;
+
+ const toastText = intl.formatMessage({
+ id: 'catalogs.catalogSearchResults.downloadCsv.toastText',
+ defaultMessage: 'Downloaded with filters: {filters}. Check website for the most up-to-date information on courses.',
+ description: 'Toast text to be displayed when the user clicks the download button on the catalog page.',
+ }, { filters });
return (
<>
{isOpen && (
@@ -37,13 +56,40 @@ const DownloadCsvButton = ({ facets, query }) => {
{toastText}
)}
- ,
+ pending: ,
+ complete: ,
+ error: ,
+ }}
+ disabledStates={['disabled', 'pending']}
onClick={handleClick}
- >
- Download results
-
+ />
>
);
};
diff --git a/src/components/catalogSearchResults/associatedComponents/downloadCsvButton/DownloadCsvButton.test.jsx b/src/components/catalogSearchResults/associatedComponents/downloadCsvButton/DownloadCsvButton.test.jsx
index ec72de8f..ea939785 100644
--- a/src/components/catalogSearchResults/associatedComponents/downloadCsvButton/DownloadCsvButton.test.jsx
+++ b/src/components/catalogSearchResults/associatedComponents/downloadCsvButton/DownloadCsvButton.test.jsx
@@ -1,6 +1,7 @@
import React from 'react';
import { screen, act } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
+import { saveAs } from 'file-saver';
import userEvent from '@testing-library/user-event';
import DownloadCsvButton from './DownloadCsvButton';
@@ -8,12 +9,19 @@ import { renderWithRouter } from '../../../tests/testUtils';
import EnterpriseCatalogApiService from '../../../../data/services/EnterpriseCatalogAPIService';
// file-saver mocks
-jest.mock('file-saver', () => ({ saveAs: jest.fn() }));
+jest.mock('file-saver', () => ({
+ ...jest.requireActual('file-saver'),
+ saveAs: jest.fn(),
+}));
// eslint-disable-next-line func-names
global.Blob = function (content, options) {
return { content, options };
};
+const mockDate = new Date('2024-01-06T12:00:00Z');
+const mockTimestamp = mockDate.toISOString();
+global.Date = jest.fn(() => mockDate);
+
const mockCatalogApiService = jest.spyOn(
EnterpriseCatalogApiService,
'generateCsvDownloadLink',
@@ -37,6 +45,9 @@ delete global.location;
global.location = { href: assignMock };
describe('Download button', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
test('button renders and is clickable', async () => {
// Render the component
renderWithRouter();
@@ -48,10 +59,17 @@ describe('Download button', () => {
const input = screen.getByText('Download results');
userEvent.click(input);
});
- expect(mockCatalogApiService).toBeCalledWith(facets, 'foo');
+ expect(mockCatalogApiService).toHaveBeenCalledWith(facets, 'foo');
});
test('download button url encodes queries', async () => {
process.env.CATALOG_SERVICE_BASE_URL = 'foobar.com';
+ const mockResponse = {
+ data: 'mock-excel-data',
+ headers: {
+ 'content-type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ },
+ };
+ mockCatalogApiService.mockResolvedValue(mockResponse);
// Render the component
renderWithRouter();
// Expect to be in the default state
@@ -62,8 +80,17 @@ describe('Download button', () => {
const input = screen.getByText('Download results');
userEvent.click(input);
});
- const expectedWindowLocation = `${process.env.CATALOG_SERVICE_BASE_URL}/api/v1/enterprise-catalogs/catalog_workbook?availability=Available`
- + '+Now&availability=Upcoming&query=math%20%26%20science';
- expect(window.location.href).toEqual(expectedWindowLocation);
+ expect(mockCatalogApiService).toHaveBeenCalledTimes(1);
+ expect(mockCatalogApiService).toHaveBeenCalledWith(smallFacets, 'math & science');
+
+ expect(saveAs).toHaveBeenCalledWith(
+ expect.objectContaining({
+ content: ['mock-excel-data'],
+ options: {
+ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ },
+ }),
+ `Enterprise-Catalog-Export-${mockTimestamp}.xlsx`,
+ );
});
});
diff --git a/src/components/catalogs/styles/_enterprise_catalogs.scss b/src/components/catalogs/styles/_enterprise_catalogs.scss
index 400921f2..2773c0d0 100644
--- a/src/components/catalogs/styles/_enterprise_catalogs.scss
+++ b/src/components/catalogs/styles/_enterprise_catalogs.scss
@@ -33,9 +33,12 @@
}
.landing-page-download {
- float: right;
- @media only screen and (max-width: map-get($grid-breakpoints, 'md')) {
- float: none !important;
+ position: absolute;
+ top: -60px;
+ right: 0;
+ @media only screen and (max-width: map-get($grid-breakpoints, 'xl')) {
+ position: relative;
+ top: 0;
}
}
@@ -43,6 +46,7 @@
justify-content: space-between;
display: flex;
clear: right;
+ align-items: center;
}
.partner-logo-thumbnail {
diff --git a/src/data/services/EnterpriseCatalogAPIService.js b/src/data/services/EnterpriseCatalogAPIService.js
index e053060b..21b91835 100644
--- a/src/data/services/EnterpriseCatalogAPIService.js
+++ b/src/data/services/EnterpriseCatalogAPIService.js
@@ -1,3 +1,4 @@
+import axios from 'axios';
import { createQueryParams } from '../../utils/catalogUtils';
class EnterpriseCatalogApiService {
@@ -9,7 +10,8 @@ class EnterpriseCatalogApiService {
const enterpriseListUrl = `${
EnterpriseCatalogApiService.enterpriseCatalogServiceApiUrl
}/catalog_workbook?${queryParams}${facetQuery}`;
- return enterpriseListUrl;
+
+ return axios.get(enterpriseListUrl, { responseType: 'blob' });
}
}
diff --git a/src/data/services/EnterpriseCatalogAPIService.test.js b/src/data/services/EnterpriseCatalogAPIService.test.js
index 31f8e974..e3d55a51 100644
--- a/src/data/services/EnterpriseCatalogAPIService.test.js
+++ b/src/data/services/EnterpriseCatalogAPIService.test.js
@@ -1,17 +1,38 @@
import '@testing-library/jest-dom/extend-expect';
-
+import axios from 'axios';
import EnterpriseCatalogApiService from './EnterpriseCatalogAPIService';
-describe('generateCsvDownloadLink', () => {
- test('correctly formats csv download link', () => {
- const options = { enterprise_catalog_query_titles: 'test' };
- const query = 'somequery';
- const generatedDownloadLink = EnterpriseCatalogApiService.generateCsvDownloadLink(
- options,
- query,
- );
- expect(generatedDownloadLink).toEqual(
- `${process.env.CATALOG_SERVICE_BASE_URL}/api/v1/enterprise-catalogs/catalog_workbook?enterprise_catalog_query_titles=test&query=${query}`,
- );
+jest.mock('axios');
+
+describe('EnterpriseCatalogApiService', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('generateCsvDownloadLink', () => {
+ it('makes correct axios GET request with query parameters', async () => {
+ const options = { enterprise_catalog_query_titles: 'test' };
+ const query = 'somequery';
+ const expectedUrl = `${process.env.CATALOG_SERVICE_BASE_URL}/api/v1/enterprise-catalogs/catalog_workbook?enterprise_catalog_query_titles=test&query=somequery`;
+
+ const mockResponse = { data: 'test-data' };
+ axios.get.mockResolvedValue(mockResponse);
+
+ const result = await EnterpriseCatalogApiService.generateCsvDownloadLink(options, query);
+
+ expect(axios.get).toHaveBeenCalledTimes(1);
+ expect(axios.get).toHaveBeenCalledWith(expectedUrl, { responseType: 'blob' });
+ expect(result).toEqual(mockResponse);
+ });
+
+ it('handles axios error', async () => {
+ const options = { enterprise_catalog_query_titles: 'test' };
+ const error = new Error('Network error');
+ axios.get.mockRejectedValue(error);
+
+ await expect(
+ EnterpriseCatalogApiService.generateCsvDownloadLink(options),
+ ).rejects.toThrow('Network error');
+ });
});
});