diff --git a/cypress/component/Dialog.cy.tsx b/cypress/component/Dialog.cy.tsx index e86e421..ad73b83 100644 --- a/cypress/component/Dialog.cy.tsx +++ b/cypress/component/Dialog.cy.tsx @@ -1,9 +1,10 @@ import { Button, Typography } from "@mui/material" +import { FC } from "react" import { useToggle } from "react-use" import { Dialog } from "#components/Dialog" -const SampleComponentWithDialog: React.FC = () => { +const SampleComponentWithDialog: FC = () => { const [isMyDialogOpen, toggleIsMyDialogOpen] = useToggle(false) return ( diff --git a/cypress/e2e/budget-board-creating.cy.ts b/cypress/e2e/budget-board-creating.cy.ts index bfba3fa..e5f1809 100644 --- a/cypress/e2e/budget-board-creating.cy.ts +++ b/cypress/e2e/budget-board-creating.cy.ts @@ -13,7 +13,7 @@ describe("Budget board creating", () => { cy.contains('"clever-budgetiers" budget board already exists.').should("be.visible") }) - it("board craeted successfully", () => { + it("board created successfully", () => { cy.authorize(testUsers.johnDoe.id) cy.visit("/boards") diff --git a/cypress/e2e/budget-board-settings.cy.ts b/cypress/e2e/budget-board-settings.cy.ts index 05ff113..0ebf04b 100644 --- a/cypress/e2e/budget-board-settings.cy.ts +++ b/cypress/e2e/budget-board-settings.cy.ts @@ -48,8 +48,7 @@ describe("Budget board settings", () => { it("board default currency is edited correctly", () => { cy.authorize(testUsers.johnDoe.id) - cy.visit("/boards/1/records") - cy.get("[aria-label='Add record']").click() + cy.visit("/boards/1/records/add") cy.get("#mui-component-select-currencySlug").should("contain", "GEL ₾") cy.visit("/boards/1/settings") @@ -61,14 +60,13 @@ describe("Budget board settings", () => { cy.get("td").contains("GEL ₾").should("not.exist") cy.get("td").contains("USD $").should("be.visible") - cy.visit("/boards/1/records") - cy.get("[aria-label='Add record']").click() + cy.visit("/boards/1/records/add") cy.get("#mui-component-select-currencySlug").should("contain", "USD $") }) }) }) - describe.only("Budget categories settings", () => { + describe("Budget categories settings", () => { it("budget board settings are fetched and rendered correctly", () => { cy.authorize(testUsers.johnDoe.id) cy.visit("/boards/1/settings") diff --git a/cypress/e2e/budget-records.cy.ts b/cypress/e2e/budget-records.cy.ts index a34512a..22ee390 100644 --- a/cypress/e2e/budget-records.cy.ts +++ b/cypress/e2e/budget-records.cy.ts @@ -41,7 +41,7 @@ describe("Budget records", () => { cy.get("[role='progressbar'][aria-label='Loading more records']").should("not.exist") }) - it.only("is created correctly", () => { + it("is created correctly", () => { cy.authorize(testUsers.johnDoe.id) cy.visit("/boards/1/records") @@ -51,7 +51,7 @@ describe("Budget records", () => { cy.get("[role='option']").contains("education").click() cy.get("#mui-component-select-currencySlug").click() cy.get("[role='option']").contains("USD $").click() - cy.get("p").contains("amount must be a positive number").should("be.visible") + cy.get("p").contains("Number must be greater than 0").should("be.visible") cy.get("input[name='amount']").clear().type("4.53") // TODO: Enter a date. cy.get("button").contains("Add").click() diff --git a/package-lock.json b/package-lock.json index f90c1ac..806804d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "react-router-dom": "6.13.0", "react-use": "17.4.0", "recharts": "2.7.1", - "yup": "1.2.0" + "zod": "3.21.4" }, "devDependencies": { "@babel/core": "7.22.5", @@ -12213,11 +12213,6 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, - "node_modules/property-expr": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz", - "integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==" - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -13848,11 +13843,6 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, - "node_modules/tiny-case": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", - "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" - }, "node_modules/title-case": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz", @@ -13908,11 +13898,6 @@ "node": ">=0.6" } }, - "node_modules/toposort": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", - "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" - }, "node_modules/tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -14960,28 +14945,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yup": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/yup/-/yup-1.2.0.tgz", - "integrity": "sha512-PPqYKSAXjpRCgLgLKVGPA33v5c/WgEx3wi6NFjIiegz90zSwyMpvTFp/uGcVnnbx6to28pgnzp/q8ih3QRjLMQ==", - "dependencies": { - "property-expr": "^2.0.5", - "tiny-case": "^1.0.3", - "toposort": "^2.0.2", - "type-fest": "^2.19.0" - } - }, - "node_modules/yup/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/zen-observable": { "version": "0.8.15", "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", @@ -14994,6 +14957,14 @@ "dependencies": { "zen-observable": "0.8.15" } + }, + "node_modules/zod": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } }, "dependencies": { @@ -24013,11 +23984,6 @@ } } }, - "property-expr": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz", - "integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==" - }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -25287,11 +25253,6 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, - "tiny-case": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", - "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" - }, "title-case": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz", @@ -25335,11 +25296,6 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true }, - "toposort": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", - "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" - }, "tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -26089,24 +26045,6 @@ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true }, - "yup": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/yup/-/yup-1.2.0.tgz", - "integrity": "sha512-PPqYKSAXjpRCgLgLKVGPA33v5c/WgEx3wi6NFjIiegz90zSwyMpvTFp/uGcVnnbx6to28pgnzp/q8ih3QRjLMQ==", - "requires": { - "property-expr": "^2.0.5", - "tiny-case": "^1.0.3", - "toposort": "^2.0.2", - "type-fest": "^2.19.0" - }, - "dependencies": { - "type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==" - } - } - }, "zen-observable": { "version": "0.8.15", "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", @@ -26119,6 +26057,11 @@ "requires": { "zen-observable": "0.8.15" } + }, + "zod": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==" } } } diff --git a/package.json b/package.json index 36429ae..cf4c824 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "react-router-dom": "6.13.0", "react-use": "17.4.0", "recharts": "2.7.1", - "yup": "1.2.0" + "zod": "3.21.4" }, "devDependencies": { "@babel/core": "7.22.5", diff --git a/src/components/Navbar/helpers.tsx b/src/components/Navbar/helpers.tsx index 43454a9..d22e5ea 100644 --- a/src/components/Navbar/helpers.tsx +++ b/src/components/Navbar/helpers.tsx @@ -1,7 +1,8 @@ import { Dashboard as DashboardIcon, Person as PersonIcon } from "@mui/icons-material" +import { ReactElement } from "react" type TSection = { - icon: React.ReactElement + icon: ReactElement id: string path: string } diff --git a/src/components/form-contructor/RadioGroup.tsx b/src/components/form-contructor/RadioGroup.tsx deleted file mode 100644 index 8a01393..0000000 --- a/src/components/form-contructor/RadioGroup.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { - FormControl, - FormControlLabel, - FormHelperText, - FormLabel, - RadioGroup as MuiRadioGroup, - Radio, -} from "@mui/material" -import { FC } from "react" -import { UseFormRegister } from "react-hook-form" - -type TRadioGroupOption = { - label: string - value: number | string -} -type TRadioGroupProps = { - fieldValue: TRadioGroupOption["value"] - helperText: string | undefined - label: string - name: string - options: TRadioGroupOption[] - register: UseFormRegister // eslint-disable-line @typescript-eslint/no-explicit-any - setValue(fieldName: string, newValue: string | number): void -} - -export const RadioGroup: FC = ({ - fieldValue, - helperText, - label, - name, - options, - register, - setValue, -}) => { - return ( - - {label} - - {options.map(({ label, value }) => ( - } - key={String(value)} - label={label} - // onChange={() => setValue(name, value)} - value={value} - /> - ))} - - {helperText} - - ) -} diff --git a/src/utils/getChildByDisplayName.ts b/src/utils/getChildByDisplayName.ts index 5976d1b..50a5aee 100644 --- a/src/utils/getChildByDisplayName.ts +++ b/src/utils/getChildByDisplayName.ts @@ -1,12 +1,6 @@ -import { Children, isValidElement } from "react" +import { Children, ReactNode, isValidElement } from "react" -export const getChildByDisplayName = ({ - children, - displayName, -}: { - children: React.ReactNode - displayName: string -}) => { +export const getChildByDisplayName = ({ children, displayName }: { children: ReactNode; displayName: string }) => { return Children.map(children, (child) => { if (!isValidElement(child)) return null // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/views/auth/Login/form-helpers.ts b/src/views/auth/Login/form-helpers.ts index 0e1a070..2a3e853 100644 --- a/src/views/auth/Login/form-helpers.ts +++ b/src/views/auth/Login/form-helpers.ts @@ -1,20 +1,20 @@ -import * as yup from "yup" +import { z } from "zod" export enum FieldName { Password = "password", Username = "username", } -export const validationSchema = yup - .object({ - [FieldName.Password]: yup.string().required(), - [FieldName.Username]: yup.string().required(), - }) - .required() +export const validationSchema = z.object({ + [FieldName.Password]: z.string().nonempty(), + [FieldName.Username]: z.string().nonempty(), +}) -export const defaultValues: TFormValues = { - password: "", - username: "", -} +export type TFormValidValues = z.infer + +export type TFormDefaultValues = TFormValidValues -export type TFormValues = yup.InferType +export const defaultValues: TFormValidValues = { + [FieldName.Password]: "", + [FieldName.Username]: "", +} diff --git a/src/views/auth/Login/index.tsx b/src/views/auth/Login/index.tsx index 2fada3f..ed79185 100644 --- a/src/views/auth/Login/index.tsx +++ b/src/views/auth/Login/index.tsx @@ -1,4 +1,4 @@ -import { yupResolver } from "@hookform/resolvers/yup" +import { zodResolver } from "@hookform/resolvers/zod" import { Button, TextField, Typography } from "@mui/material" import { FC } from "react" import { useForm } from "react-hook-form" @@ -10,7 +10,7 @@ import { RowGroup } from "#components/RowGroup" import { apolloClient } from "#utils/apolloClient" import { Container } from "../components" -import { FieldName, TFormValues, defaultValues, validationSchema } from "./form-helpers" +import { FieldName, TFormDefaultValues, TFormValidValues, defaultValues, validationSchema } from "./form-helpers" export const Login: FC = () => { const navigate = useNavigate() @@ -21,10 +21,10 @@ export const Login: FC = () => { handleSubmit, register, setError, - } = useForm({ + } = useForm({ defaultValues, mode: "onChange", - resolver: yupResolver(validationSchema), + resolver: zodResolver(validationSchema), }) const onSubmit = handleSubmit(async ({ password, username }) => { diff --git a/src/views/boards/BoardRecords/RecordFormDialog/form-helpers.ts b/src/views/boards/BoardRecords/RecordFormDialog/form-helpers.ts index 422dfae..8cdaf88 100644 --- a/src/views/boards/BoardRecords/RecordFormDialog/form-helpers.ts +++ b/src/views/boards/BoardRecords/RecordFormDialog/form-helpers.ts @@ -1,6 +1,4 @@ -import * as Yup from "yup" - -import { BudgetRecord } from "#api/types" +import { z } from "zod" export enum FieldName { Amount = "amount", @@ -10,18 +8,20 @@ export enum FieldName { Date = "date", } -export type TFormValues = { - [FieldName.Amount]: BudgetRecord["amount"] | null - [FieldName.CategoryId]: BudgetRecord["category"]["id"] | null - [FieldName.Comment]: BudgetRecord["comment"] - [FieldName.CurrencySlug]: BudgetRecord["currency"]["slug"] - [FieldName.Date]: BudgetRecord["date"] -} - -export const validationSchema = Yup.object({ - [FieldName.Amount]: Yup.number().required().positive(), - [FieldName.CategoryId]: Yup.number().required(), - [FieldName.Comment]: Yup.string(), - [FieldName.CurrencySlug]: Yup.string().required(), - [FieldName.Date]: Yup.string().required(), +export const validationSchema = z.object({ + [FieldName.Amount]: z.number().positive(), + [FieldName.CategoryId]: z.number(), + [FieldName.Comment]: z.string(), + [FieldName.CurrencySlug]: z.string().nonempty(), + [FieldName.Date]: z.string(), }) + +export type TFormValidValues = z.infer + +export type TFormDefaultValues = { + [FieldName.Amount]: number | null + [FieldName.CategoryId]: number | null + [FieldName.Comment]: string + [FieldName.CurrencySlug]: string | null + [FieldName.Date]: string +} diff --git a/src/views/boards/BoardRecords/RecordFormDialog/index.tsx b/src/views/boards/BoardRecords/RecordFormDialog/index.tsx index bd16541..59d69bd 100644 --- a/src/views/boards/BoardRecords/RecordFormDialog/index.tsx +++ b/src/views/boards/BoardRecords/RecordFormDialog/index.tsx @@ -1,7 +1,7 @@ -import { yupResolver } from "@hookform/resolvers/yup" +import { zodResolver } from "@hookform/resolvers/zod" import { Box, Button, FormControl, InputLabel, MenuItem, Select, TextField, Typography } from "@mui/material" import { format as formatDate } from "date-fns" -import { FC } from "react" +import { FC, useEffect } from "react" import { useForm } from "react-hook-form" import { Link, useNavigate, useParams } from "react-router-dom" @@ -19,7 +19,7 @@ import { Dialog } from "#components/Dialog" import { RowGroup } from "#components/RowGroup" import { theme } from "#styles/theme" -import { FieldName, TFormValues, validationSchema } from "./form-helpers" +import { FieldName, TFormDefaultValues, TFormValidValues, validationSchema } from "./form-helpers" const budgetCategoryIndicatorColorByBudgetCategoryType = new Map([ [1, theme.palette.error.main], @@ -56,7 +56,7 @@ export const RecordFormDialog: FC = ({ record }) => { const getAuthorizedUserResult = useGetUserQuery({ variables: { id: 0 } }) const authorizedUser = getAuthorizedUserResult.data?.user - const defaultValues = record + const defaultValues: TFormDefaultValues = record ? { amount: record.amount, categoryId: record.category.id, @@ -68,18 +68,23 @@ export const RecordFormDialog: FC = ({ record }) => { amount: null, categoryId: null, comment: "", - currencySlug: board?.defaultCurrency?.slug ?? "", + currencySlug: null, date: formatDate(new Date(), "yyyy-MM-dd"), } - const { formState, handleSubmit, register } = useForm({ + const { formState, handleSubmit, register, watch, setValue } = useForm({ defaultValues, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore TODO: Migrate to zod. - resolver: yupResolver(validationSchema), mode: "onChange", + resolver: zodResolver(validationSchema), }) + useEffect(() => { + // Is it possible for a board to have default currency nul | undefined? + if (board?.defaultCurrency && watch("currencySlug") === null) { + setValue(FieldName.CurrencySlug, board.defaultCurrency.slug) + } + }, [board, setValue, watch]) + const getCurrenciesResult = useGetCurrenciesQuery() const getBoardBudgetCategoriesResult = useGetBudgetCategoriesQuery({ variables: { boardsIds: [Number(params.boardId)], orderingByType: "ASC" }, @@ -108,9 +113,6 @@ export const RecordFormDialog: FC = ({ record }) => { const closeDialogHref = `/boards/${params.boardId}/records${location.search}` const submitRecordForm = handleSubmit((formValues) => { - if (formValues.amount === null) return - if (formValues.categoryId === null) return - if (record === undefined) { createBudgetRecord({ variables: { @@ -160,8 +162,8 @@ export const RecordFormDialog: FC = ({ record }) => { Currency