add required peer review link to assignment header
add a link to the assignment header that allows the user to navigate between assigned peer reviews closes EVAL-3182 flag=peer_reviews_for_a2 test plan: - Have a course with at three students - Create an assignment with anonymous peer review disabled - Assign the same student the other two peer reviews - As the primary student with the two peer reviews, do not submit the assignment. - Go to the non-anonymous peer review assignment page and see that there is a link called Required Peer Reviews on the header - Confirm that all two students are under the Not Yet Submitted category when the link is clicked on and the popover is displayed - Click on one of the students in the popover and see that it will navigate to the student's peer review page - Confirm that the student whose peer review page is being viewed is the same student on the popover that has a grey box - Confirm that there is a required peer review link on the page and the page says that you need to first submit before you can peer review - As the primary student, submit the assignment and click on another student in the required peer review link - Confirm that the page says the no submissions are available to review yet and the peer review link is shown on the page - Go to one of the peer reviewee's account and submit the assignment - Go back to the primary student's account and go to that assignments page. - Confirm that the peer reviewee who just submitted is now in the Ready to Review section of the popover - Click on the peer reviewee in the popover and submit a peer review - Open the peer review popover again and confirm that the peer reviewee is now in the Completed Peer Reviews Section - Perform the same steps with an anonymous assignment and confirm that the names on the popover says Anonymous with a number. - Perform the same steps with a non-anonymous assignment with a rubric - Create an assignment with peer reviews enabled but no peer reviews assigned - Confirm that there is no required peer review link for the assignment Change-Id: I314cb0842620cf5cf7129ec0bbe17064eb8f4dbe Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/323717 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Spencer Olson <solson@instructure.com> Reviewed-by: Derek Williams <derek.williams@instructure.com> QA-Review: Derek Williams <derek.williams@instructure.com> Product-Review: Ravi Koll <ravi.koll@instructure.com>
This commit is contained in:
parent
6c2a88c254
commit
e7e9f2aa4d
|
@ -278,12 +278,12 @@ export type AssessmentRequest = Readonly<{
|
|||
export type AssignedAssessments = {
|
||||
assetId: string
|
||||
workflowState: string
|
||||
assetSubmissionType: string
|
||||
assetSubmissionType: string | null
|
||||
anonymizedUser?: {
|
||||
displayName: string
|
||||
_id: string
|
||||
}
|
||||
anonymousId?: string
|
||||
} | null
|
||||
anonymousId?: string | null
|
||||
}
|
||||
|
||||
export type AttachmentData = Readonly<{
|
||||
|
|
|
@ -37,7 +37,7 @@ import StudentViewContext from '../Context'
|
|||
import {SUBMISSION_COMMENT_QUERY} from '@canvas/assignments/graphql/student/Queries'
|
||||
import {Submission} from '@canvas/assignments/graphql/student/Submission'
|
||||
import {useQuery} from 'react-apollo'
|
||||
import {bool} from 'prop-types'
|
||||
import {bool, func} from 'prop-types'
|
||||
import PeerReviewPromptModal from '../PeerReviewPromptModal'
|
||||
import {
|
||||
getRedirectUrlToFirstPeerReview,
|
||||
|
@ -189,6 +189,7 @@ export default function CommentsTrayBody(props) {
|
|||
onSendCommentSuccess={() => {
|
||||
if (props.isPeerReviewEnabled && !props.assignment.rubric) {
|
||||
handlePeerReviewPromptModal()
|
||||
props.onSuccessfulPeerReview?.(props.reviewerSubmission)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -221,6 +222,7 @@ CommentsTrayBody.propTypes = {
|
|||
submission: Submission.shape.isRequired,
|
||||
reviewerSubmission: Submission.shape,
|
||||
isPeerReviewEnabled: bool,
|
||||
onSuccessfulPeerReview: func,
|
||||
}
|
||||
|
||||
CommentsTrayBody.defaultProps = {
|
||||
|
|
|
@ -60,6 +60,7 @@ function TrayContent(props) {
|
|||
submission={props.submission}
|
||||
reviewerSubmission={props.reviewerSubmission}
|
||||
isPeerReviewEnabled={props.isPeerReviewEnabled}
|
||||
onSuccessfulPeerReview={props.onSuccessfulPeerReview}
|
||||
/>
|
||||
</Suspense>
|
||||
)
|
||||
|
@ -70,6 +71,7 @@ TrayContent.propTypes = {
|
|||
submission: Submission.shape.isRequired,
|
||||
reviewerSubmission: Submission.shape,
|
||||
isPeerReviewEnabled: bool,
|
||||
onSuccessfulPeerReview: func,
|
||||
}
|
||||
|
||||
TrayContent.defaultProps = {
|
||||
|
@ -120,6 +122,7 @@ export default function CommentsTray(props) {
|
|||
assignment={props.assignment}
|
||||
submission={props.submission}
|
||||
reviewerSubmission={props.reviewerSubmission}
|
||||
onSuccessfulPeerReview={props.onSuccessfulPeerReview}
|
||||
/>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
|
@ -135,6 +138,7 @@ CommentsTray.propTypes = {
|
|||
closeTray: func.isRequired,
|
||||
open: bool.isRequired,
|
||||
isPeerReviewEnabled: bool,
|
||||
onSuccessfulPeerReview: func,
|
||||
}
|
||||
|
||||
CommentsTray.defaultProps = {
|
||||
|
|
|
@ -25,6 +25,7 @@ import SubmissionManager from './SubmissionManager'
|
|||
import {Submission} from '@canvas/assignments/graphql/student/Submission'
|
||||
import {Tabs} from '@instructure/ui-tabs'
|
||||
import {View} from '@instructure/ui-view'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const I18n = useI18nScope('assignments_2')
|
||||
|
||||
|
@ -40,6 +41,7 @@ ContentTabs.propTypes = {
|
|||
assignment: Assignment.shape,
|
||||
submission: Submission.shape,
|
||||
reviewerSubmission: Submission.shape,
|
||||
onSuccessfulPeerReview: PropTypes.func,
|
||||
}
|
||||
|
||||
function LoggedInContentTabs(props) {
|
||||
|
@ -52,6 +54,7 @@ function LoggedInContentTabs(props) {
|
|||
assignment={props.assignment}
|
||||
submission={props.submission}
|
||||
reviewerSubmission={props.reviewerSubmission}
|
||||
onSuccessfulPeerReview={props.onSuccessfulPeerReview}
|
||||
/>
|
||||
</View>
|
||||
</div>
|
||||
|
|
|
@ -30,7 +30,6 @@ import {Link} from '@instructure/ui-link'
|
|||
import {IconChatLine, IconQuestionLine} from '@instructure/ui-icons'
|
||||
import {useScope as useI18nScope} from '@canvas/i18n'
|
||||
import LatePolicyToolTipContent from './LatePolicyStatusDisplay/LatePolicyToolTipContent'
|
||||
import {Popover} from '@instructure/ui-popover'
|
||||
import {arrayOf, func} from 'prop-types'
|
||||
import OriginalityReport from './OriginalityReport'
|
||||
import React from 'react'
|
||||
|
@ -43,10 +42,12 @@ import {Text} from '@instructure/ui-text'
|
|||
import {Tooltip} from '@instructure/ui-tooltip'
|
||||
import {View} from '@instructure/ui-view'
|
||||
import CommentsTray from './CommentsTray/index'
|
||||
import {Popover} from '@instructure/ui-popover'
|
||||
import {
|
||||
getOriginalityData,
|
||||
isOriginalityReportVisible,
|
||||
} from '@canvas/grading/originalityReportHelper'
|
||||
import PeerReviewNavigationLink from './PeerReviewNavigationLink'
|
||||
|
||||
const I18n = useI18nScope('assignments_2_student_header')
|
||||
|
||||
|
@ -57,6 +58,8 @@ class Header extends React.Component {
|
|||
onChangeSubmission: func,
|
||||
submission: Submission.shape,
|
||||
reviewerSubmission: Submission.shape,
|
||||
peerReviewLinkData: Submission.shape,
|
||||
onSuccessfulPeerReview: func,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -86,11 +89,11 @@ class Header extends React.Component {
|
|||
)
|
||||
}
|
||||
|
||||
currentAssessmentIndex = () => {
|
||||
currentAssessmentIndex = assignedAssessments => {
|
||||
const userId = this.props.assignment.env.revieweeId
|
||||
const anonymousId = this.props.assignment.env.anonymousAssetId
|
||||
const value =
|
||||
this.props.reviewerSubmission?.assignedAssessments?.findIndex(assessment => {
|
||||
assignedAssessments?.findIndex(assessment => {
|
||||
return (
|
||||
(userId && userId === assessment.anonymizedUser._id) ||
|
||||
(anonymousId && assessment.anonymousId === anonymousId)
|
||||
|
@ -276,15 +279,52 @@ class Header extends React.Component {
|
|||
let topRightComponent
|
||||
if (this.isPeerReviewModeEnabled()) {
|
||||
topRightComponent = (
|
||||
<Flex.Item margin="0 small 0 0">
|
||||
<PeerReviewsCounter
|
||||
current={this.currentAssessmentIndex()}
|
||||
total={this.props.reviewerSubmission?.assignedAssessments?.length || 0}
|
||||
/>
|
||||
</Flex.Item>
|
||||
<Flex direction="column" alignItems="end">
|
||||
{this.props.peerReviewLinkData ? (
|
||||
<Flex.Item padding="0 small 0 0">
|
||||
<PeerReviewNavigationLink
|
||||
assignedAssessments={this.props.peerReviewLinkData?.assignedAssessments}
|
||||
currentAssessmentIndex={this.currentAssessmentIndex(
|
||||
this.props.peerReviewLinkData?.assignedAssessments
|
||||
)}
|
||||
/>
|
||||
</Flex.Item>
|
||||
) : (
|
||||
<>
|
||||
<Flex.Item padding="0 small 0 0">
|
||||
<PeerReviewsCounter
|
||||
current={this.currentAssessmentIndex(
|
||||
this.props.reviewerSubmission?.assignedAssessments
|
||||
)}
|
||||
total={this.props.reviewerSubmission?.assignedAssessments?.length || 0}
|
||||
/>
|
||||
</Flex.Item>
|
||||
<Flex.Item padding="0 small 0 0">
|
||||
<PeerReviewNavigationLink
|
||||
assignedAssessments={this.props.reviewerSubmission?.assignedAssessments}
|
||||
currentAssessmentIndex={this.currentAssessmentIndex(
|
||||
this.props.reviewerSubmission?.assignedAssessments
|
||||
)}
|
||||
/>
|
||||
</Flex.Item>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
)
|
||||
} else {
|
||||
topRightComponent = <Flex.Item>{this.renderLatestGrade()}</Flex.Item>
|
||||
topRightComponent = (
|
||||
<Flex direction="column" alignItems="end">
|
||||
<Flex.Item padding="0 small 0 0">{this.renderLatestGrade()}</Flex.Item>
|
||||
{this.props.submission?.assignedAssessments?.length > 0 && (
|
||||
<Flex.Item overflowX="hidden" padding="0 small 0 0">
|
||||
<PeerReviewNavigationLink
|
||||
assignedAssessments={this.props.submission.assignedAssessments}
|
||||
currentAssessmentIndex={0}
|
||||
/>
|
||||
</Flex.Item>
|
||||
)}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -301,10 +341,13 @@ class Header extends React.Component {
|
|||
<ScreenReaderContent> {this.props.assignment.name} </ScreenReaderContent>
|
||||
</Heading>
|
||||
|
||||
<Flex as="div" margin="0" wrap="wrap" alignItems="start">
|
||||
<Flex as="div" margin="0" wrap="wrap" alignItems="start" padding="0 0 large 0">
|
||||
<Flex.Item shouldShrink={true}>
|
||||
<AssignmentDetails assignment={this.props.assignment} />
|
||||
</Flex.Item>
|
||||
{this.props.peerReviewLinkData && (
|
||||
<Flex.Item shouldGrow={true}>{topRightComponent}</Flex.Item>
|
||||
)}
|
||||
{this.props.submission && (
|
||||
<Flex.Item shouldGrow={true}>
|
||||
<Flex as="div" justifyItems="end" alignItems="center">
|
||||
|
@ -323,6 +366,7 @@ class Header extends React.Component {
|
|||
open={this.state.commentsTrayOpen}
|
||||
closeTray={this.closeCommentsTray}
|
||||
isPeerReviewEnabled={this.isPeerReviewModeEnabled()}
|
||||
onSuccessfulPeerReview={this.props.onSuccessfulPeerReview}
|
||||
/>
|
||||
</Flex.Item>
|
||||
)}
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* Copyright (C) 2023 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, {useState} from 'react'
|
||||
import {useScope as useI18nScope} from '@canvas/i18n'
|
||||
// @ts-expect-error
|
||||
import {Popover} from '@instructure/ui-popover'
|
||||
// @ts-expect-error
|
||||
import {IconPeerReviewLine, IconCheckLine} from '@instructure/ui-icons'
|
||||
// @ts-expect-error
|
||||
import {TruncateText} from '@instructure/ui-truncate-text'
|
||||
// @ts-expect-error
|
||||
import {Tooltip} from '@instructure/ui-tooltip'
|
||||
import {Menu} from '@instructure/ui-menu'
|
||||
import {Flex} from '@instructure/ui-flex'
|
||||
import {View} from '@instructure/ui-view'
|
||||
import {Text} from '@instructure/ui-text'
|
||||
import {Link} from '@instructure/ui-link'
|
||||
import {getPeerReviewUrl} from '../helpers/PeerReviewHelpers'
|
||||
import {AssignedAssessments} from 'api'
|
||||
import {AccessibleContent} from '@instructure/ui-a11y-content'
|
||||
|
||||
const I18n = useI18nScope('assignments_2_student_header')
|
||||
|
||||
const {Item: MenuItem, Group: MenuGroup} = Menu as any
|
||||
|
||||
type PeerReviewNavigationLinkProps = {
|
||||
assignedAssessments: AssignedAssessments[]
|
||||
currentAssessmentIndex: number
|
||||
}
|
||||
|
||||
type NavigationMenuItemLabelProps = {
|
||||
assessment: AssignedAssessments
|
||||
index: number
|
||||
peerReviewStatus: string
|
||||
}
|
||||
|
||||
const TruncateWithTooltip = ({children}: {children: React.ReactNode}) => {
|
||||
const [isTruncated, setIsTruncated] = useState(false)
|
||||
return isTruncated ? (
|
||||
<Tooltip as="div" placement="start" renderTip={children}>
|
||||
<TruncateText position="middle" onUpdate={setIsTruncated}>
|
||||
{children}
|
||||
</TruncateText>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<TruncateText onUpdate={setIsTruncated} position="middle">
|
||||
{children}
|
||||
</TruncateText>
|
||||
)
|
||||
}
|
||||
|
||||
const NavigationMenuItemLabel = ({
|
||||
assessment,
|
||||
index,
|
||||
peerReviewStatus,
|
||||
}: NavigationMenuItemLabelProps) => {
|
||||
const label = assessment.anonymizedUser
|
||||
? assessment.anonymizedUser?.displayName
|
||||
: I18n.t('Anonymous %{peerReviewNumber}', {peerReviewNumber: index + 1})
|
||||
return (
|
||||
<AccessibleContent alt={`${label} ${peerReviewStatus}`}>
|
||||
<TruncateWithTooltip>{label}</TruncateWithTooltip>
|
||||
</AccessibleContent>
|
||||
)
|
||||
}
|
||||
|
||||
export default ({assignedAssessments, currentAssessmentIndex}: PeerReviewNavigationLinkProps) => {
|
||||
const renderNavigationMenuItem = (
|
||||
assessment: AssignedAssessments,
|
||||
index: number,
|
||||
testId: string,
|
||||
peerReviewStatus: string
|
||||
) => {
|
||||
return (
|
||||
<MenuItem
|
||||
key={assessment.assetId}
|
||||
href={getPeerReviewUrl(assessment)}
|
||||
/* currentAssessmentIndex is a passed in prop that is already 1-indexed while the parameter 'index' is 0-indexed, hence the need for a +1.
|
||||
This is comparing to see if the current peer review page we are on matches the current item in the map, which will then add a custom background
|
||||
to the theme to the rendered menu item.
|
||||
*/
|
||||
theme={
|
||||
currentAssessmentIndex === index + 1 ? {background: '#6B7780', labelColor: 'white'} : null
|
||||
}
|
||||
data-testid={`${testId}-${assessment.assetId}`}
|
||||
>
|
||||
<Flex as="div">
|
||||
{testId === 'peer-review-completed' ? (
|
||||
<View margin="0 x-small 0 0">
|
||||
<IconCheckLine />
|
||||
</View>
|
||||
) : (
|
||||
<View margin="0 medium 0 0" />
|
||||
)}
|
||||
<NavigationMenuItemLabel
|
||||
assessment={assessment}
|
||||
index={index}
|
||||
peerReviewStatus={peerReviewStatus}
|
||||
/>
|
||||
</Flex>
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
renderTrigger={
|
||||
<View as="div" margin="xx-small 0 xx-small xx-small" data-testid="header-peer-review-link">
|
||||
<View margin="0 xx-small 0 0">
|
||||
<IconPeerReviewLine />
|
||||
</View>
|
||||
<Link as="button" isWithinText={false}>
|
||||
<Text weight="bold" size="small" color="brand">
|
||||
{I18n.t('Required Peer Reviews')}
|
||||
</Text>
|
||||
</Link>
|
||||
</View>
|
||||
}
|
||||
on="click"
|
||||
placement="bottom end"
|
||||
>
|
||||
<Menu>
|
||||
<MenuGroup label={I18n.t('Ready to Review')} />
|
||||
{assignedAssessments?.map(
|
||||
(assessment, index) =>
|
||||
assessment.assetSubmissionType != null &&
|
||||
assessment.workflowState === 'assigned' &&
|
||||
renderNavigationMenuItem(
|
||||
assessment,
|
||||
index,
|
||||
'peer-review-ready',
|
||||
I18n.t('Ready to Review')
|
||||
)
|
||||
)}
|
||||
|
||||
<MenuGroup label={I18n.t('Not Yet Submitted')} />
|
||||
{assignedAssessments?.map(
|
||||
(assessment, index) =>
|
||||
assessment.assetSubmissionType === null &&
|
||||
renderNavigationMenuItem(
|
||||
assessment,
|
||||
index,
|
||||
'peer-review-not-submitted',
|
||||
I18n.t('Peer Review Not Yet Submitted')
|
||||
)
|
||||
)}
|
||||
|
||||
<MenuGroup label={I18n.t('Completed Peer Reviews')} />
|
||||
{assignedAssessments?.map(
|
||||
(assessment, index) =>
|
||||
assessment.assetSubmissionType != null &&
|
||||
assessment.workflowState === 'completed' &&
|
||||
renderNavigationMenuItem(
|
||||
assessment,
|
||||
index,
|
||||
'peer-review-completed',
|
||||
I18n.t('Peer Review Completed')
|
||||
)
|
||||
)}
|
||||
</Menu>
|
||||
</Popover>
|
||||
)
|
||||
}
|
|
@ -28,7 +28,7 @@ import MarkAsDoneButton from './MarkAsDoneButton'
|
|||
import LoadingIndicator from '@canvas/loading-indicator'
|
||||
import MissingPrereqs from './MissingPrereqs'
|
||||
import DateLocked from '../DateLocked'
|
||||
import React, {Suspense, lazy, useContext, useEffect} from 'react'
|
||||
import React, {Suspense, lazy, useContext, useEffect, useState} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {Spinner} from '@instructure/ui-spinner'
|
||||
import {Submission} from '@canvas/assignments/graphql/student/Submission'
|
||||
|
@ -128,7 +128,8 @@ function renderAttemptsAndAvailability(assignment) {
|
|||
|
||||
function renderContentBaseOnAvailability(
|
||||
{assignment, submission, reviewerSubmission},
|
||||
alertContext
|
||||
alertContext,
|
||||
onSuccessfulPeerReview
|
||||
) {
|
||||
if (assignment.env.modulePrereq) {
|
||||
return <MissingPrereqs moduleUrl={assignment.env.moduleUrl} />
|
||||
|
@ -195,6 +196,7 @@ function renderContentBaseOnAvailability(
|
|||
assignment={assignment}
|
||||
submission={submission}
|
||||
reviewerSubmission={reviewerSubmission}
|
||||
onSuccessfulPeerReview={onSuccessfulPeerReview}
|
||||
/>
|
||||
) : (
|
||||
<SubmissionlessFooter onMarkAsDoneError={onMarkAsDoneError} />
|
||||
|
@ -216,6 +218,7 @@ function renderContentBaseOnAvailability(
|
|||
|
||||
function StudentContent(props) {
|
||||
const alertContext = useContext(AlertManagerContext)
|
||||
const [assignedAssessments, setAssignedAssessments] = useState([])
|
||||
|
||||
const {description, name} = props.assignment
|
||||
useEffect(() => {
|
||||
|
@ -247,6 +250,10 @@ function StudentContent(props) {
|
|||
setUpImmersiveReader()
|
||||
}, [description, name])
|
||||
|
||||
const onSuccessfulPeerReview = (assignedAssessments) => {
|
||||
setAssignedAssessments(assignedAssessments)
|
||||
}
|
||||
|
||||
// TODO: Move the button provider up one level
|
||||
return (
|
||||
<div data-testid="assignments-2-student-view">
|
||||
|
@ -257,8 +264,9 @@ function StudentContent(props) {
|
|||
scrollThreshold={150}
|
||||
submission={props.submission}
|
||||
reviewerSubmission={props.reviewerSubmission}
|
||||
onSuccessfulPeerReview={onSuccessfulPeerReview}
|
||||
/>
|
||||
{renderContentBaseOnAvailability(props, alertContext)}
|
||||
{renderContentBaseOnAvailability(props, alertContext, onSuccessfulPeerReview)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -269,6 +277,7 @@ StudentContent.propTypes = {
|
|||
onChangeSubmission: PropTypes.func.isRequired,
|
||||
submission: Submission.shape,
|
||||
reviewerSubmission: Submission.shape,
|
||||
onSuccessfulPeerReview: PropTypes.func,
|
||||
}
|
||||
|
||||
StudentContent.defaultProps = {
|
||||
|
|
|
@ -61,7 +61,7 @@ class SubmissionHistoriesQuery extends React.Component {
|
|||
if (shouldDisplayNeedsSubmissionPeerReview(this.props.initialQueryData)) {
|
||||
return (
|
||||
<>
|
||||
<Header scrollThreshold={150} assignment={this.props.initialQueryData.assignment} />
|
||||
<Header scrollThreshold={150} assignment={this.props.initialQueryData.assignment} peerReviewLinkData={this.props.initialQueryData.reviewerSubmission} />
|
||||
<AssignmentToggleDetails
|
||||
description={this.props.initialQueryData.assignment.description}
|
||||
/>
|
||||
|
@ -73,7 +73,7 @@ class SubmissionHistoriesQuery extends React.Component {
|
|||
if (shouldDisplayUnavailablePeerReview(this.props.initialQueryData)) {
|
||||
return (
|
||||
<>
|
||||
<Header scrollThreshold={150} assignment={this.props.initialQueryData.assignment} />
|
||||
<Header scrollThreshold={150} assignment={this.props.initialQueryData.assignment} peerReviewLinkData={this.props.initialQueryData.reviewerSubmission}/>
|
||||
<AssignmentToggleDetails
|
||||
description={this.props.initialQueryData.assignment.description}
|
||||
/>
|
||||
|
|
|
@ -167,7 +167,7 @@ CancelAttemptButton.propTypes = {
|
|||
submission: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
const SubmissionManager = ({assignment, submission, reviewerSubmission}) => {
|
||||
const SubmissionManager = ({assignment, submission, reviewerSubmission, onSuccessfulPeerReview}) => {
|
||||
const [draftStatus, setDraftStatus] = useState(null)
|
||||
const [editingDraft, setEditingDraft] = useState(false)
|
||||
const [focusAttemptOnInit, setFocusAttemptOnInit] = useState(false)
|
||||
|
@ -570,6 +570,7 @@ const SubmissionManager = ({assignment, submission, reviewerSubmission}) => {
|
|||
if (assignment.env.peerReviewModeEnabled) {
|
||||
const matchingAssessment = assignedAssessments.find(x => x.assetId === submission._id)
|
||||
if (matchingAssessment) matchingAssessment.workflowState = 'completed'
|
||||
onSuccessfulPeerReview?.(assignedAssessments)
|
||||
}
|
||||
const {availableCount, unavailableCount} = availableAndUnavailableCounts(assignedAssessments)
|
||||
handlePeerReviewPromptSettings(availableCount, unavailableCount)
|
||||
|
@ -868,6 +869,7 @@ SubmissionManager.propTypes = {
|
|||
assignment: Assignment.shape,
|
||||
submission: Submission.shape,
|
||||
reviewerSubmission: Submission.shape,
|
||||
onSuccessfulPeerReview: PropTypes.func,
|
||||
}
|
||||
|
||||
export default SubmissionManager
|
||||
|
|
|
@ -91,6 +91,7 @@ class ViewManager extends React.Component {
|
|||
displayedAttempt: null,
|
||||
dummyNextSubmission: null,
|
||||
submissions: [],
|
||||
reviewerSubmission: [],
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
|
|
|
@ -1024,5 +1024,30 @@ describe('CommentsTrayBody', () => {
|
|||
|
||||
expect(queryByTestId('peer-review-prompt-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls the onSuccessfulPeerReview function to re-render page when a peer review comment is successful', async () => {
|
||||
const mocks = await Promise.all([
|
||||
mockSubmissionCommentQuery({}, {peerReview: true}),
|
||||
mockCreateSubmissionComment(),
|
||||
])
|
||||
const onSuccessfulPeerReviewMockFunction = jest.fn()
|
||||
const props = {
|
||||
...(await getDefaultPropsWithReviewerSubmission('assigned')),
|
||||
onSuccessfulPeerReview: onSuccessfulPeerReviewMockFunction,
|
||||
}
|
||||
props.isPeerReviewEnabled = true
|
||||
const {findByPlaceholderText, getByText} = render(
|
||||
mockContext(
|
||||
<MockedProvider mocks={mocks}>
|
||||
<CommentsTrayBody {...props} />
|
||||
</MockedProvider>
|
||||
)
|
||||
)
|
||||
const textArea = await findByPlaceholderText('Submit a Comment')
|
||||
fireEvent.change(textArea, {target: {value: 'lion'}})
|
||||
fireEvent.click(getByText('Send Comment'))
|
||||
await waitFor(() => expect(onSuccessfulPeerReviewMockFunction).toHaveBeenCalled())
|
||||
expect(props.reviewerSubmission.assignedAssessments[0].workflowState).toEqual('completed')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -373,6 +373,7 @@ it('does not render the attempt select if peerReviewModeEnabled is set to true',
|
|||
...props.submission,
|
||||
assignedAssessments: [
|
||||
{
|
||||
assetId: '1',
|
||||
anonymousUser: null,
|
||||
anonymousId: 'xaU9cd',
|
||||
workflowState: 'assigned',
|
||||
|
@ -443,6 +444,7 @@ describe('submission workflow tracker', () => {
|
|||
...props.submission,
|
||||
assignedAssessments: [
|
||||
{
|
||||
assetId: '1',
|
||||
anonymousUser: null,
|
||||
anonymousId: 'xaU9cd',
|
||||
workflowState: 'assigned',
|
||||
|
@ -768,6 +770,7 @@ describe('Add Comment/View Feedback button', () => {
|
|||
...props.submission,
|
||||
assignedAssessments: [
|
||||
{
|
||||
assetId: '1',
|
||||
anonymousUser: null,
|
||||
anonymousId: 'xaU9cd',
|
||||
workflowState: 'assigned',
|
||||
|
@ -794,6 +797,7 @@ describe('Add Comment/View Feedback button', () => {
|
|||
...props.submission,
|
||||
assignedAssessments: [
|
||||
{
|
||||
assetId: '1',
|
||||
anonymousUser: null,
|
||||
anonymousId: 'xaU9cd',
|
||||
workflowState: 'assigned',
|
||||
|
@ -825,6 +829,7 @@ describe('Peer reviews counter', () => {
|
|||
...props.submission,
|
||||
assignedAssessments: [
|
||||
{
|
||||
assetId: '1',
|
||||
anonymousUser: null,
|
||||
anonymousId: 'xaU9cd',
|
||||
workflowState: 'assigned',
|
||||
|
@ -854,16 +859,19 @@ describe('Peer reviews counter', () => {
|
|||
...props.submission,
|
||||
assignedAssessments: [
|
||||
{
|
||||
assetId: '1',
|
||||
anonymousId: 'xaU9cd',
|
||||
workflowState: 'assigned',
|
||||
assetSubmissionType: 'online_text_entry',
|
||||
},
|
||||
{
|
||||
assetId: '2',
|
||||
anonymousId: 'maT9fd',
|
||||
workflowState: 'assigned',
|
||||
assetSubmissionType: 'online_text_entry',
|
||||
},
|
||||
{
|
||||
assetId: '3',
|
||||
anonymousId: 'vaN9fd',
|
||||
workflowState: 'assigned',
|
||||
assetSubmissionType: 'online_text_entry',
|
||||
|
@ -902,19 +910,22 @@ describe('Peer reviews counter', () => {
|
|||
...props.submission,
|
||||
assignedAssessments: [
|
||||
{
|
||||
anonymizedUser: {_id: '1'},
|
||||
assetId: '1',
|
||||
anonymizedUser: {_id: '1', displayName: "Jim"},
|
||||
anonymousId: null,
|
||||
workflowState: 'assigned',
|
||||
assetSubmissionType: 'online_text_entry',
|
||||
},
|
||||
{
|
||||
anonymizedUser: {_id: '2'},
|
||||
assetId: '2',
|
||||
anonymizedUser: {_id: '2', displayName: "Bob"},
|
||||
anonymousId: null,
|
||||
workflowState: 'assigned',
|
||||
assetSubmissionType: 'online_text_entry',
|
||||
},
|
||||
{
|
||||
anonymizedUser: {_id: '3'},
|
||||
assetId: '3',
|
||||
anonymizedUser: {_id: '3', displayName: "Tim"},
|
||||
anonymousId: null,
|
||||
workflowState: 'assigned',
|
||||
assetSubmissionType: 'online_text_entry',
|
||||
|
@ -951,11 +962,13 @@ describe('Peer reviews counter', () => {
|
|||
...props.submission,
|
||||
assignedAssessments: [
|
||||
{
|
||||
assetId: '1',
|
||||
anonymousId: 'xaU9cd',
|
||||
workflowState: 'assigned',
|
||||
assetSubmissionType: 'online_text_entry',
|
||||
},
|
||||
{
|
||||
assetId: '2',
|
||||
anonymousId: 'maT9fd',
|
||||
workflowState: 'assigned',
|
||||
assetSubmissionType: 'online_text_entry',
|
||||
|
@ -966,4 +979,78 @@ describe('Peer reviews counter', () => {
|
|||
const assessmentsCount = props.reviewerSubmission.assignedAssessments.length
|
||||
expect(queryByTestId('total-counter')).toHaveTextContent(assessmentsCount.toString())
|
||||
})
|
||||
|
||||
describe('required peer reviews link in assignment header with peer review mode disabled', () => {
|
||||
let props
|
||||
beforeAll(async () => {
|
||||
props = await mockAssignmentAndSubmission({
|
||||
Submission: {...SubmissionMocks.submitted},
|
||||
})
|
||||
props.allSubmissions = [props.submission]
|
||||
props.assignment.env.peerReviewModeEnabled = false
|
||||
props.submission.assignedAssessments = [
|
||||
{
|
||||
assetId: '1',
|
||||
anonymizedUser: {_id: '1', displayName: 'Jim'},
|
||||
anonymousId: null,
|
||||
workflowState: 'assigned',
|
||||
assetSubmissionType: null,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
it('renders the required peer review link with peer reviews assigned', () => {
|
||||
const {queryByTestId} = render(<Header {...props} />)
|
||||
expect(queryByTestId('assignment-student-header')).toHaveTextContent('Required Peer Reviews')
|
||||
})
|
||||
|
||||
it('does not render the required peer review link with the number of peer reviews assigned when no peer reviews are assigned', () => {
|
||||
props.submission.assignedAssessments = []
|
||||
const {queryByTestId} = render(<Header {...props} />)
|
||||
expect(queryByTestId('header-peer-review-link')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('required peer reviews link in assignment header with peer review mode enabled', () => {
|
||||
let props
|
||||
beforeAll(async () => {
|
||||
props = await mockAssignmentAndSubmission()
|
||||
props.assignment.env.peerReviewModeEnabled = true
|
||||
})
|
||||
|
||||
it('renders the required peer review link with peer reviews assigned when both the reviewer and reviewee have submitted to the assignment', () => {
|
||||
props.reviewerSubmission = {
|
||||
...props.submission,
|
||||
assignedAssessments: [
|
||||
{
|
||||
assetId: '1',
|
||||
anonymousUser: null,
|
||||
anonymousId: 'xaU9cd',
|
||||
workflowState: 'assigned',
|
||||
assetSubmissionType: 'online_text_entry',
|
||||
},
|
||||
],
|
||||
}
|
||||
const {queryByTestId} = render(<Header {...props} />)
|
||||
expect(queryByTestId('assignment-student-header')).toHaveTextContent('Required Peer Reviews')
|
||||
})
|
||||
|
||||
it('renders the required peer review link with peer reviews assigned when no reviews are ready or reviewer has not submitted to the assignment', () => {
|
||||
props.reviewerSubmission = null
|
||||
props.peerReviewLinkData = {
|
||||
...props.submission,
|
||||
assignedAssessments: [
|
||||
{
|
||||
assetId: '1',
|
||||
anonymousUser: null,
|
||||
anonymousId: 'xaU9cd',
|
||||
workflowState: 'assigned',
|
||||
assetSubmissionType: null,
|
||||
},
|
||||
],
|
||||
}
|
||||
const {queryByTestId} = render(<Header {...props} />)
|
||||
expect(queryByTestId('assignment-student-header')).toHaveTextContent('Required Peer Reviews')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* Copyright (C) 2023 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {render, fireEvent} from '@testing-library/react'
|
||||
|
||||
import PeerReviewNavigationLink from '../PeerReviewNavigationLink'
|
||||
|
||||
const props = {
|
||||
assignedAssessments: [
|
||||
{
|
||||
assetId: '1',
|
||||
anonymizedUser: {_id: '1', displayName: 'Jim'},
|
||||
anonymousId: null,
|
||||
workflowState: 'completed',
|
||||
assetSubmissionType: 'online_text_entry',
|
||||
},
|
||||
{
|
||||
assetId: '2',
|
||||
anonymizedUser: {_id: '2', displayName: 'Bob'},
|
||||
anonymousId: null,
|
||||
workflowState: 'assigned',
|
||||
assetSubmissionType: null,
|
||||
},
|
||||
{
|
||||
assetId: '3',
|
||||
anonymizedUser: {_id: '3', displayName: 'Jill'},
|
||||
anonymousId: null,
|
||||
workflowState: 'assigned',
|
||||
assetSubmissionType: 'online_text_entry',
|
||||
},
|
||||
],
|
||||
currentAssessmentIndex: 1,
|
||||
}
|
||||
|
||||
it('displays the labels for peer review states', () => {
|
||||
const {getByTestId, getByText} = render(<PeerReviewNavigationLink {...props} />)
|
||||
fireEvent.click(getByTestId('header-peer-review-link'))
|
||||
expect(getByText('Ready to Review')).toBeInTheDocument()
|
||||
expect(getByText('Not Yet Submitted')).toBeInTheDocument()
|
||||
expect(getByText('Completed Peer Reviews')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('displays a gray highlight on the current peer review', () => {
|
||||
const {getByTestId} = render(<PeerReviewNavigationLink {...props} />)
|
||||
fireEvent.click(getByTestId('header-peer-review-link'))
|
||||
const completedMenuItem = getByTestId('peer-review-completed-1')
|
||||
const style = window.getComputedStyle(completedMenuItem as Element) as {[key: string]: any}
|
||||
const labelColorKey = Object.keys(style._values).find(key => key.includes('labelColor'))
|
||||
const backgroundKey = Object.keys(style._values).find(key => key.includes('background'))
|
||||
expect(style._values[labelColorKey ?? '']).toEqual('white')
|
||||
expect(style._values[backgroundKey ?? '']).toEqual('#6B7780')
|
||||
})
|
||||
|
||||
describe('required peer review link when the anonymous peer review option is disabled', () => {
|
||||
it('displays a ready peer review in the Ready to Review section', () => {
|
||||
const {getByTestId} = render(<PeerReviewNavigationLink {...props} />)
|
||||
fireEvent.click(getByTestId('header-peer-review-link'))
|
||||
expect(getByTestId('peer-review-ready-3')).toHaveTextContent('Jill')
|
||||
})
|
||||
|
||||
it('displays a ready peer review in the Not Yet Submitted section', () => {
|
||||
const {getByTestId} = render(<PeerReviewNavigationLink {...props} />)
|
||||
fireEvent.click(getByTestId('header-peer-review-link'))
|
||||
expect(getByTestId('peer-review-not-submitted-2')).toHaveTextContent('Bob')
|
||||
})
|
||||
|
||||
it('displays a ready peer review in the Completed Peer Reviews section', () => {
|
||||
const {getByTestId} = render(<PeerReviewNavigationLink {...props} />)
|
||||
fireEvent.click(getByTestId('header-peer-review-link'))
|
||||
expect(getByTestId('peer-review-completed-1')).toHaveTextContent('Jim')
|
||||
})
|
||||
|
||||
it('contains the correct url for the item is clicked on', () => {
|
||||
ENV.COURSE_ID = '1'
|
||||
ENV.ASSIGNMENT_ID = '1'
|
||||
const {getByTestId} = render(<PeerReviewNavigationLink {...props} />)
|
||||
fireEvent.click(getByTestId('header-peer-review-link'))
|
||||
expect(getByTestId('peer-review-completed-1')).toHaveAttribute(
|
||||
'href',
|
||||
'/courses/1/assignments/1?reviewee_id=1'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('required peer review link when the anonymous peer review option is enabled', () => {
|
||||
const props = {
|
||||
assignedAssessments: [
|
||||
{
|
||||
assetId: '1',
|
||||
anonymizedUser: null,
|
||||
anonymousId: 'anon_1',
|
||||
workflowState: 'completed',
|
||||
assetSubmissionType: 'online_text_entry',
|
||||
},
|
||||
{
|
||||
assetId: '2',
|
||||
anonymizedUser: null,
|
||||
anonymousId: 'anon_2',
|
||||
workflowState: 'assigned',
|
||||
assetSubmissionType: null,
|
||||
},
|
||||
{
|
||||
assetId: '3',
|
||||
anonymizedUser: null,
|
||||
anonymousId: 'anon_3',
|
||||
workflowState: 'assigned',
|
||||
assetSubmissionType: 'online_text_entry',
|
||||
},
|
||||
],
|
||||
currentAssessmentIndex: 1,
|
||||
}
|
||||
|
||||
it('displays a ready peer review in the Ready to Review section', () => {
|
||||
const {getByTestId} = render(<PeerReviewNavigationLink {...props} />)
|
||||
fireEvent.click(getByTestId('header-peer-review-link'))
|
||||
expect(getByTestId('peer-review-ready-3')).toHaveTextContent('Anonymous 3')
|
||||
})
|
||||
|
||||
it('displays a ready peer review in the Not Yet Submitted section', () => {
|
||||
const {getByTestId} = render(<PeerReviewNavigationLink {...props} />)
|
||||
fireEvent.click(getByTestId('header-peer-review-link'))
|
||||
expect(getByTestId('peer-review-not-submitted-2')).toHaveTextContent('Anonymous 2')
|
||||
})
|
||||
|
||||
it('displays a ready peer review in the Completed Peer Reviews section', () => {
|
||||
const {getByTestId} = render(<PeerReviewNavigationLink {...props} />)
|
||||
fireEvent.click(getByTestId('header-peer-review-link'))
|
||||
expect(getByTestId('peer-review-completed-1')).toHaveTextContent('Anonymous 1')
|
||||
})
|
||||
|
||||
it('contains the correct url for the item is clicked on', () => {
|
||||
ENV.COURSE_ID = '1'
|
||||
ENV.ASSIGNMENT_ID = '1'
|
||||
const {getByTestId} = render(<PeerReviewNavigationLink {...props} />)
|
||||
fireEvent.click(getByTestId('header-peer-review-link'))
|
||||
expect(getByTestId('peer-review-completed-1')).toHaveAttribute(
|
||||
'href',
|
||||
'/courses/1/assignments/1?anonymous_asset_id=anon_1'
|
||||
)
|
||||
})
|
||||
})
|
|
@ -1964,6 +1964,58 @@ describe('SubmissionManager', () => {
|
|||
expect(await findByText(COMPLETED_PEER_REVIEW_TEXT)).toBeTruthy()
|
||||
})
|
||||
|
||||
it('calls the onSuccessfulPeerReview function to re-render page when a peer review with rubric is successful', async () => {
|
||||
setOtherUserAsAssessmentOwner()
|
||||
store.setState({
|
||||
displayedAssessment: {
|
||||
score: 5,
|
||||
data: [
|
||||
generateAssessmentItem(props.assignment.rubric.criteria[0].id, {hasComments: true}),
|
||||
generateAssessmentItem(props.assignment.rubric.criteria[1].id, {hasComments: true}),
|
||||
],
|
||||
},
|
||||
})
|
||||
const assetId = props.submission._id
|
||||
const reviewerSubmission = {
|
||||
id: 'test-id',
|
||||
_id: 'test-id',
|
||||
assignedAssessments: [
|
||||
{
|
||||
assetId,
|
||||
workflowState: 'assigned',
|
||||
assetSubmissionType: 'online-text',
|
||||
},
|
||||
{
|
||||
assetId: 'some other user id',
|
||||
workflowState: 'assigned',
|
||||
assetSubmissionType: 'online-text',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
props.reviewerSubmission = reviewerSubmission
|
||||
props.assignment.env.peerReviewModeEnabled = true
|
||||
props.assignment.env.peerReviewAvailable = true
|
||||
const onSuccessfulPeerReviewMockFunction = jest.fn()
|
||||
|
||||
const prop = {
|
||||
...props,
|
||||
onSuccessfulPeerReview: onSuccessfulPeerReviewMockFunction,
|
||||
}
|
||||
|
||||
const {getByText, findByText} = render(
|
||||
<MockedProvider mocks={mocks}>
|
||||
<SubmissionManager {...prop} />
|
||||
</MockedProvider>
|
||||
)
|
||||
await new Promise(resolve => setTimeout(resolve, 1))
|
||||
const submitButton = getByText('Submit')
|
||||
fireEvent.click(submitButton)
|
||||
|
||||
await waitFor(() => expect(onSuccessfulPeerReviewMockFunction).toHaveBeenCalled())
|
||||
expect(props.reviewerSubmission.assignedAssessments[0].workflowState).toEqual('completed')
|
||||
})
|
||||
|
||||
it('creates an error alert when the http request fails', async () => {
|
||||
setOtherUserAsAssessmentOwner()
|
||||
doFetchApi.mockImplementation(() => Promise.reject(new Error('Network error')))
|
||||
|
|
|
@ -31,6 +31,10 @@ export const getRedirectUrlToFirstPeerReview = (
|
|||
if (!assessment) {
|
||||
return
|
||||
}
|
||||
return getPeerReviewUrl(assessment)
|
||||
}
|
||||
|
||||
export const getPeerReviewUrl = (assessment: AssignedAssessments) => {
|
||||
let url = `/courses/${ENV.COURSE_ID}/assignments/${ENV.ASSIGNMENT_ID}`
|
||||
if (assessment.anonymizedUser) {
|
||||
url += `?reviewee_id=${assessment.anonymizedUser._id}`
|
||||
|
|
Loading…
Reference in New Issue