diff --git a/lms/static/scripts/frontend_apps/api-types.ts b/lms/static/scripts/frontend_apps/api-types.ts index 726503bdeb..d391d7e5aa 100644 --- a/lms/static/scripts/frontend_apps/api-types.ts +++ b/lms/static/scripts/frontend_apps/api-types.ts @@ -230,16 +230,33 @@ export type AutoGradingConfig = { }; /** - * Response for `/api/dashboard/assignments/{assignment_id}` call. + * Represents an assignment group or section, which maps to an h group. */ -export type AssignmentDetails = AssignmentWithCourse & { - /** - * If defined, it indicates this assignment was configured with auto grading - * enabled. - */ - auto_grading_config?: AutoGradingConfig; +export type AssignmentSegment = { + h_authority_provided_id: string; + name: string; }; +/** + * Assignments will have either sections, groups or none of them, but never + * both. + */ +type WithSegments = + | { sections?: AssignmentSegment[] } + | { groups?: AssignmentSegment[] }; + +/** + * Response for `/api/dashboard/assignments/{assignment_id}` call. + */ +export type AssignmentDetails = AssignmentWithCourse & + WithSegments & { + /** + * If defined, it indicates this assignment was configured with auto grading + * enabled. + */ + auto_grading_config?: AutoGradingConfig; + }; + export type AssignmentWithMetrics = AssignmentWithCourse & { annotation_metrics: AnnotationMetrics; }; diff --git a/lms/static/scripts/frontend_apps/components/dashboard/AssignmentActivity.tsx b/lms/static/scripts/frontend_apps/components/dashboard/AssignmentActivity.tsx index 9abaa7ac72..633e0c7e1e 100644 --- a/lms/static/scripts/frontend_apps/components/dashboard/AssignmentActivity.tsx +++ b/lms/static/scripts/frontend_apps/components/dashboard/AssignmentActivity.tsx @@ -12,6 +12,7 @@ import { useDashboardFilters } from '../../utils/dashboard/hooks'; import { courseURL } from '../../utils/dashboard/navigation'; import { useDocumentTitle } from '../../utils/hooks'; import { replaceURLParams } from '../../utils/url'; +import type { DashboardActivityFiltersProps } from './DashboardActivityFilters'; import DashboardActivityFilters from './DashboardActivityFilters'; import DashboardBreadcrumbs from './DashboardBreadcrumbs'; import FormattedDate from './FormattedDate'; @@ -40,7 +41,7 @@ export default function AssignmentActivity() { }>(); const { filters, updateFilters, urlWithFilters } = useDashboardFilters(); - const { studentIds } = filters; + const { studentIds, segmentIds } = filters; const search = useSearch(); const [, navigate] = useLocation(); @@ -48,11 +49,35 @@ export default function AssignmentActivity() { replaceURLParams(routes.assignment, { assignment_id: assignmentId }), ); const autoGradingEnabled = !!assignment.data?.auto_grading_config; + const segments = useMemo((): DashboardActivityFiltersProps['segments'] => { + const { data } = assignment; + if (!data) { + return undefined; + } + + const hasSections = 'sections' in data; + const entries = hasSections + ? data.sections + : 'groups' in data + ? data.groups + : undefined; + if (!entries) { + return undefined; + } + + return { + type: hasSections ? 'sections' : 'groups', + entries, + selectedIds: segmentIds, + onChange: segmentIds => updateFilters({ segmentIds }), + }; + }, [assignment, segmentIds, updateFilters]); const students = useAPIFetch( routes.students_metrics, { h_userid: studentIds, + segment_authority_provided_id: segmentIds, assignment_id: assignmentId, org_public_id: organizationPublicId, }, @@ -160,6 +185,7 @@ export default function AssignmentActivity() { selectedIds: studentIds, onChange: studentIds => updateFilters({ studentIds }), }} + segments={segments} onClearSelection={ studentIds.length > 0 ? () => updateFilters({ studentIds: [] }) diff --git a/lms/static/scripts/frontend_apps/components/dashboard/DashboardActivityFilters.tsx b/lms/static/scripts/frontend_apps/components/dashboard/DashboardActivityFilters.tsx index 858dfff85a..6300443ab5 100644 --- a/lms/static/scripts/frontend_apps/components/dashboard/DashboardActivityFilters.tsx +++ b/lms/static/scripts/frontend_apps/components/dashboard/DashboardActivityFilters.tsx @@ -9,6 +9,7 @@ import { useParams } from 'wouter-preact'; import type { Assignment, + AssignmentSegment, AssignmentsResponse, Course, CoursesResponse, @@ -53,6 +54,19 @@ export type ActivityFilterItem = { export type DashboardActivityFiltersProps = { courses: ActivityFilterSelection | ActivityFilterItem; assignments: ActivityFilterSelection | ActivityFilterItem; + + /** + * The segments filter is relevant only when listing students, so not + * providing this will prevent that filter to be rendered or taken into + * consideration in any way. + * As opposed to other filters, the list of entries is not loaded separately, + * but provided here. + */ + segments?: ActivityFilterSelection & { + type: 'groups' | 'sections'; + entries: AssignmentSegment[]; + }; + students: ActivityFilterSelection; onClearSelection?: () => void; }; @@ -113,6 +127,7 @@ function StudentOption({ export default function DashboardActivityFilters({ courses, assignments, + segments, students, onClearSelection, }: DashboardActivityFiltersProps) { @@ -133,10 +148,15 @@ export default function DashboardActivityFilters({ ? [assignments.selectedIds, null] : [[`${assignments.activeItem.id}`], assignments.activeItem]; }, [assignments]); + const selectedSegmentIds = useMemo( + () => segments?.selectedIds ?? [], + [segments?.selectedIds], + ); const hasSelection = students.selectedIds.length > 0 || selectedAssignmentIds.length > 0 || + selectedSegmentIds.length > 0 || selectedCourseIds.length > 0; const coursesFilters = useMemo( @@ -180,10 +200,16 @@ export default function DashboardActivityFilters({ const studentsFilters = useMemo( () => ({ assignment_id: selectedAssignmentIds, + segment_authority_provided_id: selectedSegmentIds, course_id: selectedCourseIds, org_public_id: organizationPublicId, }), - [organizationPublicId, selectedAssignmentIds, selectedCourseIds], + [ + organizationPublicId, + selectedAssignmentIds, + selectedCourseIds, + selectedSegmentIds, + ], ); const studentsResult = usePaginatedAPIFetch< 'students', @@ -262,6 +288,39 @@ export default function DashboardActivityFilters({ /> )} /> + {segments && ( + segments.onChange(newSegmentIds)} + buttonContent={ + segments.selectedIds.length === 0 ? ( + <>All {segments.type} + ) : students.selectedIds.length === 1 ? ( + segments.entries.find( + s => s.h_authority_provided_id === students.selectedIds[0], + )?.name ?? `1 ${segments.type}` + ) : ( + <> + {segments.selectedIds.length} {segments.type} + + ) + } + > + + All {segments.type} + + {segments.entries.map(entry => ( + + {entry.name} + + ))} + + )} string; }; +function asArray(value: string | string[] = []): string[] { + return Array.isArray(value) ? value : [value]; +} + /** * Hook used to read and update the state of the dashboard filters. * @@ -41,20 +51,23 @@ export function useDashboardFilters(): UseDashboardFilters { const search = useSearch(); const queryParams = useMemo(() => queryStringToRecord(search), [search]); const filters = useMemo(() => { - const { student_id = [], course_id = [], assignment_id = [] } = queryParams; - const courseIds = Array.isArray(course_id) ? course_id : [course_id]; - const assignmentIds = Array.isArray(assignment_id) - ? assignment_id - : [assignment_id]; - const studentIds = Array.isArray(student_id) ? student_id : [student_id]; + const courseIds = asArray(queryParams.course_id); + const assignmentIds = asArray(queryParams.assignment_id); + const studentIds = asArray(queryParams.student_id); + const segmentIds = asArray(queryParams.segment_id); - return { courseIds, assignmentIds, studentIds }; + return { courseIds, assignmentIds, studentIds, segmentIds }; }, [queryParams]); const [location, navigate] = useLocation(); const urlWithFilters = useCallback( ( - { courseIds, assignmentIds, studentIds }: Partial, + { + courseIds, + assignmentIds, + studentIds, + segmentIds, + }: Partial, { path = location, extend = false }: URLWithFiltersOptions = {}, ): string => { const newQueryParams = extend ? { ...queryParams } : {}; @@ -67,6 +80,9 @@ export function useDashboardFilters(): UseDashboardFilters { if (studentIds) { newQueryParams.student_id = studentIds; } + if (segmentIds) { + newQueryParams.segment_id = segmentIds; + } // The router's base URL is represented in `location` as '/', even if // that URL does not actually end with `/` (eg. `/dashboard`).