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 Nov 11, 2023
1 parent 080b03e commit 56dbdd2
Show file tree
Hide file tree
Showing 29 changed files with 962 additions and 22 deletions.
10 changes: 5 additions & 5 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 { TaxonomyLayout, TaxonomyDetailPage, TaxonomyListPage } from './taxonomy';

import 'react-datepicker/dist/react-datepicker.css';
import './index.scss';
Expand Down Expand Up @@ -53,10 +53,10 @@ const App = () => {
<Route path="/course/:courseId/*" element={<CourseAuthoringRoutes />} />
<Route path="/course_rerun/:courseId" element={<CourseRerun />} />
{process.env.ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && (
<Route
path="/taxonomy-list"
element={<TaxonomyListPage />}
/>
<Route path="/taxonomy-list" element={<TaxonomyLayout />}>
<Route index element={<TaxonomyListPage />} />
<Route path="/taxonomy-list/:taxonomyId" element={<TaxonomyDetailPage />} />
</Route>
)}
</Routes>
</QueryClientProvider>
Expand Down
14 changes: 14 additions & 0 deletions src/taxonomy/TaxonomyLayout.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { StudioFooter } from '@edx/frontend-component-footer';
import { Outlet } from 'react-router-dom';

import Header from '../header';

const TaxonomyLayout = () => (
<div className="bg-light-400">
<Header isHiddenMainMenu />
<Outlet />
<StudioFooter />
</div>
);

export default TaxonomyLayout;
48 changes: 48 additions & 0 deletions src/taxonomy/TaxonomyLayout.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { initializeMockApp } from '@edx/frontend-platform';
import { AppProvider } from '@edx/frontend-platform/react';
import { render } from '@testing-library/react';

import initializeStore from '../store';
import TaxonomyLayout from './TaxonomyLayout';

let store;

jest.mock('../header', () => jest.fn(() => <div data-testid="mock-header" />));
jest.mock('@edx/frontend-component-footer', () => ({
StudioFooter: jest.fn(() => <div data-testid="mock-footer" />),
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
Outlet: jest.fn(() => <div data-testid="mock-content" />),
}));

const RootWrapper = () => (
<AppProvider store={store}>
<IntlProvider locale="en" messages={{}}>
<TaxonomyLayout />
</IntlProvider>
</AppProvider>
);

describe('<TaxonomyLayout />', async () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
store = initializeStore();
});

it('should render page correctly', async () => {
const { getByTestId } = render(<RootWrapper />);
expect(getByTestId('mock-header')).toBeInTheDocument();
expect(getByTestId('mock-content')).toBeInTheDocument();
expect(getByTestId('mock-footer')).toBeInTheDocument();
});
});
11 changes: 0 additions & 11 deletions src/taxonomy/TaxonomyListPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import {
DataTable,
Spinner,
} from '@edx/paragon';
import { StudioFooter } from '@edx/frontend-component-footer';
import { useIntl } from '@edx/frontend-platform/i18n';
import Header from '../header';
import SubHeader from '../generic/sub-header/SubHeader';
import messages from './messages';
import TaxonomyCard from './taxonomy-card';
Expand Down Expand Up @@ -37,14 +35,6 @@ const TaxonomyListPage = () => {

return (
<>
<style>
{`
body {
background-color: #E9E6E4; /* light-400 */
}
`}
</style>
<Header isHiddenMainMenu />
<div className="pt-4.5 pr-4.5 pl-4.5 pb-2 bg-light-100 box-shadow-down-2">
<Container size="xl">
<SubHeader
Expand Down Expand Up @@ -93,7 +83,6 @@ const TaxonomyListPage = () => {
)}
</Container>
</div>
<StudioFooter />
</>
);
};
Expand Down
9 changes: 8 additions & 1 deletion src/taxonomy/data/types.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @ts-check

/**
/**
* @typedef {Object} TaxonomyData
* @property {number} id
* @property {string} name
Expand All @@ -27,6 +27,13 @@
* @property {TaxonomyListData} data
*/

/**
* @typedef {Object} ExportRequestParams
* @property {number} pk
* @property {string} format
* @property {string} name
*/

/**
* @typedef {Object} UseQueryResult
* @property {Object} data
Expand Down
3 changes: 2 additions & 1 deletion src/taxonomy/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
// eslint-disable-next-line import/prefer-default-export
export { default as TaxonomyListPage } from './TaxonomyListPage';
export { default as TaxonomyLayout } from './TaxonomyLayout';
export { TaxonomyDetailPage } from './taxonomy-detail';
68 changes: 68 additions & 0 deletions src/taxonomy/tag-list/TagListTable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// ts-check
import { useIntl } from '@edx/frontend-platform/i18n';
import {
DataTable,
} from '@edx/paragon';
import _ from 'lodash';
import Proptypes from 'prop-types';
import { useState } from 'react';

import messages from './messages';
import { useTagListDataResponse, useTagListDataStatus } from './data/selectors';

const TagListTable = ({ taxonomyId }) => {
const intl = useIntl();

const [options, setOptions] = useState({
pageIndex: 0,
});

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

const { tagList, isLoading } = useTagListData();

const fetchData = (args) => {
if (!_.isEqual(args, options)) {
setOptions({ ...args });
}
};

return (
<DataTable
isLoading={isLoading}
isPaginated
manualPagination
fetchData={fetchData}
data={tagList?.results || []}
itemCount={tagList?.count || 0}
pageCount={tagList?.numPages || 0}
initialState={options}
columns={[
{
Header: intl.formatMessage(messages.tagListColumnValueHeader),
accessor: 'value',
},
]}
>
<DataTable.TableControlBar />
<DataTable.Table />
<DataTable.EmptyTable content={intl.formatMessage(messages.noResultsFoundMessage)} />
<DataTable.TableFooter />
</DataTable>
);
};

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

export default TagListTable;
67 changes: 67 additions & 0 deletions src/taxonomy/tag-list/TagListTable.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { initializeMockApp } from '@edx/frontend-platform';
import { AppProvider } from '@edx/frontend-platform/react';
import { render } from '@testing-library/react';

import { useTagListData } from './data/api';
import initializeStore from '../../store';
import TagListTable from './TagListTable';

let store;

jest.mock('./data/api', () => ({
useTagListData: jest.fn(),
}));

const RootWrapper = () => (
<AppProvider store={store}>
<IntlProvider locale="en" messages={{}}>
<TagListTable taxonomyId="1" />
</IntlProvider>
</AppProvider>
);

describe('<TagListPage />', async () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
store = initializeStore();
});

it('shows the spinner before the query is complete', async () => {
useTagListData.mockReturnValue({
isLoading: true,
isFetched: false,
});
const { getByRole } = render(<RootWrapper />);
const spinner = getByRole('status');
expect(spinner.textContent).toEqual('loading');
});

it('should render page correctly', async () => {
useTagListData.mockReturnValue({
isSuccess: true,
isFetched: true,
isError: false,
data: {
count: 3,
numPages: 1,
results: [
{ value: 'Tag 1' },
{ value: 'Tag 2' },
{ value: 'Tag 3' },
],
},
});
const { getAllByRole } = render(<RootWrapper />);
const rows = getAllByRole('row');
expect(rows.length).toBe(3 + 1); // 3 items plus header
});
});
26 changes: 26 additions & 0 deletions src/taxonomy/tag-list/data/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// @ts-check
import { useQuery } from '@tanstack/react-query';
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
const getTagListApiUrl = (taxonomyId, page) => new URL(
`api/content_tagging/v1/taxonomies/${taxonomyId}/tags/?page=${page + 1}`,
getApiBaseUrl(),
).href;

// ToDo: fix types
/**
* @param {number} taxonomyId
* @param {import('./types.mjs').QueryOptions} options
* @returns {import('@tanstack/react-query').UseQueryResult<import('./types.mjs').TagListData>}
*/ // eslint-disable-next-line import/prefer-default-export
export const useTagListData = (taxonomyId, options) => {
const { pageIndex } = options;
return useQuery({
queryKey: ['tagList', taxonomyId, pageIndex],
queryFn: () => getAuthenticatedHttpClient().get(getTagListApiUrl(taxonomyId, pageIndex))
.then((response) => response.data)
.then(camelCaseObject),
});
};
27 changes: 27 additions & 0 deletions src/taxonomy/tag-list/data/api.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useQuery } from '@tanstack/react-query';
import {
useTagListData,
} from './api';

const mockHttpClient = {
get: jest.fn(),
};

jest.mock('@tanstack/react-query', () => ({
useQuery: jest.fn(),
}));

jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedHttpClient: jest.fn(() => mockHttpClient),
}));

describe('useTagListData', () => {
it('should call useQuery with the correct parameters', () => {
useTagListData('1', { pageIndex: 3 });

expect(useQuery).toHaveBeenCalledWith({
queryKey: ['tagList', '1', 3],
queryFn: expect.any(Function),
});
});
});
42 changes: 42 additions & 0 deletions src/taxonomy/tag-list/data/selectors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// @ts-check
import {
useTagListData,
} from './api';

/* eslint-disable max-len */
/**
* @param {number} taxonomyId
* @param {import("./types.mjs").QueryOptions} options
* @returns {Pick<import('@tanstack/react-query').UseQueryResult, "error" | "isError" | "isFetched" | "isLoading" | "isSuccess" >}
*/ /* eslint-enable max-len */
export const useTagListDataStatus = (taxonomyId, options) => {
const {
error,
isError,
isFetched,
isLoading,
isSuccess,
} = useTagListData(taxonomyId, options);
return {
error,
isError,
isFetched,
isLoading,
isSuccess,
};
};

// ToDo: fix types
/**
* @param {number} taxonomyId
* @param {import("./types.mjs").QueryOptions} options
* @returns {import("./types.mjs").TagListData | undefined}
*/
export const useTagListDataResponse = (taxonomyId, options) => {
const { isSuccess, data } = useTagListData(taxonomyId, options);
if (isSuccess) {
return data;
}

return undefined;
};
Loading

0 comments on commit 56dbdd2

Please sign in to comment.