Skip to content

Core 675 refactor exercise preview #92

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
96 changes: 53 additions & 43 deletions src/components/Exercise.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const exerciseWithStepDataProps: ExerciseWithStepDataProps = {
canUpdateCurrentStep: false,
};

const exerciseWithQuestionStatesProps = (uid?: string): ExerciseWithQuestionStatesProps => {
const exerciseWithQuestionStatesProps = (uid?: string, correctness?: string): ExerciseWithQuestionStatesProps => {
return {
exercise: {
uid: uid || '1@1',
Expand All @@ -104,11 +104,11 @@ const exerciseWithQuestionStatesProps = (uid?: string): ExerciseWithQuestionStat
is_answer_order_important: false,
answers: [{
id: '1',
correctness: undefined,
correctness: correctness,
content_html: 'True',
}, {
id: '2',
correctness: undefined,
correctness: correctness,
content_html: 'False',
}],
}],
Expand Down Expand Up @@ -146,12 +146,6 @@ const exerciseWithQuestionStatesProps = (uid?: string): ExerciseWithQuestionStat
}
};

const exerciseWithOverlayProps = (overlayChildren: React.ReactNode) => {
return {
overlayChildren,
};
}

type TextResizerValue = -2 | -1 | 0 | 1 | 2 | 3;
const textResizerScales = [0.75, 0.9, 1, 1.25, 1.5, 2];
const textResizerValues: TextResizerValue[] = [-2, -1, 0, 1, 2, 3];
Expand Down Expand Up @@ -657,12 +651,12 @@ bitterness. The discriminant <span data-math='b^2 - 4ac'></span> could perhaps a
answers: [
{
id: '1',
correctness: undefined,
correctness: '1.0',
content_html: `<span data-math='\\sqrt[3]{\\text{Apple}}'></span>`,
},
{
id: '2',
correctness: undefined,
correctness: '1.0',
content_html: `<span data-math='\\frac{\\text{Banana}^{2}}{4}'></span>`,
},
],
Expand All @@ -677,12 +671,12 @@ bitterness. The discriminant <span data-math='b^2 - 4ac'></span> could perhaps a
answers: [
{
id: '1',
correctness: undefined,
correctness: '1.0',
content_html: `<span data-math='e^{\\text{Blue}}'></span>`,
},
{
id: '2',
correctness: undefined,
correctness: '1.0',
content_html: `<span data-math='\\frac{\\pi}{2} + \\text{Red}'></span>`,
},
],
Expand Down Expand Up @@ -731,12 +725,12 @@ export const PreviewCard = () => {
const props1: ExerciseWithQuestionStatesProps = {
...exerciseWithQuestionStatesProps(),
questionStates: {
'1': {
'320733': {
available_points: '1.0',
is_completed: true,
answer_id_order: ['1', '2', '3', '4'],
answer_id: randomlyCorrectAnswer,
free_response: '',
free_response: 'Feedback info',
feedback_html: '',
correct_answer_id: randomlyCorrectAnswer.toString(),
correct_answer_feedback_html:
Expand Down Expand Up @@ -804,18 +798,24 @@ export const PreviewCard = () => {
{
id: 832300,
content_html: 'hypothalamus',
correctness: undefined,
feedback_html: 'Feedback response',
},
{
id: 832303,
content_html: 'medulla oblongata',
correctness: '1.0',
feedback_html: 'Feedback response',
},
{
id: 832301,
content_html: 'corpus callosum',
correctness: undefined,
},
{
id: 832302,
content_html: 'cerebellum',
correctness: undefined,
},
],
hints: [],
Expand All @@ -828,9 +828,17 @@ export const PreviewCard = () => {
},
};

const [showFeedback, setShowFeedback] = React.useState<boolean>(false);

return (
<TextResizerProvider>
<Exercise {...props1} className='preview-card' previewMode />
<button
onClick={()=> setShowFeedback(prev => !prev)}>{`Turn ${showFeedback ? 'off': 'on'} feedback`}
</button>
<ExercisePreview
exercise={props1.exercise}
showAllFeedback={showFeedback}
/>
</TextResizerProvider>
);
};
Expand All @@ -839,7 +847,6 @@ export const OverlayCard = () => {
const randomlyCorrectAnswer = Math.floor(Math.random() * 3) + 1;
const props1: ExerciseWithQuestionStatesProps = {
...exerciseWithQuestionStatesProps('1@123'),
...exerciseWithOverlayProps(<button>Overlay</button>),
questionStates: {
'1': {
available_points: '1.0',
Expand Down Expand Up @@ -938,17 +945,8 @@ export const OverlayCard = () => {
},
};

const [buttonVariant, setButtonVariant] = React.useState<'include' | 'remove'>('include');

const props2: ExerciseWithQuestionStatesProps = {
...exerciseWithQuestionStatesProps('1@321'),
...exerciseWithOverlayProps(
<IncludeRemoveQuestion
buttonVariant={buttonVariant}
onIncludeHandler={() => setButtonVariant('remove')}
onRemoveHandler={() => setButtonVariant('include')}
/>
),
...exerciseWithQuestionStatesProps('1@321', '1.0'),
questionStates: {
'1': {
available_points: '1.0',
Expand Down Expand Up @@ -976,29 +974,41 @@ export const OverlayCard = () => {
const includeHandler = (exerciseUid: string) => setSelectedQuestions(previous => previous.concat(exerciseUid));
const removeHandler = (exerciseUid: string) => setSelectedQuestions(previous => previous.filter((id) => id !== exerciseUid));


const includeRemoveQuestionComponent1 =
<IncludeRemoveQuestion
buttonVariant={selectedQuestions.includes(props1.exercise.uid) ? 'remove' : 'include'}
onIncludeHandler={() => includeHandler(props1.exercise.uid)}
onRemoveHandler={() => removeHandler(props1.exercise.uid)}
onClickDetails={() => setShowDetails1((previous) => !previous)}
/>;

const includeRemoveQuestionComponent2 =
<IncludeRemoveQuestion
buttonVariant={selectedQuestions.includes(props2.exercise.uid) ? 'remove' : 'include'}
onIncludeHandler={() => includeHandler(props2.exercise.uid)}
onRemoveHandler={() => removeHandler(props2.exercise.uid)}
onClickDetails={() => setShowDetails2((previous) => !previous)}
/>;

return (
<TextResizerProvider>
<h2>Exercise cards</h2>
<Exercise {...props1} className='preview-card' previewMode />
<Exercise {...props2} className='preview-card' previewMode />
<Exercise {...props1} overlayChildren={<button>Overlay</button>} className='preview-card' previewMode />
<Exercise {...props2} overlayChildren={<button>Overlay</button>} className='preview-card' previewMode />
<h2>Exercise Preview cards</h2>
{showDetails1 && <h2>Details 1!</h2>}
<ExercisePreview
selected={selectedQuestions.includes(props1.exercise.uid)}
onIncludeHandler={()=> includeHandler(props1.exercise.uid)}
onRemoveHandler={()=> removeHandler(props1.exercise.uid)}
onClickDetails={()=> setShowDetails1((previous) => !previous)}
enableOverlay
exercise={props1.exercise}
<ExercisePreview
selected={selectedQuestions.includes(props1.exercise.uid)}
overlayChildren={includeRemoveQuestionComponent1}
exercise={props1.exercise}
/>
{showDetails2 && <h2>Details 2!</h2>}
<ExercisePreview
selected={selectedQuestions.includes(props2.exercise.uid)}
onIncludeHandler={()=> includeHandler(props2.exercise.uid)}
onRemoveHandler={()=> removeHandler(props2.exercise.uid)}
onClickDetails={()=> setShowDetails2((previous) => !previous)}
enableOverlay
exercise={props2.exercise}
<ExercisePreview
selected={selectedQuestions.includes(props2.exercise.uid)}
overlayChildren={includeRemoveQuestionComponent2}
exercise={props2.exercise}
showAllFeedback
/>
</TextResizerProvider>
);
Expand Down
2 changes: 0 additions & 2 deletions src/components/Exercise/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,5 @@ export const exerciseStyles = css`
font-weight: bold;
font-size: 1.6rem;
}


}
`;
27 changes: 12 additions & 15 deletions src/components/ExercisePreview.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ describe('ExercisePreview', () => {
is_answer_order_important: false,
answers: [{
id: '1',
correctness: undefined,
correctness: '1.0',
content_html: 'True',
feedback_html: 'Feedback',
}, {
id: '2',
correctness: undefined,
Expand All @@ -45,22 +46,18 @@ describe('ExercisePreview', () => {
});

it.each`
enableOverlay | selected | description
${true} | ${true} | ${'with overlay and selected true'}
${true} | ${true} | ${'with overlay and selected false'}
${false} | ${false} | ${'without overlay'}
`('matches snapshot %description', ({ enableOverlay, selected }: { enableOverlay: boolean, selected: boolean }) => {
const onIncludeMock = jest.fn();
const onRemoveMock = jest.fn();
const onDetailsMock = jest.fn();
selected | description | overlay | showFeedback
${true} | ${'with overlay and selected true'} | ${<button>Over</button>} | ${undefined}
${true} | ${'with overlay and selected false'} | ${<button>Over</button>} | ${false}
${false} | ${'without overlay'} | ${undefined} | ${true}
`('matches snapshot $description', (
{ selected, overlay, showFeedback }: { selected: boolean, overlay: JSX.Element, showFeedback: boolean }) => {
const tree = renderer.create(
<ExercisePreview
exercise={exercise}
enableOverlay={enableOverlay}
<ExercisePreview
exercise={exercise}
selected={selected}
onIncludeHandler={onIncludeMock}
onRemoveHandler={onRemoveMock}
onClickDetails={onDetailsMock}
overlayChildren={overlay}
showAllFeedback={showFeedback}
/>
).toJSON();
expect(tree).toMatchSnapshot();
Expand Down
68 changes: 22 additions & 46 deletions src/components/ExercisePreview.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,40 @@
import React from "react";
import { ExerciseData, ExerciseQuestionData, StepBase } from "src/types";
import { IncludeRemoveQuestion } from "./IncludeRemoveQuestion";
import { Exercise } from "./Exercise";

export interface ExercisePreviewProps {
exercise: ExerciseData;
selected: boolean;
onIncludeHandler: () => void;
onRemoveHandler: () => void;
onClickDetails: () => void;
enableOverlay?: boolean;
selected?: boolean;
showAllFeedback?: boolean;
overlayChildren?: React.ReactNode;
}

/**
* An Exercise version with less interaction with card and grants an Overlay with Include/Remove component
*/
export const ExercisePreview = (
{
exercise,
selected,
onIncludeHandler,
onRemoveHandler,
onClickDetails,
enableOverlay = false,
showAllFeedback = false,
overlayChildren,
}: ExercisePreviewProps) => {

const hideAnswerFeedback = (exercise: ExerciseData) => {
exercise.questions.map(question => question.answers.map(a => a.correctness = undefined ));
return exercise;
};

const exercisePreviewProps = (exercise: ExerciseData) => {
const formatAnswerData = (questions: ExerciseQuestionData[]) => questions.map((q) => (
{ id: q.id, correct_answer_id: (q.answers.find((a) => a.correctness === '1.0')?.id || '') }));

const questionStateFields = {
available_points: '1.0',
is_completed: true,
answer_id: '1',
free_response: '',
feedback_html: '',
correct_answer_feedback_html: '',
attempts_remaining: 0,
attempt_number: 1,
incorrectAnswerId: 0
}

const questionStates = formatAnswerData(exercise.questions).reduce((acc, answer) => {
const questionStateFields = formatAnswerData(exercise.questions).reduce((acc, answer) => {
const { id, correct_answer_id } = answer;
return { ...acc, [id]: { ...questionStateFields, correct_answer_id } };
return {
...acc,
[id]: {
correct_answer_id,
is_completed: true,
}
};
}, {});

const step: StepBase = {
Expand All @@ -64,32 +55,17 @@ export const ExercisePreview = (
step: step,
questionNumber: exercise.number as number,
numberOfQuestions: exercise.questions.length,
questionStates: questionStates,
show_all_feedback: false, // Hide all feedback
questionStates: questionStateFields,
show_all_feedback: showAllFeedback,
};
};

const includeRemoveQuestionComponent = React.useMemo(() =>
<IncludeRemoveQuestion
buttonVariant={selected ? 'remove' : 'include'}
onIncludeHandler={onIncludeHandler}
onRemoveHandler={onRemoveHandler}
onClickDetails={onClickDetails}
/>
, [selected, onIncludeHandler, onRemoveHandler, onClickDetails]);

return (
<Exercise
exercise={exercise}
exercise={showAllFeedback ? exercise : hideAnswerFeedback(exercise)}
className={selected ? 'preview-card is-selected' : 'preview-card'}
previewMode
{
...(enableOverlay
? {
overlayChildren: includeRemoveQuestionComponent,
}
: {})
}
overlayChildren={overlayChildren}
{...exercisePreviewProps(exercise)}
/>
);
Expand Down
2 changes: 0 additions & 2 deletions src/components/IncludeRemoveQuestion/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ export const IncludeRemoveQuestion = (
case 'remove':
onRemoveHandler();
break;
default:
break;
}
};

Expand Down
Loading