style: UX Refinements on taxonomy pages [FC-0036] (#723)
This change makes the following updates to the UX of the taxonomy pages: * On the taxonomies list, display the full name of taxonomies in a tooltip if it's longer than what's displayed * On the taxonomy detail page, please change the title of the "Value" column to "Tag name" * On taxonomy detail page, remove the "child tags" column and put it in parentheses instead * Update tags count color * Several minor issues brought up here: https://github.com/openedx/modular-learning/issues/105#issuecomment-1829412705. * Fix issue with scroll position not being reset on navigation
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
.text-black {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.mw-300px {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ const SubHeader = ({
|
||||
<small className="sub-header-title-subtitle">{subtitle}</small>
|
||||
{title}
|
||||
{titleActions && (
|
||||
<ActionRow className="ml-auto">
|
||||
<ActionRow className="ml-auto mt-2 justify-content-start">
|
||||
{titleActions}
|
||||
</ActionRow>
|
||||
)}
|
||||
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
import { AppProvider, ErrorPage } from '@edx/frontend-platform/react';
|
||||
import React, { useEffect } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
import {
|
||||
Navigate, Route, createRoutesFromElements, createBrowserRouter, RouterProvider,
|
||||
} from 'react-router-dom';
|
||||
import {
|
||||
QueryClient,
|
||||
QueryClientProvider,
|
||||
@@ -45,31 +47,37 @@ const App = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const router = createBrowserRouter(
|
||||
createRoutesFromElements(
|
||||
<Route>
|
||||
<Route path="/home" element={<StudioHome />} />
|
||||
<Route path="/course/:courseId/*" element={<CourseAuthoringRoutes />} />
|
||||
<Route path="/course_rerun/:courseId" element={<CourseRerun />} />
|
||||
{process.env.ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && (
|
||||
<>
|
||||
{/* 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 />}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Route>,
|
||||
),
|
||||
);
|
||||
|
||||
return (
|
||||
<AppProvider store={initializeStore()}>
|
||||
<AppProvider store={initializeStore()} wrapWithRouter={false}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Head />
|
||||
<Routes>
|
||||
<Route path="/home" element={<StudioHome />} />
|
||||
<Route path="/course/:courseId/*" element={<CourseAuthoringRoutes />} />
|
||||
<Route path="/course_rerun/:courseId" element={<CourseRerun />} />
|
||||
{process.env.ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && (
|
||||
<>
|
||||
{/* 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 />}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Routes>
|
||||
<RouterProvider router={router} />
|
||||
</QueryClientProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { StudioFooter } from '@edx/frontend-component-footer';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { Outlet, ScrollRestoration } from 'react-router-dom';
|
||||
import { Toast } from '@edx/paragon';
|
||||
|
||||
import Header from '../header';
|
||||
@@ -28,6 +28,7 @@ const TaxonomyLayout = () => {
|
||||
{toastMessage}
|
||||
</Toast>
|
||||
</div>
|
||||
<ScrollRestoration />
|
||||
</TaxonomyContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ jest.mock('@edx/frontend-component-footer', () => ({
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
Outlet: jest.fn(() => <div data-testid="mock-content" />),
|
||||
ScrollRestoration: jest.fn(() => <div />),
|
||||
}));
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
|
||||
@@ -114,17 +114,22 @@ const TaxonomyListPage = () => {
|
||||
<DataTable
|
||||
disableElevation
|
||||
data={taxonomyListData.results}
|
||||
itemCount={taxonomyListData.results.length}
|
||||
columns={[
|
||||
{
|
||||
Header: 'id',
|
||||
accessor: 'id',
|
||||
},
|
||||
{
|
||||
Header: 'name',
|
||||
accessor: 'name',
|
||||
},
|
||||
{
|
||||
Header: 'description',
|
||||
accessor: 'description',
|
||||
},
|
||||
{
|
||||
Header: 'systemDefined',
|
||||
accessor: 'systemDefined',
|
||||
},
|
||||
{
|
||||
|
||||
11
src/taxonomy/export-modal/ExportModal.scss
Normal file
11
src/taxonomy/export-modal/ExportModal.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
.taxonomy-export-modal {
|
||||
.pgn__form-radio {
|
||||
// Used to extend the clickable area to all the width of the modal
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pgn__form-radio > div {
|
||||
// Used to extend the clickable area to all the width of the modal
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ const ExportModal = ({
|
||||
size="lg"
|
||||
hasCloseButton
|
||||
isFullscreenOnMobile
|
||||
className="taxonomy-export-modal"
|
||||
>
|
||||
<ModalDialog.Header>
|
||||
<ModalDialog.Title>
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
@import "taxonomy/taxonomy-card/TaxonomyCard";
|
||||
@import "taxonomy/delete-dialog/DeleteDialog";
|
||||
@import "taxonomy/system-defined-badge/SystemDefinedBadge";
|
||||
@import "taxonomy/export-modal/ExportModal";
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.system-defined-badge {
|
||||
font-size: 12px;
|
||||
}
|
||||
41
src/taxonomy/system-defined-badge/index.jsx
Normal file
41
src/taxonomy/system-defined-badge/index.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Badge,
|
||||
OverlayTrigger,
|
||||
Popover,
|
||||
} from '@edx/paragon';
|
||||
import messages from './messages';
|
||||
|
||||
const SystemDefinedBadge = ({ taxonomyId }) => {
|
||||
const intl = useIntl();
|
||||
const getToolTip = () => (
|
||||
<Popover id={`system-defined-tooltip-${taxonomyId}`} className="mw-300px">
|
||||
<Popover.Title as="h5">
|
||||
{intl.formatMessage(messages.systemTaxonomyPopoverTitle)}
|
||||
</Popover.Title>
|
||||
<Popover.Content>
|
||||
{intl.formatMessage(messages.systemTaxonomyPopoverBody)}
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
return (
|
||||
<OverlayTrigger
|
||||
key={`system-defined-overlay-${taxonomyId}`}
|
||||
placement="top"
|
||||
overlay={getToolTip()}
|
||||
>
|
||||
<Badge variant="light" className="p-1.5 font-weight-normal system-defined-badge">
|
||||
{intl.formatMessage(messages.systemDefinedBadge)}
|
||||
</Badge>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
SystemDefinedBadge.propTypes = {
|
||||
taxonomyId: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default SystemDefinedBadge;
|
||||
18
src/taxonomy/system-defined-badge/messages.js
Normal file
18
src/taxonomy/system-defined-badge/messages.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
systemTaxonomyPopoverTitle: {
|
||||
id: 'course-authoring.taxonomy-list.popover.system-defined.title',
|
||||
defaultMessage: 'System taxonomy',
|
||||
},
|
||||
systemTaxonomyPopoverBody: {
|
||||
id: 'course-authoring.taxonomy-list.popover.system-defined.body',
|
||||
defaultMessage: 'This is a system-level taxonomy and is enabled by default.',
|
||||
},
|
||||
systemDefinedBadge: {
|
||||
id: 'course-authoring.taxonomy-list.badge.system-defined.label',
|
||||
defaultMessage: 'System-level',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -24,7 +24,7 @@ const SubTagsExpanded = ({ taxonomyId, parentTagValue }) => {
|
||||
<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>
|
||||
{tagData.value} <span className="text-secondary-500">{tagData.childCount > 0 ? `(${tagData.childCount})` : null}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -39,9 +39,20 @@ SubTagsExpanded.propTypes = {
|
||||
/**
|
||||
* 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);
|
||||
const OptionalExpandLink = ({ row }) => (row.original.childCount > 0 ? <DataTable.ExpandRow row={row} /> : null);
|
||||
OptionalExpandLink.propTypes = DataTable.ExpandRow.propTypes;
|
||||
|
||||
/**
|
||||
* Custom DataTable cell to join tag value with child count
|
||||
*/
|
||||
const TagValue = ({ row }) => (
|
||||
<>
|
||||
<span>{row.original.value}</span>
|
||||
<span className="text-secondary-500">{` (${row.original.childCount})`}</span>
|
||||
</>
|
||||
);
|
||||
TagValue.propTypes = DataTable.TableCell.propTypes;
|
||||
|
||||
const TagListTable = ({ taxonomyId }) => {
|
||||
const intl = useIntl();
|
||||
const [options, setOptions] = useState({
|
||||
@@ -69,21 +80,19 @@ const TagListTable = ({ taxonomyId }) => {
|
||||
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} />}
|
||||
renderRowSubComponent={({ row }) => (
|
||||
<SubTagsExpanded taxonomyId={taxonomyId} parentTagValue={row.original.value} />
|
||||
)}
|
||||
columns={[
|
||||
{
|
||||
Header: intl.formatMessage(messages.tagListColumnValueHeader),
|
||||
accessor: 'value',
|
||||
Cell: TagValue,
|
||||
},
|
||||
{
|
||||
id: 'expander',
|
||||
Header: DataTable.ExpandAll,
|
||||
Cell: OptionalExpandLink,
|
||||
},
|
||||
{
|
||||
Header: intl.formatMessage(messages.tagListColumnChildCountHeader),
|
||||
accessor: 'childCount',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<DataTable.TableControlBar />
|
||||
|
||||
@@ -77,7 +77,7 @@ const subTagsResponse = {
|
||||
};
|
||||
const subTagsUrl = 'http://localhost:18010/api/content_tagging/v1/taxonomies/1/tags/?full_depth_threshold=10000&parent_tag=two+level+tag+1';
|
||||
|
||||
describe('<TagListPage />', () => {
|
||||
describe('<TagListTable />', () => {
|
||||
beforeAll(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
|
||||
@@ -7,11 +7,7 @@ const messages = defineMessages({
|
||||
},
|
||||
tagListColumnValueHeader: {
|
||||
id: 'course-authoring.tag-list.column.value.header',
|
||||
defaultMessage: 'Value',
|
||||
},
|
||||
tagListColumnChildCountHeader: {
|
||||
id: 'course-authoring.tag-list.column.value.header',
|
||||
defaultMessage: '# child tags',
|
||||
defaultMessage: 'Tag name',
|
||||
},
|
||||
tagListError: {
|
||||
id: 'course-authoring.tag-list.error',
|
||||
|
||||
@@ -23,13 +23,6 @@
|
||||
-webkit-line-clamp: 6;
|
||||
}
|
||||
|
||||
.pgn__card-header-title-md {
|
||||
/* Set overflow to title of the card */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.taxonomy-menu-item:focus {
|
||||
/**
|
||||
* There is a bug in the menu that auto focus the first item.
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Badge,
|
||||
Card,
|
||||
OverlayTrigger,
|
||||
Popover,
|
||||
} from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import classNames from 'classnames';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
import TaxonomyCardMenu from './TaxonomyCardMenu';
|
||||
import ExportModal from '../export-modal';
|
||||
import DeleteDialog from '../delete-dialog';
|
||||
import SystemDefinedBadge from '../system-defined-badge';
|
||||
|
||||
const orgsCountEnabled = (orgsCount) => orgsCount !== undefined && orgsCount !== 0;
|
||||
|
||||
@@ -20,30 +20,10 @@ const HeaderSubtitle = ({
|
||||
id, showSystemBadge, orgsCount,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const getSystemToolTip = () => (
|
||||
<Popover id={`system-defined-tooltip-${id}`}>
|
||||
<Popover.Title as="h5">
|
||||
{intl.formatMessage(messages.systemTaxonomyPopoverTitle)}
|
||||
</Popover.Title>
|
||||
<Popover.Content>
|
||||
{intl.formatMessage(messages.systemTaxonomyPopoverBody)}
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
// Show system defined badge
|
||||
if (showSystemBadge) {
|
||||
return (
|
||||
<OverlayTrigger
|
||||
key={`system-defined-overlay-${id}`}
|
||||
placement="top"
|
||||
overlay={getSystemToolTip()}
|
||||
>
|
||||
<Badge variant="light">
|
||||
{intl.formatMessage(messages.systemDefinedBadge)}
|
||||
</Badge>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
return <SystemDefinedBadge taxonomyId={id} />;
|
||||
}
|
||||
|
||||
// Or show orgs count
|
||||
@@ -59,10 +39,55 @@ const HeaderSubtitle = ({
|
||||
return null;
|
||||
};
|
||||
|
||||
HeaderSubtitle.defaultProps = {
|
||||
orgsCount: undefined,
|
||||
};
|
||||
|
||||
HeaderSubtitle.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
showSystemBadge: PropTypes.bool.isRequired,
|
||||
orgsCount: PropTypes.number.isRequired,
|
||||
orgsCount: PropTypes.number,
|
||||
};
|
||||
|
||||
const HeaderTitle = ({ taxonomyId, title }) => {
|
||||
const containerRef = useRef(null);
|
||||
const textRef = useRef(null);
|
||||
const [isTruncated, setIsTruncated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const containerWidth = containerRef.current.clientWidth;
|
||||
const textWidth = textRef.current.offsetWidth;
|
||||
setIsTruncated(textWidth > containerWidth);
|
||||
}, [title]);
|
||||
|
||||
const getToolTip = () => (
|
||||
<Popover
|
||||
id={`taxonomy-card-title-tooltip-${taxonomyId}`}
|
||||
className="mw-300px"
|
||||
>
|
||||
<Popover.Content>
|
||||
{title}
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
return (
|
||||
<OverlayTrigger
|
||||
key={`taxonomy-card-title-overlay-${taxonomyId}`}
|
||||
placement="top"
|
||||
overlay={getToolTip()}
|
||||
show={!isTruncated ? false : undefined}
|
||||
>
|
||||
<div ref={containerRef} className="text-truncate">
|
||||
<span ref={textRef}>{title}</span>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
HeaderTitle.propTypes = {
|
||||
taxonomyId: PropTypes.number.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const TaxonomyCard = ({ className, original, onDeleteTaxonomy }) => {
|
||||
@@ -135,13 +160,13 @@ const TaxonomyCard = ({ className, original, onDeleteTaxonomy }) => {
|
||||
<>
|
||||
<Card
|
||||
isClickable
|
||||
as={Link}
|
||||
to={`/taxonomy/${id}`}
|
||||
as={NavLink}
|
||||
to={`/taxonomy/${id}/`}
|
||||
className={classNames('taxonomy-card', className)}
|
||||
data-testid={`taxonomy-card-${id}`}
|
||||
>
|
||||
<Card.Header
|
||||
title={name}
|
||||
title={<HeaderTitle taxonomyId={id} title={name} />}
|
||||
subtitle={(
|
||||
<HeaderSubtitle
|
||||
id={id}
|
||||
|
||||
@@ -1,18 +1,6 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
systemTaxonomyPopoverTitle: {
|
||||
id: 'course-authoring.taxonomy-list.popover.system-defined.title',
|
||||
defaultMessage: 'System taxonomy',
|
||||
},
|
||||
systemTaxonomyPopoverBody: {
|
||||
id: 'course-authoring.taxonomy-list.popover.system-defined.body',
|
||||
defaultMessage: 'This is a system-level taxonomy and is enabled by default.',
|
||||
},
|
||||
systemDefinedBadge: {
|
||||
id: 'course-authoring.taxonomy-list.badge.system-defined.label',
|
||||
defaultMessage: 'System-level',
|
||||
},
|
||||
assignedToOrgsLabel: {
|
||||
id: 'course-authoring.taxonomy-list.orgs-count.label',
|
||||
defaultMessage: 'Assigned to {orgsCount} orgs',
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useTaxonomyDetailDataResponse, useTaxonomyDetailDataStatus } from './da
|
||||
import DeleteDialog from '../delete-dialog';
|
||||
import { useDeleteTaxonomy } from '../data/apiHooks';
|
||||
import { TaxonomyContext } from '../common/context';
|
||||
import SystemDefinedBadge from '../system-defined-badge';
|
||||
|
||||
const TaxonomyDetailPage = () => {
|
||||
const intl = useIntl();
|
||||
@@ -104,6 +105,13 @@ const TaxonomyDetailPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const getSystemDefinedBadge = () => {
|
||||
if (taxonomy.systemDefined) {
|
||||
return <SystemDefinedBadge taxonomyId={taxonomyId} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
@@ -120,6 +128,7 @@ const TaxonomyDetailPage = () => {
|
||||
/>
|
||||
<SubHeader
|
||||
title={taxonomy.name}
|
||||
titleActions={getSystemDefinedBadge()}
|
||||
hideBorder
|
||||
headerActions={getHeaderActions()}
|
||||
/>
|
||||
|
||||
@@ -95,6 +95,22 @@ describe('<TaxonomyDetailPage />', async () => {
|
||||
expect(getByRole('heading')).toHaveTextContent('Test taxonomy');
|
||||
});
|
||||
|
||||
it('should show system defined badge', async () => {
|
||||
useTaxonomyDetailData.mockReturnValue({
|
||||
isSuccess: true,
|
||||
isFetched: true,
|
||||
isError: false,
|
||||
data: {
|
||||
id: 1,
|
||||
name: 'Test taxonomy',
|
||||
description: 'This is a description',
|
||||
systemDefined: true,
|
||||
},
|
||||
});
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText('System-level')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should open export modal on export menu click', () => {
|
||||
useTaxonomyDetailData.mockReturnValue({
|
||||
isSuccess: true,
|
||||
|
||||
Reference in New Issue
Block a user