diff --git a/src/commons/assessment/Assessment.tsx b/src/commons/assessment/Assessment.tsx index 5efbaaf5b9..158ac7cacf 100644 --- a/src/commons/assessment/Assessment.tsx +++ b/src/commons/assessment/Assessment.tsx @@ -1,15 +1,9 @@ import { Button, - Card, Collapse, Dialog, DialogBody, DialogFooter, - Elevation, - H4, - H6, - Icon, - IconName, Intent, NonIdealState, Position, @@ -18,16 +12,13 @@ import { Tooltip } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; -import classNames from 'classnames'; import { sortBy } from 'lodash'; import React, { useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; -import { Navigate, NavLink, useLoaderData, useParams } from 'react-router'; +import { Navigate, useLoaderData, useParams } from 'react-router'; import { numberRegExp } from 'src/features/academy/AcademyTypes'; import Messages, { sendToWebview } from 'src/features/vscode/messages'; -import classes from 'src/styles/Academy.module.scss'; -import defaultCoverImage from '../../assets/default_cover_image.jpg'; import SessionActions from '../application/actions/SessionActions'; import { Role } from '../application/ApplicationTypes'; import AssessmentWorkspace, { @@ -35,14 +26,12 @@ import AssessmentWorkspace, { } from '../assessmentWorkspace/AssessmentWorkspace'; import ContentDisplay from '../ContentDisplay'; import ControlButton from '../ControlButton'; -import Markdown from '../Markdown'; -import NotificationBadge from '../notificationBadge/NotificationBadge'; -import { filterNotificationsByAssessment } from '../notificationBadge/NotificationBadgeHelper'; import Constants from '../utils/Constants'; -import { beforeNow, getPrettyDate, getPrettyDateAfterHours } from '../utils/DateHelper'; -import { useResponsive, useSession, useTypedSelector } from '../utils/Hooks'; -import { assessmentTypeLink, convertParamToInt } from '../utils/ParamParseHelper'; +import { beforeNow } from '../utils/DateHelper'; +import { useSession, useTypedSelector } from '../utils/Hooks'; +import { convertParamToInt } from '../utils/ParamParseHelper'; import AssessmentNotFound from './AssessmentNotFound'; +import AssessmentOverviewCard from './AssessmentOverviewCard'; import { AssessmentConfiguration, AssessmentOverview, @@ -52,7 +41,6 @@ import { const Assessment: React.FC = () => { const params = useParams(); - const { isMobileBreakpoint } = useResponsive(); const [betchaAssessment, setBetchaAssessment] = useState(null); const [showClosedAssessments, setShowClosedAssessments] = useState(false); const [showOpenedAssessments, setShowOpenedAssessments] = useState(true); @@ -91,7 +79,7 @@ const Assessment: React.FC = () => { const sortAssessments = (assessments: AssessmentOverview[]) => sortBy(assessments, [a => -a.id]); - const makeSubmissionButton = (overview: AssessmentOverview, index: number) => ( + const makeSubmissionButton = (overview: AssessmentOverview) => ( { disabled={overview.status !== AssessmentStatuses.attempted} icon={IconNames.CONFIRM} intent={overview.status === AssessmentStatuses.attempted ? Intent.DANGER : Intent.NONE} - minimal={true} + variant="minimal" // intentional: each listing renders its own version of onClick // tslint:disable-next-line:jsx-no-lambda onClick={() => setBetchaAssessment(overview)} @@ -112,166 +100,6 @@ const Assessment: React.FC = () => { ); - const makeAssessmentInteractButton = (overview: AssessmentOverview) => { - let icon: IconName; - let label: string; - let optionalLabel: string = ''; - - switch (overview.status) { - case AssessmentStatuses.not_attempted: - icon = IconNames.PLAY; - label = 'Attempt'; - break; - case AssessmentStatuses.attempting: - icon = IconNames.PLAY; - label = 'Continue'; - optionalLabel = ' Attempt'; - break; - case AssessmentStatuses.attempted: - icon = IconNames.EDIT; - label = 'Review'; - optionalLabel = ' Attempt'; - break; - case AssessmentStatuses.submitted: - icon = IconNames.EYE_OPEN; - label = 'Review'; - optionalLabel = ' Submission'; - break; - default: - // If we reach this case, backend data did not fit IAssessmentOverview - icon = IconNames.PLAY; - label = 'Review'; - break; - } - return ( - - - - ); - }; - - /** - * Create a series of cards to display IAssessmentOverviews. - * @param {AssessmentOverview} overview the assessment overview to display - * @param {number} index a unique number for this card (required for sequential rendering). - * See {@link https://reactjs.org/docs/lists-and-keys.html#keys} - * @param renderAttemptButton will only render the attempt button if true, regardless - * of attempt status. - * @param notifications the notifications to be passed in. - */ - const makeOverviewCard = ( - overview: AssessmentOverview, - index: number, - renderAttemptButton: boolean, - renderGradingTooltip: boolean - ) => { - return ( -
- -
- - Assessment -
-
- {makeOverviewCardTitle(overview, index, renderGradingTooltip)} -
-
- {overview.isGradingPublished - ? `XP: ${overview.xp} / ${overview.maxXp}` - : `Max XP: ${overview.maxXp}`} -
- {overview.earlySubmissionXp > 0 && ( - - - - )} -
-
- -
- {overview.maxTeamSize > 1 ? ( -
-
This is a team assessment.
-
- ) : ( -
-
This is an individual assessment.
-
- )} -
-
- - - {`${beforeNow(overview.openAt) ? 'Opened' : 'Opens'}: ${getPrettyDate( - overview.openAt - )}`} - - {beforeNow(overview.openAt) && ( - - - {`Due: ${getPrettyDate(overview.closeAt)}`} - - )} -
-
- {renderAttemptButton ? makeAssessmentInteractButton(overview) : null} -
-
-
-
-
- ); - }; - - const makeOverviewCardTitle = ( - overview: AssessmentOverview, - index: number, - renderProgressStatus: boolean - ) => ( -
- -

- {overview.title} - {overview.private ? ( - - - - ) : null} - {renderProgressStatus ? showGradingTooltip(overview.isGradingPublished) : null} -

-
-
{makeSubmissionButton(overview, index)}
-
- ); - // Rendering Logic const assessmentConfigToLoad = useLoaderData() as AssessmentConfiguration; const assessmentOverviews = useMemo( @@ -326,7 +154,15 @@ const Assessment: React.FC = () => { const isOverviewUpcoming = (overview: AssessmentOverview) => !beforeNow(overview.closeAt) && !beforeNow(overview.openAt); const upcomingCards = sortAssessments(assessmentOverviews.filter(isOverviewUpcoming)).map( - (overview, index) => makeOverviewCard(overview, index, role !== Role.Student, false) + overview => ( + + ) ); /** Opened assessments, that are released and can be attempted. */ @@ -336,14 +172,30 @@ const Assessment: React.FC = () => { overview.status !== AssessmentStatuses.submitted; const openedCards = sortAssessments( assessmentOverviews.filter(overview => isOverviewOpened(overview)) - ).map((overview, index) => makeOverviewCard(overview, index, true, false)); + ).map(overview => ( + + )); /** Closed assessments, that are past the due date or cannot be attempted further. */ const closedCards = sortAssessments( assessmentOverviews.filter( overview => !isOverviewOpened(overview) && !isOverviewUpcoming(overview) ) - ).map((overview, index) => makeOverviewCard(overview, index, true, true)); + ).map(overview => ( + + )); /** Render cards */ const upcomingCardsCollapsible = ( @@ -448,29 +300,6 @@ const Assessment: React.FC = () => { ); }; -const showGradingTooltip = (isGradingPublished: boolean) => { - let iconName: IconName; - let intent: Intent; - let tooltip: string; - - if (isGradingPublished) { - iconName = IconNames.TICK; - intent = Intent.SUCCESS; - tooltip = 'Fully graded'; - } else { - // shh, hide actual grading progress from users even if graded - iconName = IconNames.TIME; - intent = Intent.WARNING; - tooltip = 'Grading in progress'; - } - - return ( - - - - ); -}; - const collapseButton = (label: string, isOpen: boolean, toggleFunc: () => void) => ( = ({ overview }) => { + const courseId = useTypedSelector(state => state.session.courseId); + const dispatch = useDispatch(); + const { icon, label, optionalLabel } = createButtonConfiguration(overview.status); + + return ( + + + + ); +}; + +type ButtonConfiguration = { + icon: IconName; + label: string; + optionalLabel?: string; +}; + +const createButtonConfiguration = ( + overviewStatus: AssessmentOverview['status'] +): ButtonConfiguration => { + let icon: IconName; + let label: string; + let optionalLabel: string | undefined; + + switch (overviewStatus) { + case AssessmentStatuses.not_attempted: + icon = IconNames.PLAY; + label = 'Attempt'; + break; + case AssessmentStatuses.attempting: + icon = IconNames.PLAY; + label = 'Continue'; + optionalLabel = ' Attempt'; + break; + case AssessmentStatuses.attempted: + icon = IconNames.EDIT; + label = 'Review'; + optionalLabel = ' Attempt'; + break; + case AssessmentStatuses.submitted: + icon = IconNames.EYE_OPEN; + label = 'Review'; + optionalLabel = ' Submission'; + break; + default: + // If we reach this case, backend data did not fit IAssessmentOverview + icon = IconNames.PLAY; + label = 'Review'; + break; + } + + return { icon, label, optionalLabel }; +}; + +export default AssessmentInteractButton; diff --git a/src/commons/assessment/AssessmentOverviewCard.tsx b/src/commons/assessment/AssessmentOverviewCard.tsx new file mode 100644 index 0000000000..c370df8524 --- /dev/null +++ b/src/commons/assessment/AssessmentOverviewCard.tsx @@ -0,0 +1,157 @@ +import { Card, Elevation, H4, H6, Icon, Intent, Position, Text, Tooltip } from '@blueprintjs/core'; +import { IconName, IconNames } from '@blueprintjs/icons'; +import classNames from 'classnames'; +import classes from 'src/styles/Academy.module.scss'; + +import defaultCoverImage from '../../assets/default_cover_image.jpg'; +import Markdown from '../Markdown'; +import NotificationBadge from '../notificationBadge/NotificationBadge'; +import { filterNotificationsByAssessment } from '../notificationBadge/NotificationBadgeHelper'; +import { beforeNow, getPrettyDate, getPrettyDateAfterHours } from '../utils/DateHelper'; +import { useResponsive } from '../utils/Hooks'; +import AssessmentInteractButton from './AssessmentInteractButton'; +import { AssessmentOverview } from './AssessmentTypes'; + +type AssessmentOverviewCardProps = { + /** The assessment overview to display */ + overview: AssessmentOverview; + /** Will only render the attempt button if true, regardless of attempt status. */ + renderAttemptButton: boolean; + renderGradingTooltip: boolean; + makeSubmissionButton: (overview: AssessmentOverview) => JSX.Element; +}; + +/** A card to display `AssessmentOverview`s. */ +const AssessmentOverviewCard: React.FC = ({ + overview, + renderAttemptButton, + renderGradingTooltip, + makeSubmissionButton +}) => { + const { isMobileBreakpoint } = useResponsive(); + return ( +
+ +
+ + Assessment +
+
+ +
+
+ {overview.isGradingPublished + ? `XP: ${overview.xp} / ${overview.maxXp}` + : `Max XP: ${overview.maxXp}`} +
+ {overview.earlySubmissionXp > 0 && ( + + + + )} +
+
+ +
+ {overview.maxTeamSize > 1 ? ( +
+
This is a team assessment.
+
+ ) : ( +
+
This is an individual assessment.
+
+ )} +
+
+ + + {`${beforeNow(overview.openAt) ? 'Opened' : 'Opens'}: ${getPrettyDate( + overview.openAt + )}`} + + {beforeNow(overview.openAt) && ( + + + {`Due: ${getPrettyDate(overview.closeAt)}`} + + )} +
+
+ {renderAttemptButton ? : null} +
+
+
+
+
+ ); +}; + +type AssessmentOverviewCardTitleProps = { + overview: AssessmentOverview; + renderProgressStatus: boolean; + makeSubmissionButton: (overview: AssessmentOverview) => JSX.Element; +}; + +const AssessmentOverviewCardTitle: React.FC = ({ + overview, + renderProgressStatus, + makeSubmissionButton +}) => ( +
+ +

+ {overview.title} + {overview.private ? ( + + + + ) : null} + {renderProgressStatus ? showGradingTooltip(overview.isGradingPublished) : null} +

+
+
{makeSubmissionButton(overview)}
+
+); + +const showGradingTooltip = (isGradingPublished: boolean) => { + let iconName: IconName; + let intent: Intent; + let tooltip: string; + + if (isGradingPublished) { + iconName = IconNames.TICK; + intent = Intent.SUCCESS; + tooltip = 'Fully graded'; + } else { + // shh, hide actual grading progress from users even if graded + iconName = IconNames.TIME; + intent = Intent.WARNING; + tooltip = 'Grading in progress'; + } + + return ( + + + + ); +}; + +export default AssessmentOverviewCard; diff --git a/src/commons/assessment/__tests__/__snapshots__/Assessment.test.tsx.snap b/src/commons/assessment/__tests__/__snapshots__/Assessment.test.tsx.snap index 8526e23860..0685e273ba 100644 --- a/src/commons/assessment/__tests__/__snapshots__/Assessment.test.tsx.snap +++ b/src/commons/assessment/__tests__/__snapshots__/Assessment.test.tsx.snap @@ -1130,9 +1130,6 @@ exports[`Assessment page with multiple loaded missions renders correctly 1`] = ` > Attempt -