Skip to content

Commit e6e12b9

Browse files
authored
Merge pull request #1175 from morozov-av/issue-829-3
Support the options for Parsons problems in the new Assignment builder interface
2 parents eb1599d + 216c49c commit e6e12b9

21 files changed

Lines changed: 2842 additions & 361 deletions

File tree

bases/rsptx/assignment_server_api/assignment_builder/package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bases/rsptx/assignment_server_api/assignment_builder/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"@tiptap/suggestion": "^2.11.5",
4242
"better-react-mathjax": "^2.1.0",
4343
"classnames": "^2.5.1",
44+
"driver.js": "^1.4.0",
4445
"handsontable": "^14.2.0",
4546
"highlight.js": "^11.11.1",
4647
"html-react-parser": "^5.1.18",

bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ParsonsExercise/ParsonsExercise.tsx

Lines changed: 193 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import React, { FC, useCallback } from "react";
1+
import React, { FC, useCallback, useState } from "react";
2+
3+
import { SelectButton } from "primereact/selectbutton";
4+
import { confirmDialog, ConfirmDialog } from "primereact/confirmdialog";
25

36
import { CreateExerciseFormType } from "@/types/exercises";
47
import { createExerciseId } from "@/utils/exercise";
@@ -14,7 +17,21 @@ import { validateCommonFields } from "../../utils/validation";
1417

1518
import { ParsonsExerciseSettings } from "./ParsonsExerciseSettings";
1619
import { ParsonsPreview } from "./ParsonsPreview";
17-
import { ParsonsInstructions, ParsonsLanguageSelector, ParsonsBlocksManager } from "./components";
20+
import {
21+
ParsonsInstructions,
22+
ParsonsLanguageSelector,
23+
ParsonsBlocksManager,
24+
ParsonsOptions
25+
} from "./components";
26+
import { ParsonsExerciseTour } from "./components/ParsonsExerciseTour";
27+
import parsonsStyles from "./components/ParsonsExercise.module.css";
28+
29+
export type ParsonsMode = "simple" | "enhanced";
30+
31+
const MODE_OPTIONS = [
32+
{ label: "Simple", value: "simple" },
33+
{ label: "Enhanced", value: "enhanced" }
34+
];
1835

1936
const PARSONS_STEPS = [
2037
{ label: "Language" },
@@ -39,7 +56,13 @@ const getDefaultFormData = (): ParsonsData => ({
3956
question_type: "parsonsprob",
4057
language: "",
4158
instructions: "",
42-
blocks: [{ id: `block-${Date.now()}`, content: "", indent: 0 }]
59+
blocks: [{ id: `block-${Date.now()}`, content: "", indent: 0 }],
60+
adaptive: true,
61+
numbered: "left",
62+
noindent: false,
63+
grader: "line",
64+
orderMode: "random",
65+
customOrder: []
4366
});
4467

4568
const generatePreview = (data: ParsonsData): string => {
@@ -48,10 +71,13 @@ const generatePreview = (data: ParsonsData): string => {
4871
blocks: data.blocks || [],
4972
name: data.name || "parsons_exercise",
5073
language: data.language || "python",
51-
adaptive: true,
52-
numbered: "left",
53-
noindent: false,
54-
questionLabel: data.name
74+
adaptive: data.adaptive ?? true,
75+
numbered: data.numbered ?? "left",
76+
noindent: data.noindent ?? false,
77+
questionLabel: data.name,
78+
grader: data.grader ?? "line",
79+
orderMode: data.orderMode ?? "random",
80+
customOrder: data.customOrder
5581
});
5682
};
5783

@@ -61,10 +87,13 @@ const generateExerciseHtmlSrc = (data: ParsonsData): string => {
6187
instructions: data.instructions || "",
6288
blocks: data.blocks || [],
6389
language: data.language || "python",
64-
adaptive: true,
65-
numbered: "left",
66-
noindent: false,
67-
questionLabel: data.name
90+
adaptive: data.adaptive ?? true,
91+
numbered: data.numbered ?? "left",
92+
noindent: data.noindent ?? false,
93+
questionLabel: data.name,
94+
grader: data.grader ?? "line",
95+
orderMode: data.orderMode ?? "random",
96+
customOrder: data.customOrder
6897
});
6998
};
7099

@@ -144,6 +173,91 @@ export const ParsonsExercise: FC<ExerciseComponentProps> = ({
144173
[updateFormData, formData.language, formData.tags]
145174
);
146175

176+
const handleAddBlock = useCallback(() => {
177+
const newBlock: ParsonsBlock = {
178+
id: `block-${Date.now()}`,
179+
content: "",
180+
indent: 0
181+
};
182+
updateFormData("blocks", [...(formData.blocks || []), newBlock]);
183+
}, [updateFormData, formData.blocks]);
184+
185+
// --- Mode switcher ---
186+
const isEnhancedExercise =
187+
isEdit &&
188+
(initialData?.grader === "dag" ||
189+
initialData?.orderMode === "custom" ||
190+
initialData?.numbered === "right" ||
191+
initialData?.numbered === "none" ||
192+
initialData?.noindent === true);
193+
194+
const [mode, setMode] = useState<ParsonsMode>(isEnhancedExercise ? "enhanced" : "simple");
195+
196+
const directSetMode = useCallback((newMode: ParsonsMode) => {
197+
setMode(newMode);
198+
}, []);
199+
200+
const tourButton = (
201+
<ParsonsExerciseTour
202+
mode={mode}
203+
formData={formData}
204+
onModeChange={directSetMode}
205+
updateFormData={updateFormData as (key: string, value: any) => void}
206+
/>
207+
);
208+
209+
const handleModeChange = useCallback(
210+
(newMode: ParsonsMode) => {
211+
if (newMode === mode || !newMode) return;
212+
213+
if (newMode === "simple") {
214+
// Enhanced → Simple: confirm and reset
215+
confirmDialog({
216+
message:
217+
"Switching to Simple Mode will reset Grader, Order, Line Numbers, and No Indent to their default values. Block dropdown settings (DAG tags, dependencies, custom order) will be cleared. Continue?",
218+
header: "Switch to Simple Mode",
219+
icon: "pi pi-exclamation-triangle",
220+
acceptClassName: "p-button-warning",
221+
accept: () => {
222+
// Reset locked fields
223+
updateFormData("grader", "line");
224+
updateFormData("orderMode", "random");
225+
updateFormData("numbered", "left");
226+
updateFormData("noindent", false);
227+
updateFormData("adaptive", true);
228+
updateFormData("customOrder", []);
229+
// Clear DAG / order block fields
230+
const clearedBlocks = (formData.blocks || []).map((block) => ({
231+
...block,
232+
tag: undefined,
233+
depends: undefined,
234+
displayOrder: undefined
235+
}));
236+
updateFormData("blocks", clearedBlocks);
237+
setMode("simple");
238+
}
239+
});
240+
} else {
241+
// Simple → Enhanced: no data loss, just switch
242+
setMode("enhanced");
243+
}
244+
},
245+
[mode, updateFormData, formData.blocks]
246+
);
247+
248+
const modeSwitcher = (
249+
<div className={parsonsStyles.modeSwitcher} data-tour="mode-switcher">
250+
<span className={parsonsStyles.modeSwitcherLabel}>Mode</span>
251+
<SelectButton
252+
value={mode}
253+
options={MODE_OPTIONS}
254+
onChange={(e) => handleModeChange(e.value)}
255+
className={parsonsStyles.modeSwitcherButton}
256+
allowEmpty={false}
257+
/>
258+
</div>
259+
);
260+
147261
const renderStepContent = () => {
148262
switch (activeStep) {
149263
case 0:
@@ -164,11 +278,46 @@ export const ParsonsExercise: FC<ExerciseComponentProps> = ({
164278

165279
case 2:
166280
return (
167-
<ParsonsBlocksManager
168-
blocks={formData.blocks || []}
169-
onChange={(blocks: ParsonsBlock[]) => updateFormData("blocks", blocks)}
170-
language={formData.language || "python"}
171-
/>
281+
<div className="flex flex-column gap-4">
282+
<ParsonsOptions
283+
adaptive={formData.adaptive ?? true}
284+
numbered={formData.numbered ?? "left"}
285+
noindent={formData.noindent ?? false}
286+
grader={formData.grader ?? "line"}
287+
orderMode={formData.orderMode ?? "random"}
288+
mode={mode}
289+
onAdaptiveChange={(value: boolean) => updateFormData("adaptive", value)}
290+
onNumberedChange={(value: "left" | "right" | "none") =>
291+
updateFormData("numbered", value)
292+
}
293+
onNoindentChange={(value: boolean) => updateFormData("noindent", value)}
294+
onGraderChange={(value: "line" | "dag") => {
295+
updateFormData("grader", value);
296+
if (value === "dag") {
297+
updateFormData("adaptive", false);
298+
// Auto-assign tags to blocks that don't have them
299+
const updatedBlocks = (formData.blocks || []).map((block, idx) => {
300+
if (!block.tag && !block.isDistractor && !block.groupId) {
301+
return { ...block, tag: String(idx) };
302+
}
303+
return block;
304+
});
305+
updateFormData("blocks", updatedBlocks);
306+
}
307+
}}
308+
onOrderModeChange={(value: "random" | "custom") => updateFormData("orderMode", value)}
309+
onAddBlock={handleAddBlock}
310+
tourButton={tourButton}
311+
/>
312+
<ParsonsBlocksManager
313+
blocks={formData.blocks || []}
314+
onChange={(blocks: ParsonsBlock[]) => updateFormData("blocks", blocks)}
315+
language={formData.language || "python"}
316+
grader={formData.grader ?? "line"}
317+
orderMode={formData.orderMode ?? "random"}
318+
mode={mode}
319+
/>
320+
</div>
172321
);
173322

174323
case 3:
@@ -181,10 +330,13 @@ export const ParsonsExercise: FC<ExerciseComponentProps> = ({
181330
blocks={formData.blocks || []}
182331
language={formData.language || "python"}
183332
name={formData.name || ""}
184-
adaptive={true}
185-
numbered="left"
186-
noindent={false}
333+
adaptive={formData.adaptive ?? true}
334+
numbered={formData.numbered ?? "left"}
335+
noindent={formData.noindent ?? false}
187336
questionLabel={formData.name}
337+
grader={formData.grader ?? "line"}
338+
orderMode={formData.orderMode ?? "random"}
339+
customOrder={formData.customOrder}
188340
/>
189341
);
190342

@@ -194,23 +346,27 @@ export const ParsonsExercise: FC<ExerciseComponentProps> = ({
194346
};
195347

196348
return (
197-
<ExerciseLayout
198-
title="Parsons Exercise"
199-
exerciseType="parsonsprob"
200-
isEdit={isEdit}
201-
steps={PARSONS_STEPS}
202-
activeStep={activeStep}
203-
isCurrentStepValid={isCurrentStepValid}
204-
isSaving={isSaving}
205-
stepsValidity={stepsValidity}
206-
onCancel={onCancel}
207-
onBack={goToPrevStep}
208-
onNext={handleNext}
209-
onSave={handleSave}
210-
onStepSelect={handleStepSelect}
211-
validation={validation}
212-
>
213-
{renderStepContent()}
214-
</ExerciseLayout>
349+
<>
350+
<ConfirmDialog />
351+
<ExerciseLayout
352+
title="Parsons Exercise"
353+
exerciseType="parsonsprob"
354+
isEdit={isEdit}
355+
steps={PARSONS_STEPS}
356+
activeStep={activeStep}
357+
isCurrentStepValid={isCurrentStepValid}
358+
isSaving={isSaving}
359+
stepsValidity={stepsValidity}
360+
onCancel={onCancel}
361+
onBack={goToPrevStep}
362+
onNext={handleNext}
363+
onSave={handleSave}
364+
onStepSelect={handleStepSelect}
365+
validation={validation}
366+
headerExtra={modeSwitcher}
367+
>
368+
{renderStepContent()}
369+
</ExerciseLayout>
370+
</>
215371
);
216372
};

bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ParsonsExercise/ParsonsPreview.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ interface ParsonsPreviewProps {
1212
numbered?: "left" | "right" | "none";
1313
noindent?: boolean;
1414
questionLabel?: string;
15+
grader?: "line" | "dag";
16+
orderMode?: "random" | "custom";
17+
customOrder?: number[];
1518
}
1619

1720
export const ParsonsPreview: FC<ParsonsPreviewProps> = ({
@@ -22,7 +25,10 @@ export const ParsonsPreview: FC<ParsonsPreviewProps> = ({
2225
adaptive = true,
2326
numbered = "left",
2427
noindent = false,
25-
questionLabel
28+
questionLabel,
29+
grader = "line",
30+
orderMode = "random",
31+
customOrder
2632
}) => {
2733
return (
2834
<div style={{ display: "flex", alignItems: "start", justifyContent: "center" }}>
@@ -35,7 +41,10 @@ export const ParsonsPreview: FC<ParsonsPreviewProps> = ({
3541
adaptive,
3642
numbered,
3743
noindent,
38-
questionLabel: questionLabel || name
44+
questionLabel: questionLabel || name,
45+
grader,
46+
orderMode,
47+
customOrder
3948
})}
4049
/>
4150
</div>

0 commit comments

Comments
 (0)