Skip to content

Commit

Permalink
feat: Use windows.location.href to call the export endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
ChrisChV committed Oct 20, 2023
1 parent 5e12a8f commit 09aa8b6
Show file tree
Hide file tree
Showing 7 changed files with 39 additions and 93 deletions.
37 changes: 5 additions & 32 deletions src/taxonomy/api/hooks/api.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
// @ts-check
import { useQuery, useMutation } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { downloadDataAsFile } from '../../../utils';

const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
const getTaxonomyListApiUrl = () => new URL('api/content_tagging/v1/taxonomies/?enabled=true', getApiBaseUrl()).href;
const getExportTaxonomyApiUrl = (pk, format) => new URL(
`api/content_tagging/v1/taxonomies/${pk}/export/?output_format=${format}`,
export const getExportTaxonomyApiUrl = (pk, format) => new URL(
`api/content_tagging/v1/taxonomies/${pk}/export/?output_format=${format}&download=1`,
getApiBaseUrl(),
).href;

Expand All @@ -22,32 +21,6 @@ export const useTaxonomyListData = () => (
})
);

export const useExportTaxonomy = () => {
/**
* Calls the export request and downloads the file.
*
* Extra logic is needed to download the exported file,
* because it is not possible to download the file using the Content-Disposition header
* Ref: https://medium.com/@drevets/you-cant-prompt-a-file-download-with-the-content-disposition-header-using-axios-xhr-sorry-56577aa706d6
*
* @param {import("../types.mjs").ExportRequestParams} params
* @returns {Promise<void>}
*/
const exportTaxonomy = async (params) => {
const { pk, format, name } = params;
const response = await getAuthenticatedHttpClient().get(getExportTaxonomyApiUrl(pk, format));
const contentType = response.headers['content-type'];
let fileExtension = '';
let data;
if (contentType === 'application/json') {
fileExtension = 'json';
data = JSON.stringify(response.data, null, 2);
} else {
fileExtension = 'csv';
data = response.data;
}
downloadDataAsFile(data, contentType, `${name}.${fileExtension}`);
};

return useMutation(exportTaxonomy);
export const exportTaxonomy = (pk, format) => {
window.location.href = getExportTaxonomyApiUrl(pk, format);
};
54 changes: 22 additions & 32 deletions src/taxonomy/api/hooks/api.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useQuery, useMutation } from '@tanstack/react-query';
import { useTaxonomyListData, useExportTaxonomy } from './api';
import { downloadDataAsFile } from '../../../utils';
import { useQuery } from '@tanstack/react-query';
import { useTaxonomyListData, exportTaxonomy } from './api';

