Skip to content

Commit f323b9b

Browse files
fix(materials): display custom error page when file is missing in course materials
1 parent 80f1e6b commit f323b9b

File tree

6 files changed

+131
-11
lines changed

6 files changed

+131
-11
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { LoaderFunction, redirect } from 'react-router-dom';
2+
import { getIdFromUnknown } from 'utilities';
3+
4+
import CourseAPI from 'api/course';
5+
6+
const folderLoader: LoaderFunction = async ({ params }) => {
7+
const folderId = getIdFromUnknown(params?.folderId);
8+
if (!folderId) return redirect('/');
9+
10+
const { data } = await CourseAPI.folders.fetch(folderId);
11+
12+
return data;
13+
};
14+
15+
export default folderLoader;

client/app/bundles/course/material/folders/handles.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,26 @@ const getFolderTitle = async (
77
courseUrl: string,
88
folderId: number,
99
): Promise<CrumbPath> => {
10-
const { data } = await CourseAPI.folders.fetch(folderId);
10+
try {
11+
const { data } = await CourseAPI.folders.fetch(folderId);
12+
const workbinUrl = `${courseUrl}/materials/folders/${data.breadcrumbs[0].id}`;
1113

12-
const workbinUrl = `${courseUrl}/materials/folders/${data.breadcrumbs[0].id}`;
13-
14-
return {
15-
activePath: workbinUrl,
16-
content: data.breadcrumbs.map((crumb) => ({
17-
title: crumb.name,
18-
url: `materials/folders/${crumb.id}`,
19-
})),
20-
};
14+
return {
15+
activePath: workbinUrl,
16+
content: data.breadcrumbs.map((crumb) => ({
17+
title: crumb.name,
18+
url: `materials/folders/${crumb.id}`,
19+
})),
20+
};
21+
} catch (error) {
22+
return {
23+
activePath: courseUrl,
24+
content: {
25+
title: 'Folder Not Found',
26+
url: courseUrl,
27+
},
28+
};
29+
}
2130
};
2231

2332
/**
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { ReactNode } from 'react';
2+
import { Typography } from '@mui/material';
3+
4+
import Page from 'lib/components/core/layouts/Page';
5+
6+
interface BaseRetrievePageProps {
7+
illustration: ReactNode;
8+
title: string;
9+
description: string;
10+
children?: ReactNode;
11+
}
12+
13+
const BaseRetrievePage = (
14+
props: BaseRetrievePageProps,
15+
): JSX.Element => (
16+
<Page className="h-full m-auto flex flex-col items-center justify-center text-center">
17+
{props.illustration}
18+
19+
<Typography className="mt-5" variant="h6">
20+
{props.title}
21+
</Typography>
22+
23+
<Typography
24+
className="max-w-3xl mt-2"
25+
color="text.secondary"
26+
variant="body2"
27+
>
28+
{props.description}
29+
</Typography>
30+
31+
{props.children}
32+
</Page>
33+
);
34+
35+
export default BaseRetrievePage;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import React from 'react';
2+
import { defineMessages } from 'react-intl';
3+
import { useParams } from 'react-router-dom';
4+
import { Cancel, InsertDriveFileOutlined } from '@mui/icons-material';
5+
6+
import Link from 'lib/components/core/Link';
7+
import useTranslation from 'lib/hooks/useTranslation';
8+
9+
import BaseRetrievePage from './BaseRetrievePage';
10+
11+
const translations = defineMessages({
12+
problemRetrievingFolder: {
13+
id: 'course.material.folders.ErrorRetrievingFolderPage.problemRetrievingFolder',
14+
defaultMessage: 'Problem retrieving folder',
15+
},
16+
problemRetrievingFolderDescription: {
17+
id: 'course.material.folders.ErrorRetrievingFolderPage.problemRetrievingFolderDescription',
18+
defaultMessage:
19+
"Either it no longer exists, you don't have the permission to access it, or something unexpected happened when we were trying to retrieve it.",
20+
},
21+
goToTheWorkbin: {
22+
id: 'course.material.folders.ErrorRetrievingFolderPage.goToTheWorkbin',
23+
defaultMessage: 'Go to the Workbin',
24+
},
25+
});
26+
27+
const ErrorRetrievingFolderPage = (): JSX.Element => {
28+
const { t } = useTranslation();
29+
30+
const params = useParams();
31+
const workbinURL = `/courses/${params.courseId}`;
32+
33+
return (
34+
<BaseRetrievePage
35+
description={t(translations.problemRetrievingFolderDescription)}
36+
illustration={
37+
<div className="relative">
38+
<InsertDriveFileOutlined className="text-[6rem]" color="disabled" />
39+
40+
<Cancel
41+
className="absolute bottom-0 -right-2 text-[4rem] bg-white rounded-full"
42+
color="error"
43+
/>
44+
</div>
45+
}
46+
title={t(translations.problemRetrievingFolder)}
47+
>
48+
<Link className="mt-10" to={workbinURL}>
49+
{t(translations.goToTheWorkbin)}
50+
</Link>
51+
</BaseRetrievePage>
52+
);
53+
};
54+
55+
export default ErrorRetrievingFolderPage;

client/app/bundles/course/material/folders/pages/FolderShow/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { FC, ReactElement, useEffect, useState } from 'react';
22
import { defineMessages } from 'react-intl';
3-
import { useParams } from 'react-router-dom';
3+
import {useLoaderData, useParams} from 'react-router-dom';
44

55
import EditButton from 'lib/components/core/buttons/EditButton';
66
import Page from 'lib/components/core/layouts/Page';
@@ -37,6 +37,8 @@ const FolderShow: FC = () => {
3737
const { t } = useTranslation();
3838
const dispatch = useAppDispatch();
3939

40+
const data = useLoaderData();
41+
if (!data) throw new Error('No download data. This should never happen.');
4042
// For new folder form dialog
4143
const [isNewFolderOpen, setIsNewFolderOpen] = useState(false);
4244
// For edit folder form dialog

client/app/routers/AuthenticatedApp.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import LessonPlanShow from 'bundles/course/lesson-plan/pages/LessonPlanShow';
6767
import LevelsIndex from 'bundles/course/level/pages/LevelsIndex';
6868
import DownloadingFilePage from 'bundles/course/material/files/DownloadingFilePage';
6969
import ErrorRetrievingFilePage from 'bundles/course/material/files/ErrorRetrievingFilePage';
70+
import ErrorRetrievingFolderPage from 'bundles/course/material/folders/pages/ErrorRetrievingFolderPage';
7071
import FolderShow from 'bundles/course/material/folders/pages/FolderShow';
7172
import TimelineDesigner from 'bundles/course/reference-timelines/TimelineDesigner';
7273
import ResponseEdit from 'bundles/course/survey/pages/ResponseEdit';
@@ -127,6 +128,7 @@ import {
127128
forumTopicHandle,
128129
} from 'course/forum/handles';
129130
import { leaderboardHandle } from 'course/leaderboard/handles';
131+
import folderLoader from 'course/material/folderLoader';
130132
import { folderHandle } from 'course/material/folders/handles';
131133
import materialLoader from 'course/material/materialLoader';
132134
import { videoWatchHistoryHandle } from 'course/statistics/handles';
@@ -221,7 +223,9 @@ const authenticatedRouter: Translated<RouteObject[]> = (t) =>
221223
children: [
222224
{
223225
index: true,
226+
loader: folderLoader,
224227
element: <FolderShow />,
228+
errorElement: <ErrorRetrievingFolderPage />,
225229
},
226230
{
227231
path: 'files/:materialId',

0 commit comments

Comments
 (0)