Skip to content

Commit dab5c89

Browse files
feat: implement rerun links UI and integrate backend API
1 parent d5a3568 commit dab5c89

15 files changed

+2762
-129
lines changed

src/optimizer-page/CourseOptimizerPage.test.js

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -207,9 +207,9 @@ describe('CourseOptimizerPage', () => {
207207
fireEvent.click(collapsibleTrigger);
208208

209209
await waitFor(() => {
210-
expect(getByText('Test Locked Links')).toBeInTheDocument();
211-
expect(queryByText('Test Broken Links')).not.toBeInTheDocument();
212-
expect(queryByText('Test Manual Links')).not.toBeInTheDocument();
210+
expect(getByText('https://example.com/locked-link')).toBeInTheDocument();
211+
expect(queryByText('https://example.com/broken-link')).not.toBeInTheDocument();
212+
expect(queryByText('https://outsider.com/forbidden-link')).not.toBeInTheDocument();
213213
});
214214
});
215215

@@ -229,9 +229,9 @@ describe('CourseOptimizerPage', () => {
229229
fireEvent.click(collapsibleTrigger);
230230

231231
await waitFor(() => {
232-
expect(getByText('Test Broken Links')).toBeInTheDocument();
233-
expect(queryByText('Test Locked Links')).not.toBeInTheDocument();
234-
expect(queryByText('Test Manual Links')).not.toBeInTheDocument();
232+
expect(getByText('https://example.com/broken-link')).toBeInTheDocument();
233+
expect(queryByText('https://example.com/locked-link')).not.toBeInTheDocument();
234+
expect(queryByText('https://outsider.com/forbidden-link')).not.toBeInTheDocument();
235235
});
236236
});
237237

@@ -252,19 +252,19 @@ describe('CourseOptimizerPage', () => {
252252
fireEvent.click(collapsibleTrigger);
253253

254254
await waitFor(() => {
255-
expect(getByText('Test Manual Links')).toBeInTheDocument();
256-
expect(queryByText('Test Broken Links')).not.toBeInTheDocument();
257-
expect(queryByText('Test Locked Links')).not.toBeInTheDocument();
255+
expect(getByText('https://outsider.com/forbidden-link')).toBeInTheDocument();
256+
expect(queryByText('https://example.com/broken-link')).not.toBeInTheDocument();
257+
expect(queryByText('https://example.com/locked-link')).not.toBeInTheDocument();
258258
});
259259

260260
// Click the manual links checkbox again to clear the filter
261261
fireEvent.click(getByLabelText(scanResultsMessages.manualLabel.defaultMessage));
262262

263263
// Assert that all links are displayed after clearing the filter
264264
await waitFor(() => {
265-
expect(getByText('Test Broken Links')).toBeInTheDocument();
266-
expect(getByText('Test Manual Links')).toBeInTheDocument();
267-
expect(getByText('Test Locked Links')).toBeInTheDocument();
265+
expect(getByText('https://example.com/broken-link')).toBeInTheDocument();
266+
expect(getByText('https://outsider.com/forbidden-link')).toBeInTheDocument();
267+
expect(getByText('https://example.com/locked-link')).toBeInTheDocument();
268268
});
269269
});
270270

@@ -287,9 +287,9 @@ describe('CourseOptimizerPage', () => {
287287
fireEvent.click(collapsibleTrigger);
288288

289289
await waitFor(() => {
290-
expect(getByText('Test Manual Links')).toBeInTheDocument();
291-
expect(getByText('Test Locked Links')).toBeInTheDocument();
292-
expect(queryByText('Test Broken Links')).not.toBeInTheDocument();
290+
expect(getByText('https://outsider.com/forbidden-link')).toBeInTheDocument();
291+
expect(getByText('https://example.com/locked-link')).toBeInTheDocument();
292+
expect(queryByText('https://example.com/broken-link')).not.toBeInTheDocument();
293293
});
294294
});
295295

@@ -313,9 +313,9 @@ describe('CourseOptimizerPage', () => {
313313
fireEvent.click(collapsibleTrigger);
314314

315315
await waitFor(() => {
316-
expect(getByText('Test Broken Links')).toBeInTheDocument();
317-
expect(getByText('Test Manual Links')).toBeInTheDocument();
318-
expect(getByText('Test Locked Links')).toBeInTheDocument();
316+
expect(getByText('https://example.com/broken-link')).toBeInTheDocument();
317+
expect(getByText('https://outsider.com/forbidden-link')).toBeInTheDocument();
318+
expect(getByText('https://example.com/locked-link')).toBeInTheDocument();
319319
});
320320
});
321321