const mockHttpClient = {
get: jest.fn(),
Expand All @@ -19,53 +18,44 @@ jest.mock('../../../utils', () => ({
downloadDataAsFile: jest.fn(),
}));

describe('taxonomy API', () => {
describe('useTaxonomyListData', () => {
afterEach(() => {
jest.clearAllMocks();
});

it('useTaxonomyListData should call useQuery with the correct parameters', () => {
it('should call useQuery with the correct parameters', () => {
useTaxonomyListData();

expect(useQuery).toHaveBeenCalledWith({
queryKey: ['taxonomyList'],
queryFn: expect.any(Function),
});
});
});

it('useExportTaxonomy should export data correctly', async () => {
useMutation.mockImplementation((exportFunc) => exportFunc);
describe('exportTaxonomy', () => {
const { location } = window;

const mockResponseJson = {
headers: {
'content-type': 'application/json',
},
data: { tags: 'tags' },
};
const mockResponseCsv = {
headers: {
'content-type': 'text',
},
data: 'This is a CSV',
beforeAll(() => {
delete window.location;
window.location = {
href: '',
};
});

const exportTaxonomy = useExportTaxonomy();
afterAll(() => {
window.location = location;
});

mockHttpClient.get.mockResolvedValue(mockResponseJson);
await exportTaxonomy({ pk: 1, format: 'json', name: 'testFile' });
it('should set window.location.href correctly', () => {
const pk = 1;
const format = 'json';

expect(downloadDataAsFile).toHaveBeenCalledWith(
JSON.stringify(mockResponseJson.data, null, 2),
'application/json',
'testFile.json',
);
exportTaxonomy(pk, format);

mockHttpClient.get.mockResolvedValue(mockResponseCsv);
await exportTaxonomy({ pk: 1, format: 'csv', name: 'testFile' });
expect(downloadDataAsFile).toHaveBeenCalledWith(
mockResponseCsv.data,
'text',
'testFile.csv',
expect(window.location.href).toEqual(
'http://localhost:18010/api/content_tagging/'
+ 'v1/taxonomies/1/export/?output_format=json&download=1',
);
});
});
6 changes: 3 additions & 3 deletions src/taxonomy/api/hooks/selectors.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @ts-check
import {
useTaxonomyListData,
useExportTaxonomy,
exportTaxonomy,
} from './api';

/**
Expand All @@ -22,6 +22,6 @@ export const useIsTaxonomyListDataLoaded = () => (
useTaxonomyListData().status === 'success'
);

export const useExportTaxonomyMutation = () => (
useExportTaxonomy()
export const callExportTaxonomy = (pk, format) => (
exportTaxonomy(pk, format)
);
14 changes: 7 additions & 7 deletions src/taxonomy/api/hooks/selectors.test.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import {
useTaxonomyListDataResponse,
useIsTaxonomyListDataLoaded,
useExportTaxonomyMutation,
callExportTaxonomy,
} from './selectors';
import { useTaxonomyListData, useExportTaxonomy } from './api';
import { useTaxonomyListData, exportTaxonomy } from './api';

jest.mock('./api', () => ({
__esModule: true,
useTaxonomyListData: jest.fn(),
useExportTaxonomy: jest.fn(),
exportTaxonomy: jest.fn(),
}));

describe('useTaxonomyListDataResponse', () => {
Expand Down Expand Up @@ -47,10 +47,10 @@ describe('useIsTaxonomyListDataLoaded', () => {
});
});

describe('useExportTaxonomyMutation', () => {
it('should trigger useExportTaxonomy', () => {
useExportTaxonomyMutation();
describe('callExportTaxonomy', () => {
it('should trigger exportTaxonomy', () => {
callExportTaxonomy(1, 'csv');

expect(useExportTaxonomy).toHaveBeenCalled();
expect(exportTaxonomy).toHaveBeenCalled();
});
});
11 changes: 2 additions & 9 deletions src/taxonomy/modals/ExportModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,19 @@ import {
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from '../messages';
import { useExportTaxonomyMutation } from '../api/hooks/selectors';
import { callExportTaxonomy } from '../api/hooks/selectors';

const ExportModal = ({
taxonomyId,
taxonomyName,
isOpen,
onClose,
intl,
}) => {
const [outputFormat, setOutputFormat] = useState('csv');
const exportMutation = useExportTaxonomyMutation();

const onClickExport = () => {
onClose();
exportMutation.mutate({
pk: taxonomyId,
format: outputFormat,
name: taxonomyName,
});
callExportTaxonomy(taxonomyId, outputFormat);

Check warning on line 23 in src/taxonomy/modals/ExportModal.jsx

View check run for this annotation

Codecov / codecov/patch

src/taxonomy/modals/ExportModal.jsx#L22-L23

Added lines #L22 - L23 were not covered by tests
};

return (
Expand Down Expand Up @@ -84,7 +78,6 @@ const ExportModal = ({

ExportModal.propTypes = {
taxonomyId: PropTypes.number.isRequired,
taxonomyName: PropTypes.string.isRequired,
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
intl: intlShape.isRequired,
Expand Down
1 change: 0 additions & 1 deletion src/taxonomy/taxonomy-card/TaxonomyCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ const TaxonomyCard = ({ className, original, intl }) => {
isOpen={isExportModalOpen}
onClose={() => setIsExportModalOpen(false)}
taxonomyId={id}
taxonomyName={name}
/>
)}
</>
Expand Down
9 changes: 0 additions & 9 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -256,12 +256,3 @@ export const isValidDate = (date) => {

return Boolean(formattedValue.length <= 10);
};

export const downloadDataAsFile = (data, contentType, fileName) => {
const url = window.URL.createObjectURL(new Blob([data], { type: contentType }));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
};

0 comments on commit 09aa8b6

Please sign in to comment.