diff --git a/lms/static/scripts/frontend_apps/api-types.ts b/lms/static/scripts/frontend_apps/api-types.ts index 726503bdeb..a181c2724a 100644 --- a/lms/static/scripts/frontend_apps/api-types.ts +++ b/lms/static/scripts/frontend_apps/api-types.ts @@ -278,3 +278,10 @@ export type StudentsResponse = { students: Student[]; pagination: Pagination; }; + +/** + * Response for `/api/dashboard/assignments/{assignment_id}/grading/sync` + */ +export type GradingSync = { + status: 'scheduled' | 'in_progress' | 'finished' | 'failed'; +}; diff --git a/lms/static/scripts/frontend_apps/components/dashboard/AssignmentActivity.tsx b/lms/static/scripts/frontend_apps/components/dashboard/AssignmentActivity.tsx index 9abaa7ac72..60005debb6 100644 --- a/lms/static/scripts/frontend_apps/components/dashboard/AssignmentActivity.tsx +++ b/lms/static/scripts/frontend_apps/components/dashboard/AssignmentActivity.tsx @@ -18,6 +18,7 @@ import FormattedDate from './FormattedDate'; import GradeIndicator from './GradeIndicator'; import type { OrderableActivityTableColumn } from './OrderableActivityTable'; import OrderableActivityTable from './OrderableActivityTable'; +import SyncGradesButton from './SyncGradesButton'; type StudentsTableRow = { lms_id: string; @@ -33,7 +34,7 @@ type StudentsTableRow = { */ export default function AssignmentActivity() { const { dashboard } = useConfig(['dashboard']); - const { routes } = dashboard; + const { routes, user } = dashboard; const { assignmentId, organizationPublicId } = useParams<{ assignmentId: string; organizationPublicId?: string; @@ -58,13 +59,32 @@ export default function AssignmentActivity() { }, ); + const studentsToSync = useMemo(() => { + if (!autoGradingEnabled) { + return []; + } + + return (students.data?.students ?? []).map( + ({ h_userid, auto_grading_grade = 0 }) => ({ + h_userid, + grade: auto_grading_grade, + }), + ); + }, [autoGradingEnabled, students.data?.students]); const rows: StudentsTableRow[] = useMemo( () => (students.data?.students ?? []).map( - ({ lms_id, display_name, auto_grading_grade, annotation_metrics }) => ({ + ({ + lms_id, + display_name, + auto_grading_grade, + h_userid, + annotation_metrics, + }) => ({ lms_id, display_name, auto_grading_grade, + h_userid, ...annotation_metrics, }), ), @@ -133,40 +153,45 @@ export default function AssignmentActivity() { {assignment.data && title} - {assignment.data && ( - - navigate( - urlWithFilters( - { studentIds, assignmentIds: [assignmentId] }, - { path: '' }, +
+ {assignment.data && ( + + navigate( + urlWithFilters( + { studentIds, assignmentIds: [assignmentId] }, + { path: '' }, + ), ), - ), - }} - assignments={{ - activeItem: assignment.data, - // When active assignment is cleared, navigate to its course page, - // but keep other query params intact - onClear: () => { - const query = search.length === 0 ? '' : `?${search}`; - navigate(`${courseURL(assignment.data!.course.id)}${query}`); - }, - }} - students={{ - selectedIds: studentIds, - onChange: studentIds => updateFilters({ studentIds }), - }} - onClearSelection={ - studentIds.length > 0 - ? () => updateFilters({ studentIds: [] }) - : undefined - } - /> - )} + }} + assignments={{ + activeItem: assignment.data, + // When active assignment is cleared, navigate to its course page, + // but keep other query params intact + onClear: () => { + const query = search.length === 0 ? '' : `?${search}`; + navigate(`${courseURL(assignment.data!.course.id)}${query}`); + }, + }} + students={{ + selectedIds: studentIds, + onChange: studentIds => updateFilters({ studentIds }), + }} + onClearSelection={ + studentIds.length > 0 + ? () => updateFilters({ studentIds: [] }) + : undefined + } + /> + )} + {autoGradingEnabled && !user.is_staff && ( + + )} +
; +}; + +export default function SyncGradesButton({ + studentsToSync, +}: SyncGradesButtonProps) { + const { assignmentId } = useParams<{ assignmentId: string }>(); + const { dashboard } = useConfig(['dashboard']); + const { routes } = dashboard; + + const syncStatus = useAPIFetch( + replaceURLParams(routes.assignment_grades_sync, { + assignment_id: assignmentId, + }), + ); + const buttonContent = useMemo(() => { + if (syncStatus.isLoading || !syncStatus.data) { + return 'Loading...'; + } + + const { status } = syncStatus.data; + if (['scheduled', 'in_progress'].includes(status)) { + return 'Syncing grades'; + } + + if (status === 'failed') { + return 'Error syncing'; + } + + if (studentsToSync.length > 0) { + return `Sync ${studentsToSync.length} students`; + } + + return 'Grades synced'; + }, [studentsToSync.length, syncStatus.data, syncStatus.isLoading]); + const buttonDisabled = useMemo(() => { + // TODO This needs more checks, and there are more reasons for the button + // to be disabled + return syncStatus.isLoading || studentsToSync.length === 0; + }, [studentsToSync.length, syncStatus.isLoading]); + + const syncGrades = useCallback(() => { + console.log(studentsToSync); + }, [studentsToSync]); + + return ( + + ); +} 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 9685f853b0..c080bbe645 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 @@ -71,6 +71,9 @@ describe('AssignmentActivity', () => { assignment: '/api/assignments/:assignment_id', students_metrics: '/api/students/metrics', }, + user: { + is_staff: true, + }, }, }; @@ -390,6 +393,25 @@ describe('AssignmentActivity', () => { }); }, ); + + [ + { isStaff: true, autoGradingEnabled: true, shouldShowButton: false }, + { isStaff: false, autoGradingEnabled: true, shouldShowButton: true }, + { isStaff: true, autoGradingEnabled: false, shouldShowButton: false }, + { isStaff: false, autoGradingEnabled: false, shouldShowButton: false }, + ].forEach(({ autoGradingEnabled, isStaff, shouldShowButton }) => { + it('shows sync button for non-staff users only', () => { + setUpFakeUseAPIFetch({ + ...activeAssignment, + auto_grading_config: autoGradingEnabled ? {} : null, + }); + fakeConfig.dashboard.user.is_staff = isStaff; + + const wrapper = createComponent(); + + assert.equal(wrapper.exists('SyncGradesButton'), shouldShowButton); + }); + }); }); it( diff --git a/lms/static/scripts/frontend_apps/config.ts b/lms/static/scripts/frontend_apps/config.ts index b47843e8aa..fcdb53862d 100644 --- a/lms/static/scripts/frontend_apps/config.ts +++ b/lms/static/scripts/frontend_apps/config.ts @@ -269,6 +269,9 @@ export type DashboardRoutes = { assignments: string; /** Fetch list of students */ students: string; + + /** Sync grades (POST) or check sync status (GET) */ + assignment_grades_sync: string; }; export type DashboardUser = {