+
{stats.title}
);
diff --git a/lms/static/scripts/frontend_apps/components/dashboard/DashboardApp.tsx b/lms/static/scripts/frontend_apps/components/dashboard/DashboardApp.tsx
index cb71922975..213835faa3 100644
--- a/lms/static/scripts/frontend_apps/components/dashboard/DashboardApp.tsx
+++ b/lms/static/scripts/frontend_apps/components/dashboard/DashboardApp.tsx
@@ -1,11 +1,16 @@
import classnames from 'classnames';
-import { Route, Switch } from 'wouter-preact';
+import { Route, Switch, useParams } from 'wouter-preact';
import AssignmentActivity from './AssignmentActivity';
import CourseActivity from './CourseActivity';
import DashboardFooter from './DashboardFooter';
+import OrganizationActivity from './OrganizationActivity';
export default function DashboardApp() {
+ const { organizationPublicId } = useParams<{
+ organizationPublicId: string;
+ }>();
+
return (
diff --git a/lms/static/scripts/frontend_apps/components/dashboard/DashboardBreadcrumbs.tsx b/lms/static/scripts/frontend_apps/components/dashboard/DashboardBreadcrumbs.tsx
index 68586b6caa..e56ba27a56 100644
--- a/lms/static/scripts/frontend_apps/components/dashboard/DashboardBreadcrumbs.tsx
+++ b/lms/static/scripts/frontend_apps/components/dashboard/DashboardBreadcrumbs.tsx
@@ -4,6 +4,7 @@ import {
Link,
} from '@hypothesis/frontend-shared';
import classnames from 'classnames';
+import { useMemo } from 'preact/hooks';
import { Link as RouterLink } from 'wouter-preact';
export type BreadcrumbLink = {
@@ -12,7 +13,7 @@ export type BreadcrumbLink = {
};
export type DashboardBreadcrumbsProps = {
- links: BreadcrumbLink[];
+ links?: BreadcrumbLink[];
};
function BreadcrumbLink({ title, href }: BreadcrumbLink) {
@@ -30,15 +31,20 @@ function BreadcrumbLink({ title, href }: BreadcrumbLink) {
* Navigation breadcrumbs showing a list of links
*/
export default function DashboardBreadcrumbs({
- links,
+ links = [],
}: DashboardBreadcrumbsProps) {
+ const linksWithHome = useMemo(
+ (): BreadcrumbLink[] => [{ title: 'Home', href: '' }, ...links],
+ [links],
+ );
+
return (
- {links.map(({ title, href }, index) => {
- const isLastLink = index === links.length - 1;
+ {linksWithHome.map(({ title, href }, index) => {
+ const isLastLink = index === linksWithHome.length - 1;
return (
4,
+ 'md:max-w-[50%]': linksWithHome.length === 2,
+ 'md:max-w-[33%]': linksWithHome.length === 3,
+ 'md:max-w-[25%]': linksWithHome.length === 4,
+ 'md:max-w-[230px]': linksWithHome.length > 4,
})}
>
diff --git a/lms/static/scripts/frontend_apps/components/dashboard/OrderableActivityTable.tsx b/lms/static/scripts/frontend_apps/components/dashboard/OrderableActivityTable.tsx
index 28c4d406f9..955180064e 100644
--- a/lms/static/scripts/frontend_apps/components/dashboard/OrderableActivityTable.tsx
+++ b/lms/static/scripts/frontend_apps/components/dashboard/OrderableActivityTable.tsx
@@ -4,9 +4,7 @@ import { useOrderedRows } from '@hypothesis/frontend-shared';
import type { OrderDirection } from '@hypothesis/frontend-shared/lib/types';
import { useMemo, useState } from 'preact/hooks';
-import type { BaseDashboardStats } from '../../api-types';
-
-export type OrderableActivityTableProps = Pick<
+export type OrderableActivityTableProps = Pick<
DataTableProps,
'emptyMessage' | 'rows' | 'renderItem' | 'loading' | 'title'
> & {
@@ -32,7 +30,7 @@ const descendingOrderColumns: readonly string[] = [
* Annotation activity table for dashboard views. Includes built-in support for
* sorting columns.
*/
-export default function OrderableActivityTable({
+export default function OrderableActivityTable({
defaultOrderField,
rows,
columnNames,
diff --git a/lms/static/scripts/frontend_apps/components/dashboard/OrganizationActivity.tsx b/lms/static/scripts/frontend_apps/components/dashboard/OrganizationActivity.tsx
new file mode 100644
index 0000000000..7bd3a8acb5
--- /dev/null
+++ b/lms/static/scripts/frontend_apps/components/dashboard/OrganizationActivity.tsx
@@ -0,0 +1,55 @@
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ Link,
+} from '@hypothesis/frontend-shared';
+import { Link as RouterLink } from 'wouter-preact';
+
+import type { Courses } from '../../api-types';
+import { useConfig } from '../../config';
+import { urlPath, useAPIFetch } from '../../utils/api';
+import { replaceURLParams } from '../../utils/url';
+import OrderableActivityTable from './OrderableActivityTable';
+
+export type OrganizationActivityProps = {
+ organizationPublicId: string;
+};
+
+/**
+ * List of courses that belong to a specific organization
+ */
+export default function OrganizationActivity({
+ organizationPublicId,
+}: OrganizationActivityProps) {
+ const { dashboard } = useConfig(['dashboard']);
+ const { routes } = dashboard;
+ const courses = useAPIFetch(
+ replaceURLParams(routes.organization_courses, {
+ organization_public_id: organizationPublicId,
+ }),
+ );
+
+ return (
+
+
+
+ (
+
+ {stats.title}
+
+ )}
+ />
+
+
+ );
+}
diff --git a/lms/static/scripts/frontend_apps/components/dashboard/test/AssignmentActivity-test.js b/lms/static/scripts/frontend_apps/components/dashboard/test/AssignmentActivity-test.js
index a93a019ccf..173430efd5 100644
--- a/lms/static/scripts/frontend_apps/components/dashboard/test/AssignmentActivity-test.js
+++ b/lms/static/scripts/frontend_apps/components/dashboard/test/AssignmentActivity-test.js
@@ -79,7 +79,7 @@ describe('AssignmentActivity', () => {
fakeUseAPIFetch.returns({ isLoading: true });
const wrapper = createComponent();
- const titleElement = wrapper.find('[data-testid="title"]');
+ const titleElement = wrapper.find('CardTitle[data-testid="title"]');
const tableElement = wrapper.find('OrderableActivityTable');
assert.equal(titleElement.text(), 'Loading...');
@@ -90,7 +90,7 @@ describe('AssignmentActivity', () => {
fakeUseAPIFetch.returns({ error: new Error('Something failed') });
const wrapper = createComponent();
- const titleElement = wrapper.find('[data-testid="title"]');
+ const titleElement = wrapper.find('CardTitle[data-testid="title"]');
const tableElement = wrapper.find('OrderableActivityTable');
assert.equal(titleElement.text(), 'Could not load assignment title');
@@ -99,7 +99,7 @@ describe('AssignmentActivity', () => {
it('shows expected title', () => {
const wrapper = createComponent();
- const titleElement = wrapper.find('[data-testid="title"]');
+ const titleElement = wrapper.find('CardTitle[data-testid="title"]');
const tableElement = wrapper.find('OrderableActivityTable');
const expectedTitle = 'Assignment: The title';
diff --git a/lms/static/scripts/frontend_apps/components/dashboard/test/DashboardBreadcrumbs-test.js b/lms/static/scripts/frontend_apps/components/dashboard/test/DashboardBreadcrumbs-test.js
index 8206af2e7d..fd7c35013e 100644
--- a/lms/static/scripts/frontend_apps/components/dashboard/test/DashboardBreadcrumbs-test.js
+++ b/lms/static/scripts/frontend_apps/components/dashboard/test/DashboardBreadcrumbs-test.js
@@ -14,7 +14,8 @@ describe('DashboardBreadcrumbs', () => {
links: links.map(title => ({ title, href: `/${title}` })),
});
- assert.equal(wrapper.find('BreadcrumbLink').length, links.length);
+ // Breadcrumbs always renders a static extra link for the home page
+ assert.equal(wrapper.find('BreadcrumbLink').length, links.length + 1);
});
});
diff --git a/lms/static/scripts/frontend_apps/components/dashboard/test/OrganizationActivity-test.js b/lms/static/scripts/frontend_apps/components/dashboard/test/OrganizationActivity-test.js
new file mode 100644
index 0000000000..7980f2e86f
--- /dev/null
+++ b/lms/static/scripts/frontend_apps/components/dashboard/test/OrganizationActivity-test.js
@@ -0,0 +1,102 @@
+import {
+ checkAccessibility,
+ mockImportedComponents,
+} from '@hypothesis/frontend-testing';
+import { mount } from 'enzyme';
+import sinon from 'sinon';
+
+import { Config } from '../../../config';
+import OrganizationActivity, { $imports } from '../OrganizationActivity';
+
+describe('OrganizationActivity', () => {
+ const courses = [
+ {
+ id: 1,
+ title: 'Course A',
+ },
+ {
+ id: 2,
+ title: 'Course B',
+ },
+ ];
+
+ let fakeUseAPIFetch;
+ let fakeConfig;
+
+ beforeEach(() => {
+ fakeUseAPIFetch = sinon.stub().resolves(courses);
+ fakeConfig = {
+ dashboard: {
+ routes: {
+ organization_courses:
+ '/api/dashboard/organizations/:organization_public_id',
+ },
+ },
+ };
+
+ $imports.$mock(mockImportedComponents());
+ $imports.$mock({
+ '../../utils/api': {
+ useAPIFetch: fakeUseAPIFetch,
+ },
+ });
+ });
+
+ afterEach(() => {
+ $imports.$restore();
+ });
+
+ function createComponent() {
+ return mount(
+
+
+ ,
+ );
+ }
+
+ it('sets loading state in table while data is loading', () => {
+ fakeUseAPIFetch.returns({ isLoading: true });
+
+ const wrapper = createComponent();
+ const tableElement = wrapper.find('OrderableActivityTable');
+
+ assert.isTrue(tableElement.prop('loading'));
+ });
+
+ it('shows error if loading data fails', () => {
+ fakeUseAPIFetch.returns({ error: new Error('Something failed') });
+
+ const wrapper = createComponent();
+ const tableElement = wrapper.find('OrderableActivityTable');
+
+ assert.equal(tableElement.prop('emptyMessage'), 'Could not load courses');
+ });
+
+ it('shows empty courses message', () => {
+ const wrapper = createComponent();
+ const tableElement = wrapper.find('OrderableActivityTable');
+
+ assert.equal(tableElement.prop('emptyMessage'), 'No courses found');
+ });
+
+ courses.forEach(course => {
+ it('renders course links', () => {
+ const wrapper = createComponent();
+ const item = wrapper
+ .find('OrderableActivityTable')
+ .props()
+ .renderItem(course);
+ const itemWrapper = mount(item);
+
+ assert.equal(itemWrapper.text(), course.title);
+ assert.equal(itemWrapper.prop('href'), `/courses/${course.id}`);
+ });
+ });
+
+ it(
+ 'should pass a11y checks',
+ checkAccessibility({
+ content: () => createComponent(),
+ }),
+ );
+});
diff --git a/lms/static/scripts/frontend_apps/config.ts b/lms/static/scripts/frontend_apps/config.ts
index aef99b7284..0f90fb9b92 100644
--- a/lms/static/scripts/frontend_apps/config.ts
+++ b/lms/static/scripts/frontend_apps/config.ts
@@ -245,6 +245,7 @@ export type DashboardRoutes = {
assignment_stats: string;
course: string;
course_assignment_stats: string;
+ organization_courses: string;
};
export type DashboardConfig = {