diff --git a/package.json b/package.json index 557c4e8..c90f36f 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "✅ Do add `devDependencies` below that are `peerDependencies` in the CFL package." ], "dependencies": { - "codeforlife": "github:ocadotechnology/codeforlife-package-javascript#v2.3.10", + "codeforlife": "github:ocadotechnology/codeforlife-package-javascript#v2.4.1", "crypto-js": "^4.2.0" }, "devDependencies": { diff --git a/src/api/authFactor.ts b/src/api/authFactor.ts index ea92923..907098b 100644 --- a/src/api/authFactor.ts +++ b/src/api/authFactor.ts @@ -35,6 +35,7 @@ const authFactorApi = api.injectEndpoints({ method: "POST", body, }), + invalidatesTags: tagData(AUTH_FACTOR_TAG, { includeListTag: true }), }), destroyAuthFactor: build.mutation< DestroyAuthFactorResult, @@ -44,7 +45,7 @@ const authFactorApi = api.injectEndpoints({ url: buildUrl(urls.authFactor.detail, { url: { id } }), method: "DELETE", }), - invalidatesTags: tagData(AUTH_FACTOR_TAG), + invalidatesTags: tagData(AUTH_FACTOR_TAG, { includeListTag: true }), }), }), }) diff --git a/src/api/klass.ts b/src/api/klass.ts index def4fe8..e023c9a 100644 --- a/src/api/klass.ts +++ b/src/api/klass.ts @@ -61,13 +61,14 @@ const classApi = api.injectEndpoints({ method: "POST", body, }), + invalidatesTags: tagData(CLASS_TAG, { includeListTag: true }), }), destroyClass: build.mutation({ query: id => ({ url: buildUrl(urls.class.detail, { url: { id } }), method: "DELETE", }), - invalidatesTags: tagData(CLASS_TAG), + invalidatesTags: tagData(CLASS_TAG, { includeListTag: true }), }), updateClass: build.mutation({ query: ({ id, ...body }) => ({ @@ -75,7 +76,7 @@ const classApi = api.injectEndpoints({ method: "PATCH", body, }), - invalidatesTags: tagData(CLASS_TAG), + invalidatesTags: tagData(CLASS_TAG, { includeListTag: true }), }), updateClasses: build.mutation({ query: body => ({ @@ -83,7 +84,10 @@ const classApi = api.injectEndpoints({ method: "PATCH", body, }), - invalidatesTags: tagData(CLASS_TAG), + invalidatesTags: tagData(CLASS_TAG, { + argKeysAreIds: true, + includeListTag: true, + }), }), }), }) diff --git a/src/api/school.ts b/src/api/school.ts index 307b76f..f31e00f 100644 --- a/src/api/school.ts +++ b/src/api/school.ts @@ -36,6 +36,7 @@ const schoolApi = api.injectEndpoints({ method: "POST", body, }), + invalidatesTags: tagData(SCHOOL_TAG, { includeListTag: true }), }), updateSchool: build.mutation({ query: ({ id, ...body }) => ({ @@ -43,7 +44,7 @@ const schoolApi = api.injectEndpoints({ method: "PATCH", body, }), - invalidatesTags: tagData(SCHOOL_TAG), + invalidatesTags: tagData(SCHOOL_TAG, { includeListTag: true }), }), }), }) diff --git a/src/api/schoolTeacherInvitation.ts b/src/api/schoolTeacherInvitation.ts index aaa2920..c34f0a8 100644 --- a/src/api/schoolTeacherInvitation.ts +++ b/src/api/schoolTeacherInvitation.ts @@ -98,7 +98,9 @@ const schoolTeacherInvitationApi = api.injectEndpoints({ method: "DELETE", body, }), - invalidatesTags: tagData("SchoolTeacherInvitation"), + invalidatesTags: tagData("SchoolTeacherInvitation", { + includeListTag: true, + }), }), rejectSchoolTeacherInvitation: build.mutation< RejectSchoolTeacherInvitationResult, @@ -108,7 +110,9 @@ const schoolTeacherInvitationApi = api.injectEndpoints({ url: buildUrl(detailUrl + "reject/", { url: { id } }), method: "DELETE", }), - invalidatesTags: tagData("SchoolTeacherInvitation"), + invalidatesTags: tagData("SchoolTeacherInvitation", { + includeListTag: true, + }), }), refreshSchoolTeacherInvitation: build.mutation< RefreshSchoolTeacherInvitationResult, @@ -118,7 +122,9 @@ const schoolTeacherInvitationApi = api.injectEndpoints({ url: buildUrl(detailUrl, { url: { id } }), method: "PUT", }), - invalidatesTags: tagData("SchoolTeacherInvitation"), + invalidatesTags: tagData("SchoolTeacherInvitation", { + includeListTag: true, + }), }), createSchoolTeacherInvitation: build.mutation< CreateSchoolTeacherInvitationResult, @@ -129,6 +135,9 @@ const schoolTeacherInvitationApi = api.injectEndpoints({ method: "POST", body, }), + invalidatesTags: tagData("SchoolTeacherInvitation", { + includeListTag: true, + }), }), destroySchoolTeacherInvitation: build.mutation< DestroySchoolTeacherInvitationResult, @@ -138,7 +147,9 @@ const schoolTeacherInvitationApi = api.injectEndpoints({ url: buildUrl(detailUrl, { url: { id } }), method: "DELETE", }), - invalidatesTags: tagData("SchoolTeacherInvitation"), + invalidatesTags: tagData("SchoolTeacherInvitation", { + includeListTag: true, + }), }), retrieveSchoolTeacherInvitation: build.query< RetrieveSchoolTeacherInvitationResult, @@ -158,7 +169,9 @@ const schoolTeacherInvitationApi = api.injectEndpoints({ url: buildUrl(listUrl, { search }), method: "GET", }), - providesTags: tagData("SchoolTeacherInvitation"), + providesTags: tagData("SchoolTeacherInvitation", { + includeListTag: true, + }), }), }), }) diff --git a/src/api/student.ts b/src/api/student.ts index c88bb66..6443c0d 100644 --- a/src/api/student.ts +++ b/src/api/student.ts @@ -13,11 +13,13 @@ import { type Student, type User, urls } from "codeforlife/api" import api from "." +export const STUDENT_ID = "user.id" + export type CreateStudentsResult = BulkCreateResult< Student, "auto_gen_password", { - user: Result + user: Result } > export type CreateStudentsArg = BulkCreateArg< @@ -76,6 +78,10 @@ const studentApi = api.injectEndpoints({ method: "POST", body, }), + invalidatesTags: tagData("User", { + id: STUDENT_ID, + includeListTag: true, + }), }), releaseStudents: build.mutation({ query: body => ({ @@ -83,7 +89,11 @@ const studentApi = api.injectEndpoints({ method: "PUT", body, }), - invalidatesTags: tagData("User", { id: "user" }), + invalidatesTags: tagData("User", { + id: STUDENT_ID, + argKeysAreIds: true, + includeListTag: true, + }), }), transferStudents: build.mutation< TransferStudentsResult, @@ -94,7 +104,11 @@ const studentApi = api.injectEndpoints({ method: "PUT", body, }), - invalidatesTags: tagData("User", { id: "user" }), + invalidatesTags: tagData("User", { + id: STUDENT_ID, + argKeysAreIds: true, + includeListTag: true, + }), }), resetStudentsPassword: build.mutation< ResetStudentsPasswordResult, @@ -112,7 +126,10 @@ const studentApi = api.injectEndpoints({ method: "DELETE", body, }), - invalidatesTags: tagData("User", { id: "user" }), + invalidatesTags: tagData("User", { + id: STUDENT_ID, + includeListTag: true, + }), }), }), }) diff --git a/src/api/teacher.ts b/src/api/teacher.ts index 8f5cc51..f738c4e 100644 --- a/src/api/teacher.ts +++ b/src/api/teacher.ts @@ -12,6 +12,8 @@ import { type Teacher, type User, urls } from "codeforlife/api" import api from "." +export const TEACHER_ID = "user.id" + export type CreateTeacherResult = CreateResult export type CreateTeacherArg = { user: Arg & { @@ -36,6 +38,10 @@ const teacherApi = api.injectEndpoints({ method: "POST", body, }), + invalidatesTags: tagData("User", { + id: TEACHER_ID, + includeListTag: true, + }), }), removeTeacherFromSchool: build.mutation< RemoveTeacherFromSchoolResult, @@ -47,7 +53,10 @@ const teacherApi = api.injectEndpoints({ }), method: "PUT", }), - invalidatesTags: tagData("User", { id: "user" }), + invalidatesTags: tagData("User", { + id: TEACHER_ID, + includeListTag: true, + }), }), setTeacherAdminAccess: build.mutation< SetTeacherAdminAccessResult, @@ -60,14 +69,20 @@ const teacherApi = api.injectEndpoints({ method: "PUT", body, }), - invalidatesTags: tagData("User", { id: "user" }), + invalidatesTags: tagData("User", { + id: TEACHER_ID, + includeListTag: true, + }), }), destroyTeacher: build.mutation({ query: id => ({ url: buildUrl(urls.teacher.detail, { url: { id } }), method: "DELETE", }), - invalidatesTags: tagData("User", { id: "user" }), + invalidatesTags: tagData("User", { + id: TEACHER_ID, + includeListTag: true, + }), }), }), }) diff --git a/src/api/user.ts b/src/api/user.ts index 40ef524..c4df702 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -124,7 +124,7 @@ const userApi = api.injectEndpoints({ method: "PUT", body, }), - invalidatesTags: tagData(USER_TAG), + invalidatesTags: tagData(USER_TAG, { includeListTag: true }), }), requestPasswordReset: build.query< RequestPasswordResetResult, @@ -161,7 +161,7 @@ const userApi = api.injectEndpoints({ method: "PATCH", body, }), - invalidatesTags: tagData(USER_TAG), + invalidatesTags: tagData(USER_TAG, { includeListTag: true }), }), destroyIndependentUser: build.mutation< DestroyIndependentUserResult, @@ -172,7 +172,7 @@ const userApi = api.injectEndpoints({ method: "DELETE", body, }), - invalidatesTags: tagData(USER_TAG), + invalidatesTags: tagData(USER_TAG, { includeListTag: true }), }), createIndependentUser: build.mutation< CreateIndependentUserResult, @@ -183,6 +183,7 @@ const userApi = api.injectEndpoints({ method: "POST", body, }), + invalidatesTags: tagData(USER_TAG, { includeListTag: true }), }), // TODO: create action on the backend. validatePassword: build.query({ diff --git a/src/features/footer/RegisterToNewsletterForm.tsx b/src/features/footer/RegisterToNewsletterForm.tsx index c26ab01..819538e 100644 --- a/src/features/footer/RegisterToNewsletterForm.tsx +++ b/src/features/footer/RegisterToNewsletterForm.tsx @@ -1,7 +1,6 @@ import * as forms from "codeforlife/components/form" import { FormHelperText, Stack, useMediaQuery, useTheme } from "@mui/material" import { type FC } from "react" -import { submitForm } from "codeforlife/utils/form" import { useNavigate } from "react-router-dom" import { useRegisterToNewsletterMutation } from "../../api/user" @@ -11,7 +10,6 @@ export interface RegisterToNewsletterFormProps {} const RegisterToNewsletterForm: FC = () => { const theme = useTheme() const onlyXS = useMediaQuery(theme.breakpoints.only("xs")) - const [registerToNewsletter] = useRegisterToNewsletterMutation() const navigate = useNavigate() return ( @@ -25,7 +23,8 @@ const RegisterToNewsletterForm: FC = () => { email: "", over18: false, }} - onSubmit={submitForm(registerToNewsletter, { + useMutation={useRegisterToNewsletterMutation} + submitOptions={{ exclude: ["over18"], then: () => { navigate(".", { @@ -54,7 +53,7 @@ const RegisterToNewsletterForm: FC = () => { }, }) }, - })} + }} > extends FormProps { +export type BaseFormProps< + Values extends FormValues, + QueryArg extends FormValues, + ResultType, +> = FormProps & { themedBoxProps: Omit header: string subheader?: string } -const BaseForm = ({ +const BaseForm = < + Values extends FormValues = FormValues, + QueryArg extends FormValues = FormValues, + ResultType = unknown, +>({ themedBoxProps, header, subheader, ...formProps -}: BaseFormProps): JSX.Element => { +}: BaseFormProps): JSX.Element => { const theme = useTheme() return ( @@ -35,6 +43,7 @@ const BaseForm = ({ {subheader} )} + {/* @ts-expect-error props */}
diff --git a/src/pages/login/IndyForm.tsx b/src/pages/login/IndyForm.tsx index dad6392..8daab7c 100644 --- a/src/pages/login/IndyForm.tsx +++ b/src/pages/login/IndyForm.tsx @@ -2,7 +2,6 @@ import * as form from "codeforlife/components/form" import { Stack, Typography } from "@mui/material" import type { FC } from "react" import { Link } from "codeforlife/components/router" -import { submitForm } from "codeforlife/utils/form" import { useNavigate } from "codeforlife/hooks" import BaseForm from "./BaseForm" @@ -12,7 +11,6 @@ import { useLoginWithEmailMutation } from "../../api/sso" export interface IndyFormProps {} const IndyForm: FC = () => { - const [loginWithEmail] = useLoginWithEmailMutation() const navigate = useNavigate() return ( @@ -21,11 +19,12 @@ const IndyForm: FC = () => { header="Welcome" subheader="Please enter your login details." initialValues={{ email: "", password: "" }} - onSubmit={submitForm(loginWithEmail, { + useMutation={useLoginWithEmailMutation} + submitOptions={{ then: () => { navigate(paths.indy.dashboard._) }, - })} + }} > diff --git a/src/pages/login/studentForms/FirstName.tsx b/src/pages/login/studentForms/FirstName.tsx index 6bdb6db..10d9503 100644 --- a/src/pages/login/studentForms/FirstName.tsx +++ b/src/pages/login/studentForms/FirstName.tsx @@ -3,7 +3,6 @@ import { type FC, useEffect } from "react" import { useNavigate, useParams } from "codeforlife/hooks" import { ChevronRight as ChevronRightIcon } from "@mui/icons-material" import { Stack } from "@mui/material" -import { submitForm } from "codeforlife/utils/form" import BaseForm from "../BaseForm" import { classIdSchema } from "../../../app/schemas" @@ -13,7 +12,6 @@ import { useLoginAsStudentMutation } from "../../../api/sso" export interface FirstNameProps {} const FirstName: FC = () => { - const [loginAsStudent] = useLoginAsStudentMutation() const navigate = useNavigate() const params = useParams({ classId: classIdSchema.required() }) @@ -47,11 +45,12 @@ const FirstName: FC = () => { password: "", class_id: params.classId, }} - onSubmit={submitForm(loginAsStudent, { + useMutation={useLoginAsStudentMutation} + submitOptions={{ then: () => { navigate(paths.student.dashboard._) }, - })} + }} > diff --git a/src/pages/login/teacherForms/Email.tsx b/src/pages/login/teacherForms/Email.tsx index a051c17..2e01ebf 100644 --- a/src/pages/login/teacherForms/Email.tsx +++ b/src/pages/login/teacherForms/Email.tsx @@ -2,7 +2,6 @@ import * as form from "codeforlife/components/form" import { Stack, Typography } from "@mui/material" import type { FC } from "react" import { Link } from "codeforlife/components/router" -import { submitForm } from "codeforlife/utils/form" import { useNavigate } from "codeforlife/hooks" import BaseForm from "../BaseForm" @@ -12,7 +11,6 @@ import { useLoginWithEmailMutation } from "../../../api/sso" export interface EmailProps {} const Email: FC = () => { - const [loginWithEmail] = useLoginWithEmailMutation() const navigate = useNavigate() return ( @@ -21,7 +19,8 @@ const Email: FC = () => { header="Welcome" subheader="Please enter your login details." initialValues={{ email: "", password: "" }} - onSubmit={submitForm(loginWithEmail, { + useMutation={useLoginWithEmailMutation} + submitOptions={{ then: ({ auth_factors }) => { navigate( auth_factors.includes("otp") @@ -29,7 +28,7 @@ const Email: FC = () => { : paths.teacher.dashboard.tab.school._, ) }, - })} + }} > diff --git a/src/pages/login/teacherForms/Otp.tsx b/src/pages/login/teacherForms/Otp.tsx index 0b65319..29cec25 100644 --- a/src/pages/login/teacherForms/Otp.tsx +++ b/src/pages/login/teacherForms/Otp.tsx @@ -3,7 +3,6 @@ import { useNavigate, useSession } from "codeforlife/hooks" import { type FC } from "react" import { LinkButton } from "codeforlife/components/router" import { Stack } from "@mui/material" -import { submitForm } from "codeforlife/utils/form" import BaseForm from "../BaseForm" import { paths } from "../../../routes" @@ -12,7 +11,6 @@ import { useLoginWithOtpMutation } from "../../../api/sso" export interface OtpProps {} const Otp: FC = () => { - const [loginWithOtp] = useLoginWithOtpMutation() const navigate = useNavigate() return useSession( @@ -22,11 +20,12 @@ const Otp: FC = () => { header="Welcome" subheader="Please enter the token generated by your token generator." initialValues={{ otp: "" }} - onSubmit={submitForm(loginWithOtp, { + useMutation={useLoginWithOtpMutation} + submitOptions={{ then: () => { navigate(paths.teacher.dashboard.tab.school._) }, - })} + }} > {otp_bypass_token_exists && ( diff --git a/src/pages/login/teacherForms/OtpBypassToken.tsx b/src/pages/login/teacherForms/OtpBypassToken.tsx index 4344f65..612cdd7 100644 --- a/src/pages/login/teacherForms/OtpBypassToken.tsx +++ b/src/pages/login/teacherForms/OtpBypassToken.tsx @@ -4,7 +4,6 @@ import { Stack, Typography, useTheme } from "@mui/material" import { useNavigate, useSession } from "codeforlife/hooks" import type { FC } from "react" import { LinkButton } from "codeforlife/components/router" -import { submitForm } from "codeforlife/utils/form" import BaseForm from "../BaseForm" import { paths } from "../../../routes" @@ -13,7 +12,6 @@ import { useLoginWithOtpBypassTokenMutation } from "../../../api/sso" export interface OtpBypassTokenProps {} const OtpBypassToken: FC = () => { - const [loginWithOtpBypassToken] = useLoginWithOtpBypassTokenMutation() const navigate = useNavigate() const theme = useTheme() @@ -22,11 +20,12 @@ const OtpBypassToken: FC = () => { themedBoxProps={{ userType: "teacher" }} header="Welcome" initialValues={{ token: "" }} - onSubmit={submitForm(loginWithOtpBypassToken, { + useMutation={useLoginWithOtpBypassTokenMutation} + submitOptions={{ then: () => { navigate(paths.teacher.dashboard.tab.school._) }, - })} + }} > Use this form for entering backup tokens for logging in. These tokens diff --git a/src/pages/register/IndyForm.tsx b/src/pages/register/IndyForm.tsx index 30bed70..e914156 100644 --- a/src/pages/register/IndyForm.tsx +++ b/src/pages/register/IndyForm.tsx @@ -4,7 +4,6 @@ import { ChevronRight as ChevronRightIcon } from "@mui/icons-material" import { type FC } from "react" import { Link } from "codeforlife/components/router" import dayjs from "dayjs" -import { submitForm } from "codeforlife/utils/form" import { useNavigate } from "react-router-dom" import { LastNameField, NewPasswordField } from "../../components/form" @@ -16,7 +15,6 @@ export interface IndyFormProps {} const IndyForm: FC = () => { const navigate = useNavigate() - const [createIndependentUser] = useCreateIndependentUserMutation() const EmailApplicableAge = 13 const ReceiveUpdateAge = 18 @@ -39,12 +37,13 @@ const IndyForm: FC = () => { password: "", password_repeat: "", }} - onSubmit={submitForm(createIndependentUser, { + useMutation={useCreateIndependentUserMutation} + submitOptions={{ exclude: ["password_repeat", "meets_criteria"], then: () => { navigate(paths.register.emailVerification.userType.indy._) }, - })} + }} > {form => { const yearsOfAge = form.values.date_of_birth diff --git a/src/pages/register/TeacherForm.tsx b/src/pages/register/TeacherForm.tsx index 4ab82d9..7c1002a 100644 --- a/src/pages/register/TeacherForm.tsx +++ b/src/pages/register/TeacherForm.tsx @@ -3,7 +3,6 @@ import { ChevronRight as ChevronRightIcon } from "@mui/icons-material" import { type FC } from "react" import { Link } from "codeforlife/components/router" import { Stack } from "@mui/material" -import { submitForm } from "codeforlife/utils/form" import { useNavigate } from "codeforlife/hooks" import { LastNameField, NewPasswordField } from "../../components/form" @@ -15,7 +14,6 @@ export interface TeacherFormProps {} const TeacherForm: FC = () => { const navigate = useNavigate() - const [createTeacher] = useCreateTeacherMutation() return ( = () => { meets_criteria: false, }, }} - onSubmit={submitForm(createTeacher, { + useMutation={useCreateTeacherMutation} + submitOptions={{ exclude: ["user.password_repeat", "user.meets_criteria"], then: () => { navigate(paths.register.emailVerification.userType.teacher._) }, - })} + }} > diff --git a/src/pages/studentAccount/UpdateAccountForm.tsx b/src/pages/studentAccount/UpdateAccountForm.tsx index 65ebe03..6b633fc 100644 --- a/src/pages/studentAccount/UpdateAccountForm.tsx +++ b/src/pages/studentAccount/UpdateAccountForm.tsx @@ -1,6 +1,6 @@ import * as forms from "codeforlife/components/form" import { Stack, Typography } from "@mui/material" -import { getDirty, isDirty, submitForm } from "codeforlife/utils/form" +import { getDirty, isDirty } from "codeforlife/utils/form" import { type FC } from "react" import { LinkButton } from "codeforlife/components/router" import { useNavigate } from "codeforlife/hooks" @@ -8,6 +8,7 @@ import { useNavigate } from "codeforlife/hooks" import { type RetrieveUserResult, type UpdateUserArg, + type UpdateUserResult, useUpdateUserMutation, } from "../../api/user" import { indyPasswordSchema, studentPasswordSchema } from "../../app/schemas" @@ -18,7 +19,6 @@ export interface UpdateAccountFormProps { } const UpdateAccountForm: FC = ({ user }) => { - const [updateUser] = useUpdateUserMutation() const navigate = useNavigate() const initialValues = user.student @@ -69,9 +69,10 @@ const UpdateAccountForm: FC = ({ user }) => { )} { + clean: (values: typeof initialValues) => { const arg: UpdateUserArg = { id: values.id } if (user.student || isDirty(values, initialValues, "password")) { arg.password = values.password @@ -87,7 +88,7 @@ const UpdateAccountForm: FC = ({ user }) => { return arg }, - then: (_, values) => { + then: (_: UpdateUserResult, values: typeof initialValues) => { const messages = [ "Your account details have been changed successfully.", ] @@ -111,7 +112,7 @@ const UpdateAccountForm: FC = ({ user }) => { }, }) }, - })} + }} > {form => { const dirty = getDirty(form.values, initialValues, [ diff --git a/src/pages/teacherDashboard/classes/ClassTable.tsx b/src/pages/teacherDashboard/classes/ClassTable.tsx index c15c841..de0fdbd 100644 --- a/src/pages/teacherDashboard/classes/ClassTable.tsx +++ b/src/pages/teacherDashboard/classes/ClassTable.tsx @@ -62,9 +62,7 @@ const ClassTable: FC = ({ authUser }) => ( } > diff --git a/src/pages/teacherDashboard/classes/Classes.tsx b/src/pages/teacherDashboard/classes/Classes.tsx index f8f07f4..138ed0c 100644 --- a/src/pages/teacherDashboard/classes/Classes.tsx +++ b/src/pages/teacherDashboard/classes/Classes.tsx @@ -8,8 +8,8 @@ import CreateClassForm from "./CreateClassForm" import JoinClassRequest from "./joinClassRequest/JoinClassRequest" import JoinClassRequestTable from "./JoinClassRequestTable" import ReleaseStudents from "./releaseStudents/ReleaseStudents" -import ResetStudentsPassword from "./resetStudentsPassword/ResetStudentsPassword" import { type RetrieveUserResult } from "../../../api/user" +import StudentsCredentials from "./studentsCredentials/StudentsCredentials" import TransferStudents from "./transferStudents/TransferStudents" import UpdateStudentUser from "./updateStudentUser/UpdateStudentUser" @@ -18,7 +18,7 @@ export interface ClassesProps { view?: | "class" | "join-class-request" - | "reset-students-password" + | "students-credentials" | "update-student-user" | "transfer-students" | "release-students" @@ -29,7 +29,7 @@ const Classes: FC = ({ authUser, view }) => { return { class: , "join-class-request": , - "reset-students-password": , + "students-credentials": , "update-student-user": , "transfer-students": , "release-students": , diff --git a/src/pages/teacherDashboard/classes/CreateClassForm.tsx b/src/pages/teacherDashboard/classes/CreateClassForm.tsx index df58369..095ec55 100644 --- a/src/pages/teacherDashboard/classes/CreateClassForm.tsx +++ b/src/pages/teacherDashboard/classes/CreateClassForm.tsx @@ -3,7 +3,6 @@ import { Stack, Typography } from "@mui/material" import { type FC } from "react" import { type SchoolTeacherUser } from "codeforlife/api" import { generatePath } from "react-router" -import { submitForm } from "codeforlife/utils/form" import { useNavigate } from "codeforlife/hooks" import { @@ -20,7 +19,6 @@ export interface CreateClassFormProps { } const CreateClassForm: FC = ({ authUser }) => { - const [createClass] = useCreateClassMutation() const navigate = useNavigate() return ( @@ -39,7 +37,8 @@ const CreateClassForm: FC = ({ authUser }) => { teacher: authUser.teacher.id, read_classmates_data: false, }} - onSubmit={submitForm(createClass, { + useMutation={useCreateClassMutation} + submitOptions={{ then: ({ id }, { name }) => { navigate( generatePath(paths.teacher.dashboard.tab.classes.class._, { @@ -58,7 +57,7 @@ const CreateClassForm: FC = ({ authUser }) => { }, ) }, - })} + }} > diff --git a/src/pages/teacherDashboard/classes/class/AdditionalClassDetails.tsx b/src/pages/teacherDashboard/classes/class/AdditionalClassDetails.tsx new file mode 100644 index 0000000..b0892d5 --- /dev/null +++ b/src/pages/teacherDashboard/classes/class/AdditionalClassDetails.tsx @@ -0,0 +1,61 @@ +import { Button, Stack, Typography } from "@mui/material" +import { + DeleteOutlined as DeleteOutlinedIcon, + Edit as EditIcon, +} from "@mui/icons-material" +import { type FC, useState } from "react" +import { type Class } from "codeforlife/api" +import { LinkButton } from "codeforlife/components/router" +import { generatePath } from "react-router-dom" + +import DeleteClassDialog from "./DeleteClassDialog" +import { paths } from "../../../../routes" + +export interface AdditionalClassDetailsProps { + classId: Class["id"] +} + +const AdditionalClassDetails: FC = ({ + classId, +}) => { + const [dialog, setDialog] = useState<"delete-class">() + + return ( + <> + Additional class details + + Here you can change settings and permissions for the class and the + students accessing it. You can also delete your class and change level + access. + + + } + to={generatePath(paths.teacher.dashboard.tab.classes.class.edit._, { + classId, + })} + > + Edit details + + + + { + setDialog(undefined) + }} + /> + + ) +} + +export default AdditionalClassDetails diff --git a/src/pages/teacherDashboard/classes/class/Class.tsx b/src/pages/teacherDashboard/classes/class/Class.tsx index 0d07ad8..de0e2cb 100644 --- a/src/pages/teacherDashboard/classes/class/Class.tsx +++ b/src/pages/teacherDashboard/classes/class/Class.tsx @@ -1,11 +1,48 @@ import * as pages from "codeforlife/components/page" +import { type Class } from "codeforlife/api" import { type FC } from "react" +import { Link } from "codeforlife/components/router" import { Navigate } from "codeforlife/components/router" +import { Typography } from "@mui/material" +import { handleResultState } from "codeforlife/utils/api" import { useParams } from "codeforlife/hooks" +import AdditionalClassDetails from "./AdditionalClassDetails" +import CreateStudentsForm from "./CreateStudentsForm" import StudentTable from "./StudentTable" import { classIdSchema } from "../../../../app/schemas" import { paths } from "../../../../routes" +import { useRetrieveClassQuery } from "../../../../api/klass" + +const _Class: FC<{ classId: Class["id"] }> = ({ classId }) => { + return handleResultState(useRetrieveClassQuery(classId), klass => ( + <> + + + Update details for {klass.name} ({klass.id}) + + + Classes + + + Here you can view and manage all of your students within this class. + You can add new students, transfer existing students to another one of + your classes or to another teacher within your school or club, or + remove students altogether. + + + + + + + + + + + + + )) +} export interface ClassProps {} @@ -15,15 +52,7 @@ const Class: FC = () => { if (!params) return - const { classId } = params - - return ( - <> - - - - - ) + return <_Class {...params} /> } export default Class diff --git a/src/pages/teacherDashboard/classes/class/CreateStudentsForm.tsx b/src/pages/teacherDashboard/classes/class/CreateStudentsForm.tsx new file mode 100644 index 0000000..632fc62 --- /dev/null +++ b/src/pages/teacherDashboard/classes/class/CreateStudentsForm.tsx @@ -0,0 +1,126 @@ +import * as forms from "codeforlife/components/form" +import { Add as AddIcon, Upload as UploadIcon } from "@mui/icons-material" +import { type FC, type MutableRefObject, useEffect, useRef } from "react" +import { Stack, Typography } from "@mui/material" +import { type Class } from "codeforlife/api" +import { InputFileButton } from "codeforlife/components" +import { firstNameSchema } from "codeforlife/schemas/user" +import { generatePath } from "react-router-dom" +import { useNavigate } from "codeforlife/hooks" + +import { + type CreateStudentsArg, + useCreateStudentsMutation, +} from "../../../../api/student" +import { type StudentsCredentialsState } from "../studentsCredentials/StudentsCredentials" +import { paths } from "../../../../routes" + +export interface CreateStudentsFormProps { + classId: Class["id"] +} + +const CreateStudentsForm: FC = ({ classId }) => { + const fileInput = useRef() + const navigate = useNavigate() + + const reader = new FileReader() + const split = /\r\n|\n|\r|,/ + + useEffect(() => { + if (fileInput.current?.files && fileInput.current.files.length) { + reader.readAsText(fileInput.current.files[0]) + } + }, [fileInput.current?.files]) // eslint-disable-line react-hooks/exhaustive-deps + + return ( + <> + Add new students + + Add the student names to the box and separate them with a comma. + + + Student names and the class access code are required to sign in. + + } + variant="outlined" + className="body" + inputProps={{ + ref: fileInput as MutableRefObject, + accept: ".csv", + }} + > + Import CSV file + + + first_names.reduce((arg, first_name) => { + first_name = first_name.trim() + return first_name + ? [...arg, { klass: classId, user: { first_name } }] + : arg + }, [] as CreateStudentsArg), + then: students => { + navigate( + generatePath( + paths.teacher.dashboard.tab.classes.class.students.credentials + ._, + { classId }, + ), + { state: { students } }, + ) + }, + catch: () => { + navigate(".", { + state: { + notifications: [ + { + props: { + error: true, + children: "Failed to add students to class.", + }, + }, + ], + }, + }) + }, + }} + > + {({ setFieldValue }) => { + reader.onload = () => { + void setFieldValue("first_names", reader.result, true) + } + + return ( + + {/* TODO: show errors from backend */} + + }> + Add students + + + ) + }} + + + ) +} + +export default CreateStudentsForm diff --git a/src/pages/teacherDashboard/classes/class/DeleteClassDialog.tsx b/src/pages/teacherDashboard/classes/class/DeleteClassDialog.tsx new file mode 100644 index 0000000..e80710b --- /dev/null +++ b/src/pages/teacherDashboard/classes/class/DeleteClassDialog.tsx @@ -0,0 +1,57 @@ +import { type Class } from "codeforlife/api" +import { type FC } from "react" +import { useNavigate } from "codeforlife/hooks" + +import BaseDialog, { type BaseDialogProps } from "./BaseDialog" +import { paths } from "../../../../routes" +import { useDestroyClassMutation } from "../../../../api/klass" + +export interface DeleteClassDialogProps + extends Omit { + classId: Class["id"] +} + +const DeleteClassDialog: FC = ({ + classId, + ...baseDialogProps +}) => { + const [destroyClass] = useDestroyClassMutation() + const navigate = useNavigate() + + // TODO: check if the class has students and handle if true + return ( + { + destroyClass(classId) + .unwrap() + .then(() => { + navigate(paths.teacher.dashboard.tab.classes._, { + replace: true, + state: { + notifications: [ + { props: { children: "Successfully deleted class." } }, + ], + }, + }) + }) + .catch(() => { + navigate(".", { + state: { + replace: true, + notifications: [ + { + props: { error: true, children: "Failed to delete class." }, + }, + ], + }, + }) + }) + }} + /> + ) +} + +export default DeleteClassDialog diff --git a/src/pages/teacherDashboard/classes/class/DeleteStudentsDialog.tsx b/src/pages/teacherDashboard/classes/class/DeleteStudentsDialog.tsx new file mode 100644 index 0000000..cb9b145 --- /dev/null +++ b/src/pages/teacherDashboard/classes/class/DeleteStudentsDialog.tsx @@ -0,0 +1,66 @@ +import { type FC } from "react" +import { type StudentUser } from "codeforlife/api" +import { useNavigate } from "codeforlife/hooks" + +import BaseDialog, { type BaseDialogProps } from "./BaseDialog" +import { type ListUsersResult } from "../../../../api/user" +import { useDestroyStudentsMutation } from "../../../../api/student" + +export interface DeleteStudentsDialogProps + extends Omit { + studentUsers: Array> +} + +const DeleteStudentsDialog: FC = ({ + studentUsers, + onClose, + ...otherBaseDialogProps +}) => { + const [destroyStudents] = useDestroyStudentsMutation() + const navigate = useNavigate() + + return ( + { + destroyStudents(studentUsers.map(({ student: { id } }) => id)) + .unwrap() + .then(() => { + onClose() + navigate(".", { + replace: true, + state: { + notifications: [ + { + props: { + children: "Successfully deleted students from class.", + }, + }, + ], + }, + }) + }) + .catch(() => { + navigate(".", { + state: { + replace: true, + notifications: [ + { + props: { + error: true, + children: "Failed to delete students from class.", + }, + }, + ], + }, + }) + }) + }} + /> + ) +} + +export default DeleteStudentsDialog diff --git a/src/pages/teacherDashboard/classes/class/ResetStudentsPasswordDialog.tsx b/src/pages/teacherDashboard/classes/class/ResetStudentsPasswordDialog.tsx index 3589911..a278a99 100644 --- a/src/pages/teacherDashboard/classes/class/ResetStudentsPasswordDialog.tsx +++ b/src/pages/teacherDashboard/classes/class/ResetStudentsPasswordDialog.tsx @@ -1,4 +1,4 @@ -import { type Class, type StudentUser } from "codeforlife/api" +import { type Class, type Student, type StudentUser } from "codeforlife/api" import { type FC } from "react" import { generatePath } from "react-router-dom" import { useNavigate } from "codeforlife/hooks" @@ -9,7 +9,7 @@ import { useResetStudentsPasswordMutation, } from "../../../../api/student" import { type ListUsersResult } from "../../../../api/user" -import { type ResetStudentsPasswordState } from "../resetStudentsPassword/ResetStudentsPassword" +import { type StudentsCredentialsState } from "../studentsCredentials/StudentsCredentials" import { paths } from "../../../../routes" export interface ResetStudentsPasswordDialogProps @@ -32,21 +32,45 @@ const ResetStudentsPasswordDialog: FC = ({ header="Reset students' password" body="These students will have their passwords permanently changed. You will be given the option to print out the new passwords. Are you sure that you want to continue?" onConfirm={() => { - const resetStudentsPasswordArg = studentUsers.reduce( + const arg = studentUsers.reduce( (arg, { student: { id } }) => ({ ...arg, [id]: {} }), {} as ResetStudentsPasswordArg, ) - void resetStudentsPassword(resetStudentsPasswordArg) + void resetStudentsPassword(arg) .unwrap() - .then(resetStudentsPasswordResult => { - navigate( + .then(result => { + const first_names = studentUsers.reduce( + (first_names, { first_name, student: { id } }) => ({ + ...first_names, + [id]: first_name, + }), + {} as Record, + ) + + navigate( generatePath( - paths.teacher.dashboard.tab.classes.class.students.resetPassword + paths.teacher.dashboard.tab.classes.class.students.credentials ._, { classId }, ), - { state: { studentUsers, resetStudentsPasswordResult } }, + { + state: { + students: result.reduce( + (students, student) => [ + ...students, + { + ...student, + user: { + ...student.user, + first_name: first_names[student.id], + }, + }, + ], + [] as StudentsCredentialsState["students"], + ), + }, + }, ) }) .catch(() => { diff --git a/src/pages/teacherDashboard/classes/class/StudentTable.tsx b/src/pages/teacherDashboard/classes/class/StudentTable.tsx index 8933f5e..1e344c1 100644 --- a/src/pages/teacherDashboard/classes/class/StudentTable.tsx +++ b/src/pages/teacherDashboard/classes/class/StudentTable.tsx @@ -1,77 +1,199 @@ -import { Button, Stack } from "@mui/material" +import * as tables from "codeforlife/components/table" +import { Button, Checkbox, Stack, Typography } from "@mui/material" import { type Class, type StudentUser } from "codeforlife/api" +import { + DeleteOutline as DeleteOutlineIcon, + Edit as EditIcon, + SecurityOutlined as SecurityOutlinedIcon, +} from "@mui/icons-material" import { type FC, useState } from "react" import { LinkButton } from "codeforlife/components/router" -import { SecurityOutlined as SecurityOutlinedIcon } from "@mui/icons-material" +import { TablePagination } from "codeforlife/components" import { generatePath } from "react-router-dom" +import DeleteStudentsDialog from "./DeleteStudentsDialog" import { type ListUsersResult } from "../../../../api/user" import { type ReleaseStudentsState } from "../releaseStudents/ReleaseStudents" import ResetStudentsPasswordDialog from "./ResetStudentsPasswordDialog" import { type TransferStudentsState } from "../transferStudents/TransferStudents" import { paths } from "../../../../routes" +import { useLazyListUsersQuery } from "../../../../api/user" export interface StudentTableProps { classId: Class["id"] } const StudentTable: FC = ({ classId }) => { - const [dialog, setDialog] = useState<"reset-students-password">() - // @ts-expect-error temp fix until setStudentUsers is used - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [studentUsers, setStudentUsers] = useState< + const [dialog, setDialog] = useState< + "reset-students-password" | "delete-students" + >() + const [selectedStudentUsers, setSelectedStudentUsers] = useState< Record< ListUsersResult["data"][number]["id"], StudentUser > - >({ - // For testing purposes until this component is implemented - 27: { - id: 27, - first_name: "Student1", - is_active: true, - date_joined: new Date(), - student: { id: 17, klass: "ZZ111", school: 2 }, - }, - }) + >({}) + + const selectButtonDisabled = !Object.keys(selectedStudentUsers).length function closeDialog() { setDialog(undefined) } + function deselectAllStudentUsers() { + setSelectedStudentUsers({}) + } + return ( <> - - - {LinkButton({ - children: "Move", + Current students + + Select an individual student to change their details, including their + name and password. + + + Select multiple students using the checkboxes to reset their passwords, + move them to another class, release them from your school and make them + an independent Code for Life user, or delete them permanently. + + + {/* @ts-expect-error users are student-users */} + {( + studentUsers: Array>, + ) => ( + studentUser.id in selectedStudentUsers, + )} + onChange={event => { + if (event.target.checked) { + setSelectedStudentUsers( + studentUsers.reduce( + (selectedStudentUsers, studentUser) => ({ + ...selectedStudentUsers, + [studentUser.id]: studentUser, + }), + {} as typeof selectedStudentUsers, + ), + ) + } else deselectAllStudentUsers() + }} + /> + ), + }, + { align: "center", children: "Action" }, + ]} + > + {studentUsers.length ? ( + studentUsers.map(studentUser => ( + + + {studentUser.first_name} + + + { + setSelectedStudentUsers( + event.target.checked + ? previousSelectedStudentUsers => ({ + ...previousSelectedStudentUsers, + [studentUser.id]: studentUser, + }) + : previousSelectedStudentUsers => { + const selectedStudentUsers = { + ...previousSelectedStudentUsers, + } + delete selectedStudentUsers[studentUser.id] + return selectedStudentUsers + }, + ) + }} + /> + + + } + > + Edit details + + + + )) + ) : ( + + (no students) + + )} + + )} + + + {LinkButton({ + children: "Release", + disabled: selectButtonDisabled, to: generatePath( - paths.teacher.dashboard.tab.classes.class.students.transfer._, + paths.teacher.dashboard.tab.classes.class.students.release._, { classId }, ), - state: { studentUsers: Object.values(studentUsers) }, + state: { studentUsers: Object.values(selectedStudentUsers) }, })} - {LinkButton({ - children: "Release", + {LinkButton({ + children: "Move", + disabled: selectButtonDisabled, to: generatePath( - paths.teacher.dashboard.tab.classes.class.students.release._, + paths.teacher.dashboard.tab.classes.class.students.transfer._, { classId }, ), - state: { studentUsers: Object.values(studentUsers) }, + state: { studentUsers: Object.values(selectedStudentUsers) }, })} + + + ) diff --git a/src/pages/teacherDashboard/classes/joinClassRequest/HandleRequest.tsx b/src/pages/teacherDashboard/classes/joinClassRequest/HandleRequest.tsx index d6a4cf0..79d78d3 100644 --- a/src/pages/teacherDashboard/classes/joinClassRequest/HandleRequest.tsx +++ b/src/pages/teacherDashboard/classes/joinClassRequest/HandleRequest.tsx @@ -6,7 +6,6 @@ import { type FC } from "react" import { type IndependentUser } from "codeforlife/api" import { TablePagination } from "codeforlife/components" import { generatePath } from "react-router" -import { submitForm } from "codeforlife/utils/form" import { type RetrieveUserResult, @@ -26,104 +25,99 @@ const HandleRequest: FC = ({ klass, user, onAcceptRequest, -}) => { - const [handleJoinClassRequest] = useHandleJoinClassRequestMutation() - - return ( - <> - - - Add external student to class {klass.name} ({klass.id}) - - - - - - - {studentUsers => ( - - - Students currently in class - - {studentUsers.length ? ( - <> - - {user.first_name}, the new external student, will be - joining students in the class {klass.name} ({klass.id}) - - Student Name - {studentUsers.map((studentUser, index) => ( - - {studentUser.first_name} - - ))} - - ) : ( - - The new external student {user.first_name} is joining the - class {klass.name} ({klass.id}) in which there are - currently no other students. +}) => ( + <> + + + Add external student to class {klass.name} ({klass.id}) + + + + + + + {studentUsers => ( + + + Students currently in class + + {studentUsers.length ? ( + <> + + {user.first_name}, the new external student, will be + joining students in the class {klass.name} ({klass.id}) - )} - - )} - - - - - Add external student - - Please confirm the name of the new external student joining your - class. Their name will be used in their new login details, so - please ensure it is different from any other existing student in - the class. - - - - - - Cancel - - Save - - - - + Student Name + {studentUsers.map((studentUser, index) => ( + + {studentUser.first_name} + + ))} + + ) : ( + + The new external student {user.first_name} is joining the + class {klass.name} ({klass.id}) in which there are currently + no other students. + + )} + + )} + - - - Class - + + + Add external student + + Please confirm the name of the new external student joining your + class. Their name will be used in their new login details, so + please ensure it is different from any other existing student in + the class. + + + + + + Cancel + + Save + + + - - - ) -} + + + + Class + + + + +) export default HandleRequest diff --git a/src/pages/teacherDashboard/classes/releaseStudents/ReleaseStudentsForm.tsx b/src/pages/teacherDashboard/classes/releaseStudents/ReleaseStudentsForm.tsx index 803dfb4..388b09f 100644 --- a/src/pages/teacherDashboard/classes/releaseStudents/ReleaseStudentsForm.tsx +++ b/src/pages/teacherDashboard/classes/releaseStudents/ReleaseStudentsForm.tsx @@ -7,7 +7,6 @@ import { InputAdornment, Stack } from "@mui/material" import { type FC } from "react" import { LinkButton } from "codeforlife/components/router" import { type StudentUser } from "codeforlife/api" -import { submitForm } from "codeforlife/utils/form" import { useNavigate } from "codeforlife/hooks" import { @@ -25,7 +24,6 @@ const ReleaseStudentsForm: FC = ({ studentUsers, classPath, }) => { - const [releaseStudents] = useReleaseStudentsMutation() const navigate = useNavigate() return ( @@ -44,7 +42,8 @@ const ReleaseStudentsForm: FC = ({ }), {} as ReleaseStudentsArg, )} - onSubmit={submitForm(releaseStudents, { + useMutation={useReleaseStudentsMutation} + submitOptions={{ exclude: studentUsers.reduce( (exclude, studentUser) => [ ...exclude, @@ -80,7 +79,7 @@ const ReleaseStudentsForm: FC = ({ }, }) }, - })} + }} > {studentUsers.map(studentUser => ( diff --git a/src/pages/teacherDashboard/classes/resetStudentsPassword/ResetStudentsPassword.tsx b/src/pages/teacherDashboard/classes/resetStudentsPassword/ResetStudentsPassword.tsx deleted file mode 100644 index 1bb3302..0000000 --- a/src/pages/teacherDashboard/classes/resetStudentsPassword/ResetStudentsPassword.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import * as pages from "codeforlife/components/page" -import { useLocation, useParams } from "codeforlife/hooks" -import { type FC } from "react" -import { Navigate } from "codeforlife/components/router" -import { type StudentUser } from "codeforlife/api" -import { generatePath } from "react-router-dom" - -import { - type ListUsersResult, - type RetrieveUserResult, -} from "../../../../api/user" -import { type ResetStudentsPasswordResult } from "../../../../api/student" -// import { StudentCredentialsTable } from "../../../components" -import { classIdSchema } from "../../../../app/schemas" -import { paths } from "../../../../routes" - -export interface ResetStudentsPasswordState { - studentUsers: Array< - StudentUser - > - resetStudentsPasswordResult: ResetStudentsPasswordResult -} - -export interface ResetStudentsPasswordProps {} - -const ResetStudentsPassword: FC = () => { - const params = useParams({ classId: classIdSchema.required() }) - const { state } = useLocation() - - const { studentUsers, resetStudentsPasswordResult } = state || {} - const validState = - studentUsers && studentUsers.length && resetStudentsPasswordResult - - if (!params) - return - - const { classId } = params - - if (!validState) - return ( - - ) - - const students = resetStudentsPasswordResult.reduce( - (students, student) => ({ ...students, [student.id]: student }), - {} as Record< - ResetStudentsPasswordResult[number]["id"], - ResetStudentsPasswordResult[number] - >, - ) - - return ( - - {/* TODO: replace delete and replace with component when implemented. */} - {classId} - {studentUsers.map(({ student, ...fields }) => - JSON.stringify({ - ...fields, - password: students[student.id].user.password, - student: { - ...student, - auto_gen_password: students[student.id].auto_gen_password, - }, - }), - )} - {/* - JSON.stringify({ - ...fields, - password: students[student.id].user.password, - student: { - ...student, - auto_gen_password: students[student.id].auto_gen_password, - }, - }), - )} - /> */} - - ) -} - -export default ResetStudentsPassword diff --git a/src/pages/teacherDashboard/classes/studentsCredentials/StudentsCredentials.tsx b/src/pages/teacherDashboard/classes/studentsCredentials/StudentsCredentials.tsx new file mode 100644 index 0000000..c7a3c54 --- /dev/null +++ b/src/pages/teacherDashboard/classes/studentsCredentials/StudentsCredentials.tsx @@ -0,0 +1,53 @@ +import * as pages from "codeforlife/components/page" +import { type Student, type User } from "codeforlife/api" +import { useLocation, useParams } from "codeforlife/hooks" +import { type FC } from "react" +import { Navigate } from "codeforlife/components/router" +import { generatePath } from "react-router-dom" + +// import { StudentCredentialsTable } from "../../../components" +import { classIdSchema } from "../../../../app/schemas" +import { paths } from "../../../../routes" + +export interface StudentsCredentialsState { + students: Array< + Pick & { + user: Pick + } + > +} + +export interface StudentsCredentialsProps {} + +const StudentsCredentials: FC = () => { + const params = useParams({ classId: classIdSchema.required() }) + const { state } = useLocation() + + if (!params) + return + + const { classId } = params + const { students } = state || {} + + if (!students || !students.length) { + return ( + + ) + } + + return ( + + {/* TODO: replace delete and replace with component when implemented. */} + {classId} + {students.map(student => JSON.stringify(student))} + {/* */} + + ) +} + +export default StudentsCredentials diff --git a/src/pages/teacherDashboard/classes/transferStudents/TransferStudentsForm.tsx b/src/pages/teacherDashboard/classes/transferStudents/TransferStudentsForm.tsx index d6b02d9..39c1cd1 100644 --- a/src/pages/teacherDashboard/classes/transferStudents/TransferStudentsForm.tsx +++ b/src/pages/teacherDashboard/classes/transferStudents/TransferStudentsForm.tsx @@ -4,7 +4,6 @@ import { Stack, Typography } from "@mui/material" import { type FC } from "react" import { LinkButton } from "codeforlife/components/router" import { type StudentUser } from "codeforlife/api" -import { submitForm } from "codeforlife/utils/form" import { useNavigate } from "codeforlife/hooks" import { @@ -27,7 +26,6 @@ const TransferStudentsForm: FC = ({ classPath, newClass, }) => { - const [transferStudents] = useTransferStudentsMutation() const navigate = useNavigate() return ( @@ -50,7 +48,8 @@ const TransferStudentsForm: FC = ({ }), {} as TransferStudentsArg, )} - onSubmit={submitForm(transferStudents, { + useMutation={useTransferStudentsMutation} + submitOptions={{ then: () => { navigate(classPath, { state: { @@ -79,7 +78,7 @@ const TransferStudentsForm: FC = ({ }, }) }, - })} + }} > {studentUsers.map(studentUser => ( diff --git a/src/pages/teacherDashboard/classes/updateStudentUser/UpdateNameForm.tsx b/src/pages/teacherDashboard/classes/updateStudentUser/UpdateNameForm.tsx index 0192e32..6246775 100644 --- a/src/pages/teacherDashboard/classes/updateStudentUser/UpdateNameForm.tsx +++ b/src/pages/teacherDashboard/classes/updateStudentUser/UpdateNameForm.tsx @@ -2,7 +2,6 @@ import * as forms from "codeforlife/components/form" import { type FC } from "react" import { Typography } from "@mui/material" import { type User } from "codeforlife/api" -import { submitForm } from "codeforlife/utils/form" import { useNavigate } from "codeforlife/hooks" import { useUpdateUserMutation } from "../../../../api/user" @@ -16,7 +15,6 @@ const UpdateNameForm: FC = ({ classPath, studentUser, }) => { - const [updateUser] = useUpdateUserMutation() const navigate = useNavigate() return ( @@ -28,7 +26,8 @@ const UpdateNameForm: FC = ({ { navigate(classPath, { state: { @@ -43,7 +42,7 @@ const UpdateNameForm: FC = ({ }, }) }, - })} + }} > = ({ classId, user }) => { - const [resetStudentsPassword] = useResetStudentsPasswordMutation() const navigate = useNavigate() return ( @@ -36,24 +34,32 @@ const UpdatePasswordForm: FC = ({ classId, user }) => { user: { password: "", password_repeat: "" }, }, }} - onSubmit={submitForm(resetStudentsPassword, { + useMutation={useResetStudentsPasswordMutation} + submitOptions={{ exclude: [`${user.student.id}.user.password_repeat`], - then: resetStudentsPasswordResult => { - navigate( + then: ([student]) => { + navigate( generatePath( - paths.teacher.dashboard.tab.classes.class.students.resetPassword + paths.teacher.dashboard.tab.classes.class.students.credentials ._, { classId }, ), { state: { - studentUsers: [user], - resetStudentsPasswordResult, + students: [ + { + ...student, + user: { + ...student.user, + first_name: user.first_name, + }, + }, + ], }, }, ) }, - })} + }} > = ({ authUserId, user }) => { - const [updateClasses] = useUpdateClassesMutation() const [removeTeacherFromSchool] = useRemoveTeacherFromSchoolMutation() const navigate = useNavigate() @@ -96,7 +94,7 @@ const TransferClasses: FC = ({ authUserId, user }) => { }), {}, )} - onSubmit={submitForm(updateClasses)} + useMutation={useUpdateClassesMutation} > = ({ school }) => { const navigate = useNavigate() - const [updateSchool] = useUpdateSchoolMutation() return ( <> @@ -25,7 +23,8 @@ const UpdateSchoolForm: FC = ({ school }) => { { navigate(".", { state: { @@ -55,7 +54,7 @@ const UpdateSchoolForm: FC = ({ school }) => { }, }) }, - })} + }} > {form => ( <> diff --git a/src/pages/teacherDashboard/school/leave/Leave.tsx b/src/pages/teacherDashboard/school/leave/Leave.tsx index 08fb895..2e85a7c 100644 --- a/src/pages/teacherDashboard/school/leave/Leave.tsx +++ b/src/pages/teacherDashboard/school/leave/Leave.tsx @@ -8,7 +8,6 @@ import { Link, LinkButton } from "codeforlife/components/router" import { useNavigate, useParams } from "codeforlife/hooks" import { TablePagination } from "codeforlife/components" import { type User } from "codeforlife/api" -import { submitForm } from "codeforlife/utils/form" import { useLazyListClassesQuery, @@ -24,7 +23,6 @@ export interface LeaveProps { } const Leave: FC = ({ authUserId }) => { - const [updateClasses] = useUpdateClassesMutation() const [removeTeacherFromSchool] = useRemoveTeacherFromSchoolMutation() const [retrieveUser, { data: user, isLoading, isError }] = useLazyRetrieveUserQuery() @@ -123,7 +121,7 @@ const Leave: FC = ({ authUserId }) => { }), {}, )} - onSubmit={submitForm(updateClasses)} + useMutation={useUpdateClassesMutation} > } /> - } + path={paths.teacher.dashboard.tab.classes.class.students.credentials._} + element={} />