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:
Chris Chávez
2023-12-18 23:14:44 -05:00
committed by GitHub
parent a37d13f788
commit bf46008878
20 changed files with 217 additions and 86 deletions

View File

@@ -1,3 +1,7 @@
.text-black {
color: $black;
}
.mw-300px {
max-width: 300px;
}

View File

@@ -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>
)}

View File

@@ -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>
);

View File

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

View File

@@ -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'),

View File

@@ -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',
},
{

View 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%;
}
}

View File

@@ -31,6 +31,7 @@ const ExportModal = ({
size="lg"
hasCloseButton
isFullscreenOnMobile
className="taxonomy-export-modal"
>
<ModalDialog.Header>
<ModalDialog.Title>

View File

@@ -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";

View File

@@ -0,0 +1,3 @@
.system-defined-badge {
font-size: 12px;
}

View 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;

View 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;

View File

@@ -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 />

View File

@@ -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: {

View File

@@ -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',

View File

@@ -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.

View File

@@ -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}

View File

@@ -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',

View File

@@ -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()}
/>

View File

@@ -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,