Skip to content

Commit

Permalink
feat: import tags to existing taxonomy
Browse files Browse the repository at this point in the history
  • Loading branch information
rpenido committed Nov 21, 2023
1 parent bb90002 commit d8bc6c1
Show file tree
Hide file tree
Showing 10 changed files with 138 additions and 38 deletions.
2 changes: 1 addition & 1 deletion src/taxonomy/import-tags/__mocks__/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// eslint-disable-next-line import/prefer-default-export
export { default as taxonomyImportMock } from './taxonomyImportMock';
export { default as tagImportMock } from './tagImportMock';
4 changes: 4 additions & 0 deletions src/taxonomy/import-tags/__mocks__/tagImportMock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default {
name: 'Taxonomy name',
description: 'Taxonomy description',
};
31 changes: 29 additions & 2 deletions src/taxonomy/import-tags/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,20 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;

export const getTaxonomyImportApiUrl = () => new URL(
export const getTaxonomyImportNewApiUrl = () => new URL(
'api/content_tagging/v1/taxonomies/import/',
getApiBaseUrl(),
).href;

/**
* @param {number} taxonomyId
* @returns {string}
*/
export const getTagsImportApiUrl = (taxonomyId) => new URL(
`api/content_tagging/v1/taxonomies/${taxonomyId}/tags/import/`,
getApiBaseUrl(),
).href;

/**
* Import a new taxonomy
* @param {string} taxonomyName
Expand All @@ -23,7 +32,25 @@ export async function importNewTaxonomy(taxonomyName, taxonomyDescription, file)
formData.append('file', file);

const { data } = await getAuthenticatedHttpClient().post(
getTaxonomyImportApiUrl(),
getTaxonomyImportNewApiUrl(),
formData,
);

return camelCaseObject(data);
}

/**
* Import tags to an existing taxonomy, overwriting existing tags
* @param {number} taxonomyId
* @param {File} file
* @returns {Promise<Object>}
*/
export async function importTags(taxonomyId, file) {
const formData = new FormData();
formData.append('file', file);

const { data } = await getAuthenticatedHttpClient().put(
getTagsImportApiUrl(taxonomyId),
formData,
);

Expand Down
18 changes: 14 additions & 4 deletions src/taxonomy/import-tags/data/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import MockAdapter from 'axios-mock-adapter';
import { initializeMockApp } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

import { taxonomyImportMock } from '../__mocks__';
import { tagImportMock, taxonomyImportMock } from '../__mocks__';

import {
getTaxonomyImportApiUrl,
getTaxonomyImportNewApiUrl,
getTagsImportApiUrl,
importNewTaxonomy,
importTags,
} from './api';

let axiosMock;
Expand All @@ -28,11 +30,19 @@ describe('import taxonomy api calls', () => {
jest.clearAllMocks();
});

it('should call import taxonomy', async () => {
axiosMock.onPost(getTaxonomyImportApiUrl()).reply(201, taxonomyImportMock);
it('should call import new taxonomy', async () => {
axiosMock.onPost(getTaxonomyImportNewApiUrl()).reply(201, taxonomyImportMock);
const result = await importNewTaxonomy('Taxonomy name', 'Taxonomy description');

expect(axiosMock.history.post[0].url).toEqual(getTaxonomyImportApiUrl());
expect(result).toEqual(taxonomyImportMock);
});

it('should call import tags', async () => {
axiosMock.onPost(getTagsImportApiUrl(1)).reply(200, tagImportMock);
const result = await importTags(1);

expect(axiosMock.history.post[0].url).toEqual(getTagsImportApiUrl(1));
expect(result).toEqual(tagImportMock);
});
});
93 changes: 66 additions & 27 deletions src/taxonomy/import-tags/data/utils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,41 @@
// ts-check
import messages from '../messages';
import { importNewTaxonomy } from './api';
import { importNewTaxonomy, importTags } from './api';

/*
* This function get a file from the user. It does this by creating a
* file input element, and then clicking it. This allows us to get a file
* from the user without using a form. The file input element is created
* and appended to the DOM, then clicked. When the user selects a file,
* the change event is fired, and the file is resolved.
* The file input element is then removed from the DOM.
*/
const selectFile = async () => new Promise((resolve) => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.json,.csv';
fileInput.style.display = 'none';
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (!file) {
resolve(null);
}
resolve(file);
document.body.removeChild(fileInput);
}, false);

fileInput.addEventListener('cancel', () => {
resolve(null);
document.body.removeChild(fileInput);
}, false);

document.body.appendChild(fileInput);

// Calling click() directly was not working as expected, so we use setTimeout
// to ensure the file input is added to the DOM before clicking it.
setTimeout(() => fileInput.click(), 0);
});

