Skip to content

Commit

Permalink
Add breadcrumbs to the dashboard views
Browse files Browse the repository at this point in the history
  • Loading branch information
acelaya committed Jun 4, 2024
1 parent 4c486d5 commit 227a2d3
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 27 deletions.
22 changes: 10 additions & 12 deletions lms/static/scripts/frontend_apps/api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,12 +153,21 @@ export type BaseDashboardStats = {
replies: number;
};

/**
* Response for `/api/dashboard/courses/{course_id}` call.
*/
export type Course = {
id: number;
title: string;
};

/**
* Response for `/api/dashboard/assignments/{assignment_id}` call.
*/
export type Assignment = {
id: number;
title: string;
course: Course;
};

/**
Expand All @@ -170,21 +179,10 @@ export type StudentStats = BaseDashboardStats & {

export type StudentsStats = StudentStats[];

/**
* Response for `/api/dashboard/courses/{course_id}` call.
*/
export type Course = {
id: number;
title: string;
};

/**
* Response for `/api/dashboard/courses/{course_id}/assignments/stats` call.
*/
export type AssignmentStats = {
id: number;
title: string;
course: Course;
export type AssignmentStats = Assignment & {
stats: BaseDashboardStats;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '@hypothesis/frontend-shared';
import { Card, CardContent, CardHeader } from '@hypothesis/frontend-shared';
import classnames from 'classnames';
import { useParams } from 'wouter-preact';

import type { Assignment, StudentsStats } from '../../api-types';
import { useConfig } from '../../config';
import { useAPIFetch } from '../../utils/api';
import { urlPath, useAPIFetch } from '../../utils/api';
import { formatDateTime } from '../../utils/date';
import { replaceURLParams } from '../../utils/url';
import DashboardBreadcrumbs from './DashboardBreadcrumbs';
import OrderableActivityTable from './OrderableActivityTable';

/**
Expand All @@ -31,12 +28,30 @@ export default function AssignmentActivity() {

return (
<Card>
<CardHeader fullWidth>
<CardTitle tagName="h2" data-testid="title">
<CardHeader
fullWidth
classes={classnames(
// Overwriting gap-x-2 and items-center from CardHeader
'flex-col !gap-x-0 !items-start',
)}
>
{assignment.data && (
<div className="mb-3 mt-1 w-full">
<DashboardBreadcrumbs
links={[
{
title: assignment.data.course.title,
href: urlPath`/courses/${String(assignment.data.course.id)}`,
},
]}
/>
</div>
)}
<h2 data-testid="title" className="text-lg text-brand font-semibold">
{assignment.isLoading && 'Loading...'}
{assignment.error && 'Could not load assignment title'}
{assignment.data && title}
</CardTitle>
</h2>
</CardHeader>
<CardContent>
<OrderableActivityTable
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {
ArrowLeftIcon,
CaretRightIcon,
Link,
} from '@hypothesis/frontend-shared';
import classnames from 'classnames';
import { Link as RouterLink } from 'wouter-preact';

export type BreadcrumbLink = {
title: string;
href: string;
};

export type DashboardBreadcrumbsProps = {
links: BreadcrumbLink[];
};

function BreadcrumbLink({ title, href }: BreadcrumbLink) {
return (
<RouterLink href={href} asChild>
<Link underline="hover" variant="text-light" classes="truncate">
<ArrowLeftIcon className="inline-block md:hidden mr-1 align-sub" />
{title}
</Link>
</RouterLink>
);
}

/**
* Navigation breadcrumbs showing a list of links
*/
export default function DashboardBreadcrumbs({
links,
}: DashboardBreadcrumbsProps) {
return (
<div
className="flex flex-row gap-0.5 w-full font-semibold"
data-testid="breadcrumbs-container"
>
{links.map(({ title, href }, index) => {
const isLastLink = index === links.length - 1;
return (
<span
key={`${index}${href}`}
className={classnames('gap-0.5', {
// In mobile devices, show only the last link
'md:flex hidden': !isLastLink,
'flex max-w-full': isLastLink,
// Distribute max width for every link as evenly as possible.
// These must be static values for Tailwind to detect them.
// See https://tailwindcss.com/docs/content-configuration#dynamic-class-names
'md:max-w-[50%]': links.length === 2,
'md:max-w-[33.333333%]': links.length === 3,
'md:max-w-[25%]': links.length === 4,
'md:max-w-[230px]': links.length > 4,
})}
>
<BreadcrumbLink href={href} title={title} />
{!isLastLink && <CaretRightIcon />}
</span>
);
})}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,14 @@ describe('AssignmentActivity', () => {
beforeEach(() => {
fakeUseAPIFetch = sinon.stub().callsFake(url => ({
isLoading: false,
data: url.endsWith('stats') ? students : { title: 'The title' },
data: url.endsWith('stats')
? students
: {
title: 'The title',
course: {
title: 'The course',
},
},
}));
fakeConfig = {
dashboard: {
Expand Down Expand Up @@ -72,7 +79,7 @@ describe('AssignmentActivity', () => {
fakeUseAPIFetch.returns({ isLoading: true });

const wrapper = createComponent();
const titleElement = wrapper.find('CardTitle[data-testid="title"]');
const titleElement = wrapper.find('[data-testid="title"]');
const tableElement = wrapper.find('OrderableActivityTable');

assert.equal(titleElement.text(), 'Loading...');
Expand All @@ -83,7 +90,7 @@ describe('AssignmentActivity', () => {
fakeUseAPIFetch.returns({ error: new Error('Something failed') });

const wrapper = createComponent();
const titleElement = wrapper.find('CardTitle[data-testid="title"]');
const titleElement = wrapper.find('[data-testid="title"]');
const tableElement = wrapper.find('OrderableActivityTable');

assert.equal(titleElement.text(), 'Could not load assignment title');
Expand All @@ -92,9 +99,9 @@ describe('AssignmentActivity', () => {

it('shows expected title', () => {
const wrapper = createComponent();
const titleElement = wrapper.find('CardTitle[data-testid="title"]');
const titleElement = wrapper.find('[data-testid="title"]');
const tableElement = wrapper.find('OrderableActivityTable');
const expectedTitle = `Assignment: The title`;
const expectedTitle = 'Assignment: The title';

assert.equal(titleElement.text(), expectedTitle);
assert.equal(tableElement.prop('title'), expectedTitle);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { checkAccessibility } from '@hypothesis/frontend-testing';
import { mount } from 'enzyme';

import DashboardBreadcrumbs from '../DashboardBreadcrumbs';

describe('DashboardBreadcrumbs', () => {
function createComponent(props = {}) {
return mount(<DashboardBreadcrumbs {...props} />);
}

[['foo', 'bar'], [], ['one', 'two', 'three']].forEach(links => {
it('shows expected amount of links', () => {
const wrapper = createComponent({
links: links.map(title => ({ title, href: `/${title}` })),
});

assert.equal(wrapper.find('BreadcrumbLink').length, links.length);
});
});

it(
'should pass a11y checks',
checkAccessibility({
content: () =>
createComponent({
links: [
{ title: 'Foo', href: '/foo' },
{ title: 'Bar', href: '/bar' },
],
}),
}),
);
});

0 comments on commit 227a2d3

Please sign in to comment.