Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: import tags to existing taxonomy #11

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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