From a8de0f00adcb207caffb8b26bb59dd380509fc64 Mon Sep 17 00:00:00 2001 From: Sadman Soumique Date: Wed, 15 Apr 2026 12:04:15 +0600 Subject: [PATCH 01/12] feat: enhance QuestionListTable with additional quiz question types - Added new quiz question types: Mark in the Image, Range, Pin, and Graph. - Integrated conditional rendering based on legacy learning mode configuration. - Refactored question types to utilize useMemo for performance optimization. --- .../QuestionListTable.tsx | 144 ++++++++++++------ 1 file changed, 94 insertions(+), 50 deletions(-) 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..fce509122f 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'; @@ -87,56 +89,98 @@ const QuestionListTable = () => { 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, - }, - ]; + }[] = useMemo(() => { + const all: { + 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, + }, + { + 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, + }, + ]; + + if (tutorConfig.is_legacy_learning_mode) { + return all.filter( + (option) => + option.value !== 'draw_image' && + option.value !== 'pin_image' && + option.value !== 'scale' && + option.value !== 'coordinates', + ); + } + return all; + }, []); const columns: Column[] = [ { From a542ad2338355aa27f6d30de28c0cfb629b8cdf0 Mon Sep 17 00:00:00 2001 From: Sadman Soumique Date: Thu, 16 Apr 2026 13:19:21 +0600 Subject: [PATCH 02/12] =?UTF-8?q?Refactor(=F0=9F=9B=A0)=20:=20Move=20norma?= =?UTF-8?q?lizeSingleMaskQuestionAnswers=20function=20to=20shared=20utils?= =?UTF-8?q?=20and=20update=20quiz=20service=20to=20use=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entries/course-builder/services/quiz.ts | 34 +------------------ assets/src/js/v3/shared/utils/quiz.ts | 33 ++++++++++++++++++ 2 files changed, 34 insertions(+), 33 deletions(-) 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 7d623c0bab..b1d07a2270 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, @@ -373,38 +373,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/utils/quiz.ts b/assets/src/js/v3/shared/utils/quiz.ts index ebd17a2ce9..7d1c4bc02f 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, @@ -281,3 +282,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), + }; +}; From cc3987aad4cb257bc4501f51bda908189d0cf7cb Mon Sep 17 00:00:00 2001 From: Sadman Soumique Date: Thu, 16 Apr 2026 13:50:56 +0600 Subject: [PATCH 03/12] =?UTF-8?q?Enhance(=F0=9F=8E=A8)=20:=20Refactor=20Dr?= =?UTF-8?q?awImage=20component=20to=20streamline=20precision=20control=20a?= =?UTF-8?q?nd=20update=20QuestionListTable=20with=20new=20question=20type?= =?UTF-8?q?=20options?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../curriculum/question-types/DrawImage.tsx | 43 +--- .../QuestionListTable.tsx | 190 +++++++++--------- .../fields/quiz/questions/FormDrawImage.tsx | 33 ++- 3 files changed, 133 insertions(+), 133 deletions(-) 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..6e74310168 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,12 +1,10 @@ import { css } from '@emotion/react'; -import { __ } from '@wordpress/i18n'; -import { useEffect, useMemo } from 'react'; +import { useEffect } from 'react'; import { Controller, 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'; @@ -28,15 +26,6 @@ const DrawImage = () => { name: answersPath, }); - const thresholdOptions = useMemo( - () => - [40, 50, 60, 70, 80, 90, 100].map((value) => ({ - label: `${value}%`, - value, - })), - [], - ); - // Ensure there is always a single option for this question type. useEffect(() => { if (!activeQuestionId) { @@ -91,26 +80,16 @@ const DrawImage = () => { questionId={activeQuestionId} validationError={validationError} setValidationError={setValidationError} - precisionControl={ - { - thresholdControllerProps.field.onChange(option.value); - if (calculateQuizDataStatus(activeQuestionDataStatus, QuizDataStatus.UPDATE)) { - form.setValue( - `questions.${activeQuestionIndex}._data_status`, - calculateQuizDataStatus(activeQuestionDataStatus, QuizDataStatus.UPDATE) as QuizDataStatus, - ); - } - }} - /> - } + precisionControllerProps={thresholdControllerProps} + precisionTextDomain={'tutor'} + onPrecisionChange={() => { + 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/modals/ContentBankContentSelectModal/QuestionListTable.tsx b/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/QuestionListTable.tsx index fce509122f..2f20f2fa6e 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 @@ -25,6 +25,98 @@ 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, + }, +]; + +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', + ) + : ALL_QUESTION_TYPE_OPTIONS; + const QuestionListTable = () => { const { pageInfo, onPageChange, itemsPerPage, onFilterItems } = usePaginatedTable(); const form = useFormContext(); @@ -84,104 +176,6 @@ const QuestionListTable = () => { }); }; - const questionTypeOptions: { - label: string; - value: QuizQuestionType; - icon: IconCollection; - isPro: boolean; - }[] = useMemo(() => { - const all: { - 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, - }, - { - 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, - }, - ]; - - if (tutorConfig.is_legacy_learning_mode) { - return all.filter( - (option) => - option.value !== 'draw_image' && - option.value !== 'pin_image' && - option.value !== 'scale' && - option.value !== 'coordinates', - ); - } - return all; - }, []); - const columns: Column[] = [ { Header: totalItems ? ( 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 55f0e0009f..cae18ab696 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 @@ -5,6 +5,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; 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'; @@ -37,10 +38,22 @@ interface FormDrawImageProps extends FormControllerProps { type: QuizValidationErrorType; } | null> >; - precisionControl?: React.ReactNode; + precisionControllerProps?: FormControllerProps; + precisionTextDomain?: string; + onPrecisionChange?: (value: number) => void; } -const FormDrawImage = ({ field, precisionControl }: FormDrawImageProps) => { +const THRESHOLD_OPTIONS = [40, 50, 60, 70, 80, 90, 100].map((value) => ({ + label: `${value}%`, + value, +})); + +const FormDrawImage = ({ + field, + precisionControllerProps, + precisionTextDomain, + onPrecisionChange, +}: FormDrawImageProps) => { const option = field.value; const [isDrawModeActive, setIsDrawModeActive] = useState(false); @@ -530,7 +543,21 @@ const FormDrawImage = ({ field, precisionControl }: FormDrawImageProps) => { aria-label={__('Draw a lasso around the correct answer area', __TUTOR_TEXT_DOMAIN__)} /> - {precisionControl &&
{precisionControl}
} + {precisionControllerProps && precisionTextDomain && ( + { + precisionControllerProps.field.onChange(option.value); + onPrecisionChange?.(option.value); + }} + /> + )}

