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: add import taxonomy feature [FC-0036] #675

Merged
merged 48 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
0c60c37
feat: add import taxonomy feature
rpenido Nov 9, 2023
c273af1
test: add api.test.js
rpenido Nov 9, 2023
047530d
test: add import button click test
rpenido Nov 9, 2023
303e7ba
test: add action.test.js
rpenido Nov 9, 2023
3955e91
test: add more tests to action.tests.js
rpenido Nov 9, 2023
8176973
test: add more tests to action.tests.js 2
rpenido Nov 9, 2023
4f31714
Merge branch 'openedx:master' into rpenido/fal-3532-import-taxonomy
rpenido Nov 10, 2023
df441fb
fix: import
rpenido Nov 10, 2023
a0e92f0
test: simplify import test
rpenido Nov 10, 2023
dc8ed9e
fix: remove undefined var
rpenido Nov 10, 2023
120154e
refactor: rename actions.js -> utils.js
rpenido Nov 10, 2023
318bbb7
revert: change in the jest.config
rpenido Nov 10, 2023
a8c2fd5
Merge branch 'master' into rpenido/fal-3532-import-taxonomy
pomegranited Nov 16, 2023
70ac2a7
Merge branch 'openedx:master' into rpenido/fal-3532-import-taxonomy
rpenido Nov 20, 2023
adacf23
chore: trigger CD/CI
rpenido Nov 20, 2023
5983953
Merge branch 'master' into rpenido/fal-3532-import-taxonomy
rpenido Nov 20, 2023
7756c7f
feat: import tags to existing taxonomy
rpenido Nov 24, 2023
4202c27
Merge branch 'master' into rpenido/fal-3532-import-taxonomy
rpenido Nov 24, 2023
6bcd924
refactor: merges TaxonomyCardMenu and TaxonomyDetailMenu (#13)
rpenido Nov 30, 2023
f318dfc
refactor: improve menu organization
rpenido Dec 4, 2023
204b2b9
fix: types
rpenido Dec 9, 2023
5e0f599
Merge branch 'master' into rpenido/fal-3532-import-taxonomy
rpenido Dec 12, 2023
f888ecf
refactor: merging conflicts
rpenido Dec 12, 2023
3c8da66
refactor: change export syntax
rpenido Dec 12, 2023
40479c1
fix: eslint
rpenido Dec 12, 2023
46e5d1b
fix: export TaxonomyDetail
rpenido Dec 12, 2023
db8ba58
fix: eslint
rpenido Dec 12, 2023
8fe8c07
Merge branch 'openedx:master' into rpenido/fal-3532-import-taxonomy
rpenido Dec 14, 2023
1440215
fix: TaxonomyContext types
rpenido Dec 15, 2023
e5b8e58
Merge branch 'master' into rpenido/fal-3532-import-taxonomy
rpenido Dec 19, 2023
fafcb9b
chore: rebasing
rpenido Dec 19, 2023
e80194f
fix: types
rpenido Dec 19, 2023
1e0dde3
fix: remove typo
rpenido Dec 19, 2023
7cae57b
test: cleaning test removing useMutation mock
rpenido Dec 19, 2023
1002fc6
style: cleaning code and fix lint
rpenido Dec 19, 2023
fdc74c7
Merge branch 'master' into rpenido/fal-3532-import-taxonomy
rpenido Dec 21, 2023
e9893dc
fix: change expect getBy toThrow pattern to queryBy not.toBeInDocument
rpenido Dec 28, 2023
a8b4197
test: add jest-each
rpenido Dec 28, 2023
a3c40fd
fix: remove getTaxonomyMenuItems function
rpenido Dec 28, 2023
1a31aa9
fix: remove tests from temp code
rpenido Dec 28, 2023
c6d39e5
Merge branch 'openedx:master' into rpenido/fal-3532-import-taxonomy
rpenido Dec 28, 2023
583ab51
test: improve coverage
rpenido Dec 28, 2023
151ba9e
test: refactor import button test
rpenido Dec 29, 2023
df9f2a0
fix: eslint
rpenido Dec 29, 2023
67c42cb
Merge branch 'master' into rpenido/fal-3532-import-taxonomy
rpenido Jan 4, 2024
793810f
fix: errors fixing merge conflicts
rpenido Jan 4, 2024
70954b3
Merge branch 'master' into rpenido/fal-3532-import-taxonomy
pomegranited Jan 7, 2024
68e820d
fixup!: remove jest-each in favour of inbuilt functionality
xitij2000 Jan 8, 2024
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
34 changes: 26 additions & 8 deletions src/taxonomy/TaxonomyListPage.jsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,42 @@
import React from 'react';
import {
Button,
CardView,
Container,
DataTable,
Spinner,
} from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Add,
} from '@edx/paragon/icons';
import { injectIntl, intlShape, useIntl } from '@edx/frontend-platform/i18n';
import { StudioFooter } from '@edx/frontend-component-footer';

