diff --git a/src/assets/scss/_utilities.scss b/src/assets/scss/_utilities.scss index 441fe804d..6e74efb45 100644 --- a/src/assets/scss/_utilities.scss +++ b/src/assets/scss/_utilities.scss @@ -1,3 +1,7 @@ .text-black { color: $black; } + +.mw-300px { + max-width: 300px; +} diff --git a/src/generic/sub-header/SubHeader.jsx b/src/generic/sub-header/SubHeader.jsx index a0146fcd5..3f166ac2d 100644 --- a/src/generic/sub-header/SubHeader.jsx +++ b/src/generic/sub-header/SubHeader.jsx @@ -18,7 +18,7 @@ const SubHeader = ({ {subtitle} {title} {titleActions && ( - + {titleActions} )} diff --git a/src/index.jsx b/src/index.jsx index 37c0e9b6f..7ef042750 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -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( + + } /> + } /> + } /> + {process.env.ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && ( + <> + {/* TODO: remove this redirect once Studio's link is updated */} + } /> + }> + } /> + + }> + } /> + + } + /> + + )} + , + ), + ); + return ( - + - - } /> - } /> - } /> - {process.env.ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && ( - <> - {/* TODO: remove this redirect once Studio's link is updated */} - } /> - }> - } /> - - }> - } /> - - } - /> - - )} - + ); diff --git a/src/taxonomy/TaxonomyLayout.jsx b/src/taxonomy/TaxonomyLayout.jsx index c49c55ee8..093ee743d 100644 --- a/src/taxonomy/TaxonomyLayout.jsx +++ b/src/taxonomy/TaxonomyLayout.jsx @@ -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} + ); }; diff --git a/src/taxonomy/TaxonomyLayout.test.jsx b/src/taxonomy/TaxonomyLayout.test.jsx index d83380999..aeece7092 100644 --- a/src/taxonomy/TaxonomyLayout.test.jsx +++ b/src/taxonomy/TaxonomyLayout.test.jsx @@ -16,6 +16,7 @@ jest.mock('@edx/frontend-component-footer', () => ({ jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), Outlet: jest.fn(() =>
), + ScrollRestoration: jest.fn(() =>
), })); jest.mock('react', () => ({ ...jest.requireActual('react'), diff --git a/src/taxonomy/TaxonomyListPage.jsx b/src/taxonomy/TaxonomyListPage.jsx index 8f991fbfd..c80a39332 100644 --- a/src/taxonomy/TaxonomyListPage.jsx +++ b/src/taxonomy/TaxonomyListPage.jsx @@ -114,17 +114,22 @@ const TaxonomyListPage = () => { div { + // Used to extend the clickable area to all the width of the modal + width: 100%; + } +} diff --git a/src/taxonomy/export-modal/index.jsx b/src/taxonomy/export-modal/index.jsx index 21ceedfe4..ebc8def52 100644 --- a/src/taxonomy/export-modal/index.jsx +++ b/src/taxonomy/export-modal/index.jsx @@ -31,6 +31,7 @@ const ExportModal = ({ size="lg" hasCloseButton isFullscreenOnMobile + className="taxonomy-export-modal" > diff --git a/src/taxonomy/index.scss b/src/taxonomy/index.scss index be1464fd9..3655a35bc 100644 --- a/src/taxonomy/index.scss +++ b/src/taxonomy/index.scss @@ -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"; diff --git a/src/taxonomy/system-defined-badge/SystemDefinedBadge.scss b/src/taxonomy/system-defined-badge/SystemDefinedBadge.scss new file mode 100644 index 000000000..dfab7827f --- /dev/null +++ b/src/taxonomy/system-defined-badge/SystemDefinedBadge.scss @@ -0,0 +1,3 @@ +.system-defined-badge { + font-size: 12px; +} diff --git a/src/taxonomy/system-defined-badge/index.jsx b/src/taxonomy/system-defined-badge/index.jsx new file mode 100644 index 000000000..af4937342 --- /dev/null +++ b/src/taxonomy/system-defined-badge/index.jsx @@ -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 = () => ( + + + {intl.formatMessage(messages.systemTaxonomyPopoverTitle)} + + + {intl.formatMessage(messages.systemTaxonomyPopoverBody)} + + + ); + + return ( + + + {intl.formatMessage(messages.systemDefinedBadge)} + + + ); +}; + +SystemDefinedBadge.propTypes = { + taxonomyId: PropTypes.number.isRequired, +}; + +export default SystemDefinedBadge; diff --git a/src/taxonomy/system-defined-badge/messages.js b/src/taxonomy/system-defined-badge/messages.js new file mode 100644 index 000000000..59642d1e5 --- /dev/null +++ b/src/taxonomy/system-defined-badge/messages.js @@ -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; diff --git a/src/taxonomy/tag-list/TagListTable.jsx b/src/taxonomy/tag-list/TagListTable.jsx index 2fa3c042a..1e2638fae 100644 --- a/src/taxonomy/tag-list/TagListTable.jsx +++ b/src/taxonomy/tag-list/TagListTable.jsx @@ -24,7 +24,7 @@ const SubTagsExpanded = ({ taxonomyId, parentTagValue }) => {
    {subTagsData.data.results.map(tagData => (
  • - {tagData.value} {tagData.childCount > 0 ? `(${tagData.childCount})` : null} + {tagData.value} {tagData.childCount > 0 ? `(${tagData.childCount})` : null}
  • ))}
@@ -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 ? : null); +const OptionalExpandLink = ({ row }) => (row.original.childCount > 0 ? : null); OptionalExpandLink.propTypes = DataTable.ExpandRow.propTypes; +/** + * Custom DataTable cell to join tag value with child count + */ +const TagValue = ({ row }) => ( + <> + {row.original.value} + {` (${row.original.childCount})`} + +); +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 }) => } + renderRowSubComponent={({ row }) => ( + + )} columns={[ { Header: intl.formatMessage(messages.tagListColumnValueHeader), - accessor: 'value', + Cell: TagValue, }, { id: 'expander', Header: DataTable.ExpandAll, Cell: OptionalExpandLink, }, - { - Header: intl.formatMessage(messages.tagListColumnChildCountHeader), - accessor: 'childCount', - }, ]} > diff --git a/src/taxonomy/tag-list/TagListTable.test.jsx b/src/taxonomy/tag-list/TagListTable.test.jsx index dd79c107f..f2d850205 100644 --- a/src/taxonomy/tag-list/TagListTable.test.jsx +++ b/src/taxonomy/tag-list/TagListTable.test.jsx @@ -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('', () => { +describe('', () => { beforeAll(async () => { initializeMockApp({ authenticatedUser: { diff --git a/src/taxonomy/tag-list/messages.js b/src/taxonomy/tag-list/messages.js index 82db9caba..77c0efa11 100644 --- a/src/taxonomy/tag-list/messages.js +++ b/src/taxonomy/tag-list/messages.js @@ -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', diff --git a/src/taxonomy/taxonomy-card/TaxonomyCard.scss b/src/taxonomy/taxonomy-card/TaxonomyCard.scss index b4839bd1c..d124b95e8 100644 --- a/src/taxonomy/taxonomy-card/TaxonomyCard.scss +++ b/src/taxonomy/taxonomy-card/TaxonomyCard.scss @@ -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. diff --git a/src/taxonomy/taxonomy-card/index.jsx b/src/taxonomy/taxonomy-card/index.jsx index 2b9476355..2f640cbf3 100644 --- a/src/taxonomy/taxonomy-card/index.jsx +++ b/src/taxonomy/taxonomy-card/index.jsx @@ -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 = () => ( - - - {intl.formatMessage(messages.systemTaxonomyPopoverTitle)} - - - {intl.formatMessage(messages.systemTaxonomyPopoverBody)} - - - ); // Show system defined badge if (showSystemBadge) { - return ( - - - {intl.formatMessage(messages.systemDefinedBadge)} - - - ); + return ; } // 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 = () => ( + + + {title} + + + ); + + return ( + +
+ {title} +
+
+ ); +}; + +HeaderTitle.propTypes = { + taxonomyId: PropTypes.number.isRequired, + title: PropTypes.string.isRequired, }; const TaxonomyCard = ({ className, original, onDeleteTaxonomy }) => { @@ -135,13 +160,13 @@ const TaxonomyCard = ({ className, original, onDeleteTaxonomy }) => { <> } subtitle={( { const intl = useIntl(); @@ -104,6 +105,13 @@ const TaxonomyDetailPage = () => { ); }; + const getSystemDefinedBadge = () => { + if (taxonomy.systemDefined) { + return ; + } + return null; + }; + return ( <> @@ -120,6 +128,7 @@ const TaxonomyDetailPage = () => { /> diff --git a/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.test.jsx b/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.test.jsx index bd95a7500..5cd657153 100644 --- a/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.test.jsx +++ b/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.test.jsx @@ -95,6 +95,22 @@ describe('', 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(); + expect(getByText('System-level')).toBeInTheDocument(); + }); + it('should open export modal on export menu click', () => { useTaxonomyDetailData.mockReturnValue({ isSuccess: true,