Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
4dab000
feat: add draw image threshold functionality to quiz questions
saadman30 Mar 10, 2026
e5dd75e
refactor: improve DrawImage component structure and integrate precisi…
saadman30 Mar 10, 2026
5893ec4
Merge branch '4.0.0-dev' into feat/draw-image-quiz-type-precision-level
saadman30 Mar 30, 2026
8af2a1a
Implement draw on image functionality for quiz attempts, including ne…
saadman30 Mar 30, 2026
6e8b02d
Refactor draw-image question template to improve quiz attempt handlin…
saadman30 Mar 30, 2026
4347fee
Remove unused elements from draw-image question template to streamlin…
saadman30 Mar 30, 2026
0749811
Refactor SCSS and PHP for quiz attempt details. Update drop-shadow co…
saadman30 Mar 30, 2026
371a04a
Enhance FormDrawImage component to conditionally render image upload …
saadman30 Mar 30, 2026
d1cf32c
Refactor clear button in FormDrawImage component for improved styling…
saadman30 Mar 30, 2026
6d23939
Remove outdated comment from FormDrawImage component to enhance code …
saadman30 Mar 30, 2026
6d51627
Add is_legacy_learning_mode support across various components and cla…
saadman30 Apr 2, 2026
e6b64ae
Merge branch '4.0.0-dev' into feat/draw-image-quiz-type-precision-level
saadman30 Apr 2, 2026
3094fc6
Enhance QuestionList component by enabling 'Draw on Image' quiz type
saadman30 Apr 2, 2026
09f0f70
Implement template loading control for add-on only question partials …
saadman30 Apr 3, 2026
7c8d2f7
Refactor quiz utility and remove unused SCSS for draw-image review
saadman30 Apr 3, 2026
4247b55
Refactor is_legacy_learning_mode method in Utils class
saadman30 Apr 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion assets/core/ts/declaration.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,15 @@ declare global {
};
drawOnImage?: {
init: (options: {
image: HTMLImageElement;
image: HTMLImageElement | null;
canvas: HTMLCanvasElement;
hiddenInput?: HTMLInputElement | null;
brushSize?: number;
strokeStyle?: string;
initialMaskUrl?: string;
onMaskChange?: (value: string) => void;
interactionRoot?: HTMLElement | null;
activateOnHover?: boolean;
}) => { destroy: () => void };
DEFAULT_BRUSH_SIZE?: number;
DEFAULT_STROKE_STYLE?: string;
Expand Down Expand Up @@ -108,6 +111,7 @@ declare global {
ajaxurl?: string;
tutor_url?: string;
wp_date_format?: string;
is_legacy_learning_mode?: boolean;
};
}
}
Expand Down
1 change: 1 addition & 0 deletions assets/src/js/v3/@types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ declare global {
}[];
kids_icons_registry: string[];
is_kids_mode: boolean;
is_legacy_learning_mode: boolean;
current_user: {
data: {
id: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,18 +103,23 @@ const questionTypeOptions: {
icon: 'quizOrdering',
isPro: true,
},
// {
// label: __('Draw on Image', 'tutor'),
// value: 'draw_image',
// // TODO: icon is not final.
// icon: 'quizImageAnswer',
// isPro: true,
// },
{
label: __('Draw on Image', 'tutor'),
value: 'draw_image',
icon: 'quizImageAnswer',
isPro: true,
},
];

const isTutorPro = !!tutorConfig.tutor_pro_url;

const QuestionList = ({ isEditing }: { isEditing: boolean }) => {
const questionTypeOptionsForUi = useMemo(() => {
if (tutorConfig.is_legacy_learning_mode) {
return questionTypeOptions.filter((option) => option.value !== 'draw_image');
}
return questionTypeOptions;
}, []);
const [activeSortId, setActiveSortId] = useState<UniqueIdentifier | null>(null);
const [isOpen, setIsOpen] = useState(false);
const questionListRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -446,7 +451,7 @@ const QuestionList = ({ isEditing }: { isEditing: boolean }) => {
>
<div css={styles.questionOptionsWrapper}>
<span css={styles.questionTypeOptionsTitle}>{__('Select Question Type', 'tutor')}</span>
{questionTypeOptions.map((option) => (
{questionTypeOptionsForUi.map((option) => (
<Show
key={option.value}
when={option.isPro && !isTutorPro}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,42 @@
import { css } from '@emotion/react';
import { useEffect } from 'react';
import { __ } from '@wordpress/i18n';
import { useEffect, useMemo } 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';
import { QuizDataStatus, type QuizQuestionOption } from '@TutorShared/utils/types';
import { nanoid } from '@TutorShared/utils/util';

const DrawImage = () => {
const form = useFormContext<QuizForm>();
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 =
`questions.${activeQuestionIndex}.question_settings.draw_image_threshold_percent` as 'questions.0.question_settings.draw_image_threshold_percent';

const { fields: optionsFields } = useFieldArray({
control: form.control,
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) {
Expand All @@ -46,6 +62,14 @@ 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;
Expand All @@ -54,15 +78,41 @@ const DrawImage = () => {
return (
<div css={styles.optionWrapper}>
<Controller
key={JSON.stringify(optionsFields[0])}
key={optionsFields[0]?.id}
control={form.control}
name={`questions.${activeQuestionIndex}.question_answers.0` as 'questions.0.question_answers.0'}
render={(controllerProps) => (
<FormDrawImage
{...controllerProps}
questionId={activeQuestionId}
validationError={validationError}
setValidationError={setValidationError}
render={(answerControllerProps) => (
<Controller
control={form.control}
name={thresholdPath}
render={(thresholdControllerProps) => (
<FormDrawImage
{...answerControllerProps}
questionId={activeQuestionId}
validationError={validationError}
setValidationError={setValidationError}
precisionControl={
<FormSelectInput
{...thresholdControllerProps}
label={__('Precision Level', 'tutor')}
options={thresholdOptions}
helpText={__(
'Minimum % of the instructor mask the student must cover to be marked correct.',
'tutor',
)}
onChange={(option) => {
thresholdControllerProps.field.onChange(option.value);
if (calculateQuizDataStatus(activeQuestionDataStatus, QuizDataStatus.UPDATE)) {
form.setValue(
`questions.${activeQuestionIndex}._data_status`,
calculateQuizDataStatus(activeQuestionDataStatus, QuizDataStatus.UPDATE) as QuizDataStatus,
);
}
}}
/>
}
/>
)}
/>
)}
/>
Expand All @@ -75,6 +125,7 @@ export default DrawImage;
const styles = {
optionWrapper: css`
${styleUtils.display.flex('column')};
gap: ${spacing[16]};
padding-left: ${spacing[40]};
`,
};
4 changes: 4 additions & 0 deletions assets/src/js/v3/entries/course-builder/services/quiz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ interface QuizQuestionsForPayload extends Omit<QuizQuestion, 'question_settings'
show_question_mark: '0' | '1';
has_multiple_correct_answer?: '0' | '1';
is_image_matching?: '0' | '1';
draw_image_threshold_percent?: number;
};
}

Expand Down Expand Up @@ -274,6 +275,9 @@ export const convertQuizFormDataToPayload = (
...(question.question_type === 'matching' && {
is_image_matching: question.question_settings.is_image_matching ? '1' : '0',
}),
...(question.question_type === 'draw_image' && {
draw_image_threshold_percent: Number(question.question_settings.draw_image_threshold_percent ?? 70),
}),
},
question_answers: question.question_answers.map(
(answer) =>
Expand Down
Loading
Loading