Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: display all child tags in the "bare bones" taxonomy detail page [FAL-3529] [FC-0036] #10

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 16 additions & 14 deletions src/generic/Loading.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,29 @@ import React from 'react';
import { Spinner } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';

export const LoadingSpinner = () => (
<Spinner
animation="border"
role="status"
variant="primary"
screenReaderText={(
<FormattedMessage
id="authoring.loading"
defaultMessage="Loading..."
description="Screen-reader message for when a page is loading."
/>
)}
/>
);

const Loading = () => (
<div
className="d-flex justify-content-center align-items-center flex-column"
style={{
height: '100vh',
}}
>
<Spinner
animation="border"
role="status"
variant="primary"
screenReaderText={(
<span className="sr-only">
<FormattedMessage
id="authoring.loading"
defaultMessage="Loading..."
description="Screen-reader message for when a page is loading."
/>
</span>
)}
/>
<LoadingSpinner />
</div>
);

Expand Down
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();
});
});
17 changes: 6 additions & 11 deletions src/taxonomy/TaxonomyListPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ 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 { Helmet } from 'react-helmet';

import SubHeader from '../generic/sub-header/SubHeader';
import getPageHeadTitle from '../generic/utils';
import messages from './messages';
import TaxonomyCard from './taxonomy-card';
import { useTaxonomyListDataResponse, useIsTaxonomyListDataLoaded } from './data/apiHooks';
Expand Down Expand Up @@ -37,14 +38,9 @@ const TaxonomyListPage = () => {

return (
<>
<style>
{`
body {
background-color: #E9E6E4; /* light-400 */
}
`}
</style>
<Header isHiddenMainMenu />
<Helmet>
<title>{getPageHeadTitle("", intl.formatMessage(messages.headerTitle))}</title>
</Helmet>
<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 +89,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';
101 changes: 101 additions & 0 deletions src/taxonomy/tag-list/TagListTable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// ts-check
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { DataTable } from '@edx/paragon';
import _ from 'lodash';
import Proptypes from 'prop-types';
import { useState } from 'react';

import { LoadingSpinner } from '../../generic/Loading';
import messages from './messages';
import { useTagListDataResponse, useTagListDataStatus } from './data/apiHooks';
import { useSubTags } from './data/api';

const SubTagsExpanded = ({ taxonomyId, parentTagValue }) => {
const subTagsData = useSubTags(taxonomyId, parentTagValue);

if (subTagsData.isLoading) {
return <LoadingSpinner />;
}
if (subTagsData.isError) {
return <FormattedMessage {...messages.tagListError} />;
}

return (
<ul style={{ listStyleType: 'none' }}>
{subTagsData.data.results.map(tagData => (
<li key={tagData.id} style={{ paddingLeft: `${(tagData.depth - 1) * 30}px` }}>
{tagData.value} <span className="text-light-900">{tagData.childCount > 0 ? `(${tagData.childCount})` : null}</span>
</li>
))}
</ul>
);
};

SubTagsExpanded.propTypes = {
taxonomyId: Proptypes.number.isRequired,
parentTagValue: Proptypes.string.isRequired,
};

/**
* An "Expand" toggle to show/hide subtags, but one which is hidden if the given tag row has no subtags.
*/
const OptionalExpandLink = ({ row }) => (row.values.childCount > 0 ? <DataTable.ExpandRow row={row} /> : null);
OptionalExpandLink.propTypes = DataTable.ExpandRow.propTypes;

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}
isExpandable
// This is a temporary "bare bones" solution for brute-force loading all the child tags. In future we'll match
// the Figma design and do something more sophisticated.
renderRowSubComponent={({ row }) => <SubTagsExpanded taxonomyId={taxonomyId} parentTagValue={row.values.value} />}
columns={[
{
Header: intl.formatMessage(messages.tagListColumnValueHeader),
accessor: 'value',
},
{
id: 'expander',
Header: DataTable.ExpandAll,
Cell: OptionalExpandLink,
},
{
Header: intl.formatMessage(messages.tagListColumnChildCountHeader),
accessor: 'childCount',
},
]}
>
<DataTable.TableControlBar />
<DataTable.Table />
<DataTable.EmptyTable content={intl.formatMessage(messages.noResultsFoundMessage)} />
<DataTable.TableFooter />
</DataTable>
);
};

TagListTable.propTypes = {
taxonomyId: Proptypes.number.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
});
});
Loading