Skip to content

Commit

Permalink
feat: convert download button to stateful button
Browse files Browse the repository at this point in the history
  • Loading branch information
omar-sarfraz committed Jan 6, 2025
1 parent 9260cbd commit 3152c32
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 40 deletions.
16 changes: 9 additions & 7 deletions src/components/catalogSearchResults/CatalogSearchResults.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -427,13 +427,15 @@ export const BaseCatalogSearchResults = ({
/>
)}
{preview && isCourseType && searchResults?.nbHits !== 0 && (
<span className="landing-page-download mt-n5 mb-2">
<DownloadCsvButton
// eslint-disable-next-line no-underscore-dangle
facets={searchResults?._state.disjunctiveFacetsRefinements}
query={inputQuery}
/>
</span>
<div className="position-relative">

Check warning on line 430 in src/components/catalogSearchResults/CatalogSearchResults.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/catalogSearchResults/CatalogSearchResults.jsx#L430

Added line #L430 was not covered by tests
<span className="landing-page-download mb-2">
<DownloadCsvButton
// eslint-disable-next-line no-underscore-dangle
facets={searchResults?._state.disjunctiveFacetsRefinements}
query={inputQuery}
/>
</span>
</div>
)}
<div className="preview-title">
<p className="h2 ml-2 mt-3 mb-3">
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = '';
Expand All @@ -23,27 +30,66 @@ 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'));

Check warning on line 44 in src/components/catalogSearchResults/associatedComponents/downloadCsvButton/DownloadCsvButton.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/catalogSearchResults/associatedComponents/downloadCsvButton/DownloadCsvButton.jsx#L44

Added line #L44 was not covered by tests
};
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.',
});
return (
<>
{isOpen && (
<Toast onClose={close} show={isOpen}>
{toastText}
</Toast>
)}
<Button
className="ml-2 download-button"
iconBefore={Download}
<StatefulButton
state={buttonState}
variant={buttonState === 'error' ? 'danger' : 'primary'}
labels={{
default: intl.formatMessage({
id: 'catalogs.catalogSearchResults.downloadCsv.button.default',
defaultMessage: 'Download results',
description: 'Label for the download button on the catalog search results page.',
}),
pending: intl.formatMessage({
id: 'catalogs.catalogSearchResults.downloadCsv.button.pending',
defaultMessage: 'Downloading results',
description: 'Label for the download button on the catalog search results page when the download is in progress.',
}),
complete: intl.formatMessage({
id: 'catalogs.catalogSearchResults.downloadCsv.button.complete',
defaultMessage: 'Download complete',
description: 'Label for the download button on the catalog search results page when the download is complete.',
}),
error: intl.formatMessage({
id: 'catalogs.catalogSearchResults.downloadCsv.button.error',
defaultMessage: 'Error',
description: 'Label for the download button on the catalog search results page when the download fails.',
}),
}}
icons={{
default: <Icon src={Download} />,
pending: <Spinner animation="border" variant="light" size="sm" />,
complete: <Icon src={Check} />,
error: <Icon src={Close} variant="light" size="sm" />,
}}
disabledStates={['disabled', 'pending']}
onClick={handleClick}
>
Download results
</Button>
/>
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
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';
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',
Expand All @@ -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(<DownloadCsvButton {...defaultProps} />);
Expand All @@ -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(<DownloadCsvButton {...badQueryProps} />);
// Expect to be in the default state
Expand All @@ -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`,
);
});
});
10 changes: 7 additions & 3 deletions src/components/catalogs/styles/_enterprise_catalogs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,20 @@
}

.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;
}
}

.preview-title {
justify-content: space-between;
display: flex;
clear: right;
align-items: center;
}

.partner-logo-thumbnail {
Expand Down
4 changes: 3 additions & 1 deletion src/data/services/EnterpriseCatalogAPIService.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import axios from 'axios';
import { createQueryParams } from '../../utils/catalogUtils';

class EnterpriseCatalogApiService {
Expand All @@ -9,7 +10,8 @@ class EnterpriseCatalogApiService {
const enterpriseListUrl = `${
EnterpriseCatalogApiService.enterpriseCatalogServiceApiUrl
}/catalog_workbook?${queryParams}${facetQuery}`;
return enterpriseListUrl;

return axios.get(enterpriseListUrl, { responseType: 'blob' });
}
}

Expand Down
45 changes: 33 additions & 12 deletions src/data/services/EnterpriseCatalogAPIService.test.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
});

0 comments on commit 3152c32

Please sign in to comment.