Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 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
74 changes: 40 additions & 34 deletions src/taxonomy/data/apiHooks.test.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
// @ts-check
import React from 'react'; // Required to use JSX syntax without type errors

import { initializeMockApp } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

import MockAdapter from 'axios-mock-adapter';

import { initializeMocks } from '@src/testUtils';
import { apiUrls } from './api';

import {
Expand All @@ -17,6 +14,7 @@ import {
useImportTags,
useImportNewTaxonomy,
} from './apiHooks';
import { TaxonomyType } from './constants';

let axiosMock;

Expand All @@ -30,49 +28,57 @@ const queryClient = new QueryClient({

const wrapper = ({ children }) => (
<QueryClientProvider client={queryClient}>
<IntlProvider locale="en">
{children}
</IntlProvider>
<IntlProvider locale="en">{children}</IntlProvider>
</QueryClientProvider>
);

const emptyFile = new File([], 'empty.csv');

const taxonomyTypes = Object.values(TaxonomyType);

describe('import taxonomy api calls', () => {
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
({ axiosMock } = initializeMocks());
});

afterEach(() => {
jest.clearAllMocks();
});

it('should call import new taxonomy', async () => {
const mockResult = {
id: 8,
name: 'Taxonomy name',
exportId: 'taxonomy_export_id',
description: 'Taxonomy description',
};
axiosMock.onPost(apiUrls.createTaxonomyFromImport()).reply(201, mockResult);
const { result } = renderHook(() => useImportNewTaxonomy(), { wrapper });
const mutateResult = await result.current.mutateAsync({
name: 'Taxonomy name',
description: 'Taxonomy description',
file: emptyFile,
});

expect(axiosMock.history.post[0].url).toEqual(apiUrls.createTaxonomyFromImport());
expect(mutateResult).toEqual(mockResult);
});
it.each(taxonomyTypes)(
'should call import new taxonomy with the %s type',
async (taxonomyType) => {
const mockResult = {
id: 8,
name: 'Taxonomy name',
exportId: 'taxonomy_export_id',
description: 'Taxonomy description',
};
axiosMock
.onPost(apiUrls.createTaxonomyFromImport())
.reply(201, mockResult);
const { result } = renderHook(() => useImportNewTaxonomy(), {
wrapper,
});
const mutateResult = await result.current.mutateAsync({
name: 'Taxonomy name',
description: 'Taxonomy description',
taxonomyType,
file: emptyFile,
});

expect(axiosMock.history.post[0].url).toEqual(
apiUrls.createTaxonomyFromImport(),
);
const formData = axiosMock.history.post[0].data;
expect(formData.get('taxonomy_name')).toEqual('Taxonomy name');
expect(formData.get('taxonomy_description')).toEqual(
'Taxonomy description',
);
expect(formData.get('taxonomy_type')).toEqual(taxonomyType);
expect(mutateResult).toEqual(mockResult);
},
);

it('should call import tags', async () => {
const taxonomy = { id: 1, name: 'taxonomy name' };
Expand Down
5 changes: 4 additions & 1 deletion src/taxonomy/data/apiHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { apiUrls, ALL_TAXONOMIES, getApiErrorMessage } from './api';
import * as api from './api';
import type { QueryOptions, TagListData } from './types';
import { TaxonomyType } from './constants';

// Query key patterns. Allows an easy way to clear all data related to a given taxonomy.
// https://github.com/openedx/frontend-app-admin-portal/blob/2ba315d/docs/decisions/0006-tanstack-react-query.rst
Expand Down Expand Up @@ -116,11 +117,13 @@ export const useImportNewTaxonomy = () => {
mutationFn: async ({
name,
description,
taxonomyType,
file,
}: { name: string; description: string; file: File; }) => {
}: { name: string; description: string; taxonomyType: TaxonomyType; file: File; }) => {
const formData = new FormData();
formData.append('taxonomy_name', name);
formData.append('taxonomy_description', description);
formData.append('taxonomy_type', taxonomyType);
formData.append('file', file);

const { data } = await getAuthenticatedHttpClient().post(apiUrls.createTaxonomyFromImport(), formData);
Expand Down
11 changes: 11 additions & 0 deletions src/taxonomy/data/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,14 @@
* will be incomplete because the backend only supports a taxonomy size of 10,000 items or fewer.
*/
export const MAX_TAXONOMY_ITEMS = 10000;

/**
* The type of a taxonomy.
*
* Note: the backend also has a "system" type, reserved for platform-defined
* taxonomies. It is intentionally not selectable by users.
Comment thread
javoconsultant marked this conversation as resolved.
Outdated
*/
export enum TaxonomyType {
Tags = 'tags',
Competency = 'competency',
}
34 changes: 30 additions & 4 deletions src/taxonomy/import-tags/ImportTagsWizard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ import {
} from '@openedx/paragon/icons';
import PropTypes from 'prop-types';

import LoadingButton from '../../generic/loading-button';
import { LoadingSpinner } from '../../generic/Loading';
import { getFileSizeToClosestByte } from '../../utils';
import { TaxonomyContext } from '../common/context';
import { getTaxonomyExportFile, apiUrls } from '../data/api';
import { useImportTags, useImportPlan, useImportNewTaxonomy } from '../data/apiHooks';
import messages from './messages';
import { TAXONOMY_TYPE_OPTIONS } from './constants';
import { TaxonomyType } from '@src/taxonomy/data/constants';
import LoadingButton from '@src/generic/loading-button';
import { LoadingSpinner } from '@src/generic/Loading';
import { getFileSizeToClosestByte } from '@src/utils';

const linebreak = (
<>
Expand Down Expand Up @@ -198,6 +200,10 @@ const PopulateStep = ({
setTaxonomyPopulateData(updatedState);
};

const handleTypeChange = (e) => {
setTaxonomyPopulateData({ ...taxonomyPopulateData, taxonomyType: e.target.value });
};

return (
<Stepper.Step eventKey="populate" title={intl.formatMessage(messages.importWizardStepperPopulateStepTitle)}>
<Stack gap={3} data-testid="populate-step">
Expand All @@ -214,6 +220,23 @@ const PopulateStep = ({
onChange={handleDescChange}
/>
</Form.Group>
<Form.Group>
<Form.Label>
{intl.formatMessage(messages.importWizardStepPopulateTaxonomyType)}
</Form.Label>
<Form.Control
as="select"
data-testid="taxonomy-type-select"
value={taxonomyPopulateData.taxonomyType}
onChange={handleTypeChange}
>
{TAXONOMY_TYPE_OPTIONS.map(({ value, message }) => (
<option key={value} value={value}>
{intl.formatMessage(message)}
</option>
))}
</Form.Control>
</Form.Group>
</Stack>
</Stepper.Step>
);
Expand All @@ -223,6 +246,7 @@ PopulateStep.propTypes = {
taxonomyPopulateData: PropTypes.shape({
taxonomyName: PropTypes.string.isRequired,
taxonomyDesc: PropTypes.string.isRequired,
taxonomyType: PropTypes.string.isRequired,
Comment thread
javoconsultant marked this conversation as resolved.
Outdated
}).isRequired,
setTaxonomyPopulateData: PropTypes.func.isRequired,
};
Expand Down Expand Up @@ -305,18 +329,20 @@ const ImportTagsWizard = ({
const [taxonomyPopulateData, setTaxonomyPopulateData] = useState({
taxonomyName: '',
taxonomyDesc: '',
taxonomyType: TaxonomyType.Tags,
});

const importNewTaxonomyMutation = useImportNewTaxonomy();

const importNewTaxonomy = async () => {
disableDialog();
try {
const { taxonomyName, taxonomyDesc } = taxonomyPopulateData;
const { taxonomyName, taxonomyDesc, taxonomyType } = taxonomyPopulateData;
if (file) {
await importNewTaxonomyMutation.mutateAsync({
name: taxonomyName,
description: taxonomyDesc,
taxonomyType,
file,
});
}
Expand Down
14 changes: 14 additions & 0 deletions src/taxonomy/import-tags/ImportTagsWizard.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
render,
waitFor,
screen,
within,
} from '@testing-library/react';
import PropTypes from 'prop-types';

Expand Down Expand Up @@ -327,12 +328,24 @@ describe('<ImportTagsWizard />', () => {
target: { value: 'New Taxonomy Description' },
});

// The taxonomy type dropdown is a native `<select>`, whose options are rendered as `<option>`s.
const taxonomyTypeSelectEl = screen.getByLabelText('Taxonomy Type');
expect(taxonomyTypeSelectEl).toBe(getByTestId('taxonomy-type-select'));
const taxonomyTypeOptions = () => within(taxonomyTypeSelectEl).getAllByRole('option');

expect(taxonomyTypeSelectEl).toHaveValue('tags');
expect(taxonomyTypeOptions().map((option) => option.textContent)).toEqual(['Tags', 'Competency']);

fireEvent.change(taxonomyTypeSelectEl, { target: { value: 'competency' } });
expect(taxonomyTypeSelectEl).toHaveValue('competency');

// Test back button
fireEvent.click(getByTestId('back-button'));
expect(getByTestId('upload-step')).toBeInTheDocument();
fireEvent.click(getByRole('button', { name: 'Continue' }));

expect(getByTestId('populate-step')).toBeInTheDocument();
expect(getByTestId('taxonomy-type-select')).toHaveValue('competency');

if (expectedResult === 'success') {
axiosMock.onPost(doImportNewTaxonomyUrl).replyOnce(200, {});
Expand All @@ -353,6 +366,7 @@ describe('<ImportTagsWizard />', () => {
await waitFor(() => {
expect(mockSetToastMessage).toHaveBeenCalledWith(`"${newTaxonomyName}" imported`);
});
expect(axiosMock.history.post[0].data.get('taxonomy_type')).toEqual('competency');
} else {
// Alert message shown
await waitFor(() => {
Expand Down
18 changes: 18 additions & 0 deletions src/taxonomy/import-tags/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { TaxonomyType } from '@src/taxonomy/data/constants';
import messages from './messages';

/**
* The taxonomy types a user can pick when importing a taxonomy.
* The backend also supports a "system" type, but it is reserved for
* platform-defined taxonomies and must not be offered here.
*/
export const TAXONOMY_TYPE_OPTIONS = [
{
value: TaxonomyType.Tags,
message: messages.importWizardStepPopulateTaxonomyTypeTags,
},
{
value: TaxonomyType.Competency,
message: messages.importWizardStepPopulateTaxonomyTypeCompetency,
},
];
12 changes: 12 additions & 0 deletions src/taxonomy/import-tags/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,18 @@ const messages = defineMessages({
id: 'course-authoring.import-tags.wizard.step-populate.desc',
defaultMessage: 'Taxonomy Description',
},
importWizardStepPopulateTaxonomyType: {
id: 'course-authoring.import-tags.wizard.step-populate.type',
defaultMessage: 'Taxonomy Type',
},
importWizardStepPopulateTaxonomyTypeTags: {
id: 'course-authoring.import-tags.wizard.step-populate.type.tags',
defaultMessage: 'Tags',
},
importWizardStepPopulateTaxonomyTypeCompetency: {
id: 'course-authoring.import-tags.wizard.step-populate.type.competency',
defaultMessage: 'Competency',
},
importWizardStepPlanTitle: {
id: 'course-authoring.import-tags.wizard.step-plan.title',
defaultMessage: 'Differences between files',
Expand Down