diff --git a/src/taxonomy/TaxonomyListPage.jsx b/src/taxonomy/TaxonomyListPage.jsx index 79ca9982aa..44cf28029c 100644 --- a/src/taxonomy/TaxonomyListPage.jsx +++ b/src/taxonomy/TaxonomyListPage.jsx @@ -1,16 +1,39 @@ import React from 'react'; import { + Button, CardView, Container, DataTable, Spinner, } from '@edx/paragon'; +import { + Add, +} from '@edx/paragon/icons'; import { useIntl } from '@edx/frontend-platform/i18n'; import SubHeader from '../generic/sub-header/SubHeader'; +import { importTaxonomy } from './import-tags'; import messages from './messages'; import TaxonomyCard from './taxonomy-card'; import { useTaxonomyListDataResponse, useIsTaxonomyListDataLoaded } from './data/apiHooks'; +const TaxonomyListHeaderButtons = () => { + const intl = useIntl(); + return ( + <> + + + + ); +}; + const TaxonomyListPage = () => { const intl = useIntl(); const useTaxonomyListData = () => { @@ -21,12 +44,6 @@ const TaxonomyListPage = () => { const { taxonomyListData, isLoaded } = useTaxonomyListData(); - const getHeaderButtons = () => ( - // Download template and import buttons. - // TODO Add functionality to this buttons. - undefined - ); - const getOrgSelect = () => ( // Organization select component // TODO Add functionality to this component @@ -40,7 +57,7 @@ const TaxonomyListPage = () => { } hideBorder /> diff --git a/src/taxonomy/TaxonomyListPage.test.jsx b/src/taxonomy/TaxonomyListPage.test.jsx index 8e68347568..4bd0d6d02b 100644 --- a/src/taxonomy/TaxonomyListPage.test.jsx +++ b/src/taxonomy/TaxonomyListPage.test.jsx @@ -8,6 +8,7 @@ import initializeStore from '../store'; import TaxonomyListPage from './TaxonomyListPage'; import { useTaxonomyListDataResponse, useIsTaxonomyListDataLoaded } from './data/apiHooks'; +import { importTaxonomy } from './import-tags'; let store; @@ -16,6 +17,10 @@ jest.mock('./data/apiHooks', () => ({ useIsTaxonomyListDataLoaded: jest.fn(), })); +jest.mock('./import-tags', () => ({ + importTaxonomy: jest.fn(), +})); + const RootWrapper = () => ( @@ -65,4 +70,22 @@ describe('', async () => { expect(getByTestId('taxonomy-card-1')).toBeInTheDocument(); }); }); + + it('calls the import taxonomy action when the import button is clicked', async () => { + useIsTaxonomyListDataLoaded.mockReturnValue(true); + useTaxonomyListDataResponse.mockReturnValue({ + results: [{ + id: 1, + name: 'Taxonomy', + description: 'This is a description', + }], + }); + await act(async () => { + const { getByTestId } = render(); + const importButton = getByTestId('taxonomy-import-button'); + expect(importButton).toBeInTheDocument(); + importButton.click(); + expect(importTaxonomy).toHaveBeenCalled(); + }); + }); }); diff --git a/src/taxonomy/import-tags/__mocks__/index.js b/src/taxonomy/import-tags/__mocks__/index.js new file mode 100644 index 0000000000..ba0b48ccb9 --- /dev/null +++ b/src/taxonomy/import-tags/__mocks__/index.js @@ -0,0 +1,2 @@ +export { default as taxonomyImportMock } from './taxonomyImportMock'; +export { default as tagImportMock } from './tagImportMock'; diff --git a/src/taxonomy/import-tags/__mocks__/tagImportMock.js b/src/taxonomy/import-tags/__mocks__/tagImportMock.js new file mode 100644 index 0000000000..9db45b4a5e --- /dev/null +++ b/src/taxonomy/import-tags/__mocks__/tagImportMock.js @@ -0,0 +1,4 @@ +export default { + name: 'Taxonomy name', + description: 'Taxonomy description', +}; diff --git a/src/taxonomy/import-tags/__mocks__/taxonomyImportMock.js b/src/taxonomy/import-tags/__mocks__/taxonomyImportMock.js new file mode 100644 index 0000000000..9db45b4a5e --- /dev/null +++ b/src/taxonomy/import-tags/__mocks__/taxonomyImportMock.js @@ -0,0 +1,4 @@ +export default { + name: 'Taxonomy name', + description: 'Taxonomy description', +}; diff --git a/src/taxonomy/import-tags/data/api.js b/src/taxonomy/import-tags/data/api.js new file mode 100644 index 0000000000..befb2e977d --- /dev/null +++ b/src/taxonomy/import-tags/data/api.js @@ -0,0 +1,58 @@ +// @ts-check +import { camelCaseObject, getConfig } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; + +const getApiBaseUrl = () => getConfig().STUDIO_BASE_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 + * @param {string} taxonomyDescription + * @param {File} file + * @returns {Promise} + */ +export async function importNewTaxonomy(taxonomyName, taxonomyDescription, file) { + const formData = new FormData(); + formData.append('taxonomy_name', taxonomyName); + formData.append('taxonomy_description', taxonomyDescription); + formData.append('file', file); + + const { data } = await getAuthenticatedHttpClient().post( + getTaxonomyImportNewApiUrl(), + formData, + ); + + return camelCaseObject(data); +} + +/** + * Import tags to an existing taxonomy, overwriting existing tags + * @param {number} taxonomyId + * @param {File} file + * @returns {Promise} + */ +export async function importTags(taxonomyId, file) { + const formData = new FormData(); + formData.append('file', file); + + const { data } = await getAuthenticatedHttpClient().put( + getTagsImportApiUrl(taxonomyId), + formData, + ); + + return camelCaseObject(data); +} diff --git a/src/taxonomy/import-tags/data/api.test.js b/src/taxonomy/import-tags/data/api.test.js new file mode 100644 index 0000000000..0da9f84eae --- /dev/null +++ b/src/taxonomy/import-tags/data/api.test.js @@ -0,0 +1,48 @@ +import MockAdapter from 'axios-mock-adapter'; +import { initializeMockApp } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; + +import { tagImportMock, taxonomyImportMock } from '../__mocks__'; + +import { + getTaxonomyImportNewApiUrl, + getTagsImportApiUrl, + importNewTaxonomy, + importTags, +} from './api'; + +let axiosMock; + +describe('import taxonomy api calls', () => { + beforeEach(() => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + 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(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); + }); +}); diff --git a/src/taxonomy/import-tags/data/utils.js b/src/taxonomy/import-tags/data/utils.js new file mode 100644 index 0000000000..e46e76941e --- /dev/null +++ b/src/taxonomy/import-tags/data/utils.js @@ -0,0 +1,120 @@ +// ts-check +import messages from '../messages'; +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); +}); + +export const importTaxonomy = async (intl) => { + /* + * This function is a temporary "Barebones" implementation of the import + * functionality with `prompt` 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/116 + */ + /* eslint-disable no-alert */ + /* eslint-disable no-console */ + + const getTaxonomyName = () => { + let taxonomyName = null; + while (!taxonomyName) { + taxonomyName = prompt(intl.formatMessage(messages.promptTaxonomyName)); + + if (taxonomyName == null) { + break; + } + + if (!taxonomyName) { + alert(intl.formatMessage(messages.promptTaxonomyNameRequired)); + } + } + return taxonomyName; + }; + + const getTaxonomyDescription = () => prompt(intl.formatMessage(messages.promptTaxonomyDescription)); + + const file = await selectFile(); + + if (!file) { + return; + } + + const taxonomyName = getTaxonomyName(); + if (taxonomyName == null) { + return; + } + + const taxonomyDescription = getTaxonomyDescription(); + if (taxonomyDescription == null) { + return; + } + + importNewTaxonomy(taxonomyName, taxonomyDescription, file) + .then(() => { + alert(intl.formatMessage(messages.importTaxonomySuccess)); + }) + .catch((error) => { + alert(intl.formatMessage(messages.importTaxonomyError)); + 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); + }); +}; diff --git a/src/taxonomy/import-tags/data/utils.test.js b/src/taxonomy/import-tags/data/utils.test.js new file mode 100644 index 0000000000..ddcc029410 --- /dev/null +++ b/src/taxonomy/import-tags/data/utils.test.js @@ -0,0 +1,301 @@ +import { importTaxonomy, importTaxonomyTags } from './utils'; +import { importNewTaxonomy, importTags } from './api'; + +const mockAddEventListener = jest.fn(); + +const intl = { + formatMessage: jest.fn().mockImplementation((message) => message.defaultMessage), +}; + +jest.mock('./api', () => ({ + importNewTaxonomy: jest.fn().mockResolvedValue({}), + importTags: jest.fn().mockResolvedValue({}), +})); + +describe('import new taxonomy functions', () => { + let createElement; + let appendChild; + let removeChild; + + beforeEach(() => { + createElement = document.createElement; + document.createElement = jest.fn().mockImplementation((element) => { + if (element === 'input') { + return { + click: jest.fn(), + addEventListener: mockAddEventListener, + style: {}, + }; + } + return createElement(element); + }); + + appendChild = document.body.appendChild; + document.body.appendChild = jest.fn(); + + removeChild = document.body.removeChild; + document.body.removeChild = jest.fn(); + }); + + afterEach(() => { + jest.clearAllMocks(); + document.createElement = createElement; + document.body.appendChild = appendChild; + document.body.removeChild = removeChild; + }); + + describe('import new taxonomy', () => { + it('should call the api and show success alert', async () => { + jest.spyOn(window, 'prompt') + .mockReturnValueOnce('test taxonomy name') + .mockReturnValueOnce('test taxonomy description'); + jest.spyOn(window, 'alert').mockImplementation(() => {}); + + const promise = importTaxonomy(intl).then(() => { + expect(importNewTaxonomy).toHaveBeenCalledWith('test taxonomy name', 'test taxonomy description', 'mockFile'); + expect(window.alert).toHaveBeenCalledWith('Taxonomy imported successfully'); + }); + + // Capture the onChange handler from the file input element + const onChange = mockAddEventListener.mock.calls[0][1]; + const mockTarget = { + target: { + files: [ + 'mockFile', + ], + }, + }; + + onChange(mockTarget); + + return promise; + }); + + it('should ask for taxonomy name again if not provided', async () => { + jest.spyOn(window, 'prompt') + .mockReturnValueOnce('') + .mockReturnValueOnce('test taxonomy name') + .mockReturnValueOnce('test taxonomy description'); + jest.spyOn(window, 'alert').mockImplementation(() => {}); + + const promise = importTaxonomy(intl).then(() => { + expect(importNewTaxonomy).toHaveBeenCalledWith('test taxonomy name', 'test taxonomy description', 'mockFile'); + expect(window.alert).toHaveBeenCalledWith('You must enter a name for the new taxonomy'); + expect(window.alert).toHaveBeenCalledWith('Taxonomy imported successfully'); + }); + + // Capture the onChange handler from the file input element + const onChange = mockAddEventListener.mock.calls[0][1]; + const mockTarget = { + target: { + files: [ + 'mockFile', + ], + }, + }; + + onChange(mockTarget); + + return promise; + }); + + it('should call the api and return error alert', async () => { + jest.spyOn(window, 'prompt') + .mockReturnValueOnce('test taxonomy name') + .mockReturnValueOnce('test taxonomy description'); + importNewTaxonomy.mockRejectedValue(new Error('test error')); + + const promise = importTaxonomy(intl).then(() => { + expect(importNewTaxonomy).toHaveBeenCalledWith('test taxonomy name', 'test taxonomy description', 'mockFile'); + }); + + // Capture the onChange handler from the file input element + const onChange = mockAddEventListener.mock.calls[0][1]; + const mockTarget = { + target: { + files: [ + 'mockFile', + ], + }, + }; + + onChange(mockTarget); + + return promise; + }); + + it('should abort the call to the api without file', async () => { + const promise = importTaxonomy(intl).then(() => { + expect(importNewTaxonomy).not.toHaveBeenCalled(); + }); + + // Capture the onChange handler from the file input element + const onChange = mockAddEventListener.mock.calls[0][1]; + const mockTarget = { + target: { + files: [null], + }, + }; + + onChange(mockTarget); + return promise; + }); + + it('should abort the call to the api if file closed', async () => { + const promise = importTaxonomy(intl).then(() => { + expect(importNewTaxonomy).not.toHaveBeenCalled(); + }); + + // Capture the onCancel handler from the file input element + const onCancel = mockAddEventListener.mock.calls[1][1]; + + onCancel(); + return promise; + }); + + it('should abort the call to the api when cancel name prompt', async () => { + jest.spyOn(window, 'prompt').mockReturnValueOnce(null); + + const promise = importTaxonomy(intl).then(() => { + expect(importNewTaxonomy).not.toHaveBeenCalled(); + }); + + // Capture the onChange handler from the file input element + const onChange = mockAddEventListener.mock.calls[0][1]; + const mockTarget = { + target: { + files: [ + 'mockFile', + ], + }, + }; + + onChange(mockTarget); + + return promise; + }); + + it('should abort the call to the api when cancel description prompt', async () => { + jest.spyOn(window, 'prompt') + .mockReturnValueOnce('test taxonomy name') + .mockReturnValueOnce(null); + + const promise = importTaxonomy(intl).then(() => { + expect(importNewTaxonomy).not.toHaveBeenCalled(); + }); + + // Capture the onChange handler from the file input element + const onChange = mockAddEventListener.mock.calls[0][1]; + const mockTarget = { + target: { + files: [ + 'mockFile', + ], + }, + }; + + onChange(mockTarget); + return promise; + }); + }); + + describe('import tags', () => { + it('should call the api and show success alert', async () => { + jest.spyOn(window, 'confirm').mockReturnValueOnce(true); + jest.spyOn(window, 'alert').mockImplementation(() => {}); + + const promise = importTaxonomyTags(1, intl).then(() => { + expect(importTags).toHaveBeenCalledWith(1, 'mockFile'); + expect(window.alert).toHaveBeenCalledWith('Taxonomy imported successfully'); + }); + + // Capture the onChange handler from the file input element + const onChange = mockAddEventListener.mock.calls[0][1]; + const mockTarget = { + target: { + files: [ + 'mockFile', + ], + }, + }; + + onChange(mockTarget); + + return promise; + }); + + it('should abort the call to the api without file', async () => { + const promise = importTaxonomyTags(1, intl).then(() => { + expect(importTags).not.toHaveBeenCalled(); + }); + + // Capture the onChange handler from the file input element + const onChange = mockAddEventListener.mock.calls[0][1]; + const mockTarget = { + target: { + files: [null], + }, + }; + + onChange(mockTarget); + return promise; + }); + + it('should abort the call to the api if file closed', async () => { + const promise = importTaxonomyTags(1, intl).then(() => { + expect(importTags).not.toHaveBeenCalled(); + }); + + // Capture the onCancel handler from the file input element + const onCancel = mockAddEventListener.mock.calls[1][1]; + + onCancel(); + return promise; + }); + + it('should abort the call to the api when cancel the confirm dialog', async () => { + jest.spyOn(window, 'confirm').mockReturnValueOnce(null); + + const promise = importTaxonomyTags(1, intl).then(() => { + expect(importTags).not.toHaveBeenCalled(); + }); + + // Capture the onChange handler from the file input element + const onChange = mockAddEventListener.mock.calls[0][1]; + const mockTarget = { + target: { + files: [ + 'mockFile', + ], + }, + }; + + onChange(mockTarget); + + return promise; + }); + + it('should call the api and return error alert', async () => { + jest.spyOn(window, 'confirm').mockReturnValueOnce(true); + importTags.mockRejectedValue(new Error('test error')); + + const promise = importTaxonomyTags(1, intl).then(() => { + expect(importTags).toHaveBeenCalledWith(1, 'mockFile'); + }); + + // Capture the onChange handler from the file input element + const onChange = mockAddEventListener.mock.calls[0][1]; + const mockTarget = { + target: { + files: [ + 'mockFile', + ], + }, + }; + + onChange(mockTarget); + + return promise; + }); + }); +}); diff --git a/src/taxonomy/import-tags/index.js b/src/taxonomy/import-tags/index.js new file mode 100644 index 0000000000..40100badeb --- /dev/null +++ b/src/taxonomy/import-tags/index.js @@ -0,0 +1,6 @@ +import { importTaxonomyTags, importTaxonomy } from './data/utils'; + +export { + importTaxonomyTags, + importTaxonomy, +}; diff --git a/src/taxonomy/import-tags/messages.js b/src/taxonomy/import-tags/messages.js new file mode 100644 index 0000000000..eaa6780d9f --- /dev/null +++ b/src/taxonomy/import-tags/messages.js @@ -0,0 +1,33 @@ +// ts-check +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + promptTaxonomyName: { + id: 'course-authoring.import-tags.prompt.taxonomy-name', + defaultMessage: 'Enter a name for the new taxonomy', + }, + promptTaxonomyNameRequired: { + id: 'course-authoring.import-tags.prompt.taxonomy-name.required', + defaultMessage: 'You must enter a name for the new taxonomy', + }, + promptTaxonomyDescription: { + id: 'course-authoring.import-tags.prompt.taxonomy-description', + defaultMessage: 'Enter a description for the new taxonomy', + }, + importTaxonomySuccess: { + id: 'course-authoring.import-tags.success', + defaultMessage: 'Taxonomy imported successfully', + }, + importTaxonomyError: { + 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; diff --git a/src/taxonomy/taxonomy-card/TaxonomyCard.test.jsx b/src/taxonomy/taxonomy-card/TaxonomyCard.test.jsx index fd61bf0cdb..8033de6383 100644 --- a/src/taxonomy/taxonomy-card/TaxonomyCard.test.jsx +++ b/src/taxonomy/taxonomy-card/TaxonomyCard.test.jsx @@ -7,11 +7,16 @@ import PropTypes from 'prop-types'; import initializeStore from '../../store'; import { getTaxonomyExportFile } from '../data/api'; +import { importTaxonomyTags } from '../import-tags'; import TaxonomyCard from '.'; let store; const taxonomyId = 1; +jest.mock('../import-tags', () => ({ + importTaxonomyTags: jest.fn().mockResolvedValue({}), +})); + const data = { id: taxonomyId, name: 'Taxonomy 1', @@ -132,6 +137,16 @@ describe('', async () => { expect(() => getByText('Select format to export')).toThrow(); }); + test('should call import tags when menu click', () => { + const { getByTestId } = render(); + + // Click on import menu + fireEvent.click(getByTestId('taxonomy-card-menu-button-1')); + fireEvent.click(getByTestId('taxonomy-card-menu-import-1')); + + expect(importTaxonomyTags).toHaveBeenCalled(); + }); + test('should export a taxonomy', () => { const { getByTestId, getByText } = render(); diff --git a/src/taxonomy/taxonomy-card/TaxonomyCardMenu.jsx b/src/taxonomy/taxonomy-card/TaxonomyCardMenu.jsx index 1b27c8ee61..6fef8b672e 100644 --- a/src/taxonomy/taxonomy-card/TaxonomyCardMenu.jsx +++ b/src/taxonomy/taxonomy-card/TaxonomyCardMenu.jsx @@ -31,7 +31,13 @@ const TaxonomyCardMenu = ({ data-testid={`taxonomy-card-menu-button-${id}`} /> - {/* Add more menu items here */} + onClickItem(e, 'import')} + > + {intl.formatMessage(messages.taxonomyCardImportMenu)} + { const intl = useIntl(); const [isExportModalOpen, setIsExportModalOpen] = useState(false); - // Add here more menu item actions const menuItemActions = { + import: () => importTaxonomyTags(id, intl).then(), export: () => setIsExportModalOpen(true), }; diff --git a/src/taxonomy/taxonomy-card/messages.js b/src/taxonomy/taxonomy-card/messages.js index 6886c2f99c..def583a96d 100644 --- a/src/taxonomy/taxonomy-card/messages.js +++ b/src/taxonomy/taxonomy-card/messages.js @@ -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', diff --git a/src/taxonomy/taxonomy-detail/TaxonomyDetailMenu.jsx b/src/taxonomy/taxonomy-detail/TaxonomyDetailMenu.jsx index 1f2aa839d2..38ce2492d9 100644 --- a/src/taxonomy/taxonomy-detail/TaxonomyDetailMenu.jsx +++ b/src/taxonomy/taxonomy-detail/TaxonomyDetailMenu.jsx @@ -20,6 +20,9 @@ const TaxonomyDetailMenu = ({ alt={intl.formatMessage(messages.actionsButtonAlt, { name })} disabled={disabled} > + onClickMenuItem('import')}> + {intl.formatMessage(messages.importMenu)} + onClickMenuItem('export')}> {intl.formatMessage(messages.exportMenu)} diff --git a/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.jsx b/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.jsx index a22fb0bd57..d2f0361ef0 100644 --- a/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.jsx +++ b/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.jsx @@ -11,6 +11,7 @@ import { Link, useParams } from 'react-router-dom'; import ConnectionErrorAlert from '../../generic/ConnectionErrorAlert'; import Loading from '../../generic/Loading'; import SubHeader from '../../generic/sub-header/SubHeader'; +import { importTaxonomyTags } from '../import-tags'; import taxonomyMessages from '../messages'; import TaxonomyDetailMenu from './TaxonomyDetailMenu'; import TaxonomyDetailSideCard from './TaxonomyDetailSideCard'; @@ -46,16 +47,15 @@ const TaxonomyDetailPage = () => { /> ); - const onClickMenuItem = (menuName) => { - switch (menuName) { - case 'export': - setIsExportModalOpen(true); - break; - default: - break; - } + const menuItemActions = { + import: () => importTaxonomyTags(taxonomyId, intl).then(), + export: () => setIsExportModalOpen(true), }; + const onClickMenuItem = (menuName) => ( + menuItemActions[menuName]?.() + ); + const getHeaderActions = () => ( ({ useTaxonomyDetailData: jest.fn(), })); +jest.mock('../import-tags', () => ({ + importTaxonomyTags: jest.fn().mockResolvedValue({}), +})); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), // use actual for all non-hook parts useParams: () => ({ @@ -108,4 +112,25 @@ describe('', async () => { // Modal closed expect(() => getByText('Select format to export')).toThrow(); }); + + it('should call import tags when menu clicked', () => { + useTaxonomyDetailData.mockReturnValue({ + isSuccess: true, + isFetched: true, + isError: false, + data: { + id: 1, + name: 'Test taxonomy', + description: 'This is a description', + }, + }); + + const { getByRole, getByText } = render(); + + // Click on import menu + fireEvent.click(getByRole('button')); + fireEvent.click(getByText('Re-import')); + + expect(importTaxonomyTags).toHaveBeenCalled(); + }); }); diff --git a/src/taxonomy/taxonomy-detail/messages.js b/src/taxonomy/taxonomy-detail/messages.js index ec5291f6c0..0abf7bf119 100644 --- a/src/taxonomy/taxonomy-detail/messages.js +++ b/src/taxonomy/taxonomy-detail/messages.js @@ -22,6 +22,10 @@ const messages = defineMessages({ id: 'course-authoring.taxonomy-detail.action.button.alt', defaultMessage: '{name} actions', }, + importMenu: { + id: 'course-authoring.taxonomy-detail.menu.import.label', + defaultMessage: 'Re-import', + }, exportMenu: { id: 'course-authoring.taxonomy-detail.action.export', defaultMessage: 'Export',