Skip to content

Commit c2e4f7f

Browse files
Muhammad Faraz  MaqsoodAsespinel
authored andcommitted
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
1 parent 96a04d4 commit c2e4f7f

11 files changed

Lines changed: 357 additions & 116 deletions

File tree

src/optimizer-page/CourseOptimizerPage.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,23 +132,23 @@ const CourseOptimizerPage: FC<{ courseId: string }> = ({ courseId }) => {
132132
title={intl.formatMessage(messages.headingTitle)}
133133
subtitle={intl.formatMessage(messages.headingSubtitle)}
134134
/>
135-
<p className="small">{intl.formatMessage(messages.description1)}</p>
136-
<p className="small">{intl.formatMessage(messages.description2)}</p>
135+
<p className="small opt-desc-mb">{intl.formatMessage(messages.description)}</p>
137136
<Card>
138137
<Card.Header
139138
className="h3 px-3 text-black mb-4"
140139
title={intl.formatMessage(messages.card1Title)}
141140
/>
142141
{isShowExportButton && (
143142
<Card.Section className="px-3 py-1">
143+
<p className="small"> {lastScannedAt && `${intl.formatMessage(messages.lastScannedOn)} ${intl.formatDate(lastScannedAt, { year: 'numeric', month: 'long', day: 'numeric' })}`}</p>
144144
<Button
145145
size="lg"
146146
block
147147
className="mb-4"
148148
onClick={() => dispatch(startLinkCheck(courseId))}
149149
iconBefore={SearchIcon}
150150
>
151-
{intl.formatMessage(messages.buttonTitle)} {lastScannedAt && `(${intl.formatMessage(messages.lastScannedOn)} ${intl.formatDate(lastScannedAt, { year: 'numeric', month: 'long', day: 'numeric' })})`}
151+
{intl.formatMessage(messages.buttonTitle)}
152152
</Button>
153153
</Card.Section>
154154
)}

src/optimizer-page/messages.js

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,15 @@ const messages = defineMessages({
77
},
88
headingTitle: {
99
id: 'course-authoring.course-optimizer.heading.title',
10-
defaultMessage: 'Course Optimizer',
10+
defaultMessage: 'Course optimizer',
1111
},
1212
headingSubtitle: {
1313
id: 'course-authoring.course-optimizer.heading.subtitle',
1414
defaultMessage: 'Tools',
1515
},
16-
description1: {
17-
id: 'course-authoring.course-optimizer.description1',
18-
defaultMessage: `This tool will scan the published version of your course for broken links.
19-
Unpublished changes will not be included in the scan.
20-
Note that this process will take more time for larger courses.
21-
To update the scan after you have published new changes to your course,
22-
click the "Start Scanning" button again.
23-
`,
24-
},
25-
description2: {
26-
id: 'course-authoring.course-optimizer.description2',
27-
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.',
16+
description: {
17+
id: 'course-authoring.course-optimizer.description',
18+
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.',
2819
},
2920
card1Title: {
3021
id: 'course-authoring.course-optimizer.card1.title',
@@ -36,7 +27,7 @@ const messages = defineMessages({
3627
},
3728
buttonTitle: {
3829
id: 'course-authoring.course-optimizer.button.title',
39-
defaultMessage: 'Start Scanning',
30+
defaultMessage: 'Start scanning',
4031
},
4132
preparingStepTitle: {
4233
id: 'course-authoring.course-optimizer.peparing-step.title',
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import PropTypes from 'prop-types';
2+
import {
3+
Icon,
4+
OverlayTrigger,
5+
Tooltip,
6+
} from '@openedx/paragon';
7+
import { useIntl } from '@edx/frontend-platform/i18n';
8+
9+
const CustomIcon = ({
10+
icon,
11+
message1,
12+
message2,
13+
placement = 'top',
14+
}) => {
15+
const intl = useIntl();
16+
17+
return (
18+
<OverlayTrigger
19+
key="top"
20+
placement={placement}
21+
overlay={(
22+
<Tooltip variant="dark" id="tooltip-top" className={placement !== 'top' ? 'ml-3' : ''}>
23+
{intl.formatMessage(message1)}
24+
{message1 && <br />}
25+
{intl.formatMessage(message2)}
26+
</Tooltip>
27+
)}
28+
>
29+
<Icon src={icon} />
30+
</OverlayTrigger>
31+
);
32+
};
33+
34+
const messagePropsType = {
35+
id: PropTypes.string.isRequired,
36+
defaultMessage: PropTypes.string.isRequired,
37+
};
38+
39+
CustomIcon.propTypes = {
40+
icon: PropTypes.elementType.isRequired,
41+
message1: PropTypes.shape(messagePropsType).isRequired,
42+
message2: PropTypes.shape(messagePropsType).isRequired,
43+
placement: PropTypes.string,
44+
};
45+
46+
export default CustomIcon;

src/optimizer-page/scan-results/LockedInfoIcon.jsx

Lines changed: 0 additions & 30 deletions
This file was deleted.

src/optimizer-page/scan-results/ScanResults.tsx

Lines changed: 106 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
import { useState, useMemo, FC } from 'react';
22
import {
33
Card,
4-
CheckBox,
4+
Chip,
5+
Button,
6+
useCheckboxSetValues,
57
} from '@openedx/paragon';
8+
import {
9+
ArrowDropDown,
10+
CloseSmall,
11+
} from '@openedx/paragon/icons';
612
import { useIntl } from '@edx/frontend-platform/i18n';
713
import messages from './messages';
814
import SectionCollapsible from './SectionCollapsible';
915
import BrokenLinkTable from './BrokenLinkTable';
10-
import LockedInfoIcon from './LockedInfoIcon';
1116
import { LinkCheckResult } from '../types';
1217
import { countBrokenLinks } from '../utils';
18+
import FilterModal from './filterModal';
1319

1420
const InfoCard: FC<{ text: string }> = ({ text }) => (
1521
<Card className="mt-4">
@@ -28,63 +34,130 @@ interface Props {
2834

2935
const ScanResults: FC<Props> = ({ data }) => {
3036
const intl = useIntl();
31-
const [showLockedLinks, setShowLockedLinks] = useState(true);
37+
const [isModalOpen, setModalOpen] = useState(false);
38+
const initialFilters = {
39+
brokenLinks: false,
40+
lockedLinks: false,
41+
externalForbiddenLinks: false,
42+
};
43+
const [filters, setFilters] = useState(initialFilters);
44+
const [buttonRef, setButtonRef] = useState<HTMLButtonElement | null>(null);
3245

3346
const {
3447
brokenLinksCounts,
3548
lockedLinksCounts,
3649
externalForbiddenLinksCounts,
3750
} = useMemo(() => countBrokenLinks(data), [data?.sections]);
3851

52+
const activeFilters = Object.keys(filters).filter(key => filters[key]);
53+
const [filterBy, {
54+
add, remove, set, clear,
55+
}] = useCheckboxSetValues(activeFilters);
56+
3957
if (!data?.sections) {
4058
return <InfoCard text={intl.formatMessage(messages.noBrokenLinksCard)} />;
4159
}
4260

4361
const { sections } = data;
62+
const filterOptions = [
63+
{ name: intl.formatMessage(messages.brokenLabel), value: 'brokenLinks' },
64+
{ name: intl.formatMessage(messages.manualLabel), value: 'externalForbiddenLinks' },
65+
{ name: intl.formatMessage(messages.lockedLabel), value: 'lockedLinks' },
66+
];
4467

4568
return (
4669
<div className="scan-results">
47-
<div className="border-bottom border-light-400 mb-3">
70+
<div className="scan-header-title-container">
71+
<h2 className="scan-header-title">{intl.formatMessage(messages.scanHeader)}</h2>
72+
</div>
73+
<div className="scan-header-second-title-container">
4874
<header className="sub-header-content">
49-
<h2 className="sub-header-content-title">{intl.formatMessage(messages.scanHeader)}</h2>
50-
<span className="locked-links-checkbox-wrapper">
51-
<CheckBox
52-
className="locked-links-checkbox"
53-
type="checkbox"
54-
checked={showLockedLinks}
55-
onClick={() => {
56-
setShowLockedLinks(!showLockedLinks);
57-
}}
58-
label={intl.formatMessage(messages.lockedCheckboxLabel)}
59-
/>
60-
<LockedInfoIcon />
61-
</span>
75+
<h2 className="broken-links-header-title pt-2">{intl.formatMessage(messages.brokenLinksHeader)}</h2>
76+
<Button
77+
ref={setButtonRef}
78+
variant="outline-primary"
79+
onClick={() => setModalOpen(true)}
80+
disabled={false}
81+
iconAfter={ArrowDropDown}
82+
className="rounded-sm justify-content-between cadence-button"
83+
>
84+
{intl.formatMessage(messages.filterButtonLabel)}
85+
</Button>
6286
</header>
6387
</div>
88+
<FilterModal
89+
isOpen={isModalOpen}
90+
onClose={() => setModalOpen(false)}
91+
onApply={setFilters}
92+
positionRef={buttonRef}
93+
filterOptions={filterOptions}
94+
initialFilters={filters}
95+
activeFilters={activeFilters}
96+
filterBy={filterBy}
97+
add={add}
98+
remove={remove}
99+
set={set}
100+
/>
101+
{activeFilters.length > 0 && <div className="border-bottom border-light-400" />}
102+
{activeFilters.length > 0 && (
103+
<div className="scan-results-active-filters-container">
104+
<span className="scan-results-active-filters-chips">
105+
{activeFilters.map(filter => (
106+
<Chip
107+
key={filter}
108+
iconAfter={CloseSmall}
109+
iconAfterAlt="icon-after"
110+
className="scan-results-active-filters-chip"
111+
onClick={() => {
112+
remove(filter);
113+
const updatedFilters = { ...filters, [filter]: false };
114+
setFilters(updatedFilters);
115+
}}
116+
>
117+
{filterOptions.find(option => option.value === filter)?.name}
118+
</Chip>
119+
))}
120+
</span>
121+
<Button
122+
variant="link"
123+
className="clear-all-btn"
124+
onClick={() => {
125+
clear();
126+
setFilters(initialFilters);
127+
}}
128+
>
129+
{intl.formatMessage(messages.clearFilters)}
130+
</Button>
131+
</div>
132+
)}
64133

65134
{sections?.map((section, index) => (
66135
<SectionCollapsible
67136
key={section.id}
68137
title={section.displayName}
69-
redItalics={intl.formatMessage(messages.brokenLinksNumber, { count: brokenLinksCounts[index] })}
70-
yellowItalics={!showLockedLinks ? '' : intl.formatMessage(messages.lockedLinksNumber, { count: lockedLinksCounts[index] })}
71-
greenItalics={
72-
intl.formatMessage(messages.externalForbiddenLinksNumber, { count: externalForbiddenLinksCounts[index] })
73-
}
138+
brokenNumber={brokenLinksCounts[index]}
139+
manualNumber={externalForbiddenLinksCounts[index]}
140+
lockedNumber={lockedLinksCounts[index]}
141+
className="section-collapsible-header"
74142
>
75143
{section.subsections.map((subsection) => (
76144
<>
77-
<h2
78-
className="subsection-header"
79-
style={{ marginBottom: '2rem' }}
80-
>
81-
{subsection.displayName}
82-
</h2>
83-
{subsection.units.map((unit) => (
84-
<div className="unit">
85-
<BrokenLinkTable unit={unit} showLockedLinks={showLockedLinks} />
86-
</div>
87-
))}
145+
{subsection.units.map((unit) => {
146+
if (
147+
(!filters.brokenLinks && !filters.externalForbiddenLinks && !filters.lockedLinks)
148+
|| (filters.brokenLinks && unit.blocks.some(block => block.brokenLinks.length > 0))
149+
|| (filters.externalForbiddenLinks
150+
&& unit.blocks.some(block => block.externalForbiddenLinks.length > 0))
151+
|| (filters.lockedLinks && unit.blocks.some(block => block.lockedLinks.length > 0))
152+
) {
153+
return (
154+
<div className="unit">
155+
<BrokenLinkTable unit={unit} filters={filters} />
156+
</div>
157+
);
158+
}
159+
return null;
160+
})}
88161
</>
89162
))}
90163
</SectionCollapsible>

src/optimizer-page/scan-results/SectionCollapsible.tsx

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,38 +6,57 @@ import {
66
import {
77
ArrowRight,
88
ArrowDropDown,
9+
LinkOff,
910
} from '@openedx/paragon/icons';
11+
import CustomIcon from './CustomIcon';
12+
import messages from './messages';
13+
import lockedIcon from './lockedIcon';
14+
import ManualIcon from './manualIcon';
1015

1116
interface Props {
1217
title: string;
1318
children: React.ReactNode;
14-
redItalics?: string;
15-
yellowItalics?: string;
16-
greenItalics?: string;
19+
brokenNumber: number;
20+
manualNumber: number;
21+
lockedNumber: number;
1722
className?: string;
1823
}
1924

2025
const SectionCollapsible: FC<Props> = ({
21-
title, children, redItalics = '', yellowItalics = '', greenItalics = '', className = '',
26+
title, children, brokenNumber = 0, manualNumber = 0, lockedNumber = 0, className = '',
2227
}) => {
2328
const [isOpen, setIsOpen] = useState(false);
24-
const styling = 'card-lg';
29+
const styling = 'card-lg rounded-sm shadow-outline';
2530
const collapsibleTitle = (
2631
<div className={className}>
27-
<Icon src={isOpen ? ArrowDropDown : ArrowRight} className="open-arrow" />
28-
<strong>{title}</strong>
29-
<span className="red-italics">{redItalics}</span>
30-
<span className="yellow-italics">{yellowItalics}</span>
31-
<span className="green-italics">{greenItalics}</span>
32+
<div className="section-collapsible-header-item">
33+
<Icon src={isOpen ? ArrowDropDown : ArrowRight} />
34+
<strong>{title}</strong>
35+
</div>
36+
<div className="section-collapsible-header-actions">
37+
<span>
38+
<CustomIcon icon={LinkOff} message1={messages.brokenLabel} message2={messages.brokenInfoTooltip} />
39+
{brokenNumber}
40+
</span>
41+
<span>
42+
<CustomIcon icon={ManualIcon} message1={messages.manualLabel} message2={messages.manualInfoTooltip} />
43+
{manualNumber}
44+
</span>
45+
<span>
46+
<CustomIcon icon={lockedIcon} message1={messages.lockedLabel} message2={messages.lockedInfoTooltip} />
47+
{lockedNumber}
48+
</span>
49+
</div>
3250
</div>
3351
);
3452

3553
return (
3654
<div className={`section ${isOpen ? 'is-open' : ''}`}>
3755
<Collapsible
56+
className="section-collapsible-item-container"
3857
styling={styling}
3958
title={(
40-
<p>
59+
<p className="flex-grow-1 section-collapsible-item">
4160
<strong>{collapsibleTitle}</strong>
4261
</p>
4362
)}

0 commit comments

Comments
 (0)