Skip to content

Commit

Permalink
feat: add taxonomy detail page
Browse files Browse the repository at this point in the history
  • Loading branch information
rpenido committed Oct 24, 2023
1 parent 1925fae commit 31a6ad7
Show file tree
Hide file tree
Showing 10 changed files with 320 additions and 9 deletions.
22 changes: 16 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,12 +71,22 @@ 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)} />
);
}}
/>
</>
)}
<Route>
No match (404)
</Route>
</Switch>
</QueryClientProvider>
</AppProvider>
Expand Down
35 changes: 34 additions & 1 deletion src/taxonomy/api/hooks/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,15 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
const getTaxonomyListApiUrl = () => new URL('api/content_tagging/v1/taxonomies/?enabled=true', getApiBaseUrl()).href;
export const getExportTaxonomyApiUrl = (pk, format) => new URL(
const getExportTaxonomyApiUrl = (pk, format) => new URL(
`api/content_tagging/v1/taxonomies/${pk}/export/?output_format=${format}&download=1`,
).href;
const getTaxonomyDetailApiUrl = (taxonomyId) => new URL(
`api/content_tagging/v1/taxonomies/${taxonomyId}/`,
getApiBaseUrl(),
).href;
const getTagListApiUrl = (taxonomyId) => new URL(
`api/content_tagging/v1/taxonomies/${taxonomyId}/tags/`,
getApiBaseUrl(),
).href;

Expand All @@ -24,3 +31,29 @@ export const useTaxonomyListData = () => (
export const exportTaxonomy = (pk, format) => {
window.location.href = getExportTaxonomyApiUrl(pk, format);
};

/**
* @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),
})
);

/**
* @param {number} taxonomyId
* @returns {import('@tanstack/react-query').UseQueryResult<import('../types.mjs').TaxonomyData>}
*/
export const useTagListData = (taxonomyId) => (
useQuery({
queryKey: ['tagList', taxonomyId],
queryFn: () => getAuthenticatedHttpClient().get(getTagListApiUrl(taxonomyId))
.then(camelCaseObject)
.then((response) => response.data),
})
);
66 changes: 66 additions & 0 deletions src/taxonomy/api/hooks/selectors.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// @ts-check
import {
useTaxonomyDetailData,
useTaxonomyListData,
useTagListData,
exportTaxonomy,
} from './api';

Expand All @@ -25,3 +27,67 @@ export const useIsTaxonomyListDataLoaded = () => (
export const callExportTaxonomy = (pk, format) => (
exportTaxonomy(pk, format)
);

/**
* @param {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,
};
};

/**
* @param {number} taxonomyId
* @returns {import("../types.mjs").TaxonomyData | undefined}
*/
export const useTaxonomyDetailDataResponse = (taxonomyId) => {
const { isSuccess, data } = useTaxonomyDetailData(taxonomyId);
if (isSuccess) {
return data;
}

return undefined;
};

/**
* @param {number} taxonomyId
* @returns {Pick<import('@tanstack/react-query').UseQueryResult, "error" | "isError" | "isFetched" | "isSuccess">}
*/
export const useTagListDataStatus = (taxonomyId) => {
const {
isError,
error,
isFetched,
isSuccess,
} = useTagListData(taxonomyId);
return {
isError,
error,
isFetched,
isSuccess,
};
};

/**
* @param {number} taxonomyId
* @returns {import("../types.mjs").TaxonomyData | undefined}
*/
export const useTagListDataResponse = (taxonomyId) => {
const { isSuccess, data } = useTagListData(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 { default as TaxonomyDetailPage } from './taxonomy-detail/TaxonomyDetailPage';
5 changes: 4 additions & 1 deletion src/taxonomy/taxonomy-card/TaxonomyCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
} from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';

import classNames from 'classnames';
import messages from '../messages';
import TaxonomyCardMenu from './TaxonomyCardMenu';
Expand Down Expand Up @@ -101,7 +103,8 @@ const TaxonomyCard = ({ className, original, intl }) => {

return (
<>
<Card className={classNames('taxonomy-card', className)} data-testid={`taxonomy-card-${id}`}>
<Card isClickable className={classNames('taxonomy-card', className)} data-testid={`taxonomy-card-${id}`}>
<Link className="stretched-link" to={`taxonomy-list/${id}`} aria-label="view taxonomy details" />
<Card.Header
title={name}
subtitle={getHeaderSubtitle()}
Expand Down
5 changes: 5 additions & 0 deletions src/taxonomy/taxonomy-card/TaxonomyCard.scss
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
white-space: nowrap;
}

.pgn__card-header-actions, .badge {
z-index: 100;
position: relative;
}

.taxonomy-menu-item:focus {
/**
* There is a bug in the menu that auto focus the first item.
Expand Down
1 change: 1 addition & 0 deletions src/taxonomy/taxonomy-card/TaxonomyCardMenu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const TaxonomyCardMenu = ({
return (
<>
<IconButton
className="streched-link"
variant="primary"
onClick={() => setMenuIsOpen(true)}
ref={setMenuTarget}
Expand Down
50 changes: 50 additions & 0 deletions src/taxonomy/taxonomy-detail/TagListTable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {
DataTable,
TextFilter,
} from '@edx/paragon';
import Proptypes from 'prop-types';

import { useTagListDataResponse, useTagListDataStatus } from '../api/hooks/selectors';

const TagListTable = ({ taxonomyId }) => {
const useTagListData = () => {
const { isError, isFetched } = useTagListDataStatus(taxonomyId);
const tagList = useTagListDataResponse(taxonomyId);
return { isError, isFetched, tagList };
};

const { isError, isFetched, tagList } = useTagListData(taxonomyId);

if (!tagList || !tagList.results) {
if (tagList)
return JSON.stringify(tagList);

return 'Loading...';
}

return (
<DataTable
isFilterable
isSortable
defaultColumnValues={{ Filter: TextFilter }}
data={tagList.results}
columns={[
{
Header: 'Name',
accessor: 'name',
},
]}
>
<DataTable.TableControlBar />
<DataTable.Table />
<DataTable.EmptyTable content="No results found" />
<DataTable.TableFooter />
</DataTable>
);
};

TagListTable.propTypes = {
taxonomyId: Proptypes.number.isRequired,
};

export default TagListTable;
116 changes: 116 additions & 0 deletions src/taxonomy/taxonomy-detail/TaxonomyDetailPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import React, { useState } 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 ExportModal from '../modals/ExportModal';
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);

const [isExportModalOpen, setIsExportModalOpen] = useState(false);

if (isError) {
return (
<PermissionDeniedAlert />
);
}

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

const renderModals = () => (
// eslint-disable-next-line react/jsx-no-useless-fragment
<>
{isExportModalOpen && (
<ExportModal
isOpen={isExportModalOpen}
onClose={() => setIsExportModalOpen(false)}
taxonomyId={taxonomyId}
taxonomyName={taxonomy.name}
/>
)}
</>
);

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">
<Container size="xl">
<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 taxonomyId={taxonomyId} />
</Layout.Element>
<Layout.Element>
<TaxonomyDetailSideCard taxonomy={taxonomy} />
</Layout.Element>
</Layout>
</Container>
</div>
{renderModals()}
</>
);
}

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,
};

export default TaxonomyDetailSideCard;

0 comments on commit 31a6ad7

Please sign in to comment.