Skip to content

Commit

Permalink
feat: import tags to existing taxonomy
Browse files Browse the repository at this point in the history
* feat: import tags to existing taxonomy

* feat: add re-import menu to taxonomy detail

* test: add tests

* fix: clean debug code

Co-authored-by: Jillian <[email protected]>

---------

Co-authored-by: Jillian <[email protected]>
  • Loading branch information
rpenido and pomegranited authored Nov 24, 2023
1 parent 5983953 commit 7756c7f
Show file tree
Hide file tree
Showing 16 changed files with 422 additions and 160 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
20 changes: 15 additions & 5 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(axiosMock.history.post[0].url).toEqual(getTaxonomyImportNewApiUrl());
expect(result).toEqual(taxonomyImportMock);
});

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

expect(axiosMock.history.put[0].url).toEqual(getTagsImportApiUrl(1));
expect(result).toEqual(tagImportMock);
});
});
92 changes: 65 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,32 @@ 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 */
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);
});
};
Loading

0 comments on commit 7756c7f

Please sign in to comment.