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';