Merged in feature/ExamGenRework (pull request #124)

ENCOA-260, ENCOA-259

Approved-by: Tiago Ribeiro
This commit is contained in:
carlos.mesquita
2024-12-11 07:40:52 +00:00
committed by Tiago Ribeiro
12 changed files with 301 additions and 57 deletions

View File

@@ -155,11 +155,11 @@ const Templates: React.FC<Props> = ({ module, state, setState }) => {
</li> </li>
{["reading", "level"].includes(module) && ( {["reading", "level"].includes(module) && (
<li className="text-gray-700 list-disc"> <li className="text-gray-700 list-disc">
a part must only contain a reading passage and it must be between the part delineator (e.g. Part 1) and the part exercises. a part must only contain a single reading passage and it must be between the part delineator (e.g. Part 1) and the part exercises.
</li> </li>
)} )}
<li className="text-gray-700 list-disc"> <li className="text-gray-700 list-disc">
if solutions are going to be uploaded the exercise numbers/id&apos;s must match the ones in the solutions. if solutions are going to be uploaded, the exercise numbers/id&apos;s must match the ones in the solutions.
</li> </li>
</ul> </ul>
</div> </div>
@@ -186,7 +186,7 @@ const Templates: React.FC<Props> = ({ module, state, setState }) => {
} }
<div className="bg-gray-50 rounded-lg p-4"> <div className="bg-gray-50 rounded-lg p-4">
<p className="text-gray-600"> <p className="text-gray-600">
{`The downloadable template is an example of a file that can be imported. Your document doesn't need to be a carbon copy of the template - it can have different styling and formatting but it must adhere to the previous requirements${state.type === "exam" ? "and exercises of the same type should have the same formatting" : ""}.`} {`The downloadable template is an example of a file that can be imported. Your document doesn't need to be a carbon copy of the template - it can have different styling and formatting but it must adhere to the previous requirements${state.type === "exam" ? " and exercises of the same type should have the same formatting" : ""}.`}
</p> </p>
</div> </div>
<div className="w-full flex justify-between mt-4 gap-8"> <div className="w-full flex justify-between mt-4 gap-8">

View File

@@ -0,0 +1,116 @@
import Button from "@/components/Low/Button";
import { Module } from "@/interfaces";
import useExamEditorStore from "@/stores/examEditor";
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from "@headlessui/react";
import { capitalize } from "lodash";
import React, { Fragment, useCallback, useEffect, useState } from "react";
interface Props {
module: Module;
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
setNumberOfLevelParts: React.Dispatch<React.SetStateAction<number>>;
}
const ResetModule: React.FC<Props> = ({ module, isOpen, setIsOpen, setNumberOfLevelParts }) => {
const [isClosing, setIsClosing] = useState(false);
const [mounted, setMounted] = useState(false);
const { dispatch } = useExamEditorStore();
useEffect(() => {
if (isOpen) {
setMounted(true);
}
}, [isOpen]);
useEffect(() => {
if (!isOpen && mounted) {
const timer = setTimeout(() => {
setMounted(false);
setIsClosing(false);
}, 300);
return () => clearTimeout(timer);
}
}, [isOpen, mounted]);
const blockMultipleClicksClose = useCallback(() => {
if (isClosing) return;
setIsClosing(true);
setIsOpen(!isOpen);
const timer = setTimeout(() => {
setIsClosing(false);
}, 300);
return () => clearTimeout(timer);
}, [isClosing, setIsOpen, isOpen]);
if (!mounted && !isOpen) return null;
const handleResetModule = () => {
dispatch({ type: 'RESET_MODULE', payload: { module } });
setIsOpen(false);
setNumberOfLevelParts(1);
}
return (
<Transition
show={isOpen}
as={Fragment}
beforeEnter={() => setIsClosing(false)}
beforeLeave={() => setIsClosing(true)}
afterLeave={() => {
setIsClosing(false);
setMounted(false);
}}
>
<Dialog onClose={() => blockMultipleClicksClose()} className="relative z-50">
<TransitionChild
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0">
<div className="fixed inset-0 bg-black/30" />
</TransitionChild>
<TransitionChild
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95">
<div className="fixed inset-0 flex items-center justify-center p-4">
<DialogPanel className={`bg-ielts-${module}-light w-full max-w-xl h-fit p-8 rounded-xl flex flex-col gap-4`}>
<DialogTitle className="flex font-bold text-xl justify-center text-gray-700"><span>Reset {capitalize(module)} Module</span></DialogTitle>
<div className="flex flex-col w-full mt-4 gap-6">
<div className="bg-gray-50 rounded-lg p-4">
<p className="text-gray-600">
Do you want to reset the {module} module?
</p>
<br/>
<p className="text-gray-600 font-bold">This will reset all the current data in the {module} module and cannot be undone.</p>
</div>
<div className="w-full flex justify-between mt-4 gap-8">
<Button color="purple" onClick={blockMultipleClicksClose} variant="outline" className="self-end w-full bg-white">
Cancel
</Button>
<Button color="purple" onClick={handleResetModule} variant="solid" className="self-end w-full">
Reset
</Button>
</div>
</div>
</DialogPanel>
</div>
</TransitionChild>
</Dialog>
</Transition>
);
}
export default ResetModule;

View File

@@ -34,7 +34,9 @@ const SettingsEditor: React.FC<SettingsEditorProps> = ({
canPreview, canPreview,
canSubmit canSubmit
}) => { }) => {
const { dispatch } = useExamEditorStore()
const examLabel = useExamEditorStore((state) => state.modules[module].examLabel) || ''; const examLabel = useExamEditorStore((state) => state.modules[module].examLabel) || '';
const type = useExamEditorStore((s) => s.modules[module].type);
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<SectionSettings>( const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<SectionSettings>(
module, module,
sectionId sectionId
@@ -50,6 +52,18 @@ const SettingsEditor: React.FC<SettingsEditorProps> = ({
updateLocalAndScheduleGlobal({ category: text }); updateLocalAndScheduleGlobal({ category: text });
}, [updateLocalAndScheduleGlobal]); }, [updateLocalAndScheduleGlobal]);
const typeOptions = [
{ value: 'general', label: 'General' },
{ value: 'academic', label: 'Academic' }
];
const onTypeChange = useCallback((option: { value: string | null, label: string }) => {
dispatch({
type: 'UPDATE_MODULE',
payload: { module, updates: { type: option.value as "academic" | "general" | undefined } }
});
}, [dispatch, module]);
const onIntroOptionChange = useCallback((option: { value: string | null, label: string }) => { const onIntroOptionChange = useCallback((option: { value: string | null, label: string }) => {
let updates: Partial<SectionSettings> = { introOption: option }; let updates: Partial<SectionSettings> = { introOption: option };
@@ -79,7 +93,7 @@ const SettingsEditor: React.FC<SettingsEditorProps> = ({
currentIntro: text currentIntro: text
}); });
}, [updateLocalAndScheduleGlobal]); }, [updateLocalAndScheduleGlobal]);
return ( return (
<div className={`flex flex-col gap-8 border bg-ielts-${module}/20 rounded-3xl p-8 w-1/3 h-fit`}> <div className={`flex flex-col gap-8 border bg-ielts-${module}/20 rounded-3xl p-8 w-1/3 h-fit`}>
<div className={`w-full flex justify-center text-ielts-${module} font-bold text-xl`}>{sectionLabel} Settings</div> <div className={`w-full flex justify-center text-ielts-${module} font-bold text-xl`}>{sectionLabel} Settings</div>
@@ -100,6 +114,18 @@ const SettingsEditor: React.FC<SettingsEditorProps> = ({
value={localSettings.category || ''} value={localSettings.category || ''}
/> />
</Dropdown> </Dropdown>
{["reading", "writing"].includes(module) && <Dropdown
title="Type"
module={module}
open={localSettings.isTypeDropdownOpen}
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isTypeDropdownOpen: isOpen }, false)}
>
<Select
options={typeOptions}
onChange={(o) => onTypeChange({ value: o!.value, label: o!.label })}
value={typeOptions.find(o => o.value === type)}
/>
</Dropdown>}
<Dropdown <Dropdown
title="Divider" title="Divider"
module={module} module={module}

View File

@@ -31,6 +31,7 @@ const ReadingSettings: React.FC = () => {
sections, sections,
minTimer, minTimer,
isPrivate, isPrivate,
type,
} = useExamEditorStore(state => state.modules[currentModule]); } = useExamEditorStore(state => state.modules[currentModule]);
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<ReadingSectionSettings>( const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<ReadingSectionSettings>(
@@ -78,10 +79,10 @@ const ReadingSettings: React.FC = () => {
minTimer, minTimer,
module: "reading", module: "reading",
id: title, id: title,
type: "academic",
variant: sections.length === 3 ? "full" : "partial", variant: sections.length === 3 ? "full" : "partial",
difficulty, difficulty,
private: isPrivate, private: isPrivate,
type: type!
}; };
axios.post(`/api/exam/reading`, exam) axios.post(`/api/exam/reading`, exam)
@@ -112,6 +113,7 @@ const ReadingSettings: React.FC = () => {
variant: undefined, variant: undefined,
difficulty, difficulty,
private: isPrivate, private: isPrivate,
type: type!
} as ReadingExam); } as ReadingExam);
setExerciseIndex(0); setExerciseIndex(0);
setQuestionIndex(0); setQuestionIndex(0);

