diff --git a/src/commons/assessment/AssessmentTypes.ts b/src/commons/assessment/AssessmentTypes.ts index 2ace796bed..6c9c8be7a4 100644 --- a/src/commons/assessment/AssessmentTypes.ts +++ b/src/commons/assessment/AssessmentTypes.ts @@ -64,6 +64,7 @@ export type AssessmentOverview = { isPublished?: boolean; hasVotingFeatures: boolean; hasTokenCounter?: boolean; + isVotingPublished?: boolean; maxXp: number; earlySubmissionXp: number; number?: string; // For mission control diff --git a/src/commons/sagas/BackendSaga.ts b/src/commons/sagas/BackendSaga.ts index 98ee9c7290..62de8a52d4 100644 --- a/src/commons/sagas/BackendSaga.ts +++ b/src/commons/sagas/BackendSaga.ts @@ -16,6 +16,7 @@ import { GradingQuestion } from '../../features/grading/GradingTypes'; import { + ASSIGN_ENTRIES_FOR_VOTING, CHANGE_DATE_ASSESSMENT, CHANGE_TEAM_SIZE_ASSESSMENT, CONFIGURE_ASSESSMENT, @@ -1386,6 +1387,28 @@ function* BackendSaga(): SagaIterator { yield call(showSuccessMessage, 'Updated successfully!', 1000); } ); + + yield takeEvery( + ASSIGN_ENTRIES_FOR_VOTING, + function* (action: ReturnType): any { + const tokens: Tokens = yield selectTokens(); + const id = action.payload.id; + + const resp: Response | null = yield updateAssessment( + id, + { + assignEntriesForVoting: true + }, + tokens + ); + if (!resp || !resp.ok) { + return yield handleResponseError(resp); + } + + yield put(actions.fetchAssessmentOverviews()); + yield call(showSuccessMessage, 'Updated successfully!', 1000); + } + ); } function* handleReautogradeResponse(resp: Response | null): any { diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index 8654b249db..4282d05e8a 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -1140,6 +1140,7 @@ export const updateAssessment = async ( maxTeamSize?: number; hasTokenCounter?: boolean; hasVotingFeatures?: boolean; + assignEntriesForVoting?: boolean; }, tokens: Tokens ): Promise => { diff --git a/src/features/groundControl/GroundControlActions.ts b/src/features/groundControl/GroundControlActions.ts index 24c11f8966..0cf419a4ed 100644 --- a/src/features/groundControl/GroundControlActions.ts +++ b/src/features/groundControl/GroundControlActions.ts @@ -1,6 +1,7 @@ import { createAction } from '@reduxjs/toolkit'; import { + ASSIGN_ENTRIES_FOR_VOTING, CHANGE_DATE_ASSESSMENT, CHANGE_TEAM_SIZE_ASSESSMENT, CONFIGURE_ASSESSMENT, @@ -39,3 +40,7 @@ export const configureAssessment = createAction( payload: { id, hasVotingFeatures, hasTokenCounter } }) ); + +export const assignEntriesForVoting = createAction(ASSIGN_ENTRIES_FOR_VOTING, (id: number) => ({ + payload: { id } +})); diff --git a/src/features/groundControl/GroundControlTypes.ts b/src/features/groundControl/GroundControlTypes.ts index fe33589af5..f14b1b61a3 100644 --- a/src/features/groundControl/GroundControlTypes.ts +++ b/src/features/groundControl/GroundControlTypes.ts @@ -4,3 +4,4 @@ export const DELETE_ASSESSMENT = 'DELETE_ASSESSMENT'; export const PUBLISH_ASSESSMENT = 'PUBLISH_ASSESSMENT'; export const UPLOAD_ASSESSMENT = 'UPLOAD_ASSESSMENT'; export const CONFIGURE_ASSESSMENT = 'CONFIGURE_ASSESSMENT'; +export const ASSIGN_ENTRIES_FOR_VOTING = 'ASSIGN_ENTRIES_FOR_VOTING'; diff --git a/src/pages/academy/groundControl/GroundControl.tsx b/src/pages/academy/groundControl/GroundControl.tsx index 4a5e2ccd0b..a932c78d10 100644 --- a/src/pages/academy/groundControl/GroundControl.tsx +++ b/src/pages/academy/groundControl/GroundControl.tsx @@ -40,6 +40,7 @@ export type DispatchProps = { hasVotingFeatures: boolean, hasTokenCounter: boolean ) => void; + handleAssignEntriesForVoting: (id: number) => void; handleFetchCourseConfigs: () => void; }; @@ -163,7 +164,8 @@ const GroundControl: React.FC = props => { field: 'placeholderConfigure' as any, cellRenderer: ConfigureCell, cellRendererParams: { - handleConfigureAssessment: props.handleConfigureAssessment + handleConfigureAssessment: props.handleConfigureAssessment, + handleAssignEntriesForVoting: props.handleAssignEntriesForVoting }, width: 80, filter: false, diff --git a/src/pages/academy/groundControl/GroundControlContainer.ts b/src/pages/academy/groundControl/GroundControlContainer.ts index 1eec25b7b6..a73f9d319f 100644 --- a/src/pages/academy/groundControl/GroundControlContainer.ts +++ b/src/pages/academy/groundControl/GroundControlContainer.ts @@ -7,6 +7,7 @@ import { } from '../../../commons/application/actions/SessionActions'; import { OverallState } from '../../../commons/application/ApplicationTypes'; import { + assignEntriesForVoting, changeDateAssessment, changeTeamSizeAssessment, configureAssessment, @@ -28,7 +29,8 @@ const mapDispatchToProps: MapDispatchToProps = (dispatch: Dis handleUploadAssessment: uploadAssessment, handlePublishAssessment: publishAssessment, handleFetchCourseConfigs: fetchCourseConfig, - handleConfigureAssessment: configureAssessment + handleConfigureAssessment: configureAssessment, + handleAssignEntriesForVoting: assignEntriesForVoting }, dispatch ); diff --git a/src/pages/academy/groundControl/subcomponents/GroundControlConfigureCell.tsx b/src/pages/academy/groundControl/subcomponents/GroundControlConfigureCell.tsx index e8a300ab2e..d6ed7542f3 100644 --- a/src/pages/academy/groundControl/subcomponents/GroundControlConfigureCell.tsx +++ b/src/pages/academy/groundControl/subcomponents/GroundControlConfigureCell.tsx @@ -13,6 +13,7 @@ import React, { useCallback, useState } from 'react'; import { AssessmentOverview } from '../../../../commons/assessment/AssessmentTypes'; import ControlButton from '../../../../commons/ControlButton'; +import AssignEntriesButton from './configureControls/AssignEntriesButton'; type Props = { handleConfigureAssessment: ( @@ -20,24 +21,32 @@ type Props = { hasVotingFeatures: boolean, hasTokenCounter: boolean ) => void; + handleAssignEntriesForVoting: (id: number) => void; data: AssessmentOverview; }; -const ConfigureCell: React.FC = ({ handleConfigureAssessment, data }) => { +const ConfigureCell: React.FC = ({ + handleConfigureAssessment, + handleAssignEntriesForVoting, + data +}) => { const [isDialogOpen, setDialogState] = useState(false); const [hasVotingFeatures, setHasVotingFeatures] = useState(!!data.hasVotingFeatures); const [hasTokenCounter, setHasTokenCounter] = useState(!!data.hasTokenCounter); const [isTeamAssessment, setIsTeamAssessment] = useState(false); + const [isVotingPublished] = useState(!!data.isVotingPublished); const handleOpenDialog = useCallback(() => setDialogState(true), []); const handleCloseDialog = useCallback(() => setDialogState(false), []); + // Updates assessment overview with changes to hasVotingFeatures and hasTokenCounter const handleConfigure = useCallback(() => { const { id } = data; handleConfigureAssessment(id, hasVotingFeatures, hasTokenCounter); handleCloseDialog(); }, [data, handleCloseDialog, handleConfigureAssessment, hasTokenCounter, hasVotingFeatures]); + // Toggles in configuration pannel const toggleHasTokenCounter = useCallback(() => setHasTokenCounter(prev => !prev), []); const toggleVotingFeatures = useCallback(() => setHasVotingFeatures(prev => !prev), []); const toggleIsTeamAssessment = useCallback(() => setIsTeamAssessment(prev => !prev), []); @@ -117,11 +126,10 @@ const ConfigureCell: React.FC = ({ handleConfigureAssessment, data }) => label="Export Score Leaderboard (Coming soon!)" /> - diff --git a/src/pages/academy/groundControl/subcomponents/configureControls/AssignEntriesButton.tsx b/src/pages/academy/groundControl/subcomponents/configureControls/AssignEntriesButton.tsx new file mode 100644 index 0000000000..9e6ecda1ce --- /dev/null +++ b/src/pages/academy/groundControl/subcomponents/configureControls/AssignEntriesButton.tsx @@ -0,0 +1,68 @@ +import { Button, ButtonGroup, Icon } from '@blueprintjs/core'; +import { IconNames, InfoSign } from '@blueprintjs/icons'; +import { useCallback, useState } from 'react'; +import ControlButton from 'src/commons/ControlButton'; +import classes from 'src/styles/ConfigureControls.module.scss'; + +type Props = { + handleAssignEntriesForVoting: (id: number) => void; + assessmentId: number; + isVotingPublished: boolean; +}; + +const AssignEntriesButton: React.FC = ({ + handleAssignEntriesForVoting, + assessmentId, + isVotingPublished +}) => { + const [confirmAssignEntries, setConfirmAssignEntries] = useState(false); + + // OnClick and Handler functions for confirmation warnings when assigning entries for voting + const onAssignClick = useCallback(() => setConfirmAssignEntries(true), []); + const handleConfirmAssign = useCallback(() => { + handleAssignEntriesForVoting(assessmentId); + }, [assessmentId, handleAssignEntriesForVoting]); + const handleCancelAssign = useCallback(() => setConfirmAssignEntries(false), []); + + return ( + <> +
+ +

+ Current Voting Status: Entries have {!isVotingPublished && not } been assigned +

+
+ {!confirmAssignEntries ? ( +
+ +
+ ) : ( +
+ +

+ Are you sure you want to {isVotingPublished ? 're-assign' : 'assign'} entries? +

+ + + + +
+ )} + {isVotingPublished && ( +

+ All existing votes will be deleted upon reassigning entries! +

+ )} + + ); +}; + +export default AssignEntriesButton; diff --git a/src/styles/ConfigureControls.module.scss b/src/styles/ConfigureControls.module.scss new file mode 100644 index 0000000000..fa0af37e5a --- /dev/null +++ b/src/styles/ConfigureControls.module.scss @@ -0,0 +1,22 @@ +/* Container for warnings upon clicking assign entries button */ +.reassign-voting-warning { + font-size: 11px; + margin-left: 38px; +} + +.confirm-assign-voting, +.current-voting-status { + max-height: 30px; + display: flex; + align-items: center; + gap: 6px; + margin-left: 15px; + + .confirm-assign-text { + margin-top: 9px; + } + + .voting-status-text { + margin-top: 10px; + } +} diff --git a/src/styles/_groundcontrol.scss b/src/styles/_groundcontrol.scss index 8328139e8e..78d5ad9fe1 100644 --- a/src/styles/_groundcontrol.scss +++ b/src/styles/_groundcontrol.scss @@ -81,10 +81,6 @@ display: flex; margin-left: 5px; } - - .publish-voting { - margin-top: 5px; - } } .numeric-input-container {