ENCOA-260, ENCOA-259
This commit is contained in:
@@ -155,11 +155,11 @@ const Templates: React.FC<Props> = ({ module, state, setState }) => {
|
||||
</li>
|
||||
{["reading", "level"].includes(module) && (
|
||||
<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 className="text-gray-700 list-disc">
|
||||
if solutions are going to be uploaded the exercise numbers/id's must match the ones in the solutions.
|
||||
if solutions are going to be uploaded, the exercise numbers/id's must match the ones in the solutions.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -186,7 +186,7 @@ const Templates: React.FC<Props> = ({ module, state, setState }) => {
|
||||
}
|
||||
<div className="bg-gray-50 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 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>
|
||||
</div>
|
||||
<div className="w-full flex justify-between mt-4 gap-8">
|
||||
|
||||
116
src/components/ExamEditor/ResetModule.tsx
Normal file
116
src/components/ExamEditor/ResetModule.tsx
Normal 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;
|
||||
@@ -34,7 +34,9 @@ const SettingsEditor: React.FC<SettingsEditorProps> = ({
|
||||
canPreview,
|
||||
canSubmit
|
||||
}) => {
|
||||
const { dispatch } = useExamEditorStore()
|
||||
const examLabel = useExamEditorStore((state) => state.modules[module].examLabel) || '';
|
||||
const type = useExamEditorStore((s) => s.modules[module].type);
|
||||
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<SectionSettings>(
|
||||
module,
|
||||
sectionId
|
||||
@@ -50,6 +52,18 @@ const SettingsEditor: React.FC<SettingsEditorProps> = ({
|
||||
updateLocalAndScheduleGlobal({ category: text });
|
||||
}, [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 }) => {
|
||||
let updates: Partial<SectionSettings> = { introOption: option };
|
||||
|
||||
@@ -100,6 +114,18 @@ const SettingsEditor: React.FC<SettingsEditorProps> = ({
|
||||
value={localSettings.category || ''}
|
||||
/>
|
||||
</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
|
||||
title="Divider"
|
||||
module={module}
|
||||
|
||||
@@ -31,6 +31,7 @@ const ReadingSettings: React.FC = () => {
|
||||
sections,
|
||||
minTimer,
|
||||
isPrivate,
|
||||
type,
|
||||
} = useExamEditorStore(state => state.modules[currentModule]);
|
||||
|
||||
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<ReadingSectionSettings>(
|
||||
@@ -78,10 +79,10 @@ const ReadingSettings: React.FC = () => {
|
||||
minTimer,
|
||||
module: "reading",
|
||||
id: title,
|
||||
type: "academic",
|
||||
variant: sections.length === 3 ? "full" : "partial",
|
||||
difficulty,
|
||||
private: isPrivate,
|
||||
type: type!
|
||||
};
|
||||
|
||||
axios.post(`/api/exam/reading`, exam)
|
||||
@@ -112,6 +113,7 @@ const ReadingSettings: React.FC = () => {
|
||||
variant: undefined,
|
||||
difficulty,
|
||||
private: isPrivate,
|
||||
type: type!
|
||||
} as ReadingExam);
|
||||
setExerciseIndex(0);
|
||||
setQuestionIndex(0);
|
||||
|
||||
@@ -25,6 +25,7 @@ const WritingSettings: React.FC = () => {
|
||||
isPrivate,
|
||||
sections,
|
||||
focusedSection,
|
||||
type
|
||||
} = useExamEditorStore((store) => store.modules["writing"]);
|
||||
|
||||
const states = sections.flatMap((s) => s.state) as WritingExercise[];
|
||||
@@ -72,6 +73,7 @@ const WritingSettings: React.FC = () => {
|
||||
variant: undefined,
|
||||
difficulty,
|
||||
private: isPrivate,
|
||||
type: type!
|
||||
});
|
||||
setExerciseIndex(0);
|
||||
openDetachedTab("popout?type=Exam&module=writing", router)
|
||||
@@ -94,6 +96,7 @@ const WritingSettings: React.FC = () => {
|
||||
variant: undefined,
|
||||
difficulty,
|
||||
private: isPrivate,
|
||||
type: type!
|
||||
};
|
||||
|
||||
axios
|
||||
|
||||
@@ -17,6 +17,8 @@ import ListeningSettings from "./SettingsEditor/listening";
|
||||
import SpeakingSettings from "./SettingsEditor/speaking";
|
||||
import ImportOrStartFromScratch from "./ImportExam/ImportOrFromScratch";
|
||||
import { defaultSectionSettings } from "@/stores/examEditor/defaults";
|
||||
import Button from "../Low/Button";
|
||||
import ResetModule from "./ResetModule";
|
||||
|
||||
const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"];
|
||||
|
||||
@@ -34,6 +36,7 @@ const ExamEditor: React.FC<{ levelParts?: number }> = ({ levelParts = 0 }) => {
|
||||
} = useExamEditorStore(state => state.modules[currentModule]);
|
||||
|
||||
const [numberOfLevelParts, setNumberOfLevelParts] = useState(levelParts !== 0 ? levelParts : 1);
|
||||
const [isResetModuleOpen, setIsResetModuleOpen] = useState(false);
|
||||
|
||||
// For exam edits
|
||||
useEffect(() => {
|
||||
@@ -129,6 +132,7 @@ const ExamEditor: React.FC<{ levelParts?: number }> = ({ levelParts = 0 }) => {
|
||||
<>
|
||||
{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 flex-col gap-3">
|
||||
<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>
|
||||
</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>
|
||||
<Input
|
||||
type="text"
|
||||
@@ -195,6 +200,10 @@ const ExamEditor: React.FC<{ levelParts?: number }> = ({ levelParts = 0 }) => {
|
||||
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 className="flex flex-row gap-8">
|
||||
<Settings />
|
||||
<div className="flex-grow max-w-[66%]">
|
||||
|
||||
@@ -12,6 +12,7 @@ interface Props {
|
||||
padding?: string;
|
||||
onClick?: () => void;
|
||||
type?: "button" | "reset" | "submit";
|
||||
customColor?: string;
|
||||
}
|
||||
|
||||
export default function Button({
|
||||
@@ -19,6 +20,7 @@ export default function Button({
|
||||
variant = "solid",
|
||||
disabled = false,
|
||||
isLoading = false,
|
||||
customColor = undefined,
|
||||
className,
|
||||
children,
|
||||
type,
|
||||
@@ -65,7 +67,7 @@ export default function Button({
|
||||
className={clsx(
|
||||
"rounded-full transition ease-in-out duration-300 disabled:cursor-not-allowed cursor-pointer select-none",
|
||||
padding,
|
||||
colorClassNames[color][variant],
|
||||
customColor ? customColor : colorClassNames[color][variant],
|
||||
className,
|
||||
)}
|
||||
disabled={disabled || isLoading}>
|
||||
|
||||
@@ -97,6 +97,7 @@ export interface WritingExam extends ExamBase {
|
||||
module: "writing";
|
||||
enableNavigation?: boolean;
|
||||
exercises: WritingExercise[];
|
||||
type?: "academic" | "general";
|
||||
}
|
||||
|
||||
interface WordCounter {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Button from "@/components/Low/Button";
|
||||
import axios from "axios";
|
||||
import { uniqBy } from "lodash";
|
||||
import { capitalize, uniqBy } from "lodash";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import { useFilePicker } from "use-file-picker";
|
||||
@@ -19,6 +19,9 @@ import { Type, UserImport } from "../../../interfaces/IUserImport";
|
||||
import UserTable from "../../../components/UserTable";
|
||||
import { EntityWithRoles } from "@/interfaces/entity";
|
||||
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]+)*$/);
|
||||
|
||||
@@ -135,7 +138,7 @@ export default function BatchCreateUser({ user, entities = [], permissions, onFi
|
||||
}
|
||||
|
||||
setInfos(information);
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
|
||||
toast.error(
|
||||
@@ -157,7 +160,7 @@ export default function BatchCreateUser({ user, entities = [], permissions, onFi
|
||||
|
||||
const crossRefEmails = response.data;
|
||||
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 newUsersList = infos.filter(info => !existingEmails.has(info.email));
|
||||
setNewUsers(newUsersList);
|
||||
@@ -192,10 +195,10 @@ export default function BatchCreateUser({ user, entities = [], permissions, onFi
|
||||
setIsLoading(true);
|
||||
|
||||
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)!`);
|
||||
onFinish();
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.error("Something went wrong, please try again later!");
|
||||
} finally {
|
||||
@@ -210,12 +213,42 @@ 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 (
|
||||
<>
|
||||
<Modal isOpen={showHelp} onClose={() => setShowHelp(false)} title="Excel File Format">
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
<span>Please upload an Excel file with the following format:</span>
|
||||
<table className="w-full">
|
||||
<Modal isOpen={showHelp} onClose={() => setShowHelp(false)}>
|
||||
<>
|
||||
<div className="flex font-bold text-xl justify-center text-gray-700"><span>Excel File Format</span></div>
|
||||
<div className="mt-4 flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-3 bg-gray-100 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<HiOutlineDocumentText className={`w-5 h-5 text-mti-purple-light`} />
|
||||
<h2 className="text-lg font-semibold">
|
||||
The uploaded document must:
|
||||
</h2>
|
||||
</div>
|
||||
<ul className="flex flex-col pl-10 gap-2">
|
||||
<li className="text-gray-700 list-disc">
|
||||
be an Excel .xlsx document.
|
||||
</li>
|
||||
<li className="text-gray-700 list-disc">
|
||||
only have a single spreadsheet with only the following 8 columns in the <b>same order</b>:
|
||||
<div className="py-4 pr-4">
|
||||
<table className="w-full bg-white">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="border border-neutral-200 px-2 py-1">First Name</th>
|
||||
@@ -229,25 +262,64 @@ export default function BatchCreateUser({ user, entities = [], permissions, onFi
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
<span className="mt-4">
|
||||
<b>Notes:</b>
|
||||
<ul>
|
||||
<li>- All incorrect e-mails will be ignored;</li>
|
||||
<li>- All already registered e-mails will be ignored;</li>
|
||||
<li>- You may have a header row with the format above, however, it is not necessary;</li>
|
||||
<li>- All of the e-mails in the file will receive an e-mail to join EnCoach with the role selected below.</li>
|
||||
</ul>
|
||||
</span>
|
||||
</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>
|
||||
<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="flex flex-col gap-4">
|
||||
<div className="flex items-end justify-between">
|
||||
<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)}>
|
||||
<BsQuestionCircleFill />
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowHelp(true)}
|
||||
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>
|
||||
<Button onClick={openFilePicker} isLoading={isLoading} disabled={isLoading}>
|
||||
{filesContent.length > 0 ? filesContent[0].name : "Choose a file"}
|
||||
|
||||
@@ -15,6 +15,7 @@ export const defaultSettings = (module: Module) => {
|
||||
isCategoryDropdownOpen: false,
|
||||
isIntroDropdownOpen: false,
|
||||
isExerciseDropdownOpen: false,
|
||||
isTypeDropdownOpen: false,
|
||||
}
|
||||
|
||||
switch (module) {
|
||||
@@ -163,6 +164,9 @@ const defaultModuleSettings = (module: Module, minTimer: number): ModuleState =>
|
||||
importing: false,
|
||||
edit: [],
|
||||
};
|
||||
if (["reading", "writing"].includes(module)) {
|
||||
state["type"] = "general";
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,15 +7,12 @@ import { Module } from "@/interfaces";
|
||||
import { updateExamWithUserSolutions } from "@/stores/exam/utils";
|
||||
import { defaultExamUserSolutions } from "@/utils/exams";
|
||||
|
||||
type UpdateRoot = {
|
||||
type: 'UPDATE_ROOT';
|
||||
payload: {
|
||||
updates: Partial<ExamEditorStore>
|
||||
}
|
||||
};
|
||||
type RootActions = { type: 'FULL_RESET' } | { type: "INIT_EXAM_EDIT", payload: { exam: Exam; examModule: Module; id: string } };
|
||||
type RootActions = { type: 'FULL_RESET' } |
|
||||
{ type: 'INIT_EXAM_EDIT', payload: { exam: Exam; examModule: Module; id: string } } |
|
||||
{ type: 'UPDATE_ROOT'; payload: { updates: Partial<ExamEditorStore> } } |
|
||||
{ type: 'RESET_MODULE'; payload: { module: Module } };
|
||||
|
||||
export type Action = ModuleActions | SectionActions | UpdateRoot | RootActions;
|
||||
export type Action = ModuleActions | SectionActions | RootActions;
|
||||
|
||||
export const rootReducer = (
|
||||
state: ExamEditorStore,
|
||||
@@ -55,6 +52,16 @@ export const rootReducer = (
|
||||
...state,
|
||||
...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':
|
||||
return {
|
||||
title: "",
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface SectionSettings {
|
||||
currentIntro: string | undefined;
|
||||
isCategoryDropdownOpen: boolean;
|
||||
isIntroDropdownOpen: boolean;
|
||||
isTypeDropdownOpen: boolean;
|
||||
}
|
||||
|
||||
export interface SpeakingSectionSettings extends SectionSettings {
|
||||
@@ -114,6 +115,7 @@ export interface ModuleState {
|
||||
importModule: boolean;
|
||||
importing: boolean;
|
||||
edit: number[];
|
||||
type?: "general" | "academic";
|
||||
}
|
||||
|
||||
export interface Avatar {
|
||||
|
||||
Reference in New Issue
Block a user