Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
a8de0f0
feat: enhance QuestionListTable with additional quiz question types
saadman30 Apr 15, 2026
b34fedf
Merge branch '4.0.0-dev' into fix/new-quiz-types-content-bank
saadman30 Apr 15, 2026
1eadea5
Merge branch '4.0.0-dev' into fix/new-quiz-types-content-bank
saadman30 Apr 16, 2026
a542ad2
Refactor(🛠) : Move normalizeSingleMaskQuestionAnswers function to sha…
saadman30 Apr 16, 2026
cc3987a
Enhance(🎨) : Refactor DrawImage component to streamline precision con…
saadman30 Apr 16, 2026
88f96a9
Enhance(🎨) : Improve checkbox interaction in ContentListTable and Que…
saadman30 Apr 16, 2026
61817be
Enhance(🎨) : Update DrawImage component to improve data status handli…
saadman30 Apr 17, 2026
85fa68c
Merge branch '4.0.0-dev' into fix/new-quiz-types-content-bank
saadman30 Apr 20, 2026
e821ee0
Enhancement: Add new SVG icon for puzzle question type and update rel…
saadman30 Apr 20, 2026
cedec74
Enhancement: Refactor DrawImage question type to improve state manage…
saadman30 Apr 20, 2026
168badf
Enhancement: Refactor FormDrawImage and FormPuzzle components to impr…
saadman30 Apr 20, 2026
85e27d3
Enhancement: Remove precisionTextDomain prop from FormDrawImage and F…
saadman30 Apr 21, 2026
18cc019
Enhancement: Refactor DrawImage and Puzzle question types to improve …
saadman30 Apr 21, 2026
7516da7
Enhancement: Update quiz-puzzle SVG icon for improved visual consiste…
saadman30 Apr 21, 2026
7119010
Enhancement: Introduce draw_image_threshold_percent and puzzle_grid_s…
saadman30 Apr 21, 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
9 changes: 9 additions & 0 deletions assets/icons/quiz-puzzle.svg
Comment thread
b-l-i-n-d marked this conversation as resolved.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const questionTypeIconMap: Record<Exclude<QuizQuestionType, 'single_choice' | 'i
scale: 'quizRange',
pin_image: 'quizPin',
coordinates: 'quizGraph',
puzzle: 'quizImageMatching',
puzzle: 'quizPuzzle',
h5p: 'quizH5p',
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const questionTypes = {
},
puzzle: {
label: __('Puzzle', 'tutor'),
icon: 'quizImageMatching',
icon: 'quizPuzzle',
},
h5p: {
label: __('H5P', 'tutor'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ const questionTypeOptions: {
{
label: __('Puzzle', 'tutor'),
value: 'puzzle',
icon: 'quizImageMatching',
icon: 'quizPuzzle',
isPro: true,
},
];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
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';
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 =
Expand All @@ -28,15 +23,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) {
Expand Down Expand Up @@ -91,26 +77,8 @@ const DrawImage = () => {
questionId={activeQuestionId}
validationError={validationError}
setValidationError={setValidationError}
precisionControl={
<FormSelectInput
{...thresholdControllerProps}
label={__('Precision Level', 'tutor')}
options={thresholdOptions}
helpText={__(
'Minimum overlap score between student and instructor markings. Larger or smaller marked areas lower the score.',
'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,
);
}
}}
/>
}
precisionControllerProps={thresholdControllerProps}
questionDataStatusPath={`questions.${activeQuestionIndex}._data_status`}
Comment thread
saadman30 marked this conversation as resolved.
Outdated
/>
)}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,20 @@ const ContentListTable = () => {
const columns: Column<ContentBankContent>[] = [
{
Header: totalItems ? (
<Checkbox
onChange={toggleSelection}
checked={getContentsQuery.isLoading || getContentsQuery.isRefetching ? false : handleAllIsChecked()}
label={__('Title', 'tutor')}
labelCss={styles.tableTitle}
isIndeterminate={fetchedContents.length > 0 && !handleAllIsChecked() && selectedContents.length > 0}
aria-label={__('Select all contents', 'tutor')}
/>
<div
onClick={(event) => event.stopPropagation()}
onMouseDown={(event) => event.stopPropagation()}
css={styles.headerCheckboxWrapper}
>
<Checkbox
onChange={toggleSelection}
checked={getContentsQuery.isLoading || getContentsQuery.isRefetching ? false : handleAllIsChecked()}
label={__('Title', 'tutor')}
labelCss={styles.tableTitle}
isIndeterminate={fetchedContents.length > 0 && !handleAllIsChecked() && selectedContents.length > 0}
aria-label={__('Select all contents', 'tutor')}
/>
</div>
) : (
__('# Title', 'tutor')
),
Expand Down Expand Up @@ -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};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ const FilterFields = ({ onFilterChange, initialValues, type }: FilterFieldsProps
value: 'ordering',
icon: 'quizOrdering',
},
{
label: __('Puzzle', 'tutor'),
value: 'puzzle',
icon: 'quizPuzzle',
},
];

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<ContentSelectionForm>();
Expand Down Expand Up @@ -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<ContentBankContent>[] = [
{
Header: totalItems ? (
<Checkbox
onChange={handleToggleSelection}
checked={getContentsQuery.isLoading || getContentsQuery.isRefetching ? false : handleAllIsChecked()}
label={__('Title', 'tutor')}
labelCss={styles.tableTitle}
isIndeterminate={fetchedContents.length > 0 && !handleAllIsChecked() && selectedContents.length > 0}
aria-label={__('Select all questions', 'tutor')}
/>
<div
onClick={(event) => event.stopPropagation()}
onMouseDown={(event) => event.stopPropagation()}
css={styles.headerCheckboxWrapper}
>
<Checkbox
onChange={handleToggleSelection}
checked={getContentsQuery.isLoading || getContentsQuery.isRefetching ? false : handleAllIsChecked()}
label={__('Title', 'tutor')}
labelCss={styles.tableTitle}
isIndeterminate={fetchedContents.length > 0 && !handleAllIsChecked() && selectedContents.length > 0}
aria-label={__('Select all questions', 'tutor')}
/>
</div>
) : (
__('# Title', 'tutor')
),
Expand Down Expand Up @@ -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;
Expand Down
Loading
Loading