feat: add sections filter & UI changes
- TNL-11973: Previously Filters functionality was only working for subsections and units inside sections. Now sections are also filtered. - TNL-11974: New request, Show "no results found" if no results match the filters - TNL-11975: UI Change, Align filter menu popup to left side of filter button - TNL-11976: UI Change, Remove underline below "Course optimizer" title - TNL-11978: UI Change, Change title to "Scan my course" - TNL-11989: UI Change, Use empty space to display link, don't truncate text before the space runs out - TNL-11977: New request, Remove this stuff(scanning steps) when scan is complete, it'll disappear after 2.5 seconds - TNL-11979: UI Change, Move "This tool will scan your course..." text inside of Scan card - TNL-11980: UI Change, Move "Last scanned on..." date text below Scan button - TNL-11981: UI Change, Remove icon from "Start scanning" button - TNL-11983: UI Change, "Start scanning" button should be smaller, made it medium sized - TNL-11984: UI Change, Remove dividing line under subsection name in expanded card - TNL-11985: UI Change, Fix alignment of dividing lines, links, and icons in expanded cards to match Figma. - TNL-11986: UI Change, Match color of the broken icon with other Icons - TNL-11987: UI Change, Fix alignment of Filter chips to match Figma - Also added Beta Badge for course optimizer. - Added tests for codecov coverage
This commit is contained in:
committed by
Muhammad Faraz Maqsood
parent
cfb4944d43
commit
3c69733170
@@ -1,11 +1,13 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Badge } from '@openedx/paragon';
|
||||
|
||||
import { getPagePath } from '../utils';
|
||||
import { getWaffleFlags } from '../data/selectors';
|
||||
import { getStudioHomeData } from '../studio-home/data/selectors';
|
||||
import messages from './messages';
|
||||
import courseOptimizerMessages from '../optimizer-page/messages';
|
||||
|
||||
export const useContentMenuItems = courseId => {
|
||||
const intl = useIntl();
|
||||
@@ -113,7 +115,12 @@ export const useToolsMenuItems = courseId => {
|
||||
},
|
||||
...(waffleFlags.enableCourseOptimizer ? [{
|
||||
href: `/course/${courseId}/optimizer`,
|
||||
title: intl.formatMessage(messages['header.links.optimizer']),
|
||||
title: (
|
||||
<>
|
||||
{intl.formatMessage(messages['header.links.optimizer'])}
|
||||
<Badge variant="primary" className="ml-2">{intl.formatMessage(courseOptimizerMessages.beta)}</Badge>
|
||||
</>
|
||||
),
|
||||
}] : []),
|
||||
];
|
||||
return items;
|
||||
@@ -117,8 +117,10 @@ describe('header utils', () => {
|
||||
|
||||
it('when course optimizer enabled should include optimizer option', () => {
|
||||
useSelector.mockReturnValue({ enableCourseOptimizer: true });
|
||||
const actualItemsTitle = renderHook(() => useToolsMenuItems('course-123')).result.current.map((item) => item.title);
|
||||
expect(actualItemsTitle).toContain(messages['header.links.optimizer'].defaultMessage);
|
||||
const optimizerItem = renderHook(() => useToolsMenuItems('course-123')).result.current.find(
|
||||
item => item.href === '/course/course-123/optimizer',
|
||||
);
|
||||
expect(optimizerItem).toBeDefined();
|
||||
});
|
||||
|
||||
it('when course optimizer disabled should not include optimizer option', () => {
|
||||
|
||||
@@ -15,7 +15,7 @@ import generalMessages from '../messages';
|
||||
import scanResultsMessages from './scan-results/messages';
|
||||
import CourseOptimizerPage, { pollLinkCheckDuringScan } from './CourseOptimizerPage';
|
||||
import { postLinkCheckCourseApiUrl, getLinkCheckStatusApiUrl } from './data/api';
|
||||
import mockApiResponse from './mocks/mockApiResponse';
|
||||
import { mockApiResponse, mockApiResponseForNoResultFound } from './mocks/mockApiResponse';
|
||||
import * as thunks from './data/thunks';
|
||||
|
||||
let store;
|
||||
@@ -37,8 +37,8 @@ const OptimizerPage = () => (
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
const setupOptimizerPage = async () => {
|
||||
axiosMock.onGet(getLinkCheckStatusApiUrl(courseId)).reply(200, mockApiResponse);
|
||||
const setupOptimizerPage = async (apiResponse = mockApiResponse) => {
|
||||
axiosMock.onGet(getLinkCheckStatusApiUrl(courseId)).reply(200, apiResponse);
|
||||
const optimizerPage = render(<OptimizerPage />);
|
||||
|
||||
// Click the scan button
|
||||
@@ -171,6 +171,29 @@ describe('CourseOptimizerPage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should show only locked links when lockedLinks filter is selected', async () => {
|
||||
const {
|
||||
getByText,
|
||||
getByLabelText,
|
||||
queryByText,
|
||||
container,
|
||||
} = await setupOptimizerPage();
|
||||
// Check if the modal is opened
|
||||
expect(getByText('Locked')).toBeInTheDocument();
|
||||
// Select the broken links checkbox
|
||||
fireEvent.click(getByLabelText(scanResultsMessages.lockedLabel.defaultMessage));
|
||||
|
||||
const collapsibleTrigger = container.querySelector('.collapsible-trigger');
|
||||
expect(collapsibleTrigger).toBeInTheDocument();
|
||||
fireEvent.click(collapsibleTrigger);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Test Locked Links')).toBeInTheDocument();
|
||||
expect(queryByText('Test Broken Links')).not.toBeInTheDocument();
|
||||
expect(queryByText('Test Manual Links')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show only broken links when brokenLinks filter is selected', async () => {
|
||||
const {
|
||||
getByText,
|
||||
@@ -323,5 +346,20 @@ describe('CourseOptimizerPage', () => {
|
||||
expect(getByText('Test Locked Links')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show no results found message when filter with no links is selected', async () => {
|
||||
const {
|
||||
getByText,
|
||||
getByLabelText,
|
||||
} = await setupOptimizerPage(mockApiResponseForNoResultFound);
|
||||
// Check if the modal is opened
|
||||
expect(getByText('Locked')).toBeInTheDocument();
|
||||
// Select the broken links checkbox
|
||||
fireEvent.click(getByLabelText(scanResultsMessages.lockedLabel.defaultMessage));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText(scanResultsMessages.noResultsFound.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import {
|
||||
useEffect, useRef, FC, MutableRefObject,
|
||||
useEffect, useState, useRef, FC, MutableRefObject,
|
||||
} from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Container, Layout, Button, Card,
|
||||
Badge, Container, Layout, Button, Card,
|
||||
} from '@openedx/paragon';
|
||||
import { Search as SearchIcon } from '@openedx/paragon/icons';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import CourseStepper from '../generic/course-stepper';
|
||||
@@ -60,6 +59,7 @@ const CourseOptimizerPage: FC<{ courseId: string }> = ({ courseId }) => {
|
||||
const interval = useRef<number | undefined>(undefined);
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
const linkCheckPresent = currentStage != null ? currentStage >= 0 : !!currentStage;
|
||||
const [showStepper, setShowStepper] = useState(false);
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
@@ -96,6 +96,22 @@ const CourseOptimizerPage: FC<{ courseId: string }> = ({ courseId }) => {
|
||||
};
|
||||
}, [linkCheckInProgress, linkCheckResult]);
|
||||
|
||||
const stepperVisibleCondition = linkCheckPresent && ((!linkCheckResult || linkCheckInProgress) && currentStage !== 2);
|
||||
useEffect(() => {
|
||||
let timeout: NodeJS.Timeout;
|
||||
if (stepperVisibleCondition) {
|
||||
setShowStepper(true);
|
||||
} else {
|
||||
timeout = setTimeout(() => {
|
||||
// ignoring below line as we didn't wrote tests for scanning process
|
||||
// istanbul ignore next
|
||||
setShowStepper(false);
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, [stepperVisibleCondition]);
|
||||
|
||||
if (isLoadingDenied || isSavingDenied) {
|
||||
if (interval.current) { clearInterval(interval.current); }
|
||||
|
||||
@@ -129,30 +145,37 @@ const CourseOptimizerPage: FC<{ courseId: string }> = ({ courseId }) => {
|
||||
<Layout.Element>
|
||||
<article>
|
||||
<SubHeader
|
||||
title={intl.formatMessage(messages.headingTitle)}
|
||||
hideBorder
|
||||
title={
|
||||
(
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center' }}>
|
||||
{intl.formatMessage(messages.headingTitle)}
|
||||
<Badge variant="primary" className="ml-2">{intl.formatMessage(messages.beta)}</Badge>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
subtitle={intl.formatMessage(messages.headingSubtitle)}
|
||||
/>
|
||||
<p className="small opt-desc-mb">{intl.formatMessage(messages.description)}</p>
|
||||
<Card>
|
||||
<Card.Header
|
||||
className="h3 px-3 text-black mb-4"
|
||||
className="scan-header h3 px-3 text-black mb-2"
|
||||
title={intl.formatMessage(messages.card1Title)}
|
||||
/>
|
||||
<p className="px-3 py-1 small ">{intl.formatMessage(messages.description)}</p>
|
||||
{isShowExportButton && (
|
||||
<Card.Section className="px-3 py-1">
|
||||
<p className="small"> {lastScannedAt && `${intl.formatMessage(messages.lastScannedOn)} ${intl.formatDate(lastScannedAt, { year: 'numeric', month: 'long', day: 'numeric' })}`}</p>
|
||||
<Button
|
||||
size="lg"
|
||||
size="md"
|
||||
block
|
||||
className="mb-4"
|
||||
className="mb-3"
|
||||
onClick={() => dispatch(startLinkCheck(courseId))}
|
||||
iconBefore={SearchIcon}
|
||||
>
|
||||
{intl.formatMessage(messages.buttonTitle)}
|
||||
</Button>
|
||||
<p className="small"> {lastScannedAt && `${intl.formatMessage(messages.lastScannedOn)} ${intl.formatDate(lastScannedAt, { year: 'numeric', month: 'long', day: 'numeric' })}`}</p>
|
||||
</Card.Section>
|
||||
)}
|
||||
{linkCheckPresent && (
|
||||
{showStepper && (
|
||||
<Card.Section className="px-3 py-1">
|
||||
<CourseStepper
|
||||
// @ts-ignore
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import mockApiResponse from '../mocks/mockApiResponse';
|
||||
import { mockApiResponse } from '../mocks/mockApiResponse';
|
||||
import { initializeMocks } from '../../testUtils';
|
||||
import * as api from './api';
|
||||
import { LINK_CHECK_STATUSES } from './constants';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { startLinkCheck, fetchLinkCheckStatus } from './thunks';
|
||||
import * as api from './api';
|
||||
import { LINK_CHECK_STATUSES } from './constants';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import mockApiResponse from '../mocks/mockApiResponse';
|
||||
import { mockApiResponse } from '../mocks/mockApiResponse';
|
||||
|
||||
describe('startLinkCheck thunk', () => {
|
||||
const dispatch = jest.fn();
|
||||
|
||||
@@ -9,6 +9,10 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.course-optimizer.heading.title',
|
||||
defaultMessage: 'Course optimizer',
|
||||
},
|
||||
beta: {
|
||||
id: 'course-authoring.course-optimizer.beta',
|
||||
defaultMessage: 'Beta',
|
||||
},
|
||||
headingSubtitle: {
|
||||
id: 'course-authoring.course-optimizer.heading.subtitle',
|
||||
defaultMessage: 'Tools',
|
||||
@@ -19,11 +23,7 @@ const messages = defineMessages({
|
||||
},
|
||||
card1Title: {
|
||||
id: 'course-authoring.course-optimizer.card1.title',
|
||||
defaultMessage: 'Scan my course for broken links',
|
||||
},
|
||||
card2Title: {
|
||||
id: 'course-authoring.course-optimizer.card2.title',
|
||||
defaultMessage: 'Scan my course for broken links',
|
||||
defaultMessage: 'Scan my course',
|
||||
},
|
||||
buttonTitle: {
|
||||
id: 'course-authoring.course-optimizer.button.title',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const mockApiResponse = {
|
||||
export const mockApiResponse = {
|
||||
LinkCheckStatus: 'Succeeded',
|
||||
LinkCheckCreatedAt: '2024-12-14T00:26:50.838350Z',
|
||||
LinkCheckOutput: {
|
||||
@@ -123,4 +123,36 @@ const mockApiResponse = {
|
||||
},
|
||||
};
|
||||
|
||||
export default mockApiResponse;
|
||||
export const mockApiResponseForNoResultFound = {
|
||||
LinkCheckStatus: 'Succeeded',
|
||||
LinkCheckCreatedAt: '2024-12-14T00:26:50.838350Z',
|
||||
LinkCheckOutput: {
|
||||
sections: [
|
||||
{
|
||||
id: 'section-1',
|
||||
displayName: 'Introduction to Programming',
|
||||
subsections: [
|
||||
{
|
||||
id: 'subsection-1-1',
|
||||
displayName: 'Getting Started',
|
||||
units: [
|
||||
{
|
||||
id: 'unit-1-1-1',
|
||||
displayName: 'Test Broken Links',
|
||||
blocks: [
|
||||
{
|
||||
id: 'block-1-1-1-5',
|
||||
url: 'https://example.com/welcome-video',
|
||||
brokenLinks: ['https://example.com/broken-link-algo1'],
|
||||
lockedLinks: [],
|
||||
externalForbiddenLinks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@ const GoToBlock: FC<{ block: { url: string, displayName: string } }> = ({ block
|
||||
const LinksCol: FC<{ block: { url: string, displayName: string }, href: string }> = ({ block, href }) => (
|
||||
<span className="links-container">
|
||||
<GoToBlock block={{ url: block.url, displayName: block.displayName || 'Go to block' }} />
|
||||
<Icon className="arrow-forward-ios" src={ArrowForwardIos} />
|
||||
<Icon className="arrow-forward-ios" src={ArrowForwardIos} style={{ color: '#8F8F8F' }} />
|
||||
<BrokenLinkHref href={href} />
|
||||
</span>
|
||||
);
|
||||
@@ -64,7 +64,7 @@ const BrokenLinkTable: FC<BrokenLinkTableProps> = ({
|
||||
unit,
|
||||
filters,
|
||||
}) => (
|
||||
<Card className="unit-card rounded-sm pt-2 pl-3 pr-4 mb-2.5">
|
||||
<Card className="unit-card rounded-sm mb-4">
|
||||
<p className="unit-header">{unit.displayName}</p>
|
||||
<Table
|
||||
data={unit.blocks.reduce(
|
||||
|
||||
@@ -26,7 +26,7 @@ const CustomIcon = ({
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<Icon src={icon} />
|
||||
<Icon src={icon} style={{ color: '#000000' }} />
|
||||
</OverlayTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
.section {
|
||||
&.is-open {
|
||||
&:not(:first-child) {
|
||||
margin-top: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,11 +29,32 @@
|
||||
.unit-card{
|
||||
border: 1px solid #BCBCBC;
|
||||
box-shadow: 0 1px 2px rgb(0 0 0 / .15);
|
||||
padding: 14px 32px 4px !important;
|
||||
|
||||
.table{
|
||||
margin-bottom: .5rem;
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-top: 1px solid #CCCCCC;
|
||||
|
||||
td {
|
||||
padding: 16px 0;
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
td:nth-child(2) {
|
||||
padding: 16px 0 16px 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Subsection Header */
|
||||
.unit-header {
|
||||
margin-left: .5rem;
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
@@ -71,20 +92,19 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
width:38rem;
|
||||
width: 56rem;
|
||||
}
|
||||
|
||||
.go-to-block-link-container {
|
||||
max-width: 20rem;
|
||||
max-width: 60%;
|
||||
width: fit-content;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.broken-link-container {
|
||||
max-width: 20rem;
|
||||
text-wrap: nowrap;
|
||||
max-width: 95%;
|
||||
width: fit-content;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@@ -108,12 +128,14 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: #000000 !important;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.section-collapsible-header-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .2rem;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.section-collapsible-header-actions {
|
||||
@@ -127,7 +149,7 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
color: var(--gray-700, #454545);
|
||||
color: #000000;
|
||||
font-variant-numeric: lining-nums tabular-nums;
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 14px;
|
||||
@@ -139,12 +161,20 @@
|
||||
|
||||
.section-collapsible-item-container {
|
||||
box-shadow: 0 1px 2px rgb(0 0 0 / .15);
|
||||
|
||||
.collapsible-trigger{
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.section-collapsible-item {
|
||||
margin-right: -24px;
|
||||
}
|
||||
|
||||
.section-collapsible-item-body {
|
||||
margin: -16px -9px -12px -10px;
|
||||
}
|
||||
|
||||
.scan-header-title-container {
|
||||
margin-top: 32px;
|
||||
}
|
||||
@@ -198,12 +228,14 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.scan-results-active-filters-chip {
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--light-300, #F2F0EF);
|
||||
background: var(--extras-white, #FFFFFF);
|
||||
margin-left: 0
|
||||
}
|
||||
|
||||
.clear-all-btn {
|
||||
@@ -215,6 +247,29 @@
|
||||
}
|
||||
|
||||
.filter-modal {
|
||||
margin-left: -1.5rem;
|
||||
padding: 16px 16px 1px;
|
||||
}
|
||||
|
||||
.no-results-found-container{
|
||||
display: flex;
|
||||
height: 94px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.no-results-found{
|
||||
color: var(--gray-700, #454545);
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 18px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.scan-header {
|
||||
& .pgn__card-header-content {
|
||||
margin-top: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const ScanResults: FC<Props> = ({ data }) => {
|
||||
let hasSectionsRendered = false;
|
||||
const intl = useIntl();
|
||||
const [isOpen, open, close] = useToggle(false);
|
||||
const initialFilters = {
|
||||
@@ -60,6 +61,7 @@ const ScanResults: FC<Props> = ({ data }) => {
|
||||
}
|
||||
|
||||
const { sections } = data;
|
||||
|
||||
const filterOptions = [
|
||||
{ name: intl.formatMessage(messages.brokenLabel), value: 'brokenLinks' },
|
||||
{ name: intl.formatMessage(messages.manualLabel), value: 'externalForbiddenLinks' },
|
||||
@@ -135,37 +137,54 @@ const ScanResults: FC<Props> = ({ data }) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sections?.map((section, index) => (
|
||||
<SectionCollapsible
|
||||
key={section.id}
|
||||
title={section.displayName}
|
||||
brokenNumber={brokenLinksCounts[index]}
|
||||
manualNumber={externalForbiddenLinksCounts[index]}
|
||||
lockedNumber={lockedLinksCounts[index]}
|
||||
className="section-collapsible-header"
|
||||
>
|
||||
{section.subsections.map((subsection) => (
|
||||
<>
|
||||
{subsection.units.map((unit) => {
|
||||
if (
|
||||
(!filters.brokenLinks && !filters.externalForbiddenLinks && !filters.lockedLinks)
|
||||
|| (filters.brokenLinks && unit.blocks.some(block => block.brokenLinks.length > 0))
|
||||
|| (filters.externalForbiddenLinks
|
||||
&& unit.blocks.some(block => block.externalForbiddenLinks.length > 0))
|
||||
|| (filters.lockedLinks && unit.blocks.some(block => block.lockedLinks.length > 0))
|
||||
) {
|
||||
return (
|
||||
<div className="unit">
|
||||
<BrokenLinkTable unit={unit} filters={filters} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</>
|
||||
))}
|
||||
</SectionCollapsible>
|
||||
))}
|
||||
{sections?.map((section, index) => {
|
||||
const shouldRenderSection = (
|
||||
(!filters.brokenLinks && !filters.externalForbiddenLinks && !filters.lockedLinks)
|
||||
|| (filters.brokenLinks && brokenLinksCounts[index] > 0)
|
||||
|| (filters.externalForbiddenLinks && externalForbiddenLinksCounts[index] > 0)
|
||||
|| (filters.lockedLinks && lockedLinksCounts[index] > 0)
|
||||
);
|
||||
|
||||
if (shouldRenderSection) {
|
||||
hasSectionsRendered = true;
|
||||
return (
|
||||
<SectionCollapsible
|
||||
key={section.id}
|
||||
title={section.displayName}
|
||||
brokenNumber={brokenLinksCounts[index]}
|
||||
manualNumber={externalForbiddenLinksCounts[index]}
|
||||
lockedNumber={lockedLinksCounts[index]}
|
||||
className="section-collapsible-header"
|
||||
>
|
||||
{section.subsections.map((subsection) => (
|
||||
<>
|
||||
{subsection.units.map((unit) => {
|
||||
if (
|
||||
(!filters.brokenLinks && !filters.externalForbiddenLinks && !filters.lockedLinks)
|
||||
|| (filters.brokenLinks && unit.blocks.some(block => block.brokenLinks.length > 0))
|
||||
|| (filters.externalForbiddenLinks
|
||||
&& unit.blocks.some(block => block.externalForbiddenLinks.length > 0))
|
||||
|| (filters.lockedLinks && unit.blocks.some(block => block.lockedLinks.length > 0))
|
||||
) {
|
||||
return (
|
||||
<div className="unit">
|
||||
<BrokenLinkTable unit={unit} filters={filters} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</>
|
||||
))}
|
||||
</SectionCollapsible>
|
||||
);
|
||||
}
|
||||
return hasSectionsRendered === false ? (
|
||||
<div className="no-results-found-container">
|
||||
<h3 className="no-results-found">{intl.formatMessage(messages.noResultsFound)}</h3>
|
||||
</div>
|
||||
) : null;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -26,7 +26,7 @@ const SectionCollapsible: FC<Props> = ({
|
||||
title, children, brokenNumber = 0, manualNumber = 0, lockedNumber = 0, className = '',
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const styling = 'card-lg rounded-sm shadow-outline';
|
||||
const styling = 'card-lg rounded-sm';
|
||||
const collapsibleTitle = (
|
||||
<div className={className}>
|
||||
<div className="section-collapsible-header-item">
|
||||
@@ -65,7 +65,7 @@ const SectionCollapsible: FC<Props> = ({
|
||||
open={isOpen}
|
||||
onToggle={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<Collapsible.Body>{children}</Collapsible.Body>
|
||||
<Collapsible.Body className="section-collapsible-item-body">{children}</Collapsible.Body>
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -41,7 +41,7 @@ const FilterModal = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalPopup isOpen={isOpen} onClose={onClose} positionRef={positionRef} placement="bottom-start">
|
||||
<ModalPopup isOpen={isOpen} onClose={onClose} positionRef={positionRef} placement="bottom-end">
|
||||
<div className="filter-modal bg-white rounded shadow-sm w-175">
|
||||
<Form.Group>
|
||||
<Form.CheckboxSet
|
||||
|
||||
@@ -13,6 +13,10 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.course-optimizer.emptyResultsCard',
|
||||
defaultMessage: 'No broken links found',
|
||||
},
|
||||
noResultsFound: {
|
||||
id: 'course-authoring.course-optimizer.noResultsFound',
|
||||
defaultMessage: 'No results found',
|
||||
},
|
||||
scanHeader: {
|
||||
id: 'course-authoring.course-optimizer.scanHeader',
|
||||
defaultMessage: 'Scan results',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import mockApiResponse from './mocks/mockApiResponse';
|
||||
import { mockApiResponse } from './mocks/mockApiResponse';
|
||||
import { countBrokenLinks } from './utils';
|
||||
|
||||
describe('countBrokenLinks', () => {
|
||||
|
||||
Reference in New Issue
Block a user