{__('Answer zone saved. Students will be graded against this area.', __TUTOR_TEXT_DOMAIN__)} From 88f96a9b961794669684089c30ae81d0f5f36108 Mon Sep 17 00:00:00 2001 From: Sadman Soumique Date: Thu, 16 Apr 2026 15:56:16 +0600 Subject: [PATCH 04/12] =?UTF-8?q?Enhance(=F0=9F=8E=A8)=20:=20Improve=20che?= =?UTF-8?q?ckbox=20interaction=20in=20ContentListTable=20and=20QuestionLis?= =?UTF-8?q?tTable=20by=20preventing=20event=20propagation=20for=20better?= =?UTF-8?q?=20user=20experience?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ContentListTable.tsx | 26 +++++++++++++------ .../QuestionListTable.tsx | 26 +++++++++++++------ 2 files changed, 36 insertions(+), 16 deletions(-) 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/QuestionListTable.tsx b/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/QuestionListTable.tsx index 2f20f2fa6e..cf4966456f 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 @@ -179,14 +179,20 @@ const QuestionListTable = () => { 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') ), @@ -379,6 +385,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; From 61817be2083021b4f3808cd6215b1adce4531716 Mon Sep 17 00:00:00 2001 From: Sadman Soumique Date: Fri, 17 Apr 2026 13:22:01 +0600 Subject: [PATCH 05/12] =?UTF-8?q?Enhance(=F0=9F=8E=A8)=20:=20Update=20Draw?= =?UTF-8?q?Image=20component=20to=20improve=20data=20status=20handling=20f?= =?UTF-8?q?or=20quiz=20questions,=20ensuring=20accurate=20state=20updates?= =?UTF-8?q?=20during=20precision=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/curriculum/question-types/DrawImage.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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 6e74310168..907193ecc7 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 @@ -16,6 +16,8 @@ const DrawImage = () => { const { activeQuestionId, activeQuestionIndex, validationError, setValidationError } = useQuizModalContext(); const activeQuestionDataStatus = form.watch(`questions.${activeQuestionIndex}._data_status`) ?? QuizDataStatus.NO_CHANGE; + const updatedQuestionDataStatus = calculateQuizDataStatus(activeQuestionDataStatus, QuizDataStatus.UPDATE); + const nextQuestionDataStatus = updatedQuestionDataStatus as QuizDataStatus; const answersPath = `questions.${activeQuestionIndex}.question_answers` as 'questions.0.question_answers'; const thresholdPath = @@ -83,11 +85,8 @@ const DrawImage = () => { precisionControllerProps={thresholdControllerProps} precisionTextDomain={'tutor'} onPrecisionChange={() => { - if (calculateQuizDataStatus(activeQuestionDataStatus, QuizDataStatus.UPDATE)) { - form.setValue( - `questions.${activeQuestionIndex}._data_status`, - calculateQuizDataStatus(activeQuestionDataStatus, QuizDataStatus.UPDATE) as QuizDataStatus, - ); + if (updatedQuestionDataStatus) { + form.setValue(`questions.${activeQuestionIndex}._data_status`, nextQuestionDataStatus); } }} /> From e821ee02b5a57e8db4e75ff4790bc15d7d69b573 Mon Sep 17 00:00:00 2001 From: Sadman Soumique Date: Mon, 20 Apr 2026 16:05:27 +0600 Subject: [PATCH 06/12] Enhancement: Add new SVG icon for puzzle question type and update related components to use the new icon, improving visual consistency across the course builder. --- assets/icons/quiz-puzzle.svg | 9 +++++++++ .../course-builder/components/curriculum/Question.tsx | 2 +- .../components/curriculum/QuestionConditions.tsx | 2 +- .../components/curriculum/QuestionList.tsx | 2 +- .../ContentBankContentSelectModal/FilterFields.tsx | 5 +++++ .../ContentBankContentSelectModal/QuestionListTable.tsx | 9 ++++++++- assets/src/js/v3/shared/icons/types.ts | 1 + 7 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 assets/icons/quiz-puzzle.svg diff --git a/assets/icons/quiz-puzzle.svg b/assets/icons/quiz-puzzle.svg new file mode 100644 index 0000000000..e9e3445576 --- /dev/null +++ b/assets/icons/quiz-puzzle.svg @@ -0,0 +1,9 @@ + + + + + + + + + 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 Date: Mon, 20 Apr 2026 16:27:44 +0600 Subject: [PATCH 07/12] Enhancement: Refactor DrawImage question type to improve state management by removing unnecessary hooks and integrating question data status handling for better performance and maintainability. --- .../curriculum/question-types/DrawImage.tsx | 11 +--------- .../fields/quiz/questions/FormDrawImage.tsx | 21 ++++++++++++++++--- 2 files changed, 19 insertions(+), 13 deletions(-) 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 907193ecc7..6d8d372e65 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 @@ -6,7 +6,6 @@ import { useQuizModalContext } from '@CourseBuilderContexts/QuizModalContext'; import type { QuizForm } from '@CourseBuilderServices/quiz'; import FormDrawImage from '@TutorShared/components/fields/quiz/questions/FormDrawImage'; 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'; @@ -14,10 +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 updatedQuestionDataStatus = calculateQuizDataStatus(activeQuestionDataStatus, QuizDataStatus.UPDATE); - const nextQuestionDataStatus = updatedQuestionDataStatus as QuizDataStatus; const answersPath = `questions.${activeQuestionIndex}.question_answers` as 'questions.0.question_answers'; const thresholdPath = @@ -84,11 +79,7 @@ const DrawImage = () => { setValidationError={setValidationError} precisionControllerProps={thresholdControllerProps} precisionTextDomain={'tutor'} - onPrecisionChange={() => { - if (updatedQuestionDataStatus) { - form.setValue(`questions.${activeQuestionIndex}._data_status`, nextQuestionDataStatus); - } - }} + questionDataStatusPath={`questions.${activeQuestionIndex}._data_status`} /> )} /> 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 09f955e9a2..aac42826d8 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,6 +1,7 @@ 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'; @@ -40,7 +41,7 @@ interface FormDrawImageProps extends FormControllerProps { >; precisionControllerProps?: FormControllerProps; precisionTextDomain?: string; - onPrecisionChange?: (value: number) => void; + questionDataStatusPath?: string; } const THRESHOLD_OPTIONS = [40, 50, 60, 70, 80, 90, 100].map((value) => ({ @@ -52,8 +53,9 @@ const FormDrawImage = ({ field, precisionControllerProps, precisionTextDomain, - onPrecisionChange, + questionDataStatusPath, }: FormDrawImageProps) => { + const form = useFormContext(); const option = field.value; const [isDrawModeActive, setIsDrawModeActive] = useState(false); @@ -481,6 +483,10 @@ const FormDrawImage = ({ return null; } + const currentQuestionDataStatus = questionDataStatusPath + ? ((form.watch(questionDataStatusPath) as QuizDataStatus | undefined) ?? QuizDataStatus.NO_CHANGE) + : null; + const canClearSelection = hasStartedLassoDraw || Boolean(option?.answer_two_gap_match); return ( @@ -554,7 +560,16 @@ const FormDrawImage = ({ )} onChange={(option) => { precisionControllerProps.field.onChange(option.value); - onPrecisionChange?.(option.value); + if (!questionDataStatusPath || !currentQuestionDataStatus) { + return; + } + const nextQuestionDataStatus = calculateQuizDataStatus( + currentQuestionDataStatus, + QuizDataStatus.UPDATE, + ); + if (nextQuestionDataStatus) { + form.setValue(questionDataStatusPath, nextQuestionDataStatus as QuizDataStatus); + } }} /> )} From 168badffce654d67ec0ec0591ba52e0a67c2425d Mon Sep 17 00:00:00 2001 From: Sadman Soumique Date: Mon, 20 Apr 2026 17:07:30 +0600 Subject: [PATCH 08/12] Enhancement: Refactor FormDrawImage and FormPuzzle components to improve state management and question data status handling, ensuring better performance and maintainability across quiz question types. --- .../fields/quiz/questions/FormDrawImage.tsx | 9 +- .../fields/quiz/questions/FormPuzzle.tsx | 108 ++++++++++++------ 2 files changed, 75 insertions(+), 42 deletions(-) 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 aac42826d8..c41ece071c 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 @@ -483,9 +483,10 @@ const FormDrawImage = ({ return null; } - const currentQuestionDataStatus = questionDataStatusPath - ? ((form.watch(questionDataStatusPath) as QuizDataStatus | undefined) ?? QuizDataStatus.NO_CHANGE) - : null; + const currentQuestionDataStatus = + questionDataStatusPath && form + ? ((form.watch(questionDataStatusPath) as QuizDataStatus | undefined) ?? QuizDataStatus.NO_CHANGE) + : null; const canClearSelection = hasStartedLassoDraw || Boolean(option?.answer_two_gap_match); @@ -560,7 +561,7 @@ const FormDrawImage = ({ )} onChange={(option) => { precisionControllerProps.field.onChange(option.value); - if (!questionDataStatusPath || !currentQuestionDataStatus) { + if (!questionDataStatusPath || !currentQuestionDataStatus || !form) { return; } const nextQuestionDataStatus = calculateQuizDataStatus( 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..aad35a6741 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,30 +32,44 @@ interface FormPuzzleProps extends FormControllerProps { type: QuizValidationErrorType; } | null> >; + gridSizeControllerProps?: FormControllerProps; + gridSizePath?: string; + gridSizeTextDomain?: string; + questionDataStatusPath?: string; } -const FormPuzzle = ({ field, activeQuestionIndex }: FormPuzzleProps) => { - const form = useFormContext(); +const FormPuzzle = ({ + field, + activeQuestionIndex = 0, + gridSizeControllerProps, + gridSizePath, + gridSizeTextDomain, + questionDataStatusPath, +}: 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 = questionDataStatusPath ?? `questions.${activeQuestionIndex}._data_status`; + const activeQuestionDataStatus = form + ? ((form.watch(resolvedQuestionDataStatusPath) as QuizDataStatus | undefined) ?? QuizDataStatus.NO_CHANGE) + : QuizDataStatus.NO_CHANGE; + const textDomain = gridSizeTextDomain ?? __TUTOR_TEXT_DOMAIN__; const gridSizeOptions = useMemo( () => [ - { value: 2, difficulty: __('Easy', __TUTOR_TEXT_DOMAIN__) }, - { value: 3, difficulty: __('Easy', __TUTOR_TEXT_DOMAIN__) }, - { value: 4, difficulty: __('Medium', __TUTOR_TEXT_DOMAIN__) }, - { value: 5, difficulty: __('Medium', __TUTOR_TEXT_DOMAIN__) }, - { value: 6, difficulty: __('Hard', __TUTOR_TEXT_DOMAIN__) }, - { value: 7, difficulty: __('Hard', __TUTOR_TEXT_DOMAIN__) }, + { value: 2, difficulty: __('Easy', textDomain) }, + { value: 3, difficulty: __('Easy', textDomain) }, + { value: 4, difficulty: __('Medium', textDomain) }, + { value: 5, difficulty: __('Medium', textDomain) }, + { value: 6, difficulty: __('Hard', textDomain) }, + { value: 7, difficulty: __('Hard', textDomain) }, ].map(({ value, difficulty }) => ({ - label: `${difficulty} - ${value}×${value} (${value * value} ${__('pieces', __TUTOR_TEXT_DOMAIN__)})`, + label: `${difficulty} - ${value}×${value} (${value * value} ${__('pieces', textDomain)})`, value, })), - [], + [textDomain], ); const updateOption = useCallback( @@ -142,28 +155,47 @@ 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, + ); + } + }} + /> + )} + /> + )}
From 85e27d3d55cca361e431c2e822a27f5aa413823d Mon Sep 17 00:00:00 2001 From: Sadman Soumique Date: Tue, 21 Apr 2026 10:28:33 +0600 Subject: [PATCH 09/12] Enhancement: Remove precisionTextDomain prop from FormDrawImage and FormPuzzle components, replacing it with a constant for improved consistency in text domain usage across quiz question types. --- .../curriculum/question-types/DrawImage.tsx | 1 - .../fields/quiz/questions/FormDrawImage.tsx | 14 ++++------- .../fields/quiz/questions/FormPuzzle.tsx | 24 ++++++++----------- 3 files changed, 14 insertions(+), 25 deletions(-) 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 6d8d372e65..52709195fb 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 @@ -78,7 +78,6 @@ const DrawImage = () => { validationError={validationError} setValidationError={setValidationError} precisionControllerProps={thresholdControllerProps} - precisionTextDomain={'tutor'} questionDataStatusPath={`questions.${activeQuestionIndex}._data_status`} /> )} 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 c41ece071c..0389d18b11 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 @@ -40,7 +40,6 @@ interface FormDrawImageProps extends FormControllerProps { } | null> >; precisionControllerProps?: FormControllerProps; - precisionTextDomain?: string; questionDataStatusPath?: string; } @@ -49,12 +48,7 @@ const THRESHOLD_OPTIONS = [40, 50, 60, 70, 80, 90, 100].map((value) => ({ value, })); -const FormDrawImage = ({ - field, - precisionControllerProps, - precisionTextDomain, - questionDataStatusPath, -}: FormDrawImageProps) => { +const FormDrawImage = ({ field, precisionControllerProps, questionDataStatusPath }: FormDrawImageProps) => { const form = useFormContext(); const option = field.value; @@ -550,14 +544,14 @@ const FormDrawImage = ({ aria-label={__('Draw a lasso around the correct answer area', __TUTOR_TEXT_DOMAIN__)} /> - {precisionControllerProps && precisionTextDomain && ( + {precisionControllerProps && ( { precisionControllerProps.field.onChange(option.value); 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 aad35a6741..0680f604bd 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 @@ -34,7 +34,6 @@ interface FormPuzzleProps extends FormControllerProps { >; gridSizeControllerProps?: FormControllerProps; gridSizePath?: string; - gridSizeTextDomain?: string; questionDataStatusPath?: string; } @@ -43,7 +42,6 @@ const FormPuzzle = ({ activeQuestionIndex = 0, gridSizeControllerProps, gridSizePath, - gridSizeTextDomain, questionDataStatusPath, }: FormPuzzleProps) => { const form = useFormContext(); @@ -54,22 +52,20 @@ const FormPuzzle = ({ const activeQuestionDataStatus = form ? ((form.watch(resolvedQuestionDataStatusPath) as QuizDataStatus | undefined) ?? QuizDataStatus.NO_CHANGE) : QuizDataStatus.NO_CHANGE; - const textDomain = gridSizeTextDomain ?? __TUTOR_TEXT_DOMAIN__; - const gridSizeOptions = useMemo( () => [ - { value: 2, difficulty: __('Easy', textDomain) }, - { value: 3, difficulty: __('Easy', textDomain) }, - { value: 4, difficulty: __('Medium', textDomain) }, - { value: 5, difficulty: __('Medium', textDomain) }, - { value: 6, difficulty: __('Hard', textDomain) }, - { value: 7, difficulty: __('Hard', textDomain) }, + { value: 2, difficulty: __('Easy', __TUTOR_TEXT_DOMAIN__) }, + { value: 3, difficulty: __('Easy', __TUTOR_TEXT_DOMAIN__) }, + { value: 4, difficulty: __('Medium', __TUTOR_TEXT_DOMAIN__) }, + { value: 5, difficulty: __('Medium', __TUTOR_TEXT_DOMAIN__) }, + { value: 6, difficulty: __('Hard', __TUTOR_TEXT_DOMAIN__) }, + { value: 7, difficulty: __('Hard', __TUTOR_TEXT_DOMAIN__) }, ].map(({ value, difficulty }) => ({ - label: `${difficulty} - ${value}×${value} (${value * value} ${__('pieces', textDomain)})`, + label: `${difficulty} - ${value}×${value} (${value * value} ${__('pieces', __TUTOR_TEXT_DOMAIN__)})`, value, })), - [textDomain], + [], ); const updateOption = useCallback( @@ -158,7 +154,7 @@ const FormPuzzle = ({ {gridSizeControllerProps ? ( ( Date: Tue, 21 Apr 2026 11:55:10 +0600 Subject: [PATCH 10/12] Enhancement: Refactor DrawImage and Puzzle question types to improve state management by removing unnecessary useEffect hooks and integrating activeQuestionIndex for better performance and maintainability across quiz question types. --- .../curriculum/question-types/DrawImage.tsx | 10 +------ .../curriculum/question-types/Puzzle.tsx | 12 -------- .../fields/quiz/questions/FormDrawImage.tsx | 28 +++++++++---------- .../fields/quiz/questions/FormPuzzle.tsx | 13 +++------ 4 files changed, 19 insertions(+), 44 deletions(-) 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 52709195fb..c7dbff720c 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 @@ -48,14 +48,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; @@ -75,10 +67,10 @@ const DrawImage = () => { )} /> 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/shared/components/fields/quiz/questions/FormDrawImage.tsx b/assets/src/js/v3/shared/components/fields/quiz/questions/FormDrawImage.tsx index 0389d18b11..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 @@ -29,6 +29,7 @@ const LASSO_MIN_POINT_DISTANCE = 4; interface FormDrawImageProps extends FormControllerProps { questionId: ID; + activeQuestionIndex?: number; validationError?: { message: string; type: QuizValidationErrorType; @@ -40,7 +41,6 @@ interface FormDrawImageProps extends FormControllerProps { } | null> >; precisionControllerProps?: FormControllerProps; - questionDataStatusPath?: string; } const THRESHOLD_OPTIONS = [40, 50, 60, 70, 80, 90, 100].map((value) => ({ @@ -48,9 +48,15 @@ const THRESHOLD_OPTIONS = [40, 50, 60, 70, 80, 90, 100].map((value) => ({ value, })); -const FormDrawImage = ({ field, precisionControllerProps, questionDataStatusPath }: FormDrawImageProps) => { +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); @@ -477,11 +483,6 @@ const FormDrawImage = ({ field, precisionControllerProps, questionDataStatusPath return null; } - const currentQuestionDataStatus = - questionDataStatusPath && form - ? ((form.watch(questionDataStatusPath) as QuizDataStatus | undefined) ?? QuizDataStatus.NO_CHANGE) - : null; - const canClearSelection = hasStartedLassoDraw || Boolean(option?.answer_two_gap_match); return ( @@ -555,15 +556,14 @@ const FormDrawImage = ({ field, precisionControllerProps, questionDataStatusPath )} onChange={(option) => { precisionControllerProps.field.onChange(option.value); - if (!questionDataStatusPath || !currentQuestionDataStatus || !form) { + if (!form) { return; } - const nextQuestionDataStatus = calculateQuizDataStatus( - currentQuestionDataStatus, - QuizDataStatus.UPDATE, - ); - if (nextQuestionDataStatus) { - form.setValue(questionDataStatusPath, nextQuestionDataStatus as QuizDataStatus); + if (calculateQuizDataStatus(activeQuestionDataStatus, QuizDataStatus.UPDATE)) { + form.setValue( + resolvedQuestionDataStatusPath, + calculateQuizDataStatus(activeQuestionDataStatus, QuizDataStatus.UPDATE) as QuizDataStatus, + ); } }} /> 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 0680f604bd..fea299f57d 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 @@ -34,21 +34,16 @@ interface FormPuzzleProps extends FormControllerProps { >; gridSizeControllerProps?: FormControllerProps; gridSizePath?: string; - questionDataStatusPath?: string; } -const FormPuzzle = ({ - field, - activeQuestionIndex = 0, - gridSizeControllerProps, - gridSizePath, - questionDataStatusPath, -}: FormPuzzleProps) => { +const FormPuzzle = ({ field, activeQuestionIndex = 0, gridSizeControllerProps, gridSizePath }: FormPuzzleProps) => { const form = useFormContext(); const option = field.value; const resolvedGridSizePath = gridSizePath ?? (`questions.${activeQuestionIndex}.question_settings.puzzle_grid_size` as const); - const resolvedQuestionDataStatusPath = questionDataStatusPath ?? `questions.${activeQuestionIndex}._data_status`; + 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; From 7516da74545192d3a46315130dd820f153fb4485 Mon Sep 17 00:00:00 2001 From: Sadman Soumique Date: Tue, 21 Apr 2026 12:08:32 +0600 Subject: [PATCH 11/12] Enhancement: Update quiz-puzzle SVG icon for improved visual consistency and performance, streamlining the SVG structure while maintaining design integrity. --- assets/icons/quiz-puzzle.svg | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/assets/icons/quiz-puzzle.svg b/assets/icons/quiz-puzzle.svg index e9e3445576..2151f3400a 100644 --- a/assets/icons/quiz-puzzle.svg +++ b/assets/icons/quiz-puzzle.svg @@ -1,9 +1 @@ - - - - - - - - - + \ No newline at end of file From 711901025f6cb4c6d43982c79ddf7aba3baef545 Mon Sep 17 00:00:00 2001 From: Sadman Soumique Date: Tue, 21 Apr 2026 15:54:14 +0600 Subject: [PATCH 12/12] Enhancement: Introduce draw_image_threshold_percent and puzzle_grid_size properties for improved configuration of DrawImage and Puzzle question types, enhancing user experience and maintainability in quiz question management. --- .../components/curriculum/QuestionList.tsx | 6 +++++ .../curriculum/question-types/DrawImage.tsx | 27 +++++++++---------- .../components/fields/FormSelectInput.tsx | 6 ++--- .../fields/quiz/questions/FormPuzzle.tsx | 1 + 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/assets/src/js/v3/entries/course-builder/components/curriculum/QuestionList.tsx b/assets/src/js/v3/entries/course-builder/components/curriculum/QuestionList.tsx index 7adba69789..735b2046c2 100644 --- a/assets/src/js/v3/entries/course-builder/components/curriculum/QuestionList.tsx +++ b/assets/src/js/v3/entries/course-builder/components/curriculum/QuestionList.tsx @@ -349,6 +349,12 @@ const QuestionList = ({ isEditing }: { isEditing: boolean }) => { 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 c7dbff720c..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,6 +1,6 @@ import { css } from '@emotion/react'; import { useEffect } from 'react'; -import { Controller, useFieldArray, useFormContext } from 'react-hook-form'; +import { Controller, useController, useFieldArray, useFormContext } from 'react-hook-form'; import { useQuizModalContext } from '@CourseBuilderContexts/QuizModalContext'; import type { QuizForm } from '@CourseBuilderServices/quiz'; @@ -22,6 +22,11 @@ const DrawImage = () => { control: form.control, name: answersPath, }); + const thresholdControllerProps = useController({ + control: form.control, + name: thresholdPath, + defaultValue: 70, + }); // Ensure there is always a single option for this question type. useEffect(() => { @@ -60,19 +65,13 @@ const DrawImage = () => { control={form.control} name={`questions.${activeQuestionIndex}.question_answers.0` as 'questions.0.question_answers.0'} render={(answerControllerProps) => ( - ( - - )} + )} /> 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/FormPuzzle.tsx b/assets/src/js/v3/shared/components/fields/quiz/questions/FormPuzzle.tsx index fea299f57d..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 @@ -167,6 +167,7 @@ const FormPuzzle = ({ field, activeQuestionIndex = 0, gridSizeControllerProps, g (