@@ -337,9 +337,9 @@ describe('CourseOptimizerPage', () => {
337337

338338
// Assert that both links are displayed
339339
await waitFor(() => {
340-
expect(getByText('Test Broken Links')).toBeInTheDocument();
341-
expect(getByText('Test Manual Links')).toBeInTheDocument();
342-
expect(queryByText('Test Locked Links')).not.toBeInTheDocument();
340+
expect(getByText('https://example.com/broken-link')).toBeInTheDocument();
341+
expect(getByText('https://outsider.com/forbidden-link')).toBeInTheDocument();
342+
expect(queryByText('https://example.com/locked-link')).not.toBeInTheDocument();
343343
});
344344

345345
// Click on the "Broken" chip to remove the broken filter (should leave only manual)
@@ -348,9 +348,9 @@ describe('CourseOptimizerPage', () => {
348348

349349
// Assert that only manual links are displayed
350350
await waitFor(() => {
351-
expect(queryByText('Test Broken Links')).not.toBeInTheDocument();
352-
expect(getByText('Test Manual Links')).toBeInTheDocument();
353-
expect(queryByText('Test Locked Links')).not.toBeInTheDocument();
351+
expect(queryByText('https://example.com/broken-link')).not.toBeInTheDocument();
352+
expect(getByText('https://outsider.com/forbidden-link')).toBeInTheDocument();
353+
expect(queryByText('https://example.com/locked-link')).not.toBeInTheDocument();
354354
});
355355

356356
// Click the "Clear filters" button
@@ -359,9 +359,9 @@ describe('CourseOptimizerPage', () => {
359359

360360
// Assert that all links are displayed after clearing filters
361361
await waitFor(() => {
362-
expect(getByText('Test Broken Links')).toBeInTheDocument();
363-
expect(getByText('Test Manual Links')).toBeInTheDocument();
364-
expect(getByText('Test Locked Links')).toBeInTheDocument();
362+
expect(getByText('https://example.com/broken-link')).toBeInTheDocument();
363+
expect(getByText('https://outsider.com/forbidden-link')).toBeInTheDocument();
364+
expect(getByText('https://example.com/locked-link')).toBeInTheDocument();
365365
});
366366
});
367367

src/optimizer-page/CourseOptimizerPage.tsx

Lines changed: 63 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import {
55
import { useDispatch, useSelector } from 'react-redux';
66
import { useIntl } from '@edx/frontend-platform/i18n';
77
import {
8-
Badge, Container, Layout, Button, Card, Spinner,
8+
Badge, Container, Layout, Card, Spinner, StatefulButton,
99
} from '@openedx/paragon';
1010
import { Helmet } from 'react-helmet';
1111

1212
import CourseStepper from '../generic/course-stepper';
1313
import ConnectionErrorAlert from '../generic/ConnectionErrorAlert';
14+
import AlertMessage from '../generic/alert-message';
1415
import { RequestFailureStatuses } from '../data/constants';
16+
import { STATEFUL_BUTTON_STATES } from '../constants';
1517
import messages from './messages';
1618
import {
1719
getCurrentStage, getError, getLinkCheckInProgress, getLoadingStatus, getSavingStatus, getLinkCheckResult,
@@ -53,14 +55,19 @@ const CourseOptimizerPage: FC<{ courseId: string }> = ({ courseId }) => {
5355
const lastScannedAt = useSelector(getLastScannedAt);
5456
const { msg: errorMessage } = useSelector(getError);
5557
const isLoadingDenied = (RequestFailureStatuses as string[]).includes(loadingStatus);
56-
const isSavingDenied = (RequestFailureStatuses as string[]).includes(savingStatus);
5758
const interval = useRef<number | undefined>(undefined);
5859
const courseDetails = useModel('courseDetails', courseId);
5960
const linkCheckPresent = currentStage != null ? currentStage >= 0 : !!currentStage;
6061
const [showStepper, setShowStepper] = useState(false);
61-
62+
const [scanResultsError, setScanResultsError] = useState<string | null>(null);
63+
const isSavingDenied = (RequestFailureStatuses as string[]).includes(savingStatus) && !errorMessage;
6264
const intl = useIntl();
63-
65+
const getScanButtonState = () => {
66+
if (linkCheckInProgress && !errorMessage) {
67+
return STATEFUL_BUTTON_STATES.pending;
68+
}
69+
return STATEFUL_BUTTON_STATES.default;
70+
};
6471
const courseStepperSteps = [
6572
{
6673
title: intl.formatMessage(messages.preparingStepTitle),
@@ -131,10 +138,25 @@ const CourseOptimizerPage: FC<{ courseId: string }> = ({ courseId }) => {
131138
})}
132139
</title>
133140
</Helmet>
141+
{scanResultsError && (
142+
<AlertMessage
143+
variant="danger"
144+
title=""
145+
description={scanResultsError}
146+
dismissible
147+
show={!!scanResultsError}
148+
onClose={() => setScanResultsError(null)}
149+
className="mt-3"
150+
/>
151+
)}
134152
<Container size="xl" className="mt-4 px-4 export">
135153
<section className="setting-items mb-4">
136154
<Layout
137-
lg={[{ span: 12 }, { span: 0 }]}
155+
lg={[{ span: 9 }, { span: 3 }]}
156+
md={[{ span: 9 }, { span: 3 }]}
157+
sm={[{ span: 9 }, { span: 3 }]}
158+
xs={[{ span: 9 }, { span: 3 }]}
159+
xl={[{ span: 9 }, { span: 3 }]}
138160
>
139161
<Layout.Element>
140162
<article>
@@ -146,51 +168,57 @@ const CourseOptimizerPage: FC<{ courseId: string }> = ({ courseId }) => {
146168
<Badge variant="dark" className="ml-2">{intl.formatMessage(messages.new)}</Badge>
147169
</div>
148170
</div>
149-
<Button
150-
variant="primary"
151-
size="md"
171+
<StatefulButton
152172
className="px-4 rounded-0 scan-course-btn"
173+
labels={{
174+
default: intl.formatMessage(messages.buttonTitle),
175+
pending: intl.formatMessage(messages.buttonTitle),
176+
}}
177+
icons={{
178+
default: '',
179+
pending: <Spinner
180+
animation="border"
181+
size="sm"
182+
className="mr-2 spinner-icon"
183+
/>,
184+
}}
185+
state={getScanButtonState()}
153186
onClick={() => dispatch(startLinkCheck(courseId))}
154187
disabled={!!(linkCheckInProgress) && !errorMessage}
155-
>
156-
{linkCheckInProgress && !errorMessage ? (
157-
<>
158-
<Spinner
159-
animation="border"
160-
size="sm"
161-
className="mr-2 spinner-icon"
162-
/>
163-
{intl.formatMessage(messages.buttonTitle)}
164-
</>
165-
) : (
166-
intl.formatMessage(messages.buttonTitle)
167-
)}
168-
</Button>
188+
variant="primary"
189+
data-testid="scan-course"
190+
/>
169191
</div>
170192
<Card className="scan-card">
171193
<p className="px-3 py-1 small">{intl.formatMessage(messages.description)}</p>
172194
<hr />
195+
{showStepper && (
196+
<Card.Section className="px-3 py-1">
197+
<CourseStepper
198+
// @ts-ignore
199+
steps={courseStepperSteps}
200+
// @ts-ignore
201+
activeKey={currentStage}
202+
hasError={currentStage === 1 && !!errorMessage}
203+
errorMessage={errorMessage}
204+
/>
205+
</Card.Section>
206+
)}
173207
<Card.Header
174208
className="scan-header h3 px-3 text-black mb-2"
175209
title={intl.formatMessage(messages.scanHeader)}
176210
/>
177211
<Card.Section className="px-3 py-1">
178212
<p className="small"> {lastScannedAt && `${intl.formatMessage(messages.lastScannedOn)} ${intl.formatDate(lastScannedAt, { year: 'numeric', month: 'long', day: 'numeric' })}`}</p>
179213
</Card.Section>
180-
{showStepper && (
181-
<Card.Section className="px-3 py-1">
182-
<CourseStepper
183-
// @ts-ignore
184-
steps={courseStepperSteps}
185-
// @ts-ignore
186-
activeKey={currentStage}
187-
hasError={currentStage === 1 && !!errorMessage}
188-
errorMessage={errorMessage}
189-
/>
190-
</Card.Section>
191-
)}
192214
</Card>
193-
{(linkCheckPresent && linkCheckResult) && <ScanResults data={linkCheckResult} />}
215+
{linkCheckPresent && linkCheckResult && (
216+
<ScanResults
217+
data={linkCheckResult}
218+
courseId={courseId}
219+
onErrorStateChange={setScanResultsError}
220+
/>
221+
)}
194222
</article>
195223
</Layout.Element>
196224
</Layout>

0 commit comments

Comments
 (0)