diff --git a/src/index.jsx b/src/index.jsx
index f671a90f84..46ba868924 100755
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -22,7 +22,7 @@ import CourseAuthoringRoutes from './CourseAuthoringRoutes';
import Head from './head/Head';
import { StudioHome } from './studio-home';
import CourseRerun from './course-rerun';
-import { TaxonomyListPage } from './taxonomy';
+import { TaxonomyDetailPage, TaxonomyListPage } from './taxonomy';
import 'react-datepicker/dist/react-datepicker.css';
import './index.scss';
@@ -71,11 +71,18 @@ const App = () => {
}}
/>
{process.env.ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && (
-
-
-
+ <>
+
+ {
+ const { params: { taxonomyId } } = match;
+ return (
+
+ );
+ }}
+ />
+ >
)}
diff --git a/src/taxonomy/api/hooks/api.js b/src/taxonomy/api/hooks/api.js
index 3efaea019a..bb97225513 100644
--- a/src/taxonomy/api/hooks/api.js
+++ b/src/taxonomy/api/hooks/api.js
@@ -8,6 +8,13 @@ const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
const getTaxonomyListApiUrl = () => new URL('api/content_tagging/v1/taxonomies/?enabled=true', getApiBaseUrl()).href;
const getExportTaxonomyApiUrl = (pk, format) => new URL(
`api/content_tagging/v1/taxonomies/${pk}/export/?output_format=${format}`,
+);
+
+/**
+ * @param {number} taxonomyId
+ */
+export const getTaxonomyDetailApiUrl = (taxonomyId) => new URL(
+ `api/content_tagging/v1/taxonomies/${taxonomyId}/`,
getApiBaseUrl(),
).href;
@@ -48,6 +55,17 @@ export const useExportTaxonomy = () => {
}
downloadDataAsFile(data, contentType, `${name}.${fileExtension}`);
};
-
- return useMutation(exportTaxonomy);
};
+
+/**
+ * @param {number} taxonomyId
+ * @returns {import('@tanstack/react-query').UseQueryResult}
+ */
+export const useTaxonomyDetailData = (taxonomyId) => (
+ useQuery({
+ queryKey: ['taxonomyList', taxonomyId],
+ queryFn: () => getAuthenticatedHttpClient().get(getTaxonomyDetailApiUrl(taxonomyId))
+ .then(camelCaseObject)
+ .then((response) => response.data),
+ })
+);
diff --git a/src/taxonomy/api/hooks/api.test.js b/src/taxonomy/api/hooks/api.test.js
index a34ede3da6..aee14b8a7f 100644
--- a/src/taxonomy/api/hooks/api.test.js
+++ b/src/taxonomy/api/hooks/api.test.js
@@ -1,30 +1,20 @@
-import { useQuery, useMutation } from '@tanstack/react-query';
-import { useTaxonomyListData, useExportTaxonomy } from './api';
-import { downloadDataAsFile } from '../../../utils';
-
-const mockHttpClient = {
- get: jest.fn(),
-};
+import { useQuery } from '@tanstack/react-query';
+import useTaxonomyListData from './api';
jest.mock('@tanstack/react-query', () => ({
useQuery: jest.fn(),
- useMutation: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth', () => ({
- getAuthenticatedHttpClient: jest.fn(() => mockHttpClient),
-}));
-
-jest.mock('../../../utils', () => ({
- downloadDataAsFile: jest.fn(),
+ getAuthenticatedHttpClient: jest.fn(),
}));
-describe('taxonomy API', () => {
+describe('taxonomy API: useTaxonomyListData', () => {
afterEach(() => {
jest.clearAllMocks();
});
- it('useTaxonomyListData should call useQuery with the correct parameters', () => {
+ it('should call useQuery with the correct parameters', () => {
useTaxonomyListData();
expect(useQuery).toHaveBeenCalledWith({
@@ -32,40 +22,4 @@ describe('taxonomy API', () => {
queryFn: expect.any(Function),
});
});
-
- it('useExportTaxonomy should export data correctly', async () => {
- useMutation.mockImplementation((exportFunc) => exportFunc);
-
- const mockResponseJson = {
- headers: {
- 'content-type': 'application/json',
- },
- data: { tags: 'tags' },
- };
- const mockResponseCsv = {
- headers: {
- 'content-type': 'text',
- },
- data: 'This is a CSV',
- };
-
- const exportTaxonomy = useExportTaxonomy();
-
- mockHttpClient.get.mockResolvedValue(mockResponseJson);
- await exportTaxonomy({ pk: 1, format: 'json', name: 'testFile' });
-
- expect(downloadDataAsFile).toHaveBeenCalledWith(
- JSON.stringify(mockResponseJson.data, null, 2),
- 'application/json',
- 'testFile.json',
- );
-
- mockHttpClient.get.mockResolvedValue(mockResponseCsv);
- await exportTaxonomy({ pk: 1, format: 'csv', name: 'testFile' });
- expect(downloadDataAsFile).toHaveBeenCalledWith(
- mockResponseCsv.data,
- 'text',
- 'testFile.csv',
- );
- });
});
diff --git a/src/taxonomy/api/hooks/selectors.js b/src/taxonomy/api/hooks/selectors.js
index 970ae49392..c072c72e1e 100644
--- a/src/taxonomy/api/hooks/selectors.js
+++ b/src/taxonomy/api/hooks/selectors.js
@@ -1,5 +1,6 @@
// @ts-check
import {
+ useTaxonomyDetailData,
useTaxonomyListData,
useExportTaxonomy,
} from './api';
@@ -10,7 +11,7 @@ import {
export const useTaxonomyListDataResponse = () => {
const response = useTaxonomyListData();
if (response.status === 'success') {
- return response.data.data;
+ return response.data;
}
return undefined;
};
@@ -25,3 +26,34 @@ export const useIsTaxonomyListDataLoaded = () => (
export const useExportTaxonomyMutation = () => (
useExportTaxonomy()
);
+/**
+ * @params {number} taxonomyId
+ * @returns {Pick}
+ */
+export const useTaxonomyDetailDataStatus = (taxonomyId) => {
+ const {
+ isError,
+ error,
+ isFetched,
+ isSuccess,
+ } = useTaxonomyDetailData(taxonomyId);
+ return {
+ isError,
+ error,
+ isFetched,
+ isSuccess,
+ };
+};
+
+/**
+ * @params {number} taxonomyId
+ * @returns {import("../types.mjs").TaxonomyData | undefined}
+ */
+export const useTaxonomyDetailDataResponse = (taxonomyId) => {
+ const { isSuccess, data } = useTaxonomyDetailData(taxonomyId);
+ if (isSuccess) {
+ return data;
+ }
+
+ return undefined;
+};
diff --git a/src/taxonomy/index.js b/src/taxonomy/index.js
index c857f10e6c..4fc3470bb7 100644
--- a/src/taxonomy/index.js
+++ b/src/taxonomy/index.js
@@ -1,2 +1,2 @@
-// eslint-disable-next-line import/prefer-default-export
export { default as TaxonomyListPage } from './TaxonomyListPage';
+export { TaxonomyDetailPage } from './taxonomy-detail';
diff --git a/src/taxonomy/taxonomy-detail/TagListTable.jsx b/src/taxonomy/taxonomy-detail/TagListTable.jsx
new file mode 100644
index 0000000000..3b37e06336
--- /dev/null
+++ b/src/taxonomy/taxonomy-detail/TagListTable.jsx
@@ -0,0 +1,42 @@
+import {
+ DataTable,
+ TextFilter,
+} from '@edx/paragon';
+import Proptypes from 'prop-types';
+
+const tagsSample = [
+ { name: 'Tag 1' },
+ { name: 'Tag 2' },
+ { name: 'Tag 3' },
+ { name: 'Tag 4' },
+ { name: 'Tag 5' },
+ { name: 'Tag 6' },
+ { name: 'Tag 7' },
+];
+
+const TagListTable = ({ tags }) => (
+
+
+
+
+
+
+);
+
+TagListTable.propTypes = {
+ tags: Proptypes.array.isRequired,
+};
+
+export default TagListTable;
diff --git a/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.jsx b/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.jsx
new file mode 100644
index 0000000000..d422efde18
--- /dev/null
+++ b/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.jsx
@@ -0,0 +1,97 @@
+import React from 'react';
+import {
+ Container,
+ Layout,
+} from '@edx/paragon';
+import Proptypes from 'prop-types';
+
+import PermissionDeniedAlert from '../../generic/PermissionDeniedAlert';
+import Loading from '../../generic/Loading';
+import Header from '../../header';
+import SubHeader from '../../generic/sub-header/SubHeader';
+import TaxonomyDetailSideCard from './TaxonomyDetailSideCard';
+import TagListTable from './TagListTable';
+import { useTaxonomyDetailDataResponse, useTaxonomyDetailDataStatus } from '../api/hooks/selectors';
+
+const TaxonomyDetailContent = ({ taxonomyId }) => {
+ const useTaxonomyDetailData = () => {
+ const { isError, isFetched } = useTaxonomyDetailDataStatus(taxonomyId);
+ const taxonomy = useTaxonomyDetailDataResponse(taxonomyId);
+ return { isError, isFetched, taxonomy };
+ };
+
+ const { isError, isFetched, taxonomy } = useTaxonomyDetailData(taxonomyId);
+
+ if (isError) {
+ return (
+
+ );
+ }
+
+ if (!isFetched) {
+ return (
+
+ );
+ }
+
+ if (taxonomy) {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+ }
+
+ return undefined;
+};
+
+const TaxonomyDetailPage = ({ taxonomyId }) => (
+ <>
+
+
+
+ >
+);
+
+TaxonomyDetailPage.propTypes = {
+ taxonomyId: Proptypes.number,
+};
+
+TaxonomyDetailPage.defaultProps = {
+ taxonomyId: undefined,
+};
+
+TaxonomyDetailContent.propTypes = TaxonomyDetailPage.propTypes;
+TaxonomyDetailContent.defaultProps = TaxonomyDetailPage.defaultProps;
+
+export default TaxonomyDetailPage;
diff --git a/src/taxonomy/taxonomy-detail/TaxonomyDetailSideCard.jsx b/src/taxonomy/taxonomy-detail/TaxonomyDetailSideCard.jsx
new file mode 100644
index 0000000000..3a27180ff5
--- /dev/null
+++ b/src/taxonomy/taxonomy-detail/TaxonomyDetailSideCard.jsx
@@ -0,0 +1,27 @@
+import {
+ Card,
+} from '@edx/paragon';
+import Proptypes from 'prop-types';
+
+const TaxonomyDetailSideCard = ({ taxonomy }) => (
+
+
+
+ {taxonomy.name}
+
+
+
+ {taxonomy.description}
+
+
+
+ No copyright added
+
+
+);
+
+TaxonomyDetailSideCard.propTypes = {
+ taxonomy: Proptypes.object.isRequired,
+};
+
+export default TaxonomyDetailSideCard;
diff --git a/src/taxonomy/taxonomy-detail/index.js b/src/taxonomy/taxonomy-detail/index.js
new file mode 100644
index 0000000000..452695f08f
--- /dev/null
+++ b/src/taxonomy/taxonomy-detail/index.js
@@ -0,0 +1,2 @@
+// eslint-disable-next-line import/prefer-default-export
+export { default as TaxonomyDetailPage } from './TaxonomyDetailPage';