diff --git a/assets/icons/quiz-puzzle.svg b/assets/icons/quiz-puzzle.svg new file mode 100644 index 0000000000..2151f3400a --- /dev/null +++ b/assets/icons/quiz-puzzle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/src/js/v3/entries/course-builder/components/curriculum/Question.tsx b/assets/src/js/v3/entries/course-builder/components/curriculum/Question.tsx index 812950e3c6..a135ebe9d9 100644 --- a/assets/src/js/v3/entries/course-builder/components/curriculum/Question.tsx +++ b/assets/src/js/v3/entries/course-builder/components/curriculum/Question.tsx @@ -43,7 +43,7 @@ const questionTypeIconMap: Record { question_type: questionType, randomize_question: false, show_question_mark: false, + ...(questionType === 'draw_image' && { + draw_image_threshold_percent: 70, + }), + ...(questionType === 'puzzle' && { + puzzle_grid_size: 4, + }), }, } as QuizQuestion); setValidationError(null); diff --git a/assets/src/js/v3/entries/course-builder/components/curriculum/question-types/DrawImage.tsx b/assets/src/js/v3/entries/course-builder/components/curriculum/question-types/DrawImage.tsx index 1b22dc5b00..65bc74fdad 100644 --- a/assets/src/js/v3/entries/course-builder/components/curriculum/question-types/DrawImage.tsx +++ b/assets/src/js/v3/entries/course-builder/components/curriculum/question-types/DrawImage.tsx @@ -1,14 +1,11 @@ import { css } from '@emotion/react'; -import { __ } from '@wordpress/i18n'; -import { useEffect, useMemo } from 'react'; -import { Controller, useFieldArray, useFormContext } from 'react-hook-form'; +import { useEffect } from 'react'; +import { Controller, useController, useFieldArray, useFormContext } from 'react-hook-form'; import { useQuizModalContext } from '@CourseBuilderContexts/QuizModalContext'; import type { QuizForm } from '@CourseBuilderServices/quiz'; import FormDrawImage from '@TutorShared/components/fields/quiz/questions/FormDrawImage'; -import FormSelectInput from '@TutorShared/components/fields/FormSelectInput'; import { spacing } from '@TutorShared/config/styles'; -import { calculateQuizDataStatus } from '@TutorShared/utils/quiz'; import { styleUtils } from '@TutorShared/utils/style-utils'; import { QuizDataStatus, type QuizQuestionOption } from '@TutorShared/utils/types'; import { nanoid } from '@TutorShared/utils/util'; @@ -16,8 +13,6 @@ import { nanoid } from '@TutorShared/utils/util'; const DrawImage = () => { const form = useFormContext(); const { activeQuestionId, activeQuestionIndex, validationError, setValidationError } = useQuizModalContext(); - const activeQuestionDataStatus = - form.watch(`questions.${activeQuestionIndex}._data_status`) ?? QuizDataStatus.NO_CHANGE; const answersPath = `questions.${activeQuestionIndex}.question_answers` as 'questions.0.question_answers'; const thresholdPath = @@ -27,15 +22,11 @@ const DrawImage = () => { control: form.control, name: answersPath, }); - - const thresholdOptions = useMemo( - () => - [40, 50, 60, 70, 80, 90, 100].map((value) => ({ - label: `${value}%`, - value, - })), - [], - ); + const thresholdControllerProps = useController({ + control: form.control, + name: thresholdPath, + defaultValue: 70, + }); // Ensure there is always a single option for this question type. useEffect(() => { @@ -62,14 +53,6 @@ const DrawImage = () => { form.setValue(answersPath, [baseAnswer]); }, [activeQuestionId, optionsFields.length, answersPath, form]); - // Default threshold for draw-image questions if not set. - useEffect(() => { - const currentValue = form.getValues(thresholdPath); - if (currentValue === undefined || currentValue === null || Number.isNaN(Number(currentValue))) { - form.setValue(thresholdPath, 70); - } - }, [form, thresholdPath]); - // Only render Controller when the value exists to ensure field.value is always defined if (optionsFields.length === 0) { return null; @@ -82,37 +65,13 @@ const DrawImage = () => { control={form.control} name={`questions.${activeQuestionIndex}.question_answers.0` as 'questions.0.question_answers.0'} render={(answerControllerProps) => ( - ( - { - thresholdControllerProps.field.onChange(option.value); - if (calculateQuizDataStatus(activeQuestionDataStatus, QuizDataStatus.UPDATE)) { - form.setValue( - `questions.${activeQuestionIndex}._data_status`, - calculateQuizDataStatus(activeQuestionDataStatus, QuizDataStatus.UPDATE) as QuizDataStatus, - ); - } - }} - /> - } - /> - )} + )} /> diff --git a/assets/src/js/v3/entries/course-builder/components/curriculum/question-types/Puzzle.tsx b/assets/src/js/v3/entries/course-builder/components/curriculum/question-types/Puzzle.tsx index 255b02b05d..b66171506c 100644 --- a/assets/src/js/v3/entries/course-builder/components/curriculum/question-types/Puzzle.tsx +++ b/assets/src/js/v3/entries/course-builder/components/curriculum/question-types/Puzzle.tsx @@ -1,5 +1,4 @@ import { css } from '@emotion/react'; -import { useEffect } from 'react'; import { Controller, useFieldArray, useFormContext } from 'react-hook-form'; import { useQuizModalContext } from '@CourseBuilderContexts/QuizModalContext'; @@ -13,22 +12,11 @@ const Puzzle = () => { const { activeQuestionId, activeQuestionIndex, validationError, setValidationError } = useQuizModalContext(); const answersPath = `questions.${activeQuestionIndex}.question_answers` as 'questions.0.question_answers'; - const gridSizePath = - `questions.${activeQuestionIndex}.question_settings.puzzle_grid_size` as 'questions.0.question_settings.puzzle_grid_size'; - const { fields: optionsFields } = useFieldArray({ control: form.control, name: answersPath, }); - useEffect(() => { - const currentValue = form.getValues(gridSizePath); - const validGridSizes = [2, 3, 4, 5, 6, 7]; - if (!validGridSizes.includes(Number(currentValue))) { - form.setValue(gridSizePath, 4); - } - }, [form, gridSizePath]); - if (optionsFields.length === 0) { return null; } diff --git a/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/ContentListTable.tsx b/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/ContentListTable.tsx index f62243dedd..dcecb3b074 100644 --- a/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/ContentListTable.tsx +++ b/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/ContentListTable.tsx @@ -100,14 +100,20 @@ const ContentListTable = () => { const columns: Column[] = [ { Header: totalItems ? ( - 0 && !handleAllIsChecked() && selectedContents.length > 0} - aria-label={__('Select all contents', 'tutor')} - /> +
event.stopPropagation()} + onMouseDown={(event) => event.stopPropagation()} + css={styles.headerCheckboxWrapper} + > + 0 && !handleAllIsChecked() && selectedContents.length > 0} + aria-label={__('Select all contents', 'tutor')} + /> +
) : ( __('# Title', 'tutor') ), @@ -316,6 +322,10 @@ const styles = { ${typography.small('medium')}; color: ${colorTokens.text.hints}; `, + headerCheckboxWrapper: css` + display: flex; + align-items: center; + `, checkboxLabel: css` ${typography.caption('medium')}; color: ${colorTokens.text.primary}; diff --git a/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/FilterFields.tsx b/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/FilterFields.tsx index 223f8c9857..dd506dccbd 100644 --- a/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/FilterFields.tsx +++ b/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/FilterFields.tsx @@ -90,6 +90,11 @@ const FilterFields = ({ onFilterChange, initialValues, type }: FilterFieldsProps value: 'ordering', icon: 'quizOrdering', }, + { + label: __('Puzzle', 'tutor'), + value: 'puzzle', + icon: 'quizPuzzle', + }, ]; return ( diff --git a/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/QuestionListTable.tsx b/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/QuestionListTable.tsx index 5dfff7d9ff..5add5cf57e 100644 --- a/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/QuestionListTable.tsx +++ b/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/QuestionListTable.tsx @@ -3,6 +3,8 @@ import { __, _n, sprintf } from '@wordpress/i18n'; import { useMemo, useState } from 'react'; import { useFormContext } from 'react-hook-form'; +import { tutorConfig } from '@TutorShared/config/config'; + import { type ContentSelectionForm } from '@CourseBuilderComponents/modals/ContentBankContentSelectModal'; import Checkbox from '@TutorShared/atoms/CheckBox'; import { LoadingSection } from '@TutorShared/atoms/LoadingSpinner'; @@ -23,6 +25,105 @@ import SearchField from './SearchField'; type SortDirection = 'asc' | 'desc'; +type QuestionTypeOption = { + label: string; + value: QuizQuestionType; + icon: IconCollection; + isPro: boolean; +}; + +const ALL_QUESTION_TYPE_OPTIONS: QuestionTypeOption[] = [ + { + label: __('True/False', 'tutor'), + value: 'true_false', + icon: 'quizTrueFalse', + isPro: false, + }, + { + label: __('Multiple Choice', 'tutor'), + value: 'multiple_choice', + icon: 'quizMultiChoice', + isPro: false, + }, + { + label: __('Open Ended/Essay', 'tutor'), + value: 'open_ended', + icon: 'quizEssay', + isPro: false, + }, + { + label: __('Fill in the Blanks', 'tutor'), + value: 'fill_in_the_blank', + icon: 'quizFillInTheBlanks', + isPro: false, + }, + { + label: __('Short Answer', 'tutor'), + value: 'short_answer', + icon: 'quizShortAnswer', + isPro: true, + }, + { + label: __('Matching', 'tutor'), + value: 'matching', + icon: 'quizImageMatching', + isPro: true, + }, + { + label: __('Image Answering', 'tutor'), + value: 'image_answering', + icon: 'quizImageAnswer', + isPro: true, + }, + { + label: __('Ordering', 'tutor'), + value: 'ordering', + icon: 'quizOrdering', + isPro: true, + }, + { + label: __('Mark in the Image', 'tutor'), + value: 'draw_image', + icon: 'quizMarkInTheImage', + isPro: true, + }, + { + label: __('Range', 'tutor'), + value: 'scale', + icon: 'quizRange', + isPro: true, + }, + { + label: __('Pin', 'tutor'), + value: 'pin_image', + icon: 'quizPin', + isPro: true, + }, + { + label: __('Graph', 'tutor'), + value: 'coordinates', + icon: 'quizGraph', + isPro: true, + }, + { + label: __('Puzzle', 'tutor'), + value: 'puzzle', + icon: 'quizPuzzle', + isPro: true, + }, +]; + +const questionTypeOptions = tutorConfig.is_legacy_learning_mode + ? ALL_QUESTION_TYPE_OPTIONS.filter( + (option) => + option.value !== 'draw_image' && + option.value !== 'pin_image' && + option.value !== 'scale' && + option.value !== 'coordinates' && + option.value !== 'puzzle', + ) + : ALL_QUESTION_TYPE_OPTIONS; + const QuestionListTable = () => { const { pageInfo, onPageChange, itemsPerPage, onFilterItems } = usePaginatedTable(); const form = useFormContext(); @@ -82,73 +183,23 @@ const QuestionListTable = () => { }); }; - const questionTypeOptions: { - label: string; - value: QuizQuestionType; - icon: IconCollection; - isPro: boolean; - }[] = [ - { - label: __('True/False', 'tutor'), - value: 'true_false', - icon: 'quizTrueFalse', - isPro: false, - }, - { - label: __('Multiple Choice', 'tutor'), - value: 'multiple_choice', - icon: 'quizMultiChoice', - isPro: false, - }, - { - label: __('Open Ended/Essay', 'tutor'), - value: 'open_ended', - icon: 'quizEssay', - isPro: false, - }, - { - label: __('Fill in the Blanks', 'tutor'), - value: 'fill_in_the_blank', - icon: 'quizFillInTheBlanks', - isPro: false, - }, - { - label: __('Short Answer', 'tutor'), - value: 'short_answer', - icon: 'quizShortAnswer', - isPro: true, - }, - { - label: __('Matching', 'tutor'), - value: 'matching', - icon: 'quizImageMatching', - isPro: true, - }, - { - label: __('Image Answering', 'tutor'), - value: 'image_answering', - icon: 'quizImageAnswer', - isPro: true, - }, - { - label: __('Ordering', 'tutor'), - value: 'ordering', - icon: 'quizOrdering', - isPro: true, - }, - ]; - const columns: Column[] = [ { Header: totalItems ? ( - 0 && !handleAllIsChecked() && selectedContents.length > 0} - aria-label={__('Select all questions', 'tutor')} - /> +
event.stopPropagation()} + onMouseDown={(event) => event.stopPropagation()} + css={styles.headerCheckboxWrapper} + > + 0 && !handleAllIsChecked() && selectedContents.length > 0} + aria-label={__('Select all questions', 'tutor')} + /> +
) : ( __('# Title', 'tutor') ), @@ -341,6 +392,10 @@ const styles = { ${typography.small('regular')}; color: ${colorTokens.text.hints}; `, + headerCheckboxWrapper: css` + display: flex; + align-items: center; + `, checkboxLabel: css` ${styleUtils.display.flex()}; align-items: center; diff --git a/assets/src/js/v3/entries/course-builder/services/quiz.ts b/assets/src/js/v3/entries/course-builder/services/quiz.ts index a46055dbc3..f6c0509a44 100644 --- a/assets/src/js/v3/entries/course-builder/services/quiz.ts +++ b/assets/src/js/v3/entries/course-builder/services/quiz.ts @@ -10,7 +10,7 @@ import { Addons } from '@TutorShared/config/constants'; import { wpAjaxInstance } from '@TutorShared/utils/api'; import endpoints from '@TutorShared/utils/endpoints'; import type { ErrorResponse } from '@TutorShared/utils/form'; -import { convertedQuestion } from '@TutorShared/utils/quiz'; +import { convertedQuestion, normalizeSingleMaskQuestionAnswers } from '@TutorShared/utils/quiz'; import { isDefined, QuizDataStatus, @@ -377,38 +377,6 @@ export const convertQuizFormDataToPayload = ( }; }; -const normalizeSingleMaskQuestionAnswers = (questions: QuizQuestion[], deletedAnswerIds: ID[] = []) => { - const deletedAnswerIdsSet = new Set(deletedAnswerIds); - - const normalizedQuestions = questions.map((question) => { - if (question.question_type !== 'draw_image' && question.question_type !== 'pin_image') { - return question; - } - - const answers = Array.isArray(question.question_answers) ? question.question_answers : []; - if (answers.length <= 1) { - return question; - } - - const [keptAnswer, ...extraAnswers] = answers; - extraAnswers.forEach((answer) => { - if (answer._data_status !== QuizDataStatus.NEW && answer.answer_id) { - deletedAnswerIdsSet.add(answer.answer_id); - } - }); - - return { - ...question, - question_answers: keptAnswer ? [keptAnswer] : [], - }; - }); - - return { - normalizedQuestions, - deletedAnswerIds: Array.from(deletedAnswerIdsSet), - }; -}; - export const convertQuizQuestionFormDataToPayloadForUpdate = (data: QuizQuestion): QuizUpdateQuestionPayload => { return { question_id: data.question_id, diff --git a/assets/src/js/v3/shared/components/fields/FormSelectInput.tsx b/assets/src/js/v3/shared/components/fields/FormSelectInput.tsx index e8add0e344..494d4b7bae 100644 --- a/assets/src/js/v3/shared/components/fields/FormSelectInput.tsx +++ b/assets/src/js/v3/shared/components/fields/FormSelectInput.tsx @@ -131,11 +131,11 @@ const FormSelectInput = ({ event?.stopPropagation(); if (!option.disabled) { + setIsOpen(false); + setIsSearching(false); + setSearchText(''); field.onChange(option.value); onChange(option); - setSearchText(''); - setIsSearching(false); - setIsOpen(false); } }; diff --git a/assets/src/js/v3/shared/components/fields/quiz/questions/FormDrawImage.tsx b/assets/src/js/v3/shared/components/fields/quiz/questions/FormDrawImage.tsx index 2c4302d900..209de3844a 100644 --- a/assets/src/js/v3/shared/components/fields/quiz/questions/FormDrawImage.tsx +++ b/assets/src/js/v3/shared/components/fields/quiz/questions/FormDrawImage.tsx @@ -1,10 +1,12 @@ import { css } from '@emotion/react'; import { __ } from '@wordpress/i18n'; import { useCallback, useEffect, useRef, useState } from 'react'; +import { useFormContext } from 'react-hook-form'; import Button from '@TutorShared/atoms/Button'; import ImageInput from '@TutorShared/atoms/ImageInput'; import SVGIcon from '@TutorShared/atoms/SVGIcon'; +import FormSelectInput from '@TutorShared/components/fields/FormSelectInput'; import { borderRadius, Breakpoint, colorTokens, spacing } from '@TutorShared/config/styles'; import { typography } from '@TutorShared/config/typography'; @@ -27,6 +29,7 @@ const LASSO_MIN_POINT_DISTANCE = 4; interface FormDrawImageProps extends FormControllerProps { questionId: ID; + activeQuestionIndex?: number; validationError?: { message: string; type: QuizValidationErrorType; @@ -37,11 +40,23 @@ interface FormDrawImageProps extends FormControllerProps { type: QuizValidationErrorType; } | null> >; - precisionControl?: React.ReactNode; + precisionControllerProps?: FormControllerProps; } -const FormDrawImage = ({ field, precisionControl }: FormDrawImageProps) => { +const THRESHOLD_OPTIONS = [40, 50, 60, 70, 80, 90, 100].map((value) => ({ + label: `${value}%`, + value, +})); + +const FormDrawImage = ({ field, precisionControllerProps, activeQuestionIndex = 0 }: FormDrawImageProps) => { + const form = useFormContext(); const option = field.value; + const resolvedQuestionDataStatusPath = Array.isArray(form?.getValues?.('questions')) + ? (`questions.${activeQuestionIndex}._data_status` as const) + : ('_data_status' as const); + const activeQuestionDataStatus = form + ? ((form.watch(resolvedQuestionDataStatusPath) as QuizDataStatus | undefined) ?? QuizDataStatus.NO_CHANGE) + : QuizDataStatus.NO_CHANGE; const [isDrawModeActive, setIsDrawModeActive] = useState(false); const [hasStartedLassoDraw, setHasStartedLassoDraw] = useState(false); @@ -530,7 +545,29 @@ const FormDrawImage = ({ field, precisionControl }: FormDrawImageProps) => { aria-label={__('Draw a lasso around the correct answer area', __TUTOR_TEXT_DOMAIN__)} /> - {precisionControl &&
{precisionControl}
} + {precisionControllerProps && ( + { + precisionControllerProps.field.onChange(option.value); + if (!form) { + return; + } + if (calculateQuizDataStatus(activeQuestionDataStatus, QuizDataStatus.UPDATE)) { + form.setValue( + resolvedQuestionDataStatusPath, + calculateQuizDataStatus(activeQuestionDataStatus, QuizDataStatus.UPDATE) as QuizDataStatus, + ); + } + }} + /> + )}

{__('Answer zone saved. Students will be graded against this area.', __TUTOR_TEXT_DOMAIN__)} diff --git a/assets/src/js/v3/shared/components/fields/quiz/questions/FormPuzzle.tsx b/assets/src/js/v3/shared/components/fields/quiz/questions/FormPuzzle.tsx index 0d2719a031..db0c0b1ea2 100644 --- a/assets/src/js/v3/shared/components/fields/quiz/questions/FormPuzzle.tsx +++ b/assets/src/js/v3/shared/components/fields/quiz/questions/FormPuzzle.tsx @@ -3,7 +3,6 @@ import { __ } from '@wordpress/i18n'; import { useCallback, useMemo } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; -import type { QuizForm } from '@CourseBuilderServices/quiz'; import ImageInput from '@TutorShared/atoms/ImageInput'; import FormSelectInput from '@TutorShared/components/fields/FormSelectInput'; import Show from '@TutorShared/controls/Show'; @@ -22,7 +21,7 @@ import { typography } from '@TutorShared/config/typography'; interface FormPuzzleProps extends FormControllerProps { questionId: ID; - activeQuestionIndex: number; + activeQuestionIndex?: number; validationError?: { message: string; type: QuizValidationErrorType; @@ -33,16 +32,21 @@ interface FormPuzzleProps extends FormControllerProps { type: QuizValidationErrorType; } | null> >; + gridSizeControllerProps?: FormControllerProps; + gridSizePath?: string; } -const FormPuzzle = ({ field, activeQuestionIndex }: FormPuzzleProps) => { - const form = useFormContext(); +const FormPuzzle = ({ field, activeQuestionIndex = 0, gridSizeControllerProps, gridSizePath }: FormPuzzleProps) => { + const form = useFormContext(); const option = field.value; - const activeQuestionDataStatus = - form.watch(`questions.${activeQuestionIndex}._data_status`) ?? QuizDataStatus.NO_CHANGE; - const gridSizePath = - `questions.${activeQuestionIndex}.question_settings.puzzle_grid_size` as 'questions.0.question_settings.puzzle_grid_size'; - + const resolvedGridSizePath = + gridSizePath ?? (`questions.${activeQuestionIndex}.question_settings.puzzle_grid_size` as const); + const resolvedQuestionDataStatusPath = Array.isArray(form?.getValues?.('questions')) + ? (`questions.${activeQuestionIndex}._data_status` as const) + : ('_data_status' as const); + const activeQuestionDataStatus = form + ? ((form.watch(resolvedQuestionDataStatusPath) as QuizDataStatus | undefined) ?? QuizDataStatus.NO_CHANGE) + : QuizDataStatus.NO_CHANGE; const gridSizeOptions = useMemo( () => [ @@ -142,28 +146,48 @@ const FormPuzzle = ({ field, activeQuestionIndex }: FormPuzzleProps) => {

- ( - { - gridSizeControllerProps.field.onChange(selectedOption.value); - if (calculateQuizDataStatus(activeQuestionDataStatus, QuizDataStatus.UPDATE)) { - form.setValue( - `questions.${activeQuestionIndex}._data_status`, - calculateQuizDataStatus(activeQuestionDataStatus, QuizDataStatus.UPDATE) as QuizDataStatus, - ); - } - }} - /> - )} - /> + {gridSizeControllerProps ? ( + { + gridSizeControllerProps.field.onChange(selectedOption.value); + if (calculateQuizDataStatus(activeQuestionDataStatus, QuizDataStatus.UPDATE)) { + form.setValue( + resolvedQuestionDataStatusPath, + calculateQuizDataStatus(activeQuestionDataStatus, QuizDataStatus.UPDATE) as QuizDataStatus, + ); + } + }} + /> + ) : ( + ( + { + gridSizeControllerProps.field.onChange(selectedOption.value); + if (calculateQuizDataStatus(activeQuestionDataStatus, QuizDataStatus.UPDATE)) { + form.setValue( + resolvedQuestionDataStatusPath, + calculateQuizDataStatus(activeQuestionDataStatus, QuizDataStatus.UPDATE) as QuizDataStatus, + ); + } + }} + /> + )} + /> + )}
diff --git a/assets/src/js/v3/shared/icons/types.ts b/assets/src/js/v3/shared/icons/types.ts index 85bbdfbcd1..2d764c5c6e 100644 --- a/assets/src/js/v3/shared/icons/types.ts +++ b/assets/src/js/v3/shared/icons/types.ts @@ -308,6 +308,7 @@ export const icons = [ 'quizH5p', 'quizImageAnswer', 'quizImageMatching', + 'quizPuzzle', 'quizMarkInTheImage', 'quizMultiChoice', 'quizNumber', diff --git a/assets/src/js/v3/shared/utils/quiz.ts b/assets/src/js/v3/shared/utils/quiz.ts index 575e95741f..19b4c63fd3 100644 --- a/assets/src/js/v3/shared/utils/quiz.ts +++ b/assets/src/js/v3/shared/utils/quiz.ts @@ -1,5 +1,6 @@ import { QuizDataStatus, + type ID, type QuizQuestion, type QuizQuestionOption, type QuizValidationErrorType, @@ -294,3 +295,35 @@ export const convertedQuestion = (question: Omit): } as QuizQuestion; } }; + +export const normalizeSingleMaskQuestionAnswers = (questions: QuizQuestion[], deletedAnswerIds: ID[] = []) => { + const deletedAnswerIdsSet = new Set(deletedAnswerIds); + + const normalizedQuestions = questions.map((question) => { + if (question.question_type !== 'draw_image' && question.question_type !== 'pin_image') { + return question; + } + + const answers = Array.isArray(question.question_answers) ? question.question_answers : []; + if (answers.length <= 1) { + return question; + } + + const [keptAnswer, ...extraAnswers] = answers; + extraAnswers.forEach((answer) => { + if (answer._data_status !== QuizDataStatus.NEW && answer.answer_id) { + deletedAnswerIdsSet.add(answer.answer_id); + } + }); + + return { + ...question, + question_answers: keptAnswer ? [keptAnswer] : [], + }; + }); + + return { + normalizedQuestions, + deletedAnswerIds: Array.from(deletedAnswerIdsSet), + }; +};