View File

@@ -25,6 +25,7 @@ const WritingSettings: React.FC = () => {
isPrivate, isPrivate,
sections, sections,
focusedSection, focusedSection,
type
} = useExamEditorStore((store) => store.modules["writing"]); } = useExamEditorStore((store) => store.modules["writing"]);
const states = sections.flatMap((s) => s.state) as WritingExercise[]; const states = sections.flatMap((s) => s.state) as WritingExercise[];
@@ -72,6 +73,7 @@ const WritingSettings: React.FC = () => {
variant: undefined, variant: undefined,
difficulty, difficulty,
private: isPrivate, private: isPrivate,
type: type!
}); });
setExerciseIndex(0); setExerciseIndex(0);
openDetachedTab("popout?type=Exam&module=writing", router) openDetachedTab("popout?type=Exam&module=writing", router)
@@ -94,6 +96,7 @@ const WritingSettings: React.FC = () => {
variant: undefined, variant: undefined,
difficulty, difficulty,
private: isPrivate, private: isPrivate,
type: type!
}; };
axios axios

View File

@@ -17,6 +17,8 @@ import ListeningSettings from "./SettingsEditor/listening";
import SpeakingSettings from "./SettingsEditor/speaking"; import SpeakingSettings from "./SettingsEditor/speaking";
import ImportOrStartFromScratch from "./ImportExam/ImportOrFromScratch"; import ImportOrStartFromScratch from "./ImportExam/ImportOrFromScratch";
import { defaultSectionSettings } from "@/stores/examEditor/defaults"; import { defaultSectionSettings } from "@/stores/examEditor/defaults";
import Button from "../Low/Button";
import ResetModule from "./ResetModule";
const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"]; const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"];
@@ -34,6 +36,7 @@ const ExamEditor: React.FC<{ levelParts?: number }> = ({ levelParts = 0 }) => {
} = useExamEditorStore(state => state.modules[currentModule]); } = useExamEditorStore(state => state.modules[currentModule]);
const [numberOfLevelParts, setNumberOfLevelParts] = useState(levelParts !== 0 ? levelParts : 1); const [numberOfLevelParts, setNumberOfLevelParts] = useState(levelParts !== 0 ? levelParts : 1);
const [isResetModuleOpen, setIsResetModuleOpen] = useState(false);
// For exam edits // For exam edits
useEffect(() => { useEffect(() => {
@@ -129,6 +132,7 @@ const ExamEditor: React.FC<{ levelParts?: number }> = ({ levelParts = 0 }) => {
<> <>
{showImport ? <ImportOrStartFromScratch module={currentModule} setNumberOfLevelParts={updateLevelParts} /> : ( {showImport ? <ImportOrStartFromScratch module={currentModule} setNumberOfLevelParts={updateLevelParts} /> : (
<> <>
{isResetModuleOpen && <ResetModule module={currentModule} isOpen={isResetModuleOpen} setIsOpen={setIsResetModuleOpen} setNumberOfLevelParts={setNumberOfLevelParts}/>}
<div className="flex gap-4 w-full items-center"> <div className="flex gap-4 w-full items-center">
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<label className="font-normal text-base text-mti-gray-dim">Timer</label> <label className="font-normal text-base text-mti-gray-dim">Timer</label>
@@ -183,7 +187,8 @@ const ExamEditor: React.FC<{ levelParts?: number }> = ({ levelParts = 0 }) => {
</Checkbox> </Checkbox>
</div> </div>
</div> </div>
<div className="flex flex-col gap-3 w-full"> <div className="flex flex-row gap-3 w-full">
<div className="flex flex-col gap-3 flex-grow">
<label className="font-normal text-base text-mti-gray-dim">Exam Label *</label> <label className="font-normal text-base text-mti-gray-dim">Exam Label *</label>
<Input <Input
type="text" type="text"
@@ -194,6 +199,10 @@ const ExamEditor: React.FC<{ levelParts?: number }> = ({ levelParts = 0 }) => {
value={examLabel} value={examLabel}
required required
/> />
</div>
{["reading", "listening", "level"].includes(currentModule) && <Button onClick={() => setIsResetModuleOpen(true)} customColor={`bg-ielts-${currentModule}/70 hover:bg-ielts-${currentModule} border-ielts-${currentModule}`} className={`text-white self-end`}>
Reset Module
</Button>}
</div> </div>
<div className="flex flex-row gap-8"> <div className="flex flex-row gap-8">
<Settings /> <Settings />

View File

@@ -12,6 +12,7 @@ interface Props {
padding?: string; padding?: string;
onClick?: () => void; onClick?: () => void;
type?: "button" | "reset" | "submit"; type?: "button" | "reset" | "submit";
customColor?: string;
} }
export default function Button({ export default function Button({
@@ -19,6 +20,7 @@ export default function Button({
variant = "solid", variant = "solid",
disabled = false, disabled = false,
isLoading = false, isLoading = false,
customColor = undefined,
className, className,
children, children,
type, type,
@@ -65,7 +67,7 @@ export default function Button({
className={clsx( className={clsx(
"rounded-full transition ease-in-out duration-300 disabled:cursor-not-allowed cursor-pointer select-none", "rounded-full transition ease-in-out duration-300 disabled:cursor-not-allowed cursor-pointer select-none",
padding, padding,
colorClassNames[color][variant], customColor ? customColor : colorClassNames[color][variant],
className, className,
)} )}
disabled={disabled || isLoading}> disabled={disabled || isLoading}>

View File

@@ -97,6 +97,7 @@ export interface WritingExam extends ExamBase {
module: "writing"; module: "writing";
enableNavigation?: boolean; enableNavigation?: boolean;
exercises: WritingExercise[]; exercises: WritingExercise[];
type?: "academic" | "general";
} }
interface WordCounter { interface WordCounter {

View File

@@ -1,6 +1,6 @@
import Button from "@/components/Low/Button"; import Button from "@/components/Low/Button";
import axios from "axios"; import axios from "axios";
import { uniqBy } from "lodash"; import { capitalize, uniqBy } from "lodash";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { useFilePicker } from "use-file-picker"; import { useFilePicker } from "use-file-picker";
@@ -19,6 +19,9 @@ import { Type, UserImport } from "../../../interfaces/IUserImport";
import UserTable from "../../../components/UserTable"; import UserTable from "../../../components/UserTable";
import { EntityWithRoles } from "@/interfaces/entity"; import { EntityWithRoles } from "@/interfaces/entity";
import Select from "@/components/Low/Select"; import Select from "@/components/Low/Select";
import { IoInformationCircleOutline } from "react-icons/io5";
import { FaFileDownload } from "react-icons/fa";
import { HiOutlineDocumentText } from "react-icons/hi";
const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/); const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/);
@@ -135,7 +138,7 @@ export default function BatchCreateUser({ user, entities = [], permissions, onFi
} }
setInfos(information); setInfos(information);
} catch(e) { } catch (e) {
console.log(e) console.log(e)
toast.error( toast.error(
@@ -157,7 +160,7 @@ export default function BatchCreateUser({ user, entities = [], permissions, onFi
const crossRefEmails = response.data; const crossRefEmails = response.data;
if (!!crossRefEmails) { if (!!crossRefEmails) {
const existingEmails = new Set(crossRefEmails.map((x: any)=> x.email)); const existingEmails = new Set(crossRefEmails.map((x: any) => x.email));
const dupes = infos.filter(info => existingEmails.has(info.email)); const dupes = infos.filter(info => existingEmails.has(info.email));
const newUsersList = infos.filter(info => !existingEmails.has(info.email)); const newUsersList = infos.filter(info => !existingEmails.has(info.email));
setNewUsers(newUsersList); setNewUsers(newUsersList);
@@ -192,10 +195,10 @@ export default function BatchCreateUser({ user, entities = [], permissions, onFi
setIsLoading(true); setIsLoading(true);
try { try {
await axios.post("/api/batch_users", {users: newUsers.map((user) => ({...user, type, expiryDate}))}); await axios.post("/api/batch_users", { users: newUsers.map((user) => ({ ...user, type, expiryDate })) });
toast.success(`Successfully added ${newUsers.length} user(s)!`); toast.success(`Successfully added ${newUsers.length} user(s)!`);
onFinish(); onFinish();
} catch(e) { } catch (e) {
console.error(e) console.error(e)
toast.error("Something went wrong, please try again later!"); toast.error("Something went wrong, please try again later!");
} finally { } finally {
@@ -210,57 +213,126 @@ export default function BatchCreateUser({ user, entities = [], permissions, onFi
} }
}; };
const handleTemplateDownload = () => {
const fileName = "UsersTemplate.xlsx";
const url = `https://firebasestorage.googleapis.com/v0/b/encoach-staging.appspot.com/o/import_templates%2F${fileName}?alt=media&token=b771a535-bf95-4060-889c-a086df65d480`;
const link = document.createElement('a');
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
return ( return (
<> <>
<Modal isOpen={showHelp} onClose={() => setShowHelp(false)} title="Excel File Format"> <Modal isOpen={showHelp} onClose={() => setShowHelp(false)}>
<div className="mt-4 flex flex-col gap-2"> <>
<span>Please upload an Excel file with the following format:</span> <div className="flex font-bold text-xl justify-center text-gray-700"><span>Excel File Format</span></div>
<table className="w-full"> <div className="mt-4 flex flex-col gap-4">
<thead> <div className="flex flex-col gap-3 bg-gray-100 rounded-lg p-4">
<tr> <div className="flex items-center gap-2">
<th className="border border-neutral-200 px-2 py-1">First Name</th> <HiOutlineDocumentText className={`w-5 h-5 text-mti-purple-light`} />
<th className="border border-neutral-200 px-2 py-1">Last Name</th> <h2 className="text-lg font-semibold">
<th className="border border-neutral-200 px-2 py-1">Student ID</th> The uploaded document must:
<th className="border border-neutral-200 px-2 py-1">Passport/National ID</th> </h2>
<th className="border border-neutral-200 px-2 py-1">E-mail</th> </div>
<th className="border border-neutral-200 px-2 py-1">Phone Number</th> <ul className="flex flex-col pl-10 gap-2">
<th className="border border-neutral-200 px-2 py-1">Classroom Name</th> <li className="text-gray-700 list-disc">
<th className="border border-neutral-200 px-2 py-1">Country</th> be an Excel .xlsx document.
</tr> </li>
</thead> <li className="text-gray-700 list-disc">
</table> only have a single spreadsheet with only the following 8 columns in the <b>same order</b>:
<span className="mt-4"> <div className="py-4 pr-4">
<b>Notes:</b> <table className="w-full bg-white">
<ul> <thead>
<li>- All incorrect e-mails will be ignored;</li> <tr>
<li>- All already registered e-mails will be ignored;</li> <th className="border border-neutral-200 px-2 py-1">First Name</th>
<li>- You may have a header row with the format above, however, it is not necessary;</li> <th className="border border-neutral-200 px-2 py-1">Last Name</th>
<li>- All of the e-mails in the file will receive an e-mail to join EnCoach with the role selected below.</li> <th className="border border-neutral-200 px-2 py-1">Student ID</th>
</ul> <th className="border border-neutral-200 px-2 py-1">Passport/National ID</th>
</span> <th className="border border-neutral-200 px-2 py-1">E-mail</th>
</div> <th className="border border-neutral-200 px-2 py-1">Phone Number</th>
<th className="border border-neutral-200 px-2 py-1">Classroom Name</th>
<th className="border border-neutral-200 px-2 py-1">Country</th>
</tr>
</thead>
</table>
</div>
</li>
</ul>
</div>
<div className="flex flex-col gap-3 bg-gray-100 rounded-lg p-4">
<div className="flex items-center gap-2">
<IoInformationCircleOutline className={`w-5 h-5 text-mti-purple-light`} />
<h2 className="text-lg font-semibold">
Note that:
</h2>
</div>
<ul className="flex flex-col pl-10 gap-2">
<li className="text-gray-700 list-disc">
all incorrect e-mails will be ignored.
</li>
<li className="text-gray-700 list-disc">
all already registered e-mails will be ignored.
</li>
<li className="text-gray-700 list-disc">
the spreadsheet may have a header row with the format above, however, it is not necessary as long the columns are in the <b>right order</b>.
</li>
<li className="text-gray-700 list-disc">
all of the e-mails in the file will receive an e-mail to join EnCoach with the role selected below.
</li>
</ul>
</div>
<div className="bg-gray-100 rounded-lg p-4">
<p className="text-gray-600">
{`The downloadable template is an example of a file that can be imported. Your document doesn't need to be a carbon copy of the template - it can have different styling but it must adhere to the previous requirements.`}
</p>
</div>
<div className="w-full flex justify-between mt-6 gap-8">
<Button color="purple" onClick={() => setShowHelp(false)} variant="outline" className="self-end w-full bg-white">
Close
</Button>
<Button color="purple" onClick={handleTemplateDownload} variant="solid" className="self-end w-full">
<div className="flex items-center gap-2">
<FaFileDownload size={24} />
Download Template
</div>
</Button>
</div>
</div>
</>
</Modal> </Modal>
<div className="border-mti-gray-platinum flex flex-col gap-4 rounded-xl border p-4"> <div className="border-mti-gray-platinum flex flex-col gap-4 rounded-xl border p-4">
<div className="grid grid-cols-2 -md:grid-cols-1 gap-4"> <div className="grid grid-cols-2 -md:grid-cols-1 gap-4">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex items-end justify-between"> <div className="flex items-end justify-between">
<label className="text-mti-gray-dim text-base font-normal">Choose an Excel file</label> <label className="text-mti-gray-dim text-base font-normal">Choose an Excel file</label>
<div className="tooltip cursor-pointer" data-tip="Excel File Format" onClick={() => setShowHelp(true)}> <button
<BsQuestionCircleFill /> onClick={() => setShowHelp(true)}
</div> className="tooltip cursor-pointer p-1.5 hover:bg-gray-200 rounded-full transition-colors duration-200"
data-tip="Excel File Format"
>
<IoInformationCircleOutline size={24} />
</button>
</div> </div>
<Button onClick={openFilePicker} isLoading={isLoading} disabled={isLoading}> <Button onClick={openFilePicker} isLoading={isLoading} disabled={isLoading}>
{filesContent.length > 0 ? filesContent[0].name : "Choose a file"} {filesContent.length > 0 ? filesContent[0].name : "Choose a file"}
</Button> </Button>
</div> </div>
<div className={clsx("flex flex-col gap-4")}> <div className={clsx("flex flex-col gap-4")}>
<label className="font-normal text-base text-mti-gray-dim">Entity</label> <label className="font-normal text-base text-mti-gray-dim">Entity</label>
<Select <Select
defaultValue={{ value: (entities || [])[0]?.id, label: (entities || [])[0]?.label }} defaultValue={{ value: (entities || [])[0]?.id, label: (entities || [])[0]?.label }}
options={entities.map((e) => ({ value: e.id, label: e.label }))} options={entities.map((e) => ({ value: e.id, label: e.label }))}
onChange={(e) => setEntity(e?.value || undefined)} onChange={(e) => setEntity(e?.value || undefined)}
isClearable={checkAccess(user, ["admin", "developer"])} isClearable={checkAccess(user, ["admin", "developer"])}
/> />
</div> </div>
</div> </div>
{user && checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && ( {user && checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && (

View File

@@ -15,6 +15,7 @@ export const defaultSettings = (module: Module) => {
isCategoryDropdownOpen: false, isCategoryDropdownOpen: false,
isIntroDropdownOpen: false, isIntroDropdownOpen: false,
isExerciseDropdownOpen: false, isExerciseDropdownOpen: false,
isTypeDropdownOpen: false,
} }
switch (module) { switch (module) {
@@ -163,6 +164,9 @@ const defaultModuleSettings = (module: Module, minTimer: number): ModuleState =>
importing: false, importing: false,
edit: [], edit: [],
}; };
if (["reading", "writing"].includes(module)) {
state["type"] = "general";
}
return state; return state;
} }

View File

@@ -7,15 +7,12 @@ import { Module } from "@/interfaces";
import { updateExamWithUserSolutions } from "@/stores/exam/utils"; import { updateExamWithUserSolutions } from "@/stores/exam/utils";
import { defaultExamUserSolutions } from "@/utils/exams"; import { defaultExamUserSolutions } from "@/utils/exams";
type UpdateRoot = { type RootActions = { type: 'FULL_RESET' } |
type: 'UPDATE_ROOT'; { type: 'INIT_EXAM_EDIT', payload: { exam: Exam; examModule: Module; id: string } } |
payload: { { type: 'UPDATE_ROOT'; payload: { updates: Partial<ExamEditorStore> } } |
updates: Partial<ExamEditorStore> { type: 'RESET_MODULE'; payload: { module: Module } };
}
};
type RootActions = { type: 'FULL_RESET' } | { type: "INIT_EXAM_EDIT", payload: { exam: Exam; examModule: Module; id: string } };
export type Action = ModuleActions | SectionActions | UpdateRoot | RootActions; export type Action = ModuleActions | SectionActions | RootActions;
export const rootReducer = ( export const rootReducer = (
state: ExamEditorStore, state: ExamEditorStore,
@@ -55,6 +52,16 @@ export const rootReducer = (
...state, ...state,
...updates ...updates
}; };
case 'RESET_MODULE':
const { module } = action.payload;
const timer = ["reading", "writing", "level"].includes(module) ?
60 : (module === "speaking" ? 14 : 30);
return {
modules: {
...state.modules,
[module]: defaultModuleSettings(module, timer),
},
};
case 'FULL_RESET': case 'FULL_RESET':
return { return {
title: "", title: "",

View File

@@ -16,6 +16,7 @@ export interface SectionSettings {
currentIntro: string | undefined; currentIntro: string | undefined;
isCategoryDropdownOpen: boolean; isCategoryDropdownOpen: boolean;
isIntroDropdownOpen: boolean; isIntroDropdownOpen: boolean;
isTypeDropdownOpen: boolean;
} }
export interface SpeakingSectionSettings extends SectionSettings { export interface SpeakingSectionSettings extends SectionSettings {
@@ -114,6 +115,7 @@ export interface ModuleState {
importModule: boolean; importModule: boolean;
importing: boolean; importing: boolean;
edit: number[]; edit: number[];
type?: "general" | "academic";
} }
export interface Avatar { export interface Avatar {