// eslint-disable-next-line import/prefer-default-export
export const importTaxonomy = async (intl) => {
/*
* This function is a temporary "Barebones" implementation of the import
Expand All @@ -12,31 +46,6 @@ export const importTaxonomy = async (intl) => {
/* eslint-disable no-alert */
/* eslint-disable no-console */

const selectFile = async () => new Promise((resolve) => {
/*
* This function get a file from the user. It does this by creating a
* file input element, and then clicking it. This allows us to get a file
* from the user without using a form. The file input element is created
* and appended to the DOM, then clicked. When the user selects a file,
* the change event is fired, and the file is resolved.
* The file input element is then removed from the DOM.
*/
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.json,.csv';
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (!file) {
resolve(null);
}
resolve(file);
document.body.removeChild(fileInput);
});

document.body.appendChild(fileInput);
fileInput.click();
});

const getTaxonomyName = () => {
let taxonomyName = null;
while (!taxonomyName) {
Expand Down Expand Up @@ -80,3 +89,33 @@ export const importTaxonomy = async (intl) => {
console.error(error.response);
});
};

export const importTaxonomyTags = async (taxonomyId, intl) => {
/*
* This function is a temporary "Barebones" implementation of the import
* functionality with `confirm` and `alert`. It is intended to be replaced
* with a component that shows a `ModalDialog` in the future.
* See: https://github.com/openedx/modular-learning/issues/126
*/
/* eslint-disable no-alert */
/* eslint-disable no-console */
console.log(intl);
const file = await selectFile();

if (!file) {
return;
}

if (!window.confirm(intl.formatMessage(messages.confirmImportTags))) {
return;
}

importTags(taxonomyId, file)
.then(() => {
alert(intl.formatMessage(messages.importTaxonomySuccess));
})
.catch((error) => {
alert(intl.formatMessage(messages.importTaxonomyError));
console.error(error.response);
});
};
5 changes: 3 additions & 2 deletions src/taxonomy/import-tags/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { importTaxonomy } from './data/utils';
import { importTaxonomyTags, importTaxonomy } from './data/utils';

export {
importTaxonomy, // eslint-disable-line import/prefer-default-export
importTaxonomyTags,
importTaxonomy,
};
7 changes: 7 additions & 0 deletions src/taxonomy/import-tags/messages.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// ts-check
import { defineMessages } from '@edx/frontend-platform/i18n';

const messages = defineMessages({
Expand All @@ -21,6 +22,12 @@ const messages = defineMessages({
id: 'course-authoring.import-tags.error',
defaultMessage: 'Import failed - see details in the browser console',
},
confirmImportTags: {
id: 'course-authoring.import-tags.warning',
defaultMessage: 'Warning! You are about to overwrite all tags in this taxonomy. Any tags applied to course'
+ ' content will be updated or removed. This cannot be undone.'
+ '\n\nAre you sure you want to continue importing this file?',
},
});

export default messages;
8 changes: 7 additions & 1 deletion src/taxonomy/taxonomy-card/TaxonomyCardMenu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,13 @@ const TaxonomyCardMenu = ({
data-testid={`taxonomy-card-menu-button-${id}`}
/>
<Dropdown.Menu data-testid={`taxonomy-card-menu-${id}`}>
{/* Add more menu items here */}
<Dropdown.Item
className="taxonomy-menu-item"
data-testid={`taxonomy-card-menu-import-${id}`}
onClick={(e) => onClickItem(e, 'import')}
>
{intl.formatMessage(messages.taxonomyCardImportMenu)}
</Dropdown.Item>
<Dropdown.Item
className="taxonomy-menu-item"
data-testid={`taxonomy-card-menu-export-${id}`}
Expand Down
4 changes: 3 additions & 1 deletion src/taxonomy/taxonomy-card/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import classNames from 'classnames';
import { useIntl } from '@edx/frontend-platform/i18n';

import { importTaxonomyTags } from '../import-tags';
import messages from './messages';
import TaxonomyCardMenu from './TaxonomyCardMenu';
import ExportModal from '../export-modal';
Expand Down Expand Up @@ -72,8 +74,8 @@ const TaxonomyCard = ({ className, original }) => {
const intl = useIntl();
const [isExportModalOpen, setIsExportModalOpen] = useState(false);

// Add here more menu item actions
const menuItemActions = {
import: () => importTaxonomyTags(id, intl).then(() => console.log('resolved')),
export: () => setIsExportModalOpen(true),
};

Expand Down
4 changes: 4 additions & 0 deletions src/taxonomy/taxonomy-card/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ const messages = defineMessages({
id: 'course-authoring.taxonomy-list.orgs-count.label',
defaultMessage: 'Assigned to {orgsCount} orgs',
},
taxonomyCardImportMenu: {
id: 'course-authoring.taxonomy-list.menu.import.label',
defaultMessage: 'Re-import',
},
taxonomyCardExportMenu: {
id: 'course-authoring.taxonomy-list.menu.export.label',
defaultMessage: 'Export',
Expand Down

0 comments on commit d8bc6c1

Please sign in to comment.