import Header from '../header';
import SubHeader from '../generic/sub-header/SubHeader';
import { importTaxonomy } from './import-tags';
import messages from './messages';
import TaxonomyCard from './TaxonomyCard';
import { useTaxonomyListDataResponse, useIsTaxonomyListDataLoaded } from './api/hooks/selectors';

const TaxonomyListHeaderButtons = () => {
const intl = useIntl();
return (
<>
<Button variant="outline-primary" disabled>
{intl.formatMessage(messages.downloadTemplateButtonLabel)}
</Button>
<Button
iconBefore={Add}
onClick={() => importTaxonomy(intl)}
data-testid="taxonomy-import-button"
>
{intl.formatMessage(messages.importButtonLabel)}
</Button>
</>
);
};

const TaxonomyListPage = ({ intl }) => {
const useTaxonomyListData = () => {
const taxonomyListData = useTaxonomyListDataResponse();
Expand All @@ -22,12 +46,6 @@ const TaxonomyListPage = ({ intl }) => {

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
Expand All @@ -49,7 +67,7 @@ const TaxonomyListPage = ({ intl }) => {
<SubHeader
title={intl.formatMessage(messages.headerTitle)}
titleActions={getOrgSelect()}
headerActions={getHeaderButtons()}
headerActions={<TaxonomyListHeaderButtons />}
hideBorder
/>
</Container>
Expand Down
23 changes: 23 additions & 0 deletions src/taxonomy/TaxonomyListPage.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import initializeStore from '../store';

import TaxonomyListPage from './TaxonomyListPage';
import { useTaxonomyListDataResponse, useIsTaxonomyListDataLoaded } from './api/hooks/selectors';
import { importTaxonomy } from './import-tags/data/utils';

let store;

Expand All @@ -16,6 +17,10 @@ jest.mock('./api/hooks/selectors', () => ({
useIsTaxonomyListDataLoaded: jest.fn(),
}));

jest.mock('./import-tags/data/utils', () => ({
importTaxonomy: jest.fn(),
}));

const RootWrapper = () => (
<AppProvider store={store}>
<IntlProvider locale="en" messages={{}}>
Expand Down Expand Up @@ -65,4 +70,22 @@ describe('<TaxonomyListPage />', 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(<RootWrapper />);
const importButton = getByTestId('taxonomy-import-button');
expect(importButton).toBeInTheDocument();
importButton.click();
expect(importTaxonomy).toHaveBeenCalled();
});
});
});
2 changes: 2 additions & 0 deletions src/taxonomy/import-tags/__mocks__/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// eslint-disable-next-line import/prefer-default-export
export { default as taxonomyImportMock } from './taxonomyImportMock';
4 changes: 4 additions & 0 deletions src/taxonomy/import-tags/__mocks__/taxonomyImportMock.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: 31 additions & 0 deletions src/taxonomy/import-tags/data/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// @ts-check
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

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

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

/**
* Import a new taxonomy
* @param {string} taxonomyName
* @param {string} taxonomyDescription
* @param {File} file
* @returns {Promise<Object>}
*/
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(
getTaxonomyImportApiUrl(),
formData,
);

return camelCaseObject(data);
}
38 changes: 38 additions & 0 deletions src/taxonomy/import-tags/data/api.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import MockAdapter from 'axios-mock-adapter';
import { initializeMockApp } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

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

import {
getTaxonomyImportApiUrl,
importNewTaxonomy,
} 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 taxonomy', async () => {
axiosMock.onPost(getTaxonomyImportApiUrl()).reply(201, taxonomyImportMock);
const result = await importNewTaxonomy('Taxonomy name', 'Taxonomy description');

expect(axiosMock.history.post[0].url).toEqual(getTaxonomyImportApiUrl());
expect(result).toEqual(taxonomyImportMock);
});
});
82 changes: 82 additions & 0 deletions src/taxonomy/import-tags/data/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import messages from '../messages';
import { importNewTaxonomy } from './api';

// eslint-disable-next-line import/prefer-default-export
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^ Just flagging this in particular in case any reviewers are concerned about alert() and prompt() :) . This is just a temporary state during development to help get more PRs out faster and more iteratively, and will be replaced before any users ever see this (other than early beta testers).

*/
/* 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) {
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);
});
};
Loading