Skip to content

Commit

Permalink
Add logic to all dashboard section componeents for smooth routing
Browse files Browse the repository at this point in the history
  • Loading branch information
acelaya committed Jun 13, 2024
1 parent efb55cc commit 7253c62
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 146 deletions.
2 changes: 1 addition & 1 deletion lms/static/scripts/frontend_apps/components/AppRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default function AppRoot({ initialConfig, services }: AppRootProps) {
<FilePickerApp />
</DataLoader>
</Route>
<Route path="/dashboard/organizations/:organizationPublicId" nest>
<Route path="/dashboard/organizations/:organizationId" nest>
<DashboardApp />
</Route>
<Route path="/email/preferences">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,49 +1,78 @@
import type { ComponentChildren, FunctionComponent } from 'preact';
import { Card, CardContent } from '@hypothesis/frontend-shared';
import type { ComponentChildren } from 'preact';
import { useEffect, useState } from 'preact/hooks';
import { useParams, useRoute } from 'wouter-preact';

import type { ConfigObject } from '../config';
import type { ConfigObject, Ensure } from '../config';
import { useConfig } from '../config';
import type { ErrorLike } from '../errors';
import ErrorDisplay from './ErrorDisplay';

export type ComponentWithLoaderWrapperProps = {
loaderModule: Promise<{
loader: (options: {
config: ConfigObject;
params: Record<string, string | undefined>;
}) => Promise<unknown>;
default: FunctionComponent<{ loadResult: unknown }>;
}>;
export type LoaderOptions = {
config: Ensure<ConfigObject, 'dashboard' | 'api'>;
params: Record<string, string>;
signal: AbortSignal;
};

export default function ComponentWithLoaderWrapper() {
const config = useConfig();
const params = useParams();
const config = useConfig(['dashboard', 'api']);
const [component, setComponent] = useState<ComponentChildren>();
const [loading, setLoading] = useState(true);
const [isAssignment] = useRoute('/assignments/:assignmentId');
const [isCourse] = useRoute('/courses/:courseId');
const [fatalError, setFatalError] = useState<ErrorLike>();

const [isAssignment, assignmentParams] = useRoute(
'/assignments/:assignmentId',
);
const [isCourse, courseParams] = useRoute('/courses/:courseId');
const [isHome] = useRoute('');
const globalParams = useParams();
const assignmentId = assignmentParams?.assignmentId ?? '';
const courseId = courseParams?.courseId ?? '';
const organizationId = globalParams.organizationId ?? '';

useEffect(() => {
const loaderModule = isAssignment
? import('./dashboard/AssignmentActivity')
: isCourse
? import('./dashboard/CourseActivity')
: import('./dashboard/OrganizationActivity');
const params = { assignmentId, courseId, organizationId };

const abortController = new AbortController();
loaderModule.then(async ({ loader, default: Component }) => {
// TODO Error handling
setLoading(true);
const loadResult = await loader({ config, params });
setLoading(false);
setComponent(<Component loadResult={loadResult} />);
try {
const loaderResult = await loader({
config,
params,
signal: abortController.signal,
});
setComponent(<Component loaderResult={loaderResult} params={params} />);
} catch (e) {
setFatalError(e);
} finally {
setLoading(false);
}
});
}, [config, isAssignment, isCourse, params]);

return () => abortController.abort();
}, [assignmentId, courseId, organizationId, config, isAssignment, isCourse]);

if (fatalError) {
return (
<Card>
<CardContent>
<ErrorDisplay error={fatalError} />
</CardContent>
</Card>
);
}

return !component ? (
<>Initial loading...</>
<div className="text-center">Initial loading...</div>
) : (
<>
{loading && <>Transitioning...</>}
{loading && <div className="text-center">Transitioning...</div>}
{component}
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,20 @@ import { useMemo } from 'preact/hooks';
import { useParams } from 'wouter-preact';

import type { Assignment, StudentsResponse } from '../../api-types';
import type { ConfigObject } from '../../config';
import { useConfig } from '../../config';
import { apiCall, urlPath } from '../../utils/api';
import { formatDateTime } from '../../utils/date';
import { useFetch } from '../../utils/fetch';
import { replaceURLParams } from '../../utils/url';
import type { LoaderOptions } from '../ComponentWithLoaderWrapper';
import DashboardBreadcrumbs from './DashboardBreadcrumbs';
import OrderableActivityTable from './OrderableActivityTable';

export function loader({
config: { dashboard, api },
params: { assignmentId },
signal,
}: {
config: ConfigObject;
params: Record<string, string>;
signal?: AbortSignal;
}) {
if (!dashboard || !api) {
throw new Error('Missing config!'); // TODO Handle this
}

}: LoaderOptions) {
const { routes } = dashboard;
const { authToken } = api;

Expand All @@ -55,7 +47,7 @@ export function loader({
export type AssignmentActivityLoadResult = Awaited<ReturnType<typeof loader>>;

export type AssignmentActivityProps = {
loadResult?: AssignmentActivityLoadResult;
loaderResult: AssignmentActivityLoadResult;
};

type StudentsTableRow = {
Expand All @@ -70,33 +62,19 @@ type StudentsTableRow = {
* Activity in a list of students that are part of a specific assignment
*/
export default function AssignmentActivity({
loadResult,
loaderResult,
}: AssignmentActivityProps) {
const config = useConfig(['dashboard', 'api']);
const params = useParams<{ assignmentId: string }>();

const loaderResult = useFetch<AssignmentActivityLoadResult>(
'assignment',
async signal => {
if (loadResult) {
return loadResult;
}

return loader({ config, params, signal });
},
);

const title = `Assignment: ${loaderResult.data?.assignment.title}`;
const title = `Assignment: ${loaderResult.assignment.title}`;
const rows: StudentsTableRow[] = useMemo(
() =>
(loaderResult.data?.students.students ?? []).map(
loaderResult.students.students.map(
({ id, display_name, annotation_metrics }) => ({
id,
display_name,
...annotation_metrics,
}),
),
[loaderResult.data],
[loaderResult.students.students],
);

return (
Expand All @@ -108,31 +86,24 @@ export default function AssignmentActivity({
'flex-col !gap-x-0 !items-start',
)}
>
{loaderResult.data && (
<div className="mb-3 mt-1 w-full">
<DashboardBreadcrumbs
links={[
{
title: loaderResult.data.assignment.course.title,
href: urlPath`/courses/${String(loaderResult.data.assignment.course.id)}`,
},
]}
/>
</div>
)}
<div className="mb-3 mt-1 w-full">
<DashboardBreadcrumbs
links={[
{
title: loaderResult.assignment.course.title,
href: urlPath`/courses/${String(loaderResult.assignment.course.id)}`,
},
]}
/>
</div>
<CardTitle tagName="h2" data-testid="title">
{loaderResult.isLoading && 'Loading...'}
{loaderResult.error && 'Could not load assignment title'}
{loaderResult.data && title}
title
</CardTitle>
</CardHeader>
<CardContent>
<OrderableActivityTable
loading={loaderResult.isLoading}
title={loaderResult.isLoading ? 'Loading...' : title}
emptyMessage={
loaderResult.error ? 'Could not load students' : 'No students found'
}
title={title}
emptyMessage=""
rows={rows}
columnNames={{
display_name: 'Student',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,46 @@ import {
} from '@hypothesis/frontend-shared';
import classnames from 'classnames';
import { useMemo } from 'preact/hooks';
import { useParams, Link as RouterLink } from 'wouter-preact';
import { Link as RouterLink } from 'wouter-preact';

import type { AssignmentsResponse, Course } from '../../api-types';
import { useConfig } from '../../config';
import { urlPath, useAPIFetch } from '../../utils/api';
import { apiCall, urlPath } from '../../utils/api';
import { formatDateTime } from '../../utils/date';
import { replaceURLParams } from '../../utils/url';
import type { LoaderOptions } from '../ComponentWithLoaderWrapper';
import DashboardBreadcrumbs from './DashboardBreadcrumbs';
import OrderableActivityTable from './OrderableActivityTable';

export function loader() {
return undefined;
export function loader({
config: { dashboard, api },
params: { courseId },
signal,
}: LoaderOptions) {
const { routes } = dashboard;
const { authToken } = api;

return Promise.all([
apiCall<Course>({
path: replaceURLParams(routes.course, { course_id: courseId }),
authToken,
signal,
}),
apiCall<AssignmentsResponse>({
path: replaceURLParams(routes.course_assignment_stats, {
course_id: courseId,
}),
authToken,
signal,
}),
]).then(([course, assignments]) => ({ course, assignments }));
}

export type CourseActivityLoadResult = Awaited<ReturnType<typeof loader>>;

export type CourseActivityProps = {
loaderResult: CourseActivityLoadResult;
};

type AssignmentsTableRow = {
id: number;
title: string;
Expand All @@ -34,29 +60,17 @@ const assignmentURL = (id: number) => urlPath`/assignments/${String(id)}`;
/**
* Activity in a list of assignments that are part of a specific course
*/
export default function CourseActivity() {
const { courseId } = useParams<{ courseId: string }>();
const { dashboard } = useConfig(['dashboard']);
const { routes } = dashboard;
const course = useAPIFetch<Course>(
replaceURLParams(routes.course, { course_id: courseId }),
);
const assignments = useAPIFetch<AssignmentsResponse>(
replaceURLParams(routes.course_assignment_stats, {
course_id: courseId,
}),
);

export default function CourseActivity({ loaderResult }: CourseActivityProps) {
const rows: AssignmentsTableRow[] = useMemo(
() =>
(assignments.data?.assignments ?? []).map(
loaderResult.assignments.assignments.map(
({ id, title, annotation_metrics }) => ({
id,
title,
...annotation_metrics,
}),
),
[assignments.data],
[loaderResult.assignments.assignments],
);

return (
Expand All @@ -72,20 +86,13 @@ export default function CourseActivity() {
<DashboardBreadcrumbs />
</div>
<CardTitle tagName="h2" data-testid="title">
{course.isLoading && 'Loading...'}
{course.error && 'Could not load course title'}
{course.data && course.data.title}
{loaderResult.course.title}
</CardTitle>
</CardHeader>
<CardContent>
<OrderableActivityTable
loading={assignments.isLoading}
title={course.data?.title ?? 'Loading...'}
emptyMessage={
assignments.error
? 'Could not load assignments'
: 'No assignments found'
}
title={loaderResult.course.title}
emptyMessage="No assignments found"
rows={rows}
columnNames={{
title: 'Assignment',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,9 @@
import classnames from 'classnames';
import { Route, Switch, useParams, useRoute } from 'wouter-preact';

import AssignmentActivity from './AssignmentActivity';
import CourseActivity from './CourseActivity';
import ComponentWithLoaderWrapper from '../ComponentWithLoaderWrapper';
import DashboardFooter from './DashboardFooter';
import OrganizationActivity from './OrganizationActivity';

export default function DashboardApp() {
const { organizationPublicId } = useParams<{
organizationPublicId: string;
}>();
const [isAssignment] = useRoute('/assignments/:assignmentId');
const [isCourse] = useRoute('/courses/:courseId');
const [isHome] = useRoute('');

console.log({ isAssignment, isCourse, isHome });

return (
<div className="flex flex-col min-h-screen gap-5 bg-grey-2">
<div
Expand All @@ -32,19 +20,7 @@ export default function DashboardApp() {
</div>
<div className="flex-grow px-3">
<div className="mx-auto max-w-6xl">
<Switch>
<Route path="/assignments/:assignmentId">
<AssignmentActivity />
</Route>
<Route path="/courses/:courseId">
<CourseActivity />
</Route>
<Route path="">
<OrganizationActivity
organizationPublicId={organizationPublicId}
/>
</Route>
</Switch>
<ComponentWithLoaderWrapper />
</div>
</div>
<DashboardFooter />
Expand Down
Loading

0 comments on commit 7253c62

Please sign in to comment.