diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6be6785e..1eb01f01 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,9 +4,6 @@ on: push: paths-ignore: - '**/*.md' - - '**/*.code-*' - - '.vscode/**' - - '.devcontainer.json' pull_request: issues: issue_comment: diff --git a/eslint.config.js b/eslint.config.js index 6404838c..0cbe7331 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,7 +3,12 @@ import ts from "typescript-eslint" export default ts.config( ...workspaceConfig, - { ignores: ["src/scripts/*"] }, + { + ignores: [ + "src/scripts/*", + "src/server.js", // TODO: convert to src/server.ts and remove this ignore + ], + }, { languageOptions: { parserOptions: { tsconfigRootDir: import.meta.dirname }, diff --git a/src/api/createApi.ts b/src/api/createApi.ts index 35963a1b..839fc2c4 100644 --- a/src/api/createApi.ts +++ b/src/api/createApi.ts @@ -1,12 +1,12 @@ import { + type FetchArgs, createApi as _createApi, fetchBaseQuery, - type FetchArgs, } from "@reduxjs/toolkit/query/react" import { SERVICE_API_URL } from "../settings" -import defaultTagTypes from "./tagTypes" import { buildLogoutEndpoint } from "./endpoints/session" +import defaultTagTypes from "./tagTypes" import { getCsrfCookie } from "../utils/auth" import { isSafeHttpMethod } from "../utils/api" @@ -45,7 +45,7 @@ export default function createApi({ const method = typeof arg === "string" ? "GET" : arg.method || "GET" if (type === "mutation" || !isSafeHttpMethod(method)) { - let csrfToken = getCsrfCookie() + const csrfToken = getCsrfCookie() if (csrfToken) headers.set("x-csrftoken", csrfToken) } @@ -55,7 +55,7 @@ export default function createApi({ const api = _createApi({ // https://redux-toolkit.js.org/rtk-query/usage/customizing-queries#implementing-a-custom-basequery - baseQuery: async (args, api, extraOptions) => { + baseQuery: async (args: string | FetchArgs, api, extraOptions) => { if (api.type === "mutation" && getCsrfCookie() === undefined) { // Get the CSRF token. const { error } = await fetch( diff --git a/src/api/endpoints/authFactor.ts b/src/api/endpoints/authFactor.ts index 8264c5e9..784eac40 100644 --- a/src/api/endpoints/authFactor.ts +++ b/src/api/endpoints/authFactor.ts @@ -1,10 +1,10 @@ import { type EndpointBuilder } from "@reduxjs/toolkit/query/react" import { - buildUrl, - tagData, type ListArg as _ListArg, type ListResult as _ListResult, + buildUrl, + tagData, } from "../../utils/api" import type { AuthFactor } from "../models" import { type TagTypes } from "../tagTypes" diff --git a/src/api/endpoints/klass.ts b/src/api/endpoints/klass.ts index 49d8e0ea..63cbb5fe 100644 --- a/src/api/endpoints/klass.ts +++ b/src/api/endpoints/klass.ts @@ -1,19 +1,19 @@ import { type EndpointBuilder } from "@reduxjs/toolkit/query/react" +import type { + Class, + SchoolTeacher, + SchoolTeacherUser, + Teacher, +} from "../models" import { - buildUrl, - tagData, type ListArg as _ListArg, type ListResult as _ListResult, type RetrieveArg as _RetrieveArg, type RetrieveResult as _RetrieveResult, + buildUrl, + tagData, } from "../../utils/api" -import type { - Class, - Teacher, - SchoolTeacher, - SchoolTeacherUser, -} from "../models" import { type TagTypes } from "../tagTypes" import urls from "../urls" diff --git a/src/api/endpoints/school.ts b/src/api/endpoints/school.ts index e84a8545..09f7a7bf 100644 --- a/src/api/endpoints/school.ts +++ b/src/api/endpoints/school.ts @@ -1,10 +1,10 @@ import { type EndpointBuilder } from "@reduxjs/toolkit/query/react" import { - buildUrl, - tagData, type RetrieveArg as _RetrieveArg, type RetrieveResult as _RetrieveResult, + buildUrl, + tagData, } from "../../utils/api" import type { School } from "../models" import { type TagTypes } from "../tagTypes" diff --git a/src/api/endpoints/session.ts b/src/api/endpoints/session.ts index dfabdec0..6167b544 100644 --- a/src/api/endpoints/session.ts +++ b/src/api/endpoints/session.ts @@ -1,4 +1,4 @@ -import { type EndpointBuilder, type Api } from "@reduxjs/toolkit/query/react" +import { type Api, type EndpointBuilder } from "@reduxjs/toolkit/query/react" import { login, logout } from "../../slices/session" @@ -39,6 +39,7 @@ export function buildLogoutEndpoint( console.error("Failed to call logout endpoint...", error) } finally { dispatch(logout()) + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access dispatch(api.util.resetApiState()) } }, diff --git a/src/api/endpoints/user.ts b/src/api/endpoints/user.ts index 0a6d26be..bb9de02b 100644 --- a/src/api/endpoints/user.ts +++ b/src/api/endpoints/user.ts @@ -1,14 +1,14 @@ import { type EndpointBuilder } from "@reduxjs/toolkit/query/react" +import type { Class, User } from "../models" import { - buildUrl, - tagData, type ListArg as _ListArg, type ListResult as _ListResult, type RetrieveArg as _RetrieveArg, type RetrieveResult as _RetrieveResult, + buildUrl, + tagData, } from "../../utils/api" -import type { Class, User } from "../models" import { type TagTypes } from "../tagTypes" import urls from "../urls" diff --git a/src/api/models.ts b/src/api/models.ts index cd4bf3ac..7e54114c 100644 --- a/src/api/models.ts +++ b/src/api/models.ts @@ -1,5 +1,5 @@ -import type { Model } from "../utils/api" import type { CountryIsoCodes, UkCounties } from "../utils/general" +import type { Model } from "../utils/api" // ----------------------------------------------------------------------------- // User Models diff --git a/src/api/schemas.ts b/src/api/schemas.ts index 92b9d63e..73b5847c 100644 --- a/src/api/schemas.ts +++ b/src/api/schemas.ts @@ -1,31 +1,31 @@ import * as yup from "yup" -import { UK_COUNTIES, COUNTRY_ISO_CODES } from "../utils/general" import type { - User, - TeacherUser, - SchoolTeacherUser, + AdminSchoolTeacher, AdminSchoolTeacherUser, - NonAdminSchoolTeacherUser, - NonSchoolTeacherUser, - Teacher, - Student, - Class, - School, AuthFactor, - OtpBypassToken, - StudentUser, + Class, IndependentUser, - SchoolTeacher, - AdminSchoolTeacher, NonAdminSchoolTeacher, + NonAdminSchoolTeacherUser, NonSchoolTeacher, + NonSchoolTeacherUser, + OtpBypassToken, + School, + SchoolTeacher, + SchoolTeacherUser, + Student, + StudentUser, + Teacher, + TeacherUser, + User, } from "./models" +import { COUNTRY_ISO_CODES, UK_COUNTIES } from "../utils/general" import { - unicodeAlphanumericString, - uppercaseAsciiAlphanumericString, lowercaseAsciiAlphanumericString, numericId, + unicodeAlphanumericString, + uppercaseAsciiAlphanumericString, } from "../utils/schema" import { type Schemas } from "../utils/api" diff --git a/src/components/App.tsx b/src/components/App.tsx index 5acd8f04..a416ef17 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,14 +1,14 @@ +import { BrowserRouter, Routes as RouterRoutes } from "react-router-dom" import { CssBaseline, ThemeProvider } from "@mui/material" -import { type ThemeProviderProps } from "@mui/material/styles/ThemeProvider" -import { type FC, type ReactNode, type JSX } from "react" +import { type FC, type JSX, type ReactNode } from "react" import { Provider, type ProviderProps } from "react-redux" -import { BrowserRouter, Routes as RouterRoutes } from "react-router-dom" -import { StaticRouter } from "react-router-dom/server" import { type Action } from "redux" +import { StaticRouter } from "react-router-dom/server" +import { type ThemeProviderProps } from "@mui/material/styles/ThemeProvider" import "./App.css" -import { useLocation } from "../hooks" import { SSR } from "../settings" +import { useLocation } from "../hooks" // import { InactiveDialog, ScreenTimeDialog } from "../features" // import { useCountdown, useEventListener } from "../hooks" // import "../scripts" @@ -60,7 +60,9 @@ const App = ({ path, theme, store, + // eslint-disable-next-line @typescript-eslint/no-unused-vars maxIdleSeconds = 60 * 60, + // eslint-disable-next-line @typescript-eslint/no-unused-vars maxTotalSeconds = 60 * 60, ...routesProps }: AppProps): JSX.Element => { diff --git a/src/components/CopyIconButton.test.tsx b/src/components/CopyIconButton.test.tsx index 76b3491c..8a42d313 100644 --- a/src/components/CopyIconButton.test.tsx +++ b/src/components/CopyIconButton.test.tsx @@ -1,7 +1,7 @@ import { screen } from "@testing-library/react" -import { renderWithUser } from "../utils/test" import CopyIconButton from "./CopyIconButton" +import { renderWithUser } from "../utils/test" test("Clicking button should copy content", async () => { const content = "Example string to be copied." diff --git a/src/components/CopyIconButton.tsx b/src/components/CopyIconButton.tsx index 79c4a6cc..9b302ef8 100644 --- a/src/components/CopyIconButton.tsx +++ b/src/components/CopyIconButton.tsx @@ -1,5 +1,5 @@ -import { ContentCopy as ContentCopyIcon } from "@mui/icons-material" import { IconButton, type IconButtonProps } from "@mui/material" +import { ContentCopy as ContentCopyIcon } from "@mui/icons-material" import type { FC } from "react" export interface CopyIconButtonProps extends Omit { @@ -15,7 +15,7 @@ const CopyIconButton: FC = ({ { - navigator.clipboard.writeText(content) + void navigator.clipboard.writeText(content) }} {...otherIconButtonProps} > diff --git a/src/components/Countdown.tsx b/src/components/Countdown.tsx index de7e6dc5..07063e1e 100644 --- a/src/components/Countdown.tsx +++ b/src/components/Countdown.tsx @@ -1,4 +1,4 @@ -import React from "react" +import { type FC, useState } from "react" import { Typography, type TypographyProps } from "@mui/material" import { useCountdown } from "../hooks" @@ -9,7 +9,7 @@ export interface CountdownProps extends Omit { onEnd: () => void } -const Countdown: React.FC = ({ +const Countdown: FC = ({ seconds, start = true, onEnd, @@ -17,7 +17,7 @@ const Countdown: React.FC = ({ }) => { seconds = Math.floor(seconds) const _seconds = useCountdown(seconds)[0] - const [end, setEnd] = React.useState(!start) + const [end, setEnd] = useState(!start) if (_seconds === 0 && !end) { setEnd(true) diff --git a/src/components/DownloadFileButton.tsx b/src/components/DownloadFileButton.tsx index b19aad8c..f7a62d41 100644 --- a/src/components/DownloadFileButton.tsx +++ b/src/components/DownloadFileButton.tsx @@ -24,7 +24,8 @@ const DownloadFileButton: FC = ({ let url: undefined | string = undefined let anchorProps: undefined | { download?: string; href: string } = undefined if ("mimeType" in file) { - let { text, mimeType, name, charset = "utf-8", extension } = file + const { text, mimeType, name, charset = "utf-8" } = file + let { extension } = file if (!extension) extension = "." + { plain: "txt", csv: "csv" }[mimeType] diff --git a/src/components/ElevatedAppBar.tsx b/src/components/ElevatedAppBar.tsx index 5891cb8e..b05d20c5 100644 --- a/src/components/ElevatedAppBar.tsx +++ b/src/components/ElevatedAppBar.tsx @@ -1,20 +1,20 @@ -import React from "react" import { AppBar, type AppBarProps, + Container, + type ContainerProps, Toolbar, type ToolbarProps, useScrollTrigger, - Container, - type ContainerProps, } from "@mui/material" +import { type FC, cloneElement } from "react" export interface ElevatedAppBarProps extends Omit { containerProps: ContainerProps toolbarProps?: ToolbarProps } -const ElevatedAppBar: React.FC = ({ +const ElevatedAppBar: FC = ({ containerProps, toolbarProps, elevation = 4, @@ -26,7 +26,7 @@ const ElevatedAppBar: React.FC = ({ threshold: 0, }) - return React.cloneElement( + return cloneElement( {children} diff --git a/src/components/Image.tsx b/src/components/Image.tsx index a83c655d..43e0ee14 100644 --- a/src/components/Image.tsx +++ b/src/components/Image.tsx @@ -1,5 +1,5 @@ import { Box, type BoxProps } from "@mui/material" -import type React from "react" +import { type FC } from "react" import { openInNewTab } from "../utils/general" @@ -10,12 +10,12 @@ export interface ImageProps extends Omit { hrefInNewTab?: boolean } -const Image: React.FC = ({ - href, - hrefInNewTab = false, - ...props -}) => { - let { onClick, style = {}, ...otherProps } = props +const Image: FC = ({ href, hrefInNewTab = false, ...props }) => { + let { + onClick, + style = {}, + ...otherProps // eslint-disable-line prefer-const + } = props if (style.width === undefined) { style.width = "100%" diff --git a/src/components/InputFileButton.tsx b/src/components/InputFileButton.tsx index e8796859..05e8c929 100644 --- a/src/components/InputFileButton.tsx +++ b/src/components/InputFileButton.tsx @@ -1,9 +1,9 @@ +import { Button, type ButtonProps } from "@mui/material" import { - type FC, type DetailedHTMLProps, + type FC, type InputHTMLAttributes, } from "react" -import { Button, type ButtonProps } from "@mui/material" export interface InputFileButtonProps extends Omit, "component"> { diff --git a/src/components/ItemizedList.tsx b/src/components/ItemizedList.tsx index f9d02278..200c8291 100644 --- a/src/components/ItemizedList.tsx +++ b/src/components/ItemizedList.tsx @@ -1,12 +1,12 @@ -import type React from "react" +import { type FC, type ReactElement } from "react" import { List, - type ListProps, type ListItem, type ListItemText, + type ListProps, } from "@mui/material" -type ListItemElement = React.ReactElement +type ListItemElement = ReactElement export interface ItemizedListProps { styleType: @@ -33,7 +33,7 @@ export interface ItemizedListProps { children: ListItemElement | ListItemElement[] } -const ItemizedList: React.FC = ({ +const ItemizedList: FC = ({ styleType, listProps = {}, pl = 4, diff --git a/src/components/OrderedGrid.tsx b/src/components/OrderedGrid.tsx index 9ddc0f1c..3874c617 100644 --- a/src/components/OrderedGrid.tsx +++ b/src/components/OrderedGrid.tsx @@ -1,4 +1,4 @@ -import type React from "react" +import { type FC, type ReactElement } from "react" import { Unstable_Grid2 as Grid, type Grid2Props } from "@mui/material" interface ItemProps @@ -29,7 +29,7 @@ interface GlobalItemProps extends ItemProps { export interface OrderedGridProps { rows: Array< Array<{ - element: React.ReactElement + element: ReactElement itemProps?: ItemProps }> > @@ -37,7 +37,7 @@ export interface OrderedGridProps { globalItemProps: GlobalItemProps } -const OrderedGrid: React.FC = ({ +const OrderedGrid: FC = ({ rows, containerProps = {}, globalItemProps, diff --git a/src/components/ScrollIntoViewLink.tsx b/src/components/ScrollIntoViewLink.tsx index 8b6ffd6d..0d8aec06 100644 --- a/src/components/ScrollIntoViewLink.tsx +++ b/src/components/ScrollIntoViewLink.tsx @@ -1,6 +1,5 @@ -import { type FC } from "react" import { Link, type LinkProps } from "@mui/material" - +import { type FC } from "react" export interface ScrollIntoViewLinkProps extends Omit { elementId: string options?: ScrollIntoViewOptions diff --git a/src/components/SyncError.tsx b/src/components/SyncError.tsx index 1a50941b..eeac61dc 100644 --- a/src/components/SyncError.tsx +++ b/src/components/SyncError.tsx @@ -1,6 +1,6 @@ -import { SyncProblem as SyncProblemIcon } from "@mui/icons-material" import { Stack, Typography } from "@mui/material" import { type FC } from "react" +import { SyncProblem as SyncProblemIcon } from "@mui/icons-material" export interface SyncErrorProps {} diff --git a/src/components/TablePagination.tsx b/src/components/TablePagination.tsx index 133c4388..460b9704 100644 --- a/src/components/TablePagination.tsx +++ b/src/components/TablePagination.tsx @@ -1,3 +1,10 @@ +import { + type ElementType, + type JSX, + type JSXElementConstructor, + type ReactNode, + useEffect, +} from "react" import { TablePagination as MuiTablePagination, type TablePaginationProps as MuiTablePaginationProps, @@ -6,16 +13,9 @@ import { type TablePaginationBaseProps, } from "@mui/material" import type { TypedUseLazyQuery } from "@reduxjs/toolkit/query/react" -import { - type ElementType, - type JSXElementConstructor, - type ReactNode, - useEffect, - type JSX, -} from "react" -import { type Pagination, usePagination } from "../hooks/api" import { type ListArg, type ListResult, handleResultState } from "../utils/api" +import { type Pagination, usePagination } from "../hooks/api" export type TablePaginationProps< QueryArg extends ListArg, @@ -84,10 +84,17 @@ const TablePagination = < useEffect( () => { - trigger({ limit, offset, ...filters } as QueryArg, preferCacheValue) + void trigger({ limit, offset, ...filters } as QueryArg, preferCacheValue) }, // eslint-disable-next-line react-hooks/exhaustive-deps - [trigger, limit, offset, ...Object.values(filters || {}), preferCacheValue], + [ + trigger, + limit, + offset, + // eslint-disable-next-line react-hooks/exhaustive-deps,@typescript-eslint/no-unsafe-assignment + ...Object.values(filters || {}), + preferCacheValue, + ], ) const { count, max_limit } = result.data || {} @@ -115,11 +122,13 @@ const TablePagination = < rowsPerPage={limit} onRowsPerPageChange={event => { setPagination({ limit: parseInt(event.target.value), page: 0 }) + // eslint-disable-next-line @typescript-eslint/no-unsafe-call if (onRowsPerPageChange) onRowsPerPageChange(event) }} page={page} onPageChange={(event, page) => { setPagination(({ limit }) => ({ limit, page })) + // eslint-disable-next-line @typescript-eslint/no-unsafe-call if (onPageChange) onPageChange(event, page) }} // ascending order diff --git a/src/components/YouTubeVideo.tsx b/src/components/YouTubeVideo.tsx index 8097db2c..c4a407c1 100644 --- a/src/components/YouTubeVideo.tsx +++ b/src/components/YouTubeVideo.tsx @@ -1,11 +1,11 @@ -import type React from "react" import { Box, type BoxProps } from "@mui/material" +import { type FC } from "react" export interface YouTubeVideoProps extends Omit { src: string } -const YouTubeVideo: React.FC = ({ +const YouTubeVideo: FC = ({ src, style = {}, ...otherProps diff --git a/src/components/form/ApiAutocompleteField.tsx b/src/components/form/ApiAutocompleteField.tsx index 27f94234..91228afd 100644 --- a/src/components/form/ApiAutocompleteField.tsx +++ b/src/components/form/ApiAutocompleteField.tsx @@ -1,21 +1,23 @@ -import { Button, CircularProgress, type ChipTypeMap } from "@mui/material" -import type { TypedUseLazyQuery } from "@reduxjs/toolkit/query/react" +import { Button, type ChipTypeMap, CircularProgress } from "@mui/material" import { Children, + type ElementType, + type ForwardRefRenderFunction, + type HTMLAttributes, + type JSX, forwardRef, useEffect, useState, - type ElementType, - type JSX, } from "react" +import type { TypedUseLazyQuery } from "@reduxjs/toolkit/query/react" import { AutocompleteField, type AutocompleteFieldProps, } from "../../components/form" -import { usePagination } from "../../hooks/api" import type { ListArg, ListResult, ModelId } from "../../utils/api" import SyncError from "../SyncError" +import { usePagination } from "../../hooks/api" export interface ApiAutocompleteFieldProps< SearchKey extends keyof Omit, @@ -63,7 +65,7 @@ const ApiAutocompleteField = < useLazyListQuery, filterOptions, getOptionLabel, - getOptionKey = result => result.id, + getOptionKey = result => result.id as ModelId, searchKey, ...otherAutocompleteFieldProps }: ApiAutocompleteFieldProps< @@ -89,7 +91,7 @@ const ApiAutocompleteField = < useEffect( () => { const arg = { limit, offset, ...filterOptions } as QueryArg - // @ts-expect-error + // @ts-expect-error search key can index arg if (search) arg[searchKey] = search trigger(arg, true) @@ -131,6 +133,44 @@ const ApiAutocompleteField = < setPagination(({ page, limit }) => ({ page: page + 1, limit })) } + const ListboxComponent: ForwardRefRenderFunction< + unknown, + HTMLAttributes + > = ({ children, ...props }, ref) => { + const listItems = Children.toArray(children) + if (isLoading) listItems.push() + else { + if (isError) listItems.push() + if (hasMore) { + listItems.push( + , + ) + } + } + + return ( +
    { + // If not already loading and scrolled to bottom + if ( + !isLoading && + event.currentTarget.clientHeight + event.currentTarget.scrollTop >= + event.currentTarget.scrollHeight + ) { + loadNextPage() + } + }} + > + {listItems} +
+ ) + } + return ( { setSearch(reason === "input" ? value : "") }} - ListboxComponent={forwardRef(({ children, ...props }, ref) => { - const listItems = Children.toArray(children) - if (isLoading) listItems.push() - else { - if (isError) listItems.push() - if (hasMore) { - listItems.push( - , - ) - } - } - - return ( -
    { - // If not already loading and scrolled to bottom - if ( - !isLoading && - event.currentTarget.clientHeight + - event.currentTarget.scrollTop >= - event.currentTarget.scrollHeight - ) { - loadNextPage() - } - }} - > - {listItems} -
- ) - })} + ListboxComponent={forwardRef(ListboxComponent)} {...otherAutocompleteFieldProps} /> ) diff --git a/src/components/form/AutocompleteField.tsx b/src/components/form/AutocompleteField.tsx index 3edcb330..f2e57c40 100644 --- a/src/components/form/AutocompleteField.tsx +++ b/src/components/form/AutocompleteField.tsx @@ -1,19 +1,19 @@ import { Autocomplete, - TextField, type AutocompleteProps, type ChipTypeMap, + TextField, type TextFieldProps, } from "@mui/material" -import { Field, type FieldConfig, type FieldProps } from "formik" import { type ElementType, type JSX } from "react" +import { Field, type FieldConfig, type FieldProps } from "formik" import { + type ValidateOptions, number as YupNumber, string as YupString, - type ValidateOptions, } from "yup" -import { schemaToFieldValidator } from "../../utils/form" +import { type FormValues, schemaToFieldValidator } from "../../utils/form" import { getNestedProperty } from "../../utils/general" export interface AutocompleteFieldProps< @@ -86,17 +86,28 @@ const AutocompleteField = < return ( {({ form, meta }: FieldProps) => { - const value = getNestedProperty(form.values, dotPath) - const touched = getNestedProperty(form.touched, dotPath) - const error = getNestedProperty(form.errors, dotPath) + const value = getNestedProperty( + form.values as FormValues, + dotPath, + ) as string + const touched = getNestedProperty(form.touched, dotPath) as boolean + const error = getNestedProperty(form.errors, dotPath) as + | string + | undefined return ( ( + renderInput={({ + id: _, // eslint-disable-line @typescript-eslint/no-unused-vars + ...otherParams + }) => ( )} onChange={(_, value) => { - form.setFieldValue(name, value ?? undefined, true) + void form.setFieldValue(name, value ?? undefined, true) }} onBlur={form.handleBlur} {...otherAutocompleteProps} diff --git a/src/components/form/CheckboxField.tsx b/src/components/form/CheckboxField.tsx index d46aaa2d..433bb27f 100644 --- a/src/components/form/CheckboxField.tsx +++ b/src/components/form/CheckboxField.tsx @@ -1,16 +1,16 @@ import { Checkbox, + type CheckboxProps, FormControl, FormControlLabel, - FormHelperText, - type CheckboxProps, type FormControlLabelProps, + FormHelperText, } from "@mui/material" import { Field, type FieldConfig, type FieldProps } from "formik" +import { type ValidateOptions, bool as YupBool } from "yup" import { type FC } from "react" -import { bool as YupBool, type ValidateOptions } from "yup" -import { schemaToFieldValidator } from "../../utils/form" +import { type FormValues, schemaToFieldValidator } from "../../utils/form" import { getNestedProperty } from "../../utils/general" export interface CheckboxFieldProps @@ -47,9 +47,14 @@ const CheckboxField: FC = ({ return ( {({ form, meta }: FieldProps) => { - const touched = getNestedProperty(form.touched, dotPath) - const error = getNestedProperty(form.errors, dotPath) - const value = getNestedProperty(form.values, dotPath) + const touched = getNestedProperty(form.touched, dotPath) as boolean + const error = getNestedProperty(form.errors, dotPath) as + | string + | undefined + const value = getNestedProperty( + form.values as FormValues, + dotPath, + ) as boolean const hasError = touched && Boolean(error) @@ -59,7 +64,7 @@ const CheckboxField: FC = ({ = ({ } {...formControlLabelProps} /> - {hasError && {error as string}} + {hasError && {error}} ) }} diff --git a/src/components/form/CountryField.tsx b/src/components/form/CountryField.tsx index 1d81133f..de59eea3 100644 --- a/src/components/form/CountryField.tsx +++ b/src/components/form/CountryField.tsx @@ -1,13 +1,14 @@ -import { type ChipTypeMap } from "@mui/material" import { type ElementType, type JSX } from "react" +import { type ChipTypeMap } from "@mui/material" + +import AutocompleteField, { + type AutocompleteFieldProps, +} from "./AutocompleteField" import { COUNTRY_ISO_CODES, COUNTRY_ISO_CODE_MAPPING, type CountryIsoCodes, } from "../../utils/general" -import AutocompleteField, { - type AutocompleteFieldProps, -} from "./AutocompleteField" export interface CountryFieldProps< Multiple extends boolean | undefined = false, diff --git a/src/components/form/DatePickerField.tsx b/src/components/form/DatePickerField.tsx index fc831f33..5cf99ed5 100644 --- a/src/components/form/DatePickerField.tsx +++ b/src/components/form/DatePickerField.tsx @@ -1,17 +1,17 @@ +import "dayjs/locale/en-gb" import { DatePicker, - LocalizationProvider, type DatePickerProps, + LocalizationProvider, type PickerValidDate, } from "@mui/x-date-pickers" -import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs" -import dayjs, { type Dayjs } from "dayjs" -import "dayjs/locale/en-gb" import { Field, type FieldConfig, type FieldProps } from "formik" -import { date as YupDate, type ValidateOptions } from "yup" +import { type ValidateOptions, date as YupDate } from "yup" +import dayjs, { type Dayjs } from "dayjs" +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs" import { type JSX } from "react" -import { schemaToFieldValidator } from "../../utils/form" +import { type FormValues, schemaToFieldValidator } from "../../utils/form" import { getNestedProperty } from "../../utils/general" export interface DatePickerFieldProps< @@ -70,14 +70,19 @@ const DatePickerField = < return ( {({ form }: FieldProps) => { - const error = getNestedProperty(form.errors, dotPath) - const touched = getNestedProperty(form.touched, dotPath) - let value = getNestedProperty(form.values, dotPath) + const error = getNestedProperty(form.errors, dotPath) as + | string + | undefined + const touched = getNestedProperty(form.touched, dotPath) as boolean + let value: Dayjs | null | string = getNestedProperty( + form.values as FormValues, + dotPath, + ) as string value = value ? dayjs(value) : null function handleChange(value: Dayjs | null) { - form.setFieldValue( + void form.setFieldValue( name, value && value.isValid() ? value.format("YYYY-MM-DD") : null, true, @@ -89,6 +94,7 @@ const DatePickerField = < dateAdapter={AdapterDayjs} adapterLocale="en-gb" > + {/* @ts-expect-error value is compatible */} { - // @ts-expect-error + // @ts-expect-error value is compatible handleChange(value as Dayjs | null) }, onBlur: form.handleBlur, diff --git a/src/components/form/EmailField.tsx b/src/components/form/EmailField.tsx index 22fcbea0..79250e5c 100644 --- a/src/components/form/EmailField.tsx +++ b/src/components/form/EmailField.tsx @@ -1,6 +1,6 @@ import { EmailOutlined as EmailOutlinedIcon } from "@mui/icons-material" -import { InputAdornment } from "@mui/material" import type { FC } from "react" +import { InputAdornment } from "@mui/material" import { string as YupString } from "yup" import TextField, { type TextFieldProps } from "./TextField" diff --git a/src/components/form/FirstNameField.tsx b/src/components/form/FirstNameField.tsx index 1dadb9c5..aa74b249 100644 --- a/src/components/form/FirstNameField.tsx +++ b/src/components/form/FirstNameField.tsx @@ -1,6 +1,6 @@ -import { PersonOutlined as PersonOutlinedIcon } from "@mui/icons-material" -import { InputAdornment } from "@mui/material" import type { FC } from "react" +import { InputAdornment } from "@mui/material" +import { PersonOutlined as PersonOutlinedIcon } from "@mui/icons-material" import TextField, { type TextFieldProps } from "./TextField" import { schemas } from "../../api" diff --git a/src/components/form/Form.tsx b/src/components/form/Form.tsx index a34d03a2..494cc7e1 100644 --- a/src/components/form/Form.tsx +++ b/src/components/form/Form.tsx @@ -1,27 +1,27 @@ +import { + type FC, + type JSX, + type ReactNode, + type RefObject, + useEffect, + useRef, +} from "react" import { FormHelperText, type FormHelperTextProps } from "@mui/material" import { Formik, - Form as FormikForm, type FormikConfig, type FormikErrors, + Form as FormikForm, type FormikProps, } from "formik" -import { - type ReactNode, - type FC, - useRef, - useEffect, - type RefObject, - type JSX, -} from "react" import type { TypedUseMutation } from "@reduxjs/toolkit/query/react" -import { getKeyPaths } from "../../utils/general" import { - submitForm, - type SubmitFormOptions, type FormValues, + type SubmitFormOptions, + submitForm, } from "../../utils/form" +import { getKeyPaths } from "../../utils/general" const SCROLL_INTO_VIEW_OPTIONS: ScrollIntoViewOptions = { behavior: "smooth", @@ -68,7 +68,7 @@ const BaseForm = ({ ...otherFormikProps }: BaseFormProps) => ( - {/* @ts-expect-error */} + {/* @ts-expect-error value is assignable */} {(formik: _FormikProps) => { const hasErrors = Boolean(Object.keys(formik.errors).length) const hasNonFieldErrors = diff --git a/src/components/form/PasswordField.tsx b/src/components/form/PasswordField.tsx index 42b55501..96c69636 100644 --- a/src/components/form/PasswordField.tsx +++ b/src/components/form/PasswordField.tsx @@ -1,9 +1,9 @@ +import { type FC, useState } from "react" +import { IconButton, InputAdornment } from "@mui/material" import { Visibility as VisibilityIcon, VisibilityOff as VisibilityOffIcon, } from "@mui/icons-material" -import { IconButton, InputAdornment } from "@mui/material" -import { useState, type FC } from "react" import { string as YupString } from "yup" import RepeatField, { type RepeatFieldProps } from "./RepeatField" diff --git a/src/components/form/RepeatField.tsx b/src/components/form/RepeatField.tsx index 0397d363..b7ba5a6b 100644 --- a/src/components/form/RepeatField.tsx +++ b/src/components/form/RepeatField.tsx @@ -1,15 +1,15 @@ -import { TextField as MuiTextField, type TextFieldProps } from "@mui/material" -import { Field, type FieldConfig, type FieldProps } from "formik" import { - useEffect, - useState, type Dispatch, type FC, type SetStateAction, + useEffect, + useState, } from "react" -import { string as YupString, type ValidateOptions } from "yup" +import { Field, type FieldConfig, type FieldProps } from "formik" +import { TextField as MuiTextField, type TextFieldProps } from "@mui/material" +import { type ValidateOptions, string as YupString } from "yup" -import { schemaToFieldValidator } from "../../utils/form" +import { type FormValues, schemaToFieldValidator } from "../../utils/form" import { getNestedProperty } from "../../utils/general" export type RepeatFieldProps = Omit< @@ -47,12 +47,20 @@ const TextField: FC< const { form } = fieldProps const dotPath = name.split(".") - const value = getNestedProperty(form.values, dotPath) + const value = getNestedProperty(form.values as FormValues, dotPath) as string const repeatDotPath = repeatName.split(".") - const repeatValue = getNestedProperty(form.values, repeatDotPath) - const repeatTouched = getNestedProperty(form.touched, repeatDotPath) - const repeatError = getNestedProperty(form.errors, repeatDotPath) + const repeatValue = getNestedProperty( + form.values as FormValues, + repeatDotPath, + ) as string + const repeatTouched = getNestedProperty( + form.touched, + repeatDotPath, + ) as boolean + const repeatError = getNestedProperty(form.errors, repeatDotPath) as + | string + | undefined useEffect(() => { setValue(value) diff --git a/src/components/form/SubmitButton.tsx b/src/components/form/SubmitButton.tsx index 248ea2e7..009d832a 100644 --- a/src/components/form/SubmitButton.tsx +++ b/src/components/form/SubmitButton.tsx @@ -2,6 +2,8 @@ import { Button, type ButtonProps } from "@mui/material" import { Field, type FieldProps } from "formik" import type { FC } from "react" +import { type FormValues } from "../../utils/form" + export interface SubmitButtonProps extends Omit {} @@ -15,7 +17,7 @@ const SubmitButton: FC = ({ ) { touched = touched || {} for (const key in values) { - const value = values[key] + const value: unknown = values[key] touched[key] = value instanceof Object && value.constructor === Object ? getTouched(value, touched) @@ -31,15 +33,17 @@ const SubmitButton: FC = ({