Skip to content

Commit

Permalink
Add segments filter in assignment view
Browse files Browse the repository at this point in the history
  • Loading branch information
acelaya committed Sep 18, 2024
1 parent 95c279a commit edeb004
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 17 deletions.
31 changes: 24 additions & 7 deletions lms/static/scripts/frontend_apps/api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -40,19 +41,43 @@ export default function AssignmentActivity() {
}>();

const { filters, updateFilters, urlWithFilters } = useDashboardFilters();
const { studentIds } = filters;
const { studentIds, segmentIds } = filters;
const search = useSearch();
const [, navigate] = useLocation();

const assignment = useAPIFetch<AssignmentDetails>(
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<StudentsMetricsResponse>(
routes.students_metrics,
{
h_userid: studentIds,
segment_authority_provided_id: segmentIds,
assignment_id: assignmentId,
org_public_id: organizationPublicId,
},
Expand Down Expand Up @@ -160,6 +185,7 @@ export default function AssignmentActivity() {
selectedIds: studentIds,
onChange: studentIds => updateFilters({ studentIds }),
}}
segments={segments}
onClearSelection={
studentIds.length > 0
? () => updateFilters({ studentIds: [] })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useParams } from 'wouter-preact';

import type {
Assignment,
AssignmentSegment,
AssignmentsResponse,
Course,
CoursesResponse,
Expand Down Expand Up @@ -53,6 +54,19 @@ export type ActivityFilterItem<T extends Course | Assignment> = {
export type DashboardActivityFiltersProps = {
courses: ActivityFilterSelection | ActivityFilterItem<Course>;
assignments: ActivityFilterSelection | ActivityFilterItem<Assignment>;

/**
* 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;
};
Expand Down Expand Up @@ -113,6 +127,7 @@ function StudentOption({
export default function DashboardActivityFilters({
courses,
assignments,
segments,
students,
onClearSelection,
}: DashboardActivityFiltersProps) {
Expand All @@ -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(
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -262,6 +288,39 @@ export default function DashboardActivityFilters({
/>
)}
/>
{segments && (
<MultiSelect
aria-label={`Select ${segments.type}`}
containerClasses="!w-auto min-w-[180px]"
value={segments.selectedIds}
onChange={newSegmentIds => 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}
</>
)
}
>
<MultiSelect.Option value={undefined}>
All {segments.type}
</MultiSelect.Option>
{segments.entries.map(entry => (
<MultiSelect.Option
key={entry.h_authority_provided_id}
value={entry.h_authority_provided_id}
>
{entry.name}
</MultiSelect.Option>
))}
</MultiSelect>
)}
<PaginatedMultiSelect
entity="students"
data-testid="students-select"
Expand Down
32 changes: 24 additions & 8 deletions lms/static/scripts/frontend_apps/utils/dashboard/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ export type DashboardFilters = {
studentIds: string[];
assignmentIds: string[];
courseIds: string[];

/**
* The list of segments (groups or sections) to filter students by.
* This filter is relevant only when listing students in the assignment view.
*/
segmentIds: string[];
};

export type URLWithFiltersOptions = {
Expand All @@ -32,6 +38,10 @@ export type UseDashboardFilters = {
) => 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.
*
Expand All @@ -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<DashboardFilters>,
{
courseIds,
assignmentIds,
studentIds,
segmentIds,
}: Partial<DashboardFilters>,
{ path = location, extend = false }: URLWithFiltersOptions = {},
): string => {
const newQueryParams = extend ? { ...queryParams } : {};
Expand All @@ -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`).
Expand Down

0 comments on commit edeb004

Please sign in to comment.