Loading frontend/src/actionlog/InjectMessage/Content/TraineeQuestionnaireContent.tsx +9 −22 Original line number Diff line number Diff line Loading @@ -24,6 +24,8 @@ import type { NavigateOptions } from '@tanstack/react-router' import type { FC, FormEvent } from 'react' import { useCallback, useMemo, useState } from 'react' import Questionnaire from '../../../components/Questionnaire' import type { QuestionAndAnswer } from '../../../components/Questionnaire/types' import { mapQuestionsAndAnswers } from '../../../components/Questionnaire/utils' interface TraineeQuestionnaireContentProps { teamState: TeamQuestionnaireStateWithQuestionnaire Loading @@ -48,33 +50,15 @@ const AnswerState = ( teamStateId: string ) => createSyncedStore<{ questions: { question: Question answer: string[] error: string }[] questions: QuestionAndAnswer[] resetQuestions: () => void modifyAnswer: (questionId: string, answer: string) => void validate: () => boolean }>( set => ({ questions: questions.map(question => ({ question, answer: answers.find(answer => answer.question.id === question.id)?.answer || [], error: '', })), questions: mapQuestionsAndAnswers({ questions, answers }), resetQuestions: () => set({ questions: questions.map(question => ({ question, answer: answers.find(answer => answer.question.id === question.id) ?.answer || [], error: '', })), }), set({ questions: mapQuestionsAndAnswers({ questions, answers }) }), modifyAnswer: (questionId: string, answer: string) => set(({ questions }) => ({ questions: questions.map(question => { Loading Loading @@ -162,7 +146,10 @@ const TraineeQuestionnaireContent: FC<TraineeQuestionnaireContentProps> = ({ [answers, questions, teamStateId] ) const questionsAndAnswers = useStore(state, state => state.questions) const questionsAndAnswers: QuestionAndAnswer[] = useStore( state, state => state.questions ) const modifyAnswer = useStore(state, state => state.modifyAnswer) const validate = useStore(state, state => state.validate) Loading frontend/src/components/Questionnaire/AutoFreeFormQuestion.tsx +13 −28 Original line number Diff line number Diff line import { Classes, Colors, InputGroup, Label, TextArea, type Intent, } from '@blueprintjs/core' import { css, cx } from '@emotion/css' import type { AutoFreeFormQuestionDetailsType } from '@inject/graphql' import type { AutoFreeFormQuestionDetails } from '@inject/graphql' import { breakWord } from '@inject/shared' import { useMemo, type FC } from 'react' import { RenderedContent } from '../RenderedContent' import { contentWrapper, lengthDisplay } from './classes' import { contentWrapper, highlightAnswer, lengthDisplay } from './classes' import QuestionDescription from './QuestionDescription' import type { QuestionProps } from './types' import { notInInterval } from './utils' Loading @@ -26,35 +25,28 @@ const textArea = css` padding-bottom: 1.5rem !important; ` const highlight = (correct: boolean) => css` border-bottom: 0.2rem solid ${correct ? Colors.GREEN5 : Colors.RED5}; const inputGroup = css` input { padding-right: 40px; } ` const AutoFreeFormQuestion: FC<QuestionProps> = ({ type, question, answer, correct, disabled: disabledProp, onChange, inInstructor, error, exerciseId, }) => { const details = question.details as AutoFreeFormQuestionDetailsType const details = question.details as AutoFreeFormQuestionDetails const { multiline, correctAnswer, min, max, regex } = details const disabled = disabledProp || type === 'reviewing' const isCorrect = () => { if (!correctAnswer) { return false } if (regex) { return new RegExp(correctAnswer).test(answer[0]) } return correctAnswer === answer[0] } const answerString = answer.at(0) || '' const intent: Intent = useMemo( () => Loading Loading @@ -97,7 +89,7 @@ const AutoFreeFormQuestion: FC<QuestionProps> = ({ } autoResize className={cx(textArea, { [highlight(isCorrect())]: !!answer[0] && inInstructor, [highlightAnswer(correct)]: type === 'reviewing', })} /> ) : ( Loading @@ -106,16 +98,9 @@ const AutoFreeFormQuestion: FC<QuestionProps> = ({ intent={intent} fill value={answer[0]} className={cx( css` input { padding-right: 40px; } `, { [highlight(isCorrect())]: !!answer[0] && inInstructor, } )} className={cx(inputGroup, { [highlightAnswer(correct)]: type === 'reviewing', })} onChange={ onChange ? event => onChange(event.currentTarget.value) Loading @@ -127,7 +112,7 @@ const AutoFreeFormQuestion: FC<QuestionProps> = ({ {answer[0] ? answer[0].length : 0} </p> </div> {inInstructor && correctAnswer && ( {type === 'reviewing' && correctAnswer && ( <p className={cx( css` Loading frontend/src/components/Questionnaire/MultipleChoiceQuestion.tsx +32 −9 Original line number Diff line number Diff line import { Checkbox, Label } from '@blueprintjs/core' import { Checkbox, Classes, Label } from '@blueprintjs/core' import { css, cx } from '@emotion/css' import type { MultipleChoiceQuestionDetailsType } from '@inject/graphql' import { breakWord } from '@inject/shared' import type { FC } from 'react' import { RenderedContent } from '../RenderedContent' import { highlightCorrect } from './classes' import { correctOption, highlightAnswer } from './classes' import type { QuestionProps } from './types' const label = css` Loading @@ -20,6 +20,7 @@ const wrapper = css` const MultipleChoiceQuestion: FC<QuestionProps> = ({ answer, correct, inInstructor, question, exerciseId, Loading @@ -28,7 +29,7 @@ const MultipleChoiceQuestion: FC<QuestionProps> = ({ onChange, }) => { const details = question.details as MultipleChoiceQuestionDetailsType const { correctArray, labels } = details const { correctArray: correctIndexes, labels, exactMatch } = details return ( <> Loading @@ -39,15 +40,22 @@ const MultipleChoiceQuestion: FC<QuestionProps> = ({ inInstructor={inInstructor} renderedContent={question.content.rendered} /> <div className={wrapper}> <div className={cx( { [highlightAnswer(correct)]: type === 'reviewing', }, wrapper )} > {labels.map((label, index) => ( <Checkbox className={cx({ [highlightCorrect( !!correctArray.find( value => value === labels.indexOf(label) + 1 ) )]: type === 'reviewing' && inInstructor, [correctOption]: type === 'reviewing' && correctIndexes.some( index => labels.indexOf(label) + 1 === index ), })} checked={!!answer.find(val => val === (index + 1).toString())} key={label} Loading @@ -60,6 +68,21 @@ const MultipleChoiceQuestion: FC<QuestionProps> = ({ </Checkbox> ))} </div> {type === 'reviewing' && ( <p className={cx( css` padding-top: 0.6rem; margin: 0; `, Classes.TEXT_MUTED )} > {exactMatch ? 'All correct and no incorrect options must be selected' : 'At least one correct and no incorrect options must be selected'} </p> )} </Label> </> ) Loading frontend/src/components/Questionnaire/RadioButtonQuestion.tsx +12 −7 Original line number Diff line number Diff line import { Radio, RadioGroup } from '@blueprintjs/core' import { cx } from '@emotion/css' import type { RadioQuestionDetails } from '@inject/graphql' import { breakWord } from '@inject/shared' import { type FC } from 'react' import { RenderedContent } from '../RenderedContent' import { highlightCorrect } from './classes' import { correctOption, highlightAnswer } from './classes' import type { QuestionProps } from './types' const RadioButtonQuestion: FC<QuestionProps> = ({ type, question, answer, correct, disabled, onChange, inInstructor, exerciseId, }) => { const details = question.details as RadioQuestionDetails const { labels, correct } = details const { correct: correctIndex, labels } = details return ( <RadioGroup Loading @@ -33,15 +35,18 @@ const RadioButtonQuestion: FC<QuestionProps> = ({ onChange={ onChange ? event => onChange(event.currentTarget.value) : () => {} } className={cx({ [highlightAnswer(correct)]: type === 'reviewing', })} > {labels.map((label, index) => ( <Radio disabled={disabled || type === 'reviewing'} className={ type === 'reviewing' ? highlightCorrect(labels.indexOf(label) + 1 === correct) : undefined } className={cx({ [correctOption]: type === 'reviewing' && labels.indexOf(label) + 1 === correctIndex, })} key={label} value={(index + 1).toString()} label={label} Loading frontend/src/components/Questionnaire/classes.ts +32 −3 Original line number Diff line number Diff line import { Colors } from '@blueprintjs/colors' import { css } from '@emotion/css' import type { QuestionnaireAnswer } from '@inject/graphql' export const questionNumber = css` text-align: right; ` export const highlightCorrect = (correct: boolean) => css` export const correctOption = css` padding-bottom: 0.2rem; border-bottom: 0.2rem solid ${correct ? Colors.GREEN5 : 'transparent'}; border-bottom: 0.2rem solid ${Colors.GREEN5}; ` export const highlightAnswer = ( correct: QuestionnaireAnswer['correct'] | undefined ) => { if (!correct) { return css`` } const color = (() => { switch (correct) { case 'CORRECT': return Colors.GREEN5 case 'INCORRECT': return Colors.RED5 case 'PARTIALLY_CORRECT': return Colors.ORANGE5 case 'UNKNOWN': return 'transparent' } })() return css` border-bottom: 0.2rem solid ${color}; margin-bottom: 0.2rem; ` } export const contentWrapper = css` display: flex; column-gap: 0.3rem; ` export const lengthDisplay = css` margin: 0; position: absolute; z-index: 10; right: 0.3rem; bottom: 0.1rem; color: white; ` Loading
frontend/src/actionlog/InjectMessage/Content/TraineeQuestionnaireContent.tsx +9 −22 Original line number Diff line number Diff line Loading @@ -24,6 +24,8 @@ import type { NavigateOptions } from '@tanstack/react-router' import type { FC, FormEvent } from 'react' import { useCallback, useMemo, useState } from 'react' import Questionnaire from '../../../components/Questionnaire' import type { QuestionAndAnswer } from '../../../components/Questionnaire/types' import { mapQuestionsAndAnswers } from '../../../components/Questionnaire/utils' interface TraineeQuestionnaireContentProps { teamState: TeamQuestionnaireStateWithQuestionnaire Loading @@ -48,33 +50,15 @@ const AnswerState = ( teamStateId: string ) => createSyncedStore<{ questions: { question: Question answer: string[] error: string }[] questions: QuestionAndAnswer[] resetQuestions: () => void modifyAnswer: (questionId: string, answer: string) => void validate: () => boolean }>( set => ({ questions: questions.map(question => ({ question, answer: answers.find(answer => answer.question.id === question.id)?.answer || [], error: '', })), questions: mapQuestionsAndAnswers({ questions, answers }), resetQuestions: () => set({ questions: questions.map(question => ({ question, answer: answers.find(answer => answer.question.id === question.id) ?.answer || [], error: '', })), }), set({ questions: mapQuestionsAndAnswers({ questions, answers }) }), modifyAnswer: (questionId: string, answer: string) => set(({ questions }) => ({ questions: questions.map(question => { Loading Loading @@ -162,7 +146,10 @@ const TraineeQuestionnaireContent: FC<TraineeQuestionnaireContentProps> = ({ [answers, questions, teamStateId] ) const questionsAndAnswers = useStore(state, state => state.questions) const questionsAndAnswers: QuestionAndAnswer[] = useStore( state, state => state.questions ) const modifyAnswer = useStore(state, state => state.modifyAnswer) const validate = useStore(state, state => state.validate) Loading
frontend/src/components/Questionnaire/AutoFreeFormQuestion.tsx +13 −28 Original line number Diff line number Diff line import { Classes, Colors, InputGroup, Label, TextArea, type Intent, } from '@blueprintjs/core' import { css, cx } from '@emotion/css' import type { AutoFreeFormQuestionDetailsType } from '@inject/graphql' import type { AutoFreeFormQuestionDetails } from '@inject/graphql' import { breakWord } from '@inject/shared' import { useMemo, type FC } from 'react' import { RenderedContent } from '../RenderedContent' import { contentWrapper, lengthDisplay } from './classes' import { contentWrapper, highlightAnswer, lengthDisplay } from './classes' import QuestionDescription from './QuestionDescription' import type { QuestionProps } from './types' import { notInInterval } from './utils' Loading @@ -26,35 +25,28 @@ const textArea = css` padding-bottom: 1.5rem !important; ` const highlight = (correct: boolean) => css` border-bottom: 0.2rem solid ${correct ? Colors.GREEN5 : Colors.RED5}; const inputGroup = css` input { padding-right: 40px; } ` const AutoFreeFormQuestion: FC<QuestionProps> = ({ type, question, answer, correct, disabled: disabledProp, onChange, inInstructor, error, exerciseId, }) => { const details = question.details as AutoFreeFormQuestionDetailsType const details = question.details as AutoFreeFormQuestionDetails const { multiline, correctAnswer, min, max, regex } = details const disabled = disabledProp || type === 'reviewing' const isCorrect = () => { if (!correctAnswer) { return false } if (regex) { return new RegExp(correctAnswer).test(answer[0]) } return correctAnswer === answer[0] } const answerString = answer.at(0) || '' const intent: Intent = useMemo( () => Loading Loading @@ -97,7 +89,7 @@ const AutoFreeFormQuestion: FC<QuestionProps> = ({ } autoResize className={cx(textArea, { [highlight(isCorrect())]: !!answer[0] && inInstructor, [highlightAnswer(correct)]: type === 'reviewing', })} /> ) : ( Loading @@ -106,16 +98,9 @@ const AutoFreeFormQuestion: FC<QuestionProps> = ({ intent={intent} fill value={answer[0]} className={cx( css` input { padding-right: 40px; } `, { [highlight(isCorrect())]: !!answer[0] && inInstructor, } )} className={cx(inputGroup, { [highlightAnswer(correct)]: type === 'reviewing', })} onChange={ onChange ? event => onChange(event.currentTarget.value) Loading @@ -127,7 +112,7 @@ const AutoFreeFormQuestion: FC<QuestionProps> = ({ {answer[0] ? answer[0].length : 0} </p> </div> {inInstructor && correctAnswer && ( {type === 'reviewing' && correctAnswer && ( <p className={cx( css` Loading
frontend/src/components/Questionnaire/MultipleChoiceQuestion.tsx +32 −9 Original line number Diff line number Diff line import { Checkbox, Label } from '@blueprintjs/core' import { Checkbox, Classes, Label } from '@blueprintjs/core' import { css, cx } from '@emotion/css' import type { MultipleChoiceQuestionDetailsType } from '@inject/graphql' import { breakWord } from '@inject/shared' import type { FC } from 'react' import { RenderedContent } from '../RenderedContent' import { highlightCorrect } from './classes' import { correctOption, highlightAnswer } from './classes' import type { QuestionProps } from './types' const label = css` Loading @@ -20,6 +20,7 @@ const wrapper = css` const MultipleChoiceQuestion: FC<QuestionProps> = ({ answer, correct, inInstructor, question, exerciseId, Loading @@ -28,7 +29,7 @@ const MultipleChoiceQuestion: FC<QuestionProps> = ({ onChange, }) => { const details = question.details as MultipleChoiceQuestionDetailsType const { correctArray, labels } = details const { correctArray: correctIndexes, labels, exactMatch } = details return ( <> Loading @@ -39,15 +40,22 @@ const MultipleChoiceQuestion: FC<QuestionProps> = ({ inInstructor={inInstructor} renderedContent={question.content.rendered} /> <div className={wrapper}> <div className={cx( { [highlightAnswer(correct)]: type === 'reviewing', }, wrapper )} > {labels.map((label, index) => ( <Checkbox className={cx({ [highlightCorrect( !!correctArray.find( value => value === labels.indexOf(label) + 1 ) )]: type === 'reviewing' && inInstructor, [correctOption]: type === 'reviewing' && correctIndexes.some( index => labels.indexOf(label) + 1 === index ), })} checked={!!answer.find(val => val === (index + 1).toString())} key={label} Loading @@ -60,6 +68,21 @@ const MultipleChoiceQuestion: FC<QuestionProps> = ({ </Checkbox> ))} </div> {type === 'reviewing' && ( <p className={cx( css` padding-top: 0.6rem; margin: 0; `, Classes.TEXT_MUTED )} > {exactMatch ? 'All correct and no incorrect options must be selected' : 'At least one correct and no incorrect options must be selected'} </p> )} </Label> </> ) Loading
frontend/src/components/Questionnaire/RadioButtonQuestion.tsx +12 −7 Original line number Diff line number Diff line import { Radio, RadioGroup } from '@blueprintjs/core' import { cx } from '@emotion/css' import type { RadioQuestionDetails } from '@inject/graphql' import { breakWord } from '@inject/shared' import { type FC } from 'react' import { RenderedContent } from '../RenderedContent' import { highlightCorrect } from './classes' import { correctOption, highlightAnswer } from './classes' import type { QuestionProps } from './types' const RadioButtonQuestion: FC<QuestionProps> = ({ type, question, answer, correct, disabled, onChange, inInstructor, exerciseId, }) => { const details = question.details as RadioQuestionDetails const { labels, correct } = details const { correct: correctIndex, labels } = details return ( <RadioGroup Loading @@ -33,15 +35,18 @@ const RadioButtonQuestion: FC<QuestionProps> = ({ onChange={ onChange ? event => onChange(event.currentTarget.value) : () => {} } className={cx({ [highlightAnswer(correct)]: type === 'reviewing', })} > {labels.map((label, index) => ( <Radio disabled={disabled || type === 'reviewing'} className={ type === 'reviewing' ? highlightCorrect(labels.indexOf(label) + 1 === correct) : undefined } className={cx({ [correctOption]: type === 'reviewing' && labels.indexOf(label) + 1 === correctIndex, })} key={label} value={(index + 1).toString()} label={label} Loading
frontend/src/components/Questionnaire/classes.ts +32 −3 Original line number Diff line number Diff line import { Colors } from '@blueprintjs/colors' import { css } from '@emotion/css' import type { QuestionnaireAnswer } from '@inject/graphql' export const questionNumber = css` text-align: right; ` export const highlightCorrect = (correct: boolean) => css` export const correctOption = css` padding-bottom: 0.2rem; border-bottom: 0.2rem solid ${correct ? Colors.GREEN5 : 'transparent'}; border-bottom: 0.2rem solid ${Colors.GREEN5}; ` export const highlightAnswer = ( correct: QuestionnaireAnswer['correct'] | undefined ) => { if (!correct) { return css`` } const color = (() => { switch (correct) { case 'CORRECT': return Colors.GREEN5 case 'INCORRECT': return Colors.RED5 case 'PARTIALLY_CORRECT': return Colors.ORANGE5 case 'UNKNOWN': return 'transparent' } })() return css` border-bottom: 0.2rem solid ${color}; margin-bottom: 0.2rem; ` } export const contentWrapper = css` display: flex; column-gap: 0.3rem; ` export const lengthDisplay = css` margin: 0; position: absolute; z-index: 10; right: 0.3rem; bottom: 0.1rem; color: white; `