Skip to content

Commit

Permalink
feat: bare bones taxonomy detail page [FC-0036] (#655)
Browse files Browse the repository at this point in the history
* feat: System-defined tooltip added

* feat: Taxonomy card menu added. Export menu item added

* feat: Modal for export taxonomy

* feat: Connect with export API

* test: Tests for API and selectors

* feat: Use windows.location.href to call the export endpoint

* test: ExportModal.test added

* style: Delete unnecesary code

* docs: README updated with taxonomy feature

* style: TaxonomyCard updated to a better code style

* style: injectIntl replaced by useIntl on taxonomy pages and components

* refactor: Move and rename taxonomy UI components to match 0002 ADR

* refactor: Move api to data to match with 0002 ADR

* test: Refactor ExportModal tests

* chore: Fix validations

* chore: Lint

* refactor: Moving hooks to apiHooks

* feat: add taxonomy detail page

* fix: address nits in PR review

* refactor: move data/selectors to data/apiHooks

and fix tests to mock useQuery.

* fix: address nits in PR review

* fix: replace taxonomy menu ModalPopup with Dropdown menu

Avoids clicking through to the card when using the menu button to hide
a card's menu.

* fix: change taxonomy URLs

* /taxonomy-list is now /taxonomies, and there's a temporary redirect
* /taxonomy-list/:id: is now /taxonomy/:id:

---------

Co-authored-by: Christofer <[email protected]>
Co-authored-by: XnpioChV <[email protected]>
Co-authored-by: Christofer Chavez <[email protected]>
Co-authored-by: Jillian Vogel <[email protected]>
Co-authored-by: Braden MacDonald <[email protected]>
  • Loading branch information
6 people authored Nov 20, 2023
1 parent 375006d commit 02cdccc
Show file tree
Hide file tree
Showing 30 changed files with 963 additions and 55 deletions.
16 changes: 10 additions & 6 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
import { AppProvider, ErrorPage } from '@edx/frontend-platform/react';
import React, { useEffect } from 'react';
import ReactDOM from 'react-dom';
import { Route, Routes } from 'react-router-dom';
import { Navigate, Route, Routes } from 'react-router-dom';
import {
QueryClient,
QueryClientProvider,
Expand All @@ -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 { ContentTagsDrawer } from './content-tags-drawer';

import 'react-datepicker/dist/react-datepicker.css';
Expand Down Expand Up @@ -55,10 +55,14 @@ const App = () => {
<Route path="/course_rerun/:courseId" element={<CourseRerun />} />
{process.env.ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && (
<>
<Route
path="/taxonomy-list"
element={<TaxonomyListPage />}
/>
{/* TODO: remove this redirect once Studio's link is updated */}
<Route path="/taxonomy-list" element={<Navigate to="/taxonomies" />} />
<Route path="/taxonomies" element={<TaxonomyLayout />}>
<Route index element={<TaxonomyListPage />} />
</Route>
<Route path="/taxonomy" element={<TaxonomyLayout />}>
<Route path="/taxonomy/:taxonomyId" element={<TaxonomyDetailPage />} />
</Route>
<Route
path="/tagging/components/widget/:contentId"
element={<ContentTagsDrawer />}
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
6 changes: 5 additions & 1 deletion src/taxonomy/export-modal/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ const ExportModal = ({
<ModalDialog.CloseButton variant="tertiary">
{intl.formatMessage(messages.taxonomyModalsCancelLabel)}
</ModalDialog.CloseButton>
<Button variant="primary" onClick={onClickExport}>
<Button
variant="primary"
onClick={onClickExport}
data-testid={`export-button-${taxonomyId}`}
>
{intl.formatMessage(messages.exportModalSubmitButtonLabel)}
</Button>
</ActionRow>
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';
56 changes: 56 additions & 0 deletions src/taxonomy/tag-list/TagListTable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// 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/apiHooks';

const TagListTable = ({ taxonomyId }) => {
const intl = useIntl();
const [options, setOptions] = useState({
pageIndex: 0,
});
const { isLoading } = useTagListDataStatus(taxonomyId, options);
const tagList = useTagListDataResponse(taxonomyId, options);

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
});
});
27 changes: 27 additions & 0 deletions src/taxonomy/tag-list/data/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// @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: async () => {
const { data } = await getAuthenticatedHttpClient().get(getTagListApiUrl(taxonomyId, pageIndex));
return camelCaseObject(data);
},
});
};
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),
});
});
});
41 changes: 41 additions & 0 deletions src/taxonomy/tag-list/data/apiHooks.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// @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,
};
};

/**
* @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 02cdccc

Please sign in to comment.