feat: course optimizer page better design
- Add filter functionality to course optimizer broken links to check different results - modify design, make use of logo with better tooltip - change message texts in different area of the page
This commit is contained in:
committed by
Andres Espinel
parent
96a04d4492
commit
c2e4f7f51e
@@ -132,8 +132,7 @@ const CourseOptimizerPage: FC<{ courseId: string }> = ({ courseId }) => {
|
||||
title={intl.formatMessage(messages.headingTitle)}
|
||||
subtitle={intl.formatMessage(messages.headingSubtitle)}
|
||||
/>
|
||||
<p className="small">{intl.formatMessage(messages.description1)}</p>
|
||||
<p className="small">{intl.formatMessage(messages.description2)}</p>
|
||||
<p className="small opt-desc-mb">{intl.formatMessage(messages.description)}</p>
|
||||
<Card>
|
||||
<Card.Header
|
||||
className="h3 px-3 text-black mb-4"
|
||||
@@ -141,6 +140,7 @@ const CourseOptimizerPage: FC<{ courseId: string }> = ({ courseId }) => {
|
||||
/>
|
||||
{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"
|
||||
block
|
||||
@@ -148,7 +148,7 @@ const CourseOptimizerPage: FC<{ courseId: string }> = ({ courseId }) => {
|
||||
onClick={() => dispatch(startLinkCheck(courseId))}
|
||||
iconBefore={SearchIcon}
|
||||
>
|
||||
{intl.formatMessage(messages.buttonTitle)} {lastScannedAt && `(${intl.formatMessage(messages.lastScannedOn)} ${intl.formatDate(lastScannedAt, { year: 'numeric', month: 'long', day: 'numeric' })})`}
|
||||
{intl.formatMessage(messages.buttonTitle)}
|
||||
</Button>
|
||||
</Card.Section>
|
||||
)}
|
||||
|
||||
@@ -7,24 +7,15 @@ const messages = defineMessages({
|
||||
},
|
||||
headingTitle: {
|
||||
id: 'course-authoring.course-optimizer.heading.title',
|
||||
defaultMessage: 'Course Optimizer',
|
||||
defaultMessage: 'Course optimizer',
|
||||
},
|
||||
headingSubtitle: {
|
||||
id: 'course-authoring.course-optimizer.heading.subtitle',
|
||||
defaultMessage: 'Tools',
|
||||
},
|
||||
description1: {
|
||||
id: 'course-authoring.course-optimizer.description1',
|
||||
defaultMessage: `This tool will scan the published version of your course for broken links.
|
||||
Unpublished changes will not be included in the scan.
|
||||
Note that this process will take more time for larger courses.
|
||||
To update the scan after you have published new changes to your course,
|
||||
click the "Start Scanning" button again.
|
||||
`,
|
||||
},
|
||||
description2: {
|
||||
id: 'course-authoring.course-optimizer.description2',
|
||||
defaultMessage: 'Broken links are links pointing to external websites, images, or videos that do not exist or are no longer available. These links can cause issues for learners when they try to access the content.',
|
||||
description: {
|
||||
id: 'course-authoring.course-optimizer.description',
|
||||
defaultMessage: 'This tool will scan your course for broken links, and any links that point to pages in your previous course run. Unpublished changes will not be included in the scan. Note that this process will take more time for larger courses.',
|
||||
},
|
||||
card1Title: {
|
||||
id: 'course-authoring.course-optimizer.card1.title',
|
||||
@@ -36,7 +27,7 @@ const messages = defineMessages({
|
||||
},
|
||||
buttonTitle: {
|
||||
id: 'course-authoring.course-optimizer.button.title',
|
||||
defaultMessage: 'Start Scanning',
|
||||
defaultMessage: 'Start scanning',
|
||||
},
|
||||
preparingStepTitle: {
|
||||
id: 'course-authoring.course-optimizer.peparing-step.title',
|
||||
|
||||
46
src/optimizer-page/scan-results/CustomIcon.jsx
Normal file
46
src/optimizer-page/scan-results/CustomIcon.jsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Icon,
|
||||
OverlayTrigger,
|
||||
Tooltip,
|
||||
} from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const CustomIcon = ({
|
||||
icon,
|
||||
message1,
|
||||
message2,
|
||||
placement = 'top',
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<OverlayTrigger
|
||||
key="top"
|
||||
placement={placement}
|
||||
overlay={(
|
||||
<Tooltip variant="dark" id="tooltip-top" className={placement !== 'top' ? 'ml-3' : ''}>
|
||||
{intl.formatMessage(message1)}
|
||||
{message1 && <br />}
|
||||
{intl.formatMessage(message2)}
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<Icon src={icon} />
|
||||
</OverlayTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
const messagePropsType = {
|
||||
id: PropTypes.string.isRequired,
|
||||
defaultMessage: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
CustomIcon.propTypes = {
|
||||
icon: PropTypes.elementType.isRequired,
|
||||
message1: PropTypes.shape(messagePropsType).isRequired,
|
||||
message2: PropTypes.shape(messagePropsType).isRequired,
|
||||
placement: PropTypes.string,
|
||||
};
|
||||
|
||||
export default CustomIcon;
|
||||
@@ -1,30 +0,0 @@
|
||||
import {
|
||||
Icon,
|
||||
OverlayTrigger,
|
||||
Tooltip,
|
||||
} from '@openedx/paragon';
|
||||
import {
|
||||
Question,
|
||||
} from '@openedx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
|
||||
const LockedInfoIcon = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<OverlayTrigger
|
||||
key="top"
|
||||
placement="top"
|
||||
overlay={(
|
||||
<Tooltip variant="light" id="tooltip-top">
|
||||
{intl.formatMessage(messages.lockedInfoTooltip)}
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<Icon src={Question} />
|
||||
</OverlayTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
export default LockedInfoIcon;
|
||||
@@ -1,15 +1,21 @@
|
||||
import { useState, useMemo, FC } from 'react';
|
||||
import {
|
||||
Card,
|
||||
CheckBox,
|
||||
Chip,
|
||||
Button,
|
||||
useCheckboxSetValues,
|
||||
} from '@openedx/paragon';
|
||||
import {
|
||||
ArrowDropDown,
|
||||
CloseSmall,
|
||||
} from '@openedx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
import SectionCollapsible from './SectionCollapsible';
|
||||
import BrokenLinkTable from './BrokenLinkTable';
|
||||
import LockedInfoIcon from './LockedInfoIcon';
|
||||
import { LinkCheckResult } from '../types';
|
||||
import { countBrokenLinks } from '../utils';
|
||||
import FilterModal from './filterModal';
|
||||
|
||||
const InfoCard: FC<{ text: string }> = ({ text }) => (
|
||||
<Card className="mt-4">
|
||||
@@ -28,7 +34,14 @@ interface Props {
|
||||
|
||||
const ScanResults: FC<Props> = ({ data }) => {
|
||||
const intl = useIntl();
|
||||
const [showLockedLinks, setShowLockedLinks] = useState(true);
|
||||
const [isModalOpen, setModalOpen] = useState(false);
|
||||
const initialFilters = {
|
||||
brokenLinks: false,
|
||||
lockedLinks: false,
|
||||
externalForbiddenLinks: false,
|
||||
};
|
||||
const [filters, setFilters] = useState(initialFilters);
|
||||
const [buttonRef, setButtonRef] = useState<HTMLButtonElement | null>(null);
|
||||
|
||||
const {
|
||||
brokenLinksCounts,
|
||||
@@ -36,55 +49,115 @@ const ScanResults: FC<Props> = ({ data }) => {
|
||||
externalForbiddenLinksCounts,
|
||||
} = useMemo(() => countBrokenLinks(data), [data?.sections]);
|
||||
|
||||
const activeFilters = Object.keys(filters).filter(key => filters[key]);
|
||||
const [filterBy, {
|
||||
add, remove, set, clear,
|
||||
}] = useCheckboxSetValues(activeFilters);
|
||||
|
||||
if (!data?.sections) {
|
||||
return <InfoCard text={intl.formatMessage(messages.noBrokenLinksCard)} />;
|
||||
}
|
||||
|
||||
const { sections } = data;
|
||||
const filterOptions = [
|
||||
{ name: intl.formatMessage(messages.brokenLabel), value: 'brokenLinks' },
|
||||
{ name: intl.formatMessage(messages.manualLabel), value: 'externalForbiddenLinks' },
|
||||
{ name: intl.formatMessage(messages.lockedLabel), value: 'lockedLinks' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="scan-results">
|
||||
<div className="border-bottom border-light-400 mb-3">
|
||||
<div className="scan-header-title-container">
|
||||
<h2 className="scan-header-title">{intl.formatMessage(messages.scanHeader)}</h2>
|
||||
</div>
|
||||
<div className="scan-header-second-title-container">
|
||||
<header className="sub-header-content">
|
||||
<h2 className="sub-header-content-title">{intl.formatMessage(messages.scanHeader)}</h2>
|
||||
<span className="locked-links-checkbox-wrapper">
|
||||
<CheckBox
|
||||
className="locked-links-checkbox"
|
||||
type="checkbox"
|
||||
checked={showLockedLinks}
|
||||
onClick={() => {
|
||||
setShowLockedLinks(!showLockedLinks);
|
||||
}}
|
||||
label={intl.formatMessage(messages.lockedCheckboxLabel)}
|
||||
/>
|
||||
<LockedInfoIcon />
|
||||
</span>
|
||||
<h2 className="broken-links-header-title pt-2">{intl.formatMessage(messages.brokenLinksHeader)}</h2>
|
||||
<Button
|
||||
ref={setButtonRef}
|
||||
variant="outline-primary"
|
||||
onClick={() => setModalOpen(true)}
|
||||
disabled={false}
|
||||
iconAfter={ArrowDropDown}
|
||||
className="rounded-sm justify-content-between cadence-button"
|
||||
>
|
||||
{intl.formatMessage(messages.filterButtonLabel)}
|
||||
</Button>
|
||||
</header>
|
||||
</div>
|
||||
<FilterModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setModalOpen(false)}
|
||||
onApply={setFilters}
|
||||
positionRef={buttonRef}
|
||||
filterOptions={filterOptions}
|
||||
initialFilters={filters}
|
||||
activeFilters={activeFilters}
|
||||
filterBy={filterBy}
|
||||
add={add}
|
||||
remove={remove}
|
||||
set={set}
|
||||
/>
|
||||
{activeFilters.length > 0 && <div className="border-bottom border-light-400" />}
|
||||
{activeFilters.length > 0 && (
|
||||
<div className="scan-results-active-filters-container">
|
||||
<span className="scan-results-active-filters-chips">
|
||||
{activeFilters.map(filter => (
|
||||
<Chip
|
||||
key={filter}
|
||||
iconAfter={CloseSmall}
|
||||
iconAfterAlt="icon-after"
|
||||
className="scan-results-active-filters-chip"
|
||||
onClick={() => {
|
||||
remove(filter);
|
||||
const updatedFilters = { ...filters, [filter]: false };
|
||||
setFilters(updatedFilters);
|
||||
}}
|
||||
>
|
||||
{filterOptions.find(option => option.value === filter)?.name}
|
||||
</Chip>
|
||||
))}
|
||||
</span>
|
||||
<Button
|
||||
variant="link"
|
||||
className="clear-all-btn"
|
||||
onClick={() => {
|
||||
clear();
|
||||
setFilters(initialFilters);
|
||||
}}
|
||||
>
|
||||
{intl.formatMessage(messages.clearFilters)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sections?.map((section, index) => (
|
||||
<SectionCollapsible
|
||||
key={section.id}
|
||||
title={section.displayName}
|
||||
redItalics={intl.formatMessage(messages.brokenLinksNumber, { count: brokenLinksCounts[index] })}
|
||||
yellowItalics={!showLockedLinks ? '' : intl.formatMessage(messages.lockedLinksNumber, { count: lockedLinksCounts[index] })}
|
||||
greenItalics={
|
||||
intl.formatMessage(messages.externalForbiddenLinksNumber, { count: externalForbiddenLinksCounts[index] })
|
||||
}
|
||||
brokenNumber={brokenLinksCounts[index]}
|
||||
manualNumber={externalForbiddenLinksCounts[index]}
|
||||
lockedNumber={lockedLinksCounts[index]}
|
||||
className="section-collapsible-header"
|
||||
>
|
||||
{section.subsections.map((subsection) => (
|
||||
<>
|
||||
<h2
|
||||
className="subsection-header"
|
||||
style={{ marginBottom: '2rem' }}
|
||||
>
|
||||
{subsection.displayName}
|
||||
</h2>
|
||||
{subsection.units.map((unit) => (
|
||||
<div className="unit">
|
||||
<BrokenLinkTable unit={unit} showLockedLinks={showLockedLinks} />
|
||||
</div>
|
||||
))}
|
||||
{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>
|
||||
|
||||
@@ -6,38 +6,57 @@ import {
|
||||
import {
|
||||
ArrowRight,
|
||||
ArrowDropDown,
|
||||
LinkOff,
|
||||
} from '@openedx/paragon/icons';
|
||||
import CustomIcon from './CustomIcon';
|
||||
import messages from './messages';
|
||||
import lockedIcon from './lockedIcon';
|
||||
import ManualIcon from './manualIcon';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
redItalics?: string;
|
||||
yellowItalics?: string;
|
||||
greenItalics?: string;
|
||||
brokenNumber: number;
|
||||
manualNumber: number;
|
||||
lockedNumber: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const SectionCollapsible: FC<Props> = ({
|
||||
title, children, redItalics = '', yellowItalics = '', greenItalics = '', className = '',
|
||||
title, children, brokenNumber = 0, manualNumber = 0, lockedNumber = 0, className = '',
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const styling = 'card-lg';
|
||||
const styling = 'card-lg rounded-sm shadow-outline';
|
||||
const collapsibleTitle = (
|
||||
<div className={className}>
|
||||
<Icon src={isOpen ? ArrowDropDown : ArrowRight} className="open-arrow" />
|
||||
<strong>{title}</strong>
|
||||
<span className="red-italics">{redItalics}</span>
|
||||
<span className="yellow-italics">{yellowItalics}</span>
|
||||
<span className="green-italics">{greenItalics}</span>
|
||||
<div className="section-collapsible-header-item">
|
||||
<Icon src={isOpen ? ArrowDropDown : ArrowRight} />
|
||||
<strong>{title}</strong>
|
||||
</div>
|
||||
<div className="section-collapsible-header-actions">
|
||||
<span>
|
||||
<CustomIcon icon={LinkOff} message1={messages.brokenLabel} message2={messages.brokenInfoTooltip} />
|
||||
{brokenNumber}
|
||||
</span>
|
||||
<span>
|
||||
<CustomIcon icon={ManualIcon} message1={messages.manualLabel} message2={messages.manualInfoTooltip} />
|
||||
{manualNumber}
|
||||
</span>
|
||||
<span>
|
||||
<CustomIcon icon={lockedIcon} message1={messages.lockedLabel} message2={messages.lockedInfoTooltip} />
|
||||
{lockedNumber}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`section ${isOpen ? 'is-open' : ''}`}>
|
||||
<Collapsible
|
||||
className="section-collapsible-item-container"
|
||||
styling={styling}
|
||||
title={(
|
||||
<p>
|
||||
<p className="flex-grow-1 section-collapsible-item">
|
||||
<strong>{collapsibleTitle}</strong>
|
||||
</p>
|
||||
)}
|
||||
|
||||
92
src/optimizer-page/scan-results/filterModal.jsx
Normal file
92
src/optimizer-page/scan-results/filterModal.jsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { ModalPopup, Form } from '@openedx/paragon';
|
||||
import { LinkOff } from '@openedx/paragon/icons';
|
||||
import { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import CustomIcon from './CustomIcon';
|
||||
import messages from './messages';
|
||||
import LockedIcon from './lockedIcon';
|
||||
import ManualIcon from './manualIcon';
|
||||
|
||||
const FilterModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onApply,
|
||||
positionRef,
|
||||
filterOptions,
|
||||
initialFilters,
|
||||
activeFilters,
|
||||
filterBy,
|
||||
add,
|
||||
remove,
|
||||
set,
|
||||
}) => {
|
||||
const [previousFilters, setPreviousFilters] = useState(activeFilters);
|
||||
useEffect(() => {
|
||||
if (JSON.stringify(activeFilters) !== JSON.stringify(previousFilters)) {
|
||||
set(activeFilters);
|
||||
setPreviousFilters(activeFilters);
|
||||
}
|
||||
}, [activeFilters]);
|
||||
|
||||
const handleCheckboxChange = (e) => {
|
||||
const { value, checked } = e.target;
|
||||
|
||||
const updatedFilters = { ...initialFilters, [value]: checked };
|
||||
if (e.target.checked) {
|
||||
add(e.target.value);
|
||||
} else {
|
||||
remove(e.target.value);
|
||||
}
|
||||
onApply(updatedFilters);
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalPopup isOpen={isOpen} onClose={onClose} positionRef={positionRef} placement="bottom-start">
|
||||
<div className="filter-modal bg-white rounded shadow-sm w-175">
|
||||
<Form.Group>
|
||||
<Form.CheckboxSet
|
||||
name="course-optimizer-filter"
|
||||
onChange={handleCheckboxChange}
|
||||
value={filterBy}
|
||||
>
|
||||
{filterOptions.map(({ name, value }) => (
|
||||
<Form.Checkbox {...{ value, key: value }}>
|
||||
<span style={{ display: 'flex', gap: '90px' }}>
|
||||
{name}
|
||||
{ value === 'brokenLinks' && <CustomIcon icon={LinkOff} message1={messages.brokenLabel} message2={messages.brokenInfoTooltip} placement="right-end" /> }
|
||||
{ value === 'externalForbiddenLinks' && <CustomIcon icon={ManualIcon} message1={messages.manualLabel} message2={messages.manualInfoTooltip} placement="right-end" /> }
|
||||
{ value === 'lockedLinks' && <CustomIcon icon={LockedIcon} message1={messages.lockedLabel} message2={messages.lockedInfoTooltip} placement="right-end" /> }
|
||||
</span>
|
||||
</Form.Checkbox>
|
||||
))}
|
||||
</Form.CheckboxSet>
|
||||
</Form.Group>
|
||||
</div>
|
||||
</ModalPopup>
|
||||
);
|
||||
};
|
||||
|
||||
FilterModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onApply: PropTypes.func.isRequired,
|
||||
positionRef: PropTypes.shape({
|
||||
current: PropTypes.instanceOf(Element),
|
||||
}),
|
||||
filterOptions: PropTypes.arrayOf(PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
initialFilters: PropTypes.shape({
|
||||
brokenLinks: PropTypes.bool.isRequired,
|
||||
lockedLinks: PropTypes.bool.isRequired,
|
||||
externalForbiddenLinks: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
activeFilters: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
filterBy: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
add: PropTypes.func.isRequired,
|
||||
remove: PropTypes.func.isRequired,
|
||||
set: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default FilterModal;
|
||||
19
src/optimizer-page/scan-results/lockedIcon.tsx
Normal file
19
src/optimizer-page/scan-results/lockedIcon.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
|
||||
const LockedIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M18 8H17V6C17 3.24 14.76 1 12 1C9.24 1 7 3.24 7 6V8H6C4.9 8 4 8.9 4 10V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V10C20 8.9 19.1 8 18 8ZM9 6C9 4.34 10.34 3 12 3C13.66 3 15 4.34 15 6V8H9V6ZM18 20H6V10H18V20ZM12 17C13.1 17 14 16.1 14 15C14 13.9 13.1 13 12 13C10.9 13 10 13.9 10 15C10 16.1 10.9 17 12 17Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default LockedIcon;
|
||||
20
src/optimizer-page/scan-results/manualIcon.tsx
Normal file
20
src/optimizer-page/scan-results/manualIcon.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
// frontend-app-course-authoring/src/assets/icons/ManualIcon.tsx
|
||||
import React from 'react';
|
||||
|
||||
const ManualIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props} // This allows passing additional props like className, style, etc.
|
||||
>
|
||||
<path
|
||||
d="M12 6C15.79 6 19.17 8.13 20.82 11.5C19.17 14.87 15.79 17 12 17C8.21 17 4.83 14.87 3.18 11.5C4.83 8.13 8.21 6 12 6ZM12 4C7 4 2.73 7.11 1 11.5C2.73 15.89 7 19 12 19C17 19 21.27 15.89 23 11.5C21.27 7.11 17 4 12 4ZM12 9C13.38 9 14.5 10.12 14.5 11.5C14.5 12.88 13.38 14 12 14C10.62 14 9.5 12.88 9.5 11.5C9.5 10.12 10.62 9 12 9ZM12 7C9.52 7 7.5 9.02 7.5 11.5C7.5 13.98 9.52 16 12 16C14.48 16 16.5 13.98 16.5 11.5C16.5 9.02 14.48 7 12 7Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default ManualIcon;
|
||||
@@ -15,43 +15,48 @@ const messages = defineMessages({
|
||||
},
|
||||
scanHeader: {
|
||||
id: 'course-authoring.course-optimizer.scanHeader',
|
||||
defaultMessage: 'Broken Links Scan',
|
||||
defaultMessage: 'Scan results',
|
||||
},
|
||||
brokenLinksHeader: {
|
||||
id: 'course-authoring.course-optimizer.brokenLinksHeader',
|
||||
defaultMessage: 'Broken links',
|
||||
},
|
||||
filterButtonLabel: {
|
||||
id: 'course-authoring.course-optimizer.filterButtonLabel',
|
||||
defaultMessage: 'Filters',
|
||||
},
|
||||
lockedCheckboxLabel: {
|
||||
id: 'course-authoring.course-optimizer.lockedCheckboxLabel',
|
||||
defaultMessage: 'Show Locked Course Files',
|
||||
},
|
||||
brokenLinksNumber: {
|
||||
id: 'course-authoring.course-optimizer.brokenLinksNumber',
|
||||
defaultMessage: '{count} broken links',
|
||||
},
|
||||
lockedLinksNumber: {
|
||||
id: 'course-authoring.course-optimizer.lockedLinksNumber',
|
||||
defaultMessage: '{count} locked links',
|
||||
},
|
||||
externalForbiddenLinksNumber: {
|
||||
id: 'course-authoring.course-optimizer.externalForbiddenLinksNumber',
|
||||
defaultMessage: '{count} manual check',
|
||||
lockedLabel: {
|
||||
id: 'course-authoring.course-optimizer.lockedLabel',
|
||||
defaultMessage: 'Locked',
|
||||
},
|
||||
lockedInfoTooltip: {
|
||||
id: 'course-authoring.course-optimizer.lockedInfoTooltip',
|
||||
defaultMessage: 'These course files are "locked", so we cannot verify if the link can access the file.',
|
||||
defaultMessage: 'These course files are inaccessible for non-enrolled users so we cannot verify if the link can access the file.',
|
||||
},
|
||||
brokenLinkStatus: {
|
||||
id: 'course-authoring.course-optimizer.brokenLinkStatus',
|
||||
defaultMessage: 'Status: Broken',
|
||||
brokenLabel: {
|
||||
id: 'course-authoring.course-optimizer.brokenLabel',
|
||||
defaultMessage: 'Broken',
|
||||
},
|
||||
lockedLinkStatus: {
|
||||
id: 'course-authoring.course-optimizer.lockedLinkStatus',
|
||||
defaultMessage: 'Status: Locked',
|
||||
brokenInfoTooltip: {
|
||||
id: 'course-authoring.course-optimizer.brokenInfoTooltip',
|
||||
defaultMessage: `Links pointing to external websites, images, or videos that do not exist or are no longer available.
|
||||
These links can cause issues for learners when they try to access the content.`,
|
||||
},
|
||||
recommendedManualCheckText: {
|
||||
id: 'course-authoring.course-optimizer.recommendedManualCheckText',
|
||||
defaultMessage: 'Recommended Manual Check',
|
||||
manualLabel: {
|
||||
id: 'course-authoring.course-optimizer.manualLabel',
|
||||
defaultMessage: 'Manual',
|
||||
},
|
||||
recommendedManualCheckTooltip: {
|
||||
id: 'course-authoring.course-optimizer.recommendedManualCheckTooltip',
|
||||
defaultMessage: 'For websites returning 403, websites often show 403 because they don\'t want bots accessing their content',
|
||||
manualInfoTooltip: {
|
||||
id: 'course-authoring.course-optimizer.manualInfoTooltip',
|
||||
defaultMessage: 'We couldn\'t verify this link. Please check it manually.',
|
||||
},
|
||||
clearFilters: {
|
||||
id: 'course-authoring.course-optimizer.clearFilters',
|
||||
defaultMessage: 'Clear filters',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -26,3 +26,9 @@ export interface Section {
|
||||
export interface LinkCheckResult {
|
||||
sections: Section[];
|
||||
}
|
||||
|
||||
export interface Filters {
|
||||
brokenLinks: boolean,
|
||||
lockedLinks: boolean,
|
||||
externalForbiddenLinks: boolean,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user