Skip to content

Commit

Permalink
feat: add detail taxonomy page
Browse files Browse the repository at this point in the history
  • Loading branch information
rpenido committed Oct 20, 2023
1 parent 60b4359 commit 2520d22
Show file tree
Hide file tree
Showing 9 changed files with 240 additions and 61 deletions.
19 changes: 13 additions & 6 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -71,11 +71,18 @@ const App = () => {
}}
/>
{process.env.ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && (
<Route
path="/taxonomy-list"
>
<TaxonomyListPage />
</Route>
<>
<Route exact path="/taxonomy-list/" component={TaxonomyListPage} />
<Route
path="/taxonomy-list/:taxonomyId"
render={({ match }) => {
const { params: { taxonomyId } } = match;
return (
<TaxonomyDetailPage taxonomyId={Number(taxonomyId)} />
);
}}
/>
</>
)}
</Switch>
</QueryClientProvider>
Expand Down
22 changes: 20 additions & 2 deletions src/taxonomy/api/hooks/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -48,6 +55,17 @@ export const useExportTaxonomy = () => {
}
downloadDataAsFile(data, contentType, `${name}.${fileExtension}`);
};

return useMutation(exportTaxonomy);
};

/**
* @param {number} taxonomyId
* @returns {import('@tanstack/react-query').UseQueryResult<import('../types.mjs').TaxonomyData>}
*/
export const useTaxonomyDetailData = (taxonomyId) => (
useQuery({
queryKey: ['taxonomyList', taxonomyId],
queryFn: () => getAuthenticatedHttpClient().get(getTaxonomyDetailApiUrl(taxonomyId))
.then(camelCaseObject)
.then((response) => response.data),
})
);
56 changes: 5 additions & 51 deletions src/taxonomy/api/hooks/api.test.js
Original file line number Diff line number Diff line change
@@ -1,71 +1,25 @@
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';

Check failure on line 2 in src/taxonomy/api/hooks/api.test.js

View workflow job for this annotation

GitHub Actions / tests

Using exported name 'useTaxonomyListData' as identifier for default export

Check failure on line 2 in src/taxonomy/api/hooks/api.test.js

View workflow job for this annotation

GitHub Actions / tests

Using exported name 'useTaxonomyListData' as identifier for default export

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({
queryKey: ['taxonomyList'],
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',
);
});
});
34 changes: 33 additions & 1 deletion src/taxonomy/api/hooks/selectors.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @ts-check
import {
useTaxonomyDetailData,
useTaxonomyListData,
useExportTaxonomy,
} from './api';
Expand All @@ -10,7 +11,7 @@ import {
export const useTaxonomyListDataResponse = () => {
const response = useTaxonomyListData();
if (response.status === 'success') {
return response.data.data;
return response.data;
}
return undefined;
};
Expand All @@ -25,3 +26,34 @@ export const useIsTaxonomyListDataLoaded = () => (
export const useExportTaxonomyMutation = () => (
useExportTaxonomy()
);
/**
* @params {number} taxonomyId
* @returns {Pick<import('@tanstack/react-query').UseQueryResult, "error" | "isError" | "isFetched" | "isSuccess">}
*/
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;
};
2 changes: 1 addition & 1 deletion src/taxonomy/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// eslint-disable-next-line import/prefer-default-export
export { default as TaxonomyListPage } from './TaxonomyListPage';
export { TaxonomyDetailPage } from './taxonomy-detail';
42 changes: 42 additions & 0 deletions src/taxonomy/taxonomy-detail/TagListTable.jsx
Original file line number Diff line number Diff line change
@@ -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 }) => (

Check failure on line 17 in src/taxonomy/taxonomy-detail/TagListTable.jsx

View workflow job for this annotation

GitHub Actions / tests

'tags' is defined but never used

Check failure on line 17 in src/taxonomy/taxonomy-detail/TagListTable.jsx

View workflow job for this annotation

GitHub Actions / tests

'tags' is defined but never used
<DataTable
isFilterable
isSortable
defaultColumnValues={{ Filter: TextFilter }}
itemCount={tagsSample.length}
data={tagsSample}
columns={[
{
Header: 'Name',
accessor: 'name',
},
]}
>
<DataTable.TableControlBar />
<DataTable.Table />
<DataTable.EmptyTable content="No results found" />
<DataTable.TableFooter />
</DataTable>
);

TagListTable.propTypes = {
tags: Proptypes.array.isRequired,

Check failure on line 39 in src/taxonomy/taxonomy-detail/TagListTable.jsx

View workflow job for this annotation

GitHub Actions / tests

Prop type "array" is forbidden

Check failure on line 39 in src/taxonomy/taxonomy-detail/TagListTable.jsx

View workflow job for this annotation

GitHub Actions / tests

Prop type "array" is forbidden
};

export default TagListTable;
97 changes: 97 additions & 0 deletions src/taxonomy/taxonomy-detail/TaxonomyDetailPage.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<PermissionDeniedAlert />
);
}

if (!isFetched) {
return (
<Loading />
);
}

if (taxonomy) {
return (
<>
<div className="pt-4.5 pr-4.5 pl-4.5 pb-2 bg-light-100 box-shadow-down-2">
<Container size="xl">
<SubHeader
title={taxonomy.name}
hideBorder
/>
</Container>
</div>
<div className="bg-light-400 m-4">
<Layout
lg={[{ span: 9 }, { span: 3 }]}
md={[{ span: 9 }, { span: 3 }]}
sm={[{ span: 9 }, { span: 3 }]}
xs={[{ span: 9 }, { span: 3 }]}
xl={[{ span: 9 }, { span: 3 }]}
>
<Layout.Element>
<TagListTable />
</Layout.Element>
<Layout.Element>
<TaxonomyDetailSideCard taxonomy={taxonomy} />
</Layout.Element>

</Layout>
</div>
</>
);
}

return undefined;
};

const TaxonomyDetailPage = ({ taxonomyId }) => (
<>
<style>
{`
body {
background-color: #E9E6E4; /* light-400 */
}
`}
</style>
<Header isHiddenMainMenu />
<TaxonomyDetailContent taxonomyId={taxonomyId} />
</>
);

TaxonomyDetailPage.propTypes = {
taxonomyId: Proptypes.number,
};

TaxonomyDetailPage.defaultProps = {
taxonomyId: undefined,
};

TaxonomyDetailContent.propTypes = TaxonomyDetailPage.propTypes;
TaxonomyDetailContent.defaultProps = TaxonomyDetailPage.defaultProps;

export default TaxonomyDetailPage;
27 changes: 27 additions & 0 deletions src/taxonomy/taxonomy-detail/TaxonomyDetailSideCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {
Card,
} from '@edx/paragon';
import Proptypes from 'prop-types';

const TaxonomyDetailSideCard = ({ taxonomy }) => (
<Card>
<Card.Header title="Taxonomy details" />
<Card.Section title="Title">
{taxonomy.name}
</Card.Section>
<Card.Divider className="ml-3 mr-3" />
<Card.Section title="Description">
{taxonomy.description}
</Card.Section>
<Card.Divider className="ml-3 mr-3" />
<Card.Section title="Copyright">
No copyright added
</Card.Section>
</Card>
);

TaxonomyDetailSideCard.propTypes = {
taxonomy: Proptypes.object.isRequired,

Check failure on line 24 in src/taxonomy/taxonomy-detail/TaxonomyDetailSideCard.jsx

View workflow job for this annotation

GitHub Actions / tests

Prop type "object" is forbidden

Check failure on line 24 in src/taxonomy/taxonomy-detail/TaxonomyDetailSideCard.jsx

View workflow job for this annotation

GitHub Actions / tests

Prop type "object" is forbidden
};

export default TaxonomyDetailSideCard;
2 changes: 2 additions & 0 deletions src/taxonomy/taxonomy-detail/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 TaxonomyDetailPage } from './TaxonomyDetailPage';

0 comments on commit 2520d22

Please sign in to comment.