Skip to content
Closed
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
31 changes: 31 additions & 0 deletions src/generic/CompetencyIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';

/**
* The competency target icon. Kept in this repo because Paragon does not
* ship an equivalent icon.
*/
const CompetencyIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M10.8289 2.34375C12.2696 2.34375 13.6445 2.62528 14.9017 3.13602V5.15819L14.4088 5.65031C13.3242 5.13316 12.1106 4.84258 10.8289 4.84258C6.22872 4.84258 2.4995 8.5718 2.4995 13.172C2.4995 17.7722 6.22872 21.5015 10.8289 21.5015C15.4292 21.5015 19.1584 17.7722 19.1584 13.172C19.1584 11.8575 18.8523 10.6148 18.31 9.50918L18.8102 9.00974H20.8283C21.3623 10.2911 21.6572 11.6971 21.6572 13.172C21.6572 19.1523 16.8092 24.0003 10.8289 24.0003C4.84865 24.0003 0.000671387 19.1523 0.000671387 13.172C0.000671387 7.19173 4.84865 2.34375 10.8289 2.34375Z"
fill="currentColor"
/>
<path
d="M10.8289 6.50847C11.6411 6.50847 12.4193 6.65402 13.1391 6.92006L11.134 8.92514C11.0382 8.91859 10.9414 8.91457 10.8444 8.91457C8.60832 8.91503 6.782 10.6711 6.66991 12.88L6.66422 13.0956C6.66422 13.1083 6.66492 13.1211 6.66503 13.1338C6.66492 13.1465 6.66422 13.1593 6.66422 13.172C6.66422 15.4721 8.52883 17.3367 10.8289 17.3367C12.5608 17.3367 14.0451 16.2794 14.6732 14.7753C14.8993 14.261 15.026 13.6931 15.0262 13.0956C15.0262 12.9978 15.0215 12.9008 15.0148 12.8044L17.0459 10.7732C17.3334 11.5177 17.4925 12.3262 17.4925 13.172C17.4925 16.8522 14.5091 19.8356 10.8289 19.8356C7.14876 19.8356 4.16539 16.8522 4.16539 13.172C4.16539 9.49184 7.14876 6.50847 10.8289 6.50847Z"
fill="currentColor"
/>
<path
d="M19.7053 4.2421H23.8755L20.7741 7.34354H18.12L13.2142 12.2494C13.3085 12.5137 13.3597 12.7988 13.3597 13.0955C13.3595 14.4842 12.2338 15.6101 10.845 15.6101C9.45609 15.6101 8.32977 14.4838 8.32977 13.0949C8.32986 11.7062 9.45579 10.5805 10.8444 10.5802C11.1408 10.5802 11.4259 10.631 11.69 10.7252L16.5676 5.8475V3.1371L19.7047 0L19.7053 4.2421Z"
fill="currentColor"
/>
</svg>
);

export default CompetencyIcon;
17 changes: 17 additions & 0 deletions src/taxonomy/TaxonomyListPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,23 @@ describe('<TaxonomyListPage />', () => {
expect(getByTestId('taxonomy-card-1')).toBeInTheDocument();
});

it('shows the taxonomy type icon of each taxonomy', async () => {
axiosMock.onGet(listTaxonomiesUrl).reply(200, {
results: [
{ ...taxonomies[0], id: 1, taxonomy_type: 'competency' },
{ ...taxonomies[0], id: 2, taxonomy_type: 'tags' },
],
canAddTaxonomy: false,
});
const { getByTestId, queryByText } = render(<TaxonomyListPage />);
await waitFor(() => {
expect(queryByText('Loading')).toEqual(null);
});

expect(getByTestId('taxonomy-card-1')).toContainElement(getByTestId('taxonomy-type-icon-competency'));
expect(getByTestId('taxonomy-card-2')).toContainElement(getByTestId('taxonomy-type-icon-tags'));
});

it.each(['csv', 'json'] as const)('downloads the taxonomy template %s', async (fileFormat) => {
axiosMock.onGet(listTaxonomiesUrl).reply(200, { results: taxonomies, canAddTaxonomy: false });
const { findByRole, queryByText } = render(<TaxonomyListPage />);
Expand Down
2 changes: 1 addition & 1 deletion src/taxonomy/TaxonomyListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { ALL_TAXONOMIES, apiUrls, UNASSIGNED } from './data/api';
import { useTaxonomyList } from './data/apiHooks';
import { ImportTagsWizard } from './import-tags';
import messages from './messages';
import TaxonomyCard from './taxonomy-card';
import { TaxonomyCard } from './taxonomy-card';

const TaxonomyListHeaderButtons = (props: { canAddTaxonomy: boolean; }) => {
const intl = useIntl();
Expand Down
4 changes: 4 additions & 0 deletions src/taxonomy/__mocks__/taxonomyListMock.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module.exports = {
id: -2,
name: 'Content Authors',
description: 'Allows tags for any user ID created on the instance.',
taxonomyType: 'tags',
enabled: true,
allowMultiple: false,
allowFreeText: false,
Expand All @@ -23,6 +24,7 @@ module.exports = {
id: -1,
name: 'Languages',
description: 'lang lang lang lang lang lang lang lang',
taxonomyType: 'tags',
enabled: true,
allowMultiple: false,
allowFreeText: false,
Expand All @@ -35,6 +37,7 @@ module.exports = {
id: 1,
name: 'Taxonomy',
description: 'This is a Description',
taxonomyType: 'competency',
enabled: true,
allowMultiple: false,
allowFreeText: false,
Expand All @@ -47,6 +50,7 @@ module.exports = {
id: 2,
name: 'Taxonomy long long long long long long long long long long long long long long long long long long long',
description: 'This is a Description long lon',
taxonomyType: 'tags',
enabled: true,
allowMultiple: false,
allowFreeText: false,
Expand Down
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
17 changes: 13 additions & 4 deletions 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 @@ -69,7 +70,12 @@ export const taxonomyQueryKeys = {
* @param taxonomyId ID of the taxonomy
* @param fileId Some string to uniquely identify the file we want to upload
*/
importPlan: (taxonomyId: number, fileId: string) => [...taxonomyQueryKeys.all, 'importPlan', taxonomyId, fileId],
importPlan: (taxonomyId: number | undefined, fileId: string) => [
...taxonomyQueryKeys.all,
'importPlan',
taxonomyId ?? '',
fileId,
],
} satisfies Record<string, (string | number)[] | ((...args: any[]) => (string | number)[])>;

/**
Expand Down Expand Up @@ -116,11 +122,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 Expand Up @@ -165,10 +173,11 @@ export const useImportTags = () => {

/**
* Preview the results of importing the given file into an existing taxonomy.
* @param taxonomyId The ID of the taxonomy whose tags we're updating.
* @param taxonomyId The ID of the taxonomy whose tags we're updating, or undefined if a new taxonomy is
* being created (in which case there is no plan to preview).
* @param file The file that we want to import
*/
export const useImportPlan = (taxonomyId: number, file: File | null) =>
export const useImportPlan = (taxonomyId: number | undefined, file: File | null) =>
useQuery({
queryKey: taxonomyQueryKeys.importPlan(taxonomyId, file ? `${file.name}${file.lastModified}${file.size}` : ''),
queryFn: async (): Promise<string | null> => {
Expand Down
13 changes: 13 additions & 0 deletions src/taxonomy/data/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,16 @@
* will be incomplete because the backend only supports a taxonomy size of 10,000 items or fewer.
*/
export const MAX_TAXONOMY_ITEMS = 10000;

export enum TaxonomyType {
/**
* A taxonomy whose tags are only labels for content. They say what a piece of content is about,
* and carry no rules about demonstrating mastery of what they describe.
*/
Tags = 'tags',
/**
* A taxonomy of skills. Beyond labelling content, choosing this type enables the Competency
* Management page, where the rules used to demonstrate mastery of those skills are configured.
*/
Competency = 'competency',
}
3 changes: 3 additions & 0 deletions src/taxonomy/data/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import type { TaxonomyType } from './constants';

/** Metadata about a taxonomy */
export interface TaxonomyData {
id: number;
name: string;
description: string;
exportId: string;
taxonomyType: TaxonomyType;
enabled: boolean;
allowMultiple: boolean;
allowFreeText: boolean;
Expand Down
Loading