From 328c2d6c4e8114efa7804d3af8f8c451cef77e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Sun, 22 Sep 2024 20:11:58 +0200 Subject: [PATCH 1/2] refactor(frontend): remove formik from web part --- frontend/apps/mobile/src/screens/AddGroup.tsx | 1 - frontend/apps/web/src/app/app.tsx | 2 +- .../AuthenticatedLayout.tsx | 2 +- .../apps/web/src/components/AccountSelect.tsx | 2 +- .../apps/web/src/components/ShareSelect.tsx | 2 +- .../apps/web/src/components/TagSelector.tsx | 2 +- .../apps/web/src/components/TextInput.tsx | 2 +- .../components/groups/GroupCreateModal.tsx | 2 +- .../components/groups/GroupMemberSelect.tsx | 2 +- .../components/groups/InviteLinkCreate.tsx | 171 +++---- frontend/apps/web/src/components/index.ts | 4 - .../src/components/style/EditableField.tsx | 95 ---- .../apps/web/src/components/style/index.ts | 3 - frontend/apps/web/src/core/config.tsx | 2 +- frontend/apps/web/src/main.tsx | 2 +- .../accounts/AccountDetail/AccountDetail.tsx | 3 +- .../accounts/AccountDetail/AccountInfo.tsx | 2 +- .../web/src/pages/auth/ConfirmEmailChange.tsx | 2 +- .../pages/auth/ConfirmPasswordRecovery.tsx | 113 ++--- .../src/pages/auth/ConfirmRegistration.tsx | 2 +- frontend/apps/web/src/pages/auth/Login.tsx | 132 +++--- frontend/apps/web/src/pages/auth/Logout.tsx | 2 +- frontend/apps/web/src/pages/auth/Register.tsx | 169 +++---- .../pages/auth/RequestPasswordRecovery.tsx | 78 ++-- frontend/apps/web/src/pages/groups/Group.tsx | 2 +- .../web/src/pages/groups/GroupActivity.tsx | 2 +- .../apps/web/src/pages/groups/GroupInvite.tsx | 3 +- .../pages/groups/settings/GroupInvites.tsx | 2 +- .../pages/groups/settings/GroupMemberList.tsx | 133 +++--- .../pages/groups/settings/SettingsForm.tsx | 3 +- .../web/src/pages/profile/ChangeEmail.tsx | 88 ++-- .../web/src/pages/profile/ChangePassword.tsx | 115 ++--- .../apps/web/src/pages/profile/Profile.tsx | 3 +- .../web/src/pages/profile/SessionList.tsx | 3 +- .../TransactionDetail/FileGallery.tsx | 2 +- .../TransactionDetail/TransactionDetail.tsx | 3 +- .../TransactionDetail/TransactionMetadata.tsx | 3 +- .../purchase/PositionTableRow.tsx | 2 +- frontend/apps/web/tsconfig.json | 1 + frontend/libs/components/.babelrc | 12 + frontend/libs/components/.eslintrc.json | 18 + frontend/libs/components/README.md | 7 + frontend/libs/components/jest.config.ts | 11 + frontend/libs/components/project.json | 16 + frontend/libs/components/src/index.ts | 1 + .../components/src/lib}/DateInput.tsx | 2 +- .../src/lib}/DisabledFormTextField.tsx | 2 +- .../components/src/lib}/DisabledTextField.tsx | 0 .../libs/components/src/lib/FormCheckbox.tsx | 27 ++ .../components/src/lib}/FormTextField.tsx | 0 .../components/src/lib}/Loading.tsx | 0 .../components/src/lib}/NumericInput.tsx | 2 +- frontend/libs/components/src/lib/index.ts | 7 + frontend/libs/components/tsconfig.json | 20 + frontend/libs/components/tsconfig.lib.json | 19 + frontend/libs/components/tsconfig.spec.json | 20 + frontend/nx.json | 13 +- frontend/package-lock.json | 436 +++--------------- frontend/tsconfig.base.json | 1 + 59 files changed, 703 insertions(+), 1073 deletions(-) delete mode 100644 frontend/apps/web/src/components/style/EditableField.tsx create mode 100644 frontend/libs/components/.babelrc create mode 100644 frontend/libs/components/.eslintrc.json create mode 100644 frontend/libs/components/README.md create mode 100644 frontend/libs/components/jest.config.ts create mode 100644 frontend/libs/components/project.json create mode 100644 frontend/libs/components/src/index.ts rename frontend/{apps/web/src/components => libs/components/src/lib}/DateInput.tsx (95%) rename frontend/{apps/web/src/components => libs/components/src/lib}/DisabledFormTextField.tsx (93%) rename frontend/{apps/web/src/components/style => libs/components/src/lib}/DisabledTextField.tsx (100%) create mode 100644 frontend/libs/components/src/lib/FormCheckbox.tsx rename frontend/{apps/web/src/components => libs/components/src/lib}/FormTextField.tsx (100%) rename frontend/{apps/web/src/components/style => libs/components/src/lib}/Loading.tsx (100%) rename frontend/{apps/web/src/components => libs/components/src/lib}/NumericInput.tsx (96%) create mode 100644 frontend/libs/components/src/lib/index.ts create mode 100644 frontend/libs/components/tsconfig.json create mode 100644 frontend/libs/components/tsconfig.lib.json create mode 100644 frontend/libs/components/tsconfig.spec.json diff --git a/frontend/apps/mobile/src/screens/AddGroup.tsx b/frontend/apps/mobile/src/screens/AddGroup.tsx index ce971cf8..72e9c55c 100644 --- a/frontend/apps/mobile/src/screens/AddGroup.tsx +++ b/frontend/apps/mobile/src/screens/AddGroup.tsx @@ -9,7 +9,6 @@ import { CurrencySelect } from "../components/CurrencySelect"; import { useApi } from "../core/ApiProvider"; import { GroupStackScreenProps } from "../navigation/types"; import { useAppDispatch } from "../store"; -import { StackNavigationOptions } from "@react-navigation/stack"; export const AddGroup: React.FC> = ({ navigation }) => { const theme = useTheme(); diff --git a/frontend/apps/web/src/app/app.tsx b/frontend/apps/web/src/app/app.tsx index eb56fd39..8ca98e26 100644 --- a/frontend/apps/web/src/app/app.tsx +++ b/frontend/apps/web/src/app/app.tsx @@ -13,7 +13,7 @@ import { AdapterLuxon } from "@mui/x-date-pickers/AdapterLuxon"; import * as React from "react"; import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; -import { Loading } from "@/components/style"; +import { Loading } from "@abrechnung/components"; import { api, ws } from "../core/api"; import { selectTheme, useAppDispatch, useAppSelector } from "../store"; import { Router } from "./Router"; diff --git a/frontend/apps/web/src/app/authenticated-layout/AuthenticatedLayout.tsx b/frontend/apps/web/src/app/authenticated-layout/AuthenticatedLayout.tsx index 807c0d28..9c575a54 100644 --- a/frontend/apps/web/src/app/authenticated-layout/AuthenticatedLayout.tsx +++ b/frontend/apps/web/src/app/authenticated-layout/AuthenticatedLayout.tsx @@ -39,7 +39,7 @@ import { } from "@mui/icons-material"; import { useTheme } from "@mui/material/styles"; import { Banner } from "@/components/style/Banner"; -import { Loading } from "@/components/style/Loading"; +import { Loading } from "@abrechnung/components"; import styles from "./AuthenticatedLayout.module.css"; import { LanguageSelect } from "@/components/LanguageSelect"; import { useTranslation } from "react-i18next"; diff --git a/frontend/apps/web/src/components/AccountSelect.tsx b/frontend/apps/web/src/components/AccountSelect.tsx index 7e4b345c..ef520ee9 100644 --- a/frontend/apps/web/src/components/AccountSelect.tsx +++ b/frontend/apps/web/src/components/AccountSelect.tsx @@ -4,7 +4,7 @@ import { Autocomplete, Box, Popper, TextField, TextFieldProps, Typography } from import { styled } from "@mui/material/styles"; import React from "react"; import { getAccountIcon } from "./style/AbrechnungIcons"; -import { DisabledTextField } from "./style/DisabledTextField"; +import { DisabledTextField } from "@abrechnung/components"; const StyledAutocompletePopper = styled(Popper)(({ theme }) => ({ minWidth: 200, diff --git a/frontend/apps/web/src/components/ShareSelect.tsx b/frontend/apps/web/src/components/ShareSelect.tsx index b1741c25..9a76df5b 100644 --- a/frontend/apps/web/src/components/ShareSelect.tsx +++ b/frontend/apps/web/src/components/ShareSelect.tsx @@ -25,7 +25,7 @@ import { import * as React from "react"; import { Link } from "react-router-dom"; import { getAccountLink } from "../utils"; -import { NumericInput } from "./NumericInput"; +import { NumericInput } from "@abrechnung/components"; import { getAccountIcon } from "./style/AbrechnungIcons"; import { getAccountSortFunc } from "@abrechnung/core"; import { useTranslation } from "react-i18next"; diff --git a/frontend/apps/web/src/components/TagSelector.tsx b/frontend/apps/web/src/components/TagSelector.tsx index f7cc76c0..54dad19b 100644 --- a/frontend/apps/web/src/components/TagSelector.tsx +++ b/frontend/apps/web/src/components/TagSelector.tsx @@ -4,7 +4,7 @@ import { Box, Checkbox, Chip, ChipProps, ListItemIcon, ListItemText, MenuItem, T import * as React from "react"; import { useAppSelector } from "@/store"; import { AddNewTagDialog } from "./AddNewTagDialog"; -import { DisabledTextField } from "./style/DisabledTextField"; +import { DisabledTextField } from "@abrechnung/components"; import { useTranslation } from "react-i18next"; interface Props extends Omit { diff --git a/frontend/apps/web/src/components/TextInput.tsx b/frontend/apps/web/src/components/TextInput.tsx index 7326ba75..8c63c4a8 100644 --- a/frontend/apps/web/src/components/TextInput.tsx +++ b/frontend/apps/web/src/components/TextInput.tsx @@ -1,6 +1,6 @@ import * as React from "react"; import { TextFieldProps } from "@mui/material"; -import { DisabledTextField } from "./style/DisabledTextField"; +import { DisabledTextField } from "@abrechnung/components"; export type TextInputProps = { onChange: (value: string) => void; diff --git a/frontend/apps/web/src/components/groups/GroupCreateModal.tsx b/frontend/apps/web/src/components/groups/GroupCreateModal.tsx index 99c03b9f..1c80acce 100644 --- a/frontend/apps/web/src/components/groups/GroupCreateModal.tsx +++ b/frontend/apps/web/src/components/groups/GroupCreateModal.tsx @@ -7,7 +7,7 @@ import { api } from "@/core/api"; import { useAppDispatch } from "@/store"; import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { FormTextField } from "../FormTextField"; +import { FormTextField } from "@abrechnung/components"; const validationSchema = z.object({ name: z.string({ required_error: "Name is required" }), diff --git a/frontend/apps/web/src/components/groups/GroupMemberSelect.tsx b/frontend/apps/web/src/components/groups/GroupMemberSelect.tsx index cbe597a5..13fc3680 100644 --- a/frontend/apps/web/src/components/groups/GroupMemberSelect.tsx +++ b/frontend/apps/web/src/components/groups/GroupMemberSelect.tsx @@ -1,6 +1,6 @@ import React from "react"; import { Autocomplete, Box, Popper, TextField, TextFieldProps, Typography } from "@mui/material"; -import { DisabledTextField } from "../style/DisabledTextField"; +import { DisabledTextField } from "@abrechnung/components"; import { styled } from "@mui/material/styles"; import { useAppSelector } from "@/store"; import { selectGroupMemberIds, selectGroupMemberIdToUsername } from "@abrechnung/redux"; diff --git a/frontend/apps/web/src/components/groups/InviteLinkCreate.tsx b/frontend/apps/web/src/components/groups/InviteLinkCreate.tsx index 3b28453f..bb863048 100644 --- a/frontend/apps/web/src/components/groups/InviteLinkCreate.tsx +++ b/frontend/apps/web/src/components/groups/InviteLinkCreate.tsx @@ -1,21 +1,14 @@ import { Group } from "@abrechnung/api"; -import { - Button, - Checkbox, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - FormControlLabel, - LinearProgress, - TextField, -} from "@mui/material"; +import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack } from "@mui/material"; import { DateTimePicker } from "@mui/x-date-pickers"; -import { Form, Formik, FormikHelpers } from "formik"; import { DateTime } from "luxon"; import React from "react"; import { toast } from "react-toastify"; import { api } from "@/core/api"; +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { FormCheckbox, FormTextField } from "@abrechnung/components"; interface Props { group: Group; @@ -26,118 +19,102 @@ interface Props { ) => void; } -type FormValues = { - description: string; - validUntil: DateTime; - singleUse: boolean; - joinAsEditor: boolean; +const validationSchema = z.object({ + description: z.string(), + singleUse: z.boolean(), + joinAsEditor: z.boolean(), + validUntil: z.string().datetime({ offset: true }), +}); + +type FormValues = z.infer; +const nowPlusOneHour = () => { + return DateTime.now().plus({ hours: 1 }); }; export const InviteLinkCreate: React.FC = ({ show, onClose, group }) => { - const handleSubmit = (values: FormValues, { setSubmitting }: FormikHelpers) => { + const { control, handleSubmit } = useForm({ + resolver: zodResolver(validationSchema), + defaultValues: { + description: "", + validUntil: nowPlusOneHour().toISO(), + singleUse: false, + joinAsEditor: false, + }, + }); + + const onSubmit = (values: FormValues) => { api.client.groups .createInvite({ groupId: group.id, requestBody: { description: values.description, - valid_until: values.validUntil.toISO()!, + valid_until: values.validUntil, single_use: values.singleUse, join_as_editor: values.joinAsEditor, }, }) .then((result) => { toast.success("Successfully created invite token"); - setSubmitting(false); onClose({}, "completed"); }) .catch((err) => { toast.error(err); - setSubmitting(false); }); }; - const nowPlusOneHour = () => { - return DateTime.now().plus({ hours: 1 }); - }; - return ( Create Invite Link - - {({ values, setFieldValue, handleChange, handleBlur, handleSubmit, isSubmitting }) => ( -
- - setFieldValue("validUntil", val, true)} - slotProps={{ - textField: { - name: "validUntil", - sx: { marginTop: 2 }, - variant: "standard", - fullWidth: true, - }, - }} - /> - - } - /> - - } - /> + + + + ( + { + if (val != null && val.isValid) { + onChange(val.toISO()); + console.log(val.toISO()); + } + }} + slotProps={{ + textField: { + variant: "standard", + fullWidth: true, + }, + }} + /> + )} + /> + + - {isSubmitting && } - - - - - - )} -
+ + + + + +
); diff --git a/frontend/apps/web/src/components/index.ts b/frontend/apps/web/src/components/index.ts index 05c7f931..fbc5d36e 100644 --- a/frontend/apps/web/src/components/index.ts +++ b/frontend/apps/web/src/components/index.ts @@ -1,10 +1,6 @@ -export * from "./DateInput"; export * from "./ShareSelect"; export * from "./RequireAuth"; export * from "./TagSelector"; export * from "./AccountSelect"; export * from "./TextInput"; -export * from "./NumericInput"; -export * from "./FormTextField"; -export * from "./DisabledFormTextField"; export * from "./GroupArchivedDisclaimer"; diff --git a/frontend/apps/web/src/components/style/EditableField.tsx b/frontend/apps/web/src/components/style/EditableField.tsx deleted file mode 100644 index 49f3b6f3..00000000 --- a/frontend/apps/web/src/components/style/EditableField.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { IconButton } from "@mui/material"; -import { Check, Close, Edit } from "@mui/icons-material"; -import { DisabledTextField } from "./DisabledTextField"; - -interface Props { - value: string; - onChange: (newValue: string) => void; - validate?: (value: string) => boolean; - helperText?: string; - onStopEdit?: () => void; - canEdit?: boolean; -} - -export const EditableField: React.FC = ({ - value, - onChange, - validate = undefined, - helperText = undefined, - onStopEdit = undefined, - canEdit = true, - ...props -}) => { - const [currentValue, setValue] = useState(""); - const [editing, setEditing] = useState(false); - const [error, setError] = useState(false); - - useEffect(() => { - setValue(value); - }, [value]); - - const onSave = () => { - if (!error) { - onChange(currentValue); - setValue(""); - setEditing(false); - } - }; - - const startEditing = () => { - setValue(value); - setEditing(true); - }; - - const stopEditing = () => { - setValue(value); - setEditing(false); - if (onStopEdit) { - onStopEdit(); - } - }; - - const onValueChange = (event: React.ChangeEvent) => { - setValue(event.target.value); - if (validate) { - setError(!validate(event.target.value)); - } - }; - - const onKeyUp = (key: React.KeyboardEvent) => { - if (key.keyCode === 13) { - onSave(); - } - }; - - return ( -
- - {canEdit && - (editing ? ( - <> - - - - - - - - ) : ( - - - - ))} -
- ); -}; diff --git a/frontend/apps/web/src/components/style/index.ts b/frontend/apps/web/src/components/style/index.ts index 622257ed..47c2c732 100644 --- a/frontend/apps/web/src/components/style/index.ts +++ b/frontend/apps/web/src/components/style/index.ts @@ -1,7 +1,4 @@ export * from "./AbrechnungIcons"; export * from "./ListItemLink"; -export * from "./Loading"; export * from "./Search"; -export * from "./EditableField"; -export * from "./DisabledTextField"; export * from "./MobilePaper"; diff --git a/frontend/apps/web/src/core/config.tsx b/frontend/apps/web/src/core/config.tsx index 6e6f2673..0cf90213 100644 --- a/frontend/apps/web/src/core/config.tsx +++ b/frontend/apps/web/src/core/config.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { z } from "zod"; import { environment } from "@/environments/environment"; import { AlertColor } from "@mui/material/Alert/Alert"; -import { Loading } from "@/components/style"; +import { Loading } from "@abrechnung/components"; import { Alert, AlertTitle } from "@mui/material"; const configSchema = z.object({ diff --git a/frontend/apps/web/src/main.tsx b/frontend/apps/web/src/main.tsx index 8f27db15..34546193 100644 --- a/frontend/apps/web/src/main.tsx +++ b/frontend/apps/web/src/main.tsx @@ -3,7 +3,7 @@ import * as ReactDOM from "react-dom/client"; import { Provider } from "react-redux"; import { PersistGate } from "redux-persist/integration/react"; import { App } from "./app/app"; -import { Loading } from "./components/style/Loading"; +import { Loading } from "@abrechnung/components"; import "./i18n"; import { persistor, store } from "./store"; import { ConfigProvider } from "./core/config"; diff --git a/frontend/apps/web/src/pages/accounts/AccountDetail/AccountDetail.tsx b/frontend/apps/web/src/pages/accounts/AccountDetail/AccountDetail.tsx index 8b64368d..9b6e58a7 100644 --- a/frontend/apps/web/src/pages/accounts/AccountDetail/AccountDetail.tsx +++ b/frontend/apps/web/src/pages/accounts/AccountDetail/AccountDetail.tsx @@ -1,7 +1,8 @@ import { AccountTransactionList } from "@/components/accounts/AccountTransactionList"; import { BalanceHistoryGraph } from "@/components/accounts/BalanceHistoryGraph"; import { ClearingAccountDetail } from "@/components/accounts/ClearingAccountDetail"; -import { MobilePaper, Loading } from "@/components/style"; +import { MobilePaper } from "@/components/style"; +import { Loading } from "@abrechnung/components"; import { useQuery, useTitle } from "@/core/utils"; import { Grid, Typography } from "@mui/material"; import * as React from "react"; diff --git a/frontend/apps/web/src/pages/accounts/AccountDetail/AccountInfo.tsx b/frontend/apps/web/src/pages/accounts/AccountDetail/AccountInfo.tsx index be7c273f..eead7866 100644 --- a/frontend/apps/web/src/pages/accounts/AccountDetail/AccountInfo.tsx +++ b/frontend/apps/web/src/pages/accounts/AccountDetail/AccountInfo.tsx @@ -1,4 +1,4 @@ -import { DateInput } from "@/components/DateInput"; +import { DateInput } from "@abrechnung/components"; import { ShareSelect } from "@/components/ShareSelect"; import { TagSelector } from "@/components/TagSelector"; import { TextInput } from "@/components/TextInput"; diff --git a/frontend/apps/web/src/pages/auth/ConfirmEmailChange.tsx b/frontend/apps/web/src/pages/auth/ConfirmEmailChange.tsx index b62b3892..c58cca49 100644 --- a/frontend/apps/web/src/pages/auth/ConfirmEmailChange.tsx +++ b/frontend/apps/web/src/pages/auth/ConfirmEmailChange.tsx @@ -2,7 +2,7 @@ import { Button, Typography } from "@mui/material"; import React, { useState } from "react"; import { useParams } from "react-router-dom"; import { toast } from "react-toastify"; -import { Loading } from "@/components/style/Loading"; +import { Loading } from "@abrechnung/components"; import { api } from "@/core/api"; import { useTitle } from "@/core/utils"; import { Trans, useTranslation } from "react-i18next"; diff --git a/frontend/apps/web/src/pages/auth/ConfirmPasswordRecovery.tsx b/frontend/apps/web/src/pages/auth/ConfirmPasswordRecovery.tsx index e00dd51e..f0b0ef44 100644 --- a/frontend/apps/web/src/pages/auth/ConfirmPasswordRecovery.tsx +++ b/frontend/apps/web/src/pages/auth/ConfirmPasswordRecovery.tsx @@ -1,6 +1,4 @@ -import { toFormikValidationSchema } from "@abrechnung/utils"; -import { Alert, Box, Button, Container, CssBaseline, LinearProgress, Link, TextField, Typography } from "@mui/material"; -import { Form, Formik, FormikHelpers, FormikProps } from "formik"; +import { Alert, Box, Button, Container, CssBaseline, Link, Stack, Typography } from "@mui/material"; import React, { useState } from "react"; import { Link as RouterLink, useParams } from "react-router-dom"; import { z } from "zod"; @@ -8,6 +6,9 @@ import { api } from "@/core/api"; import i18n from "@/i18n"; import { Trans, useTranslation } from "react-i18next"; import { useTitle } from "@/core/utils"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { FormTextField } from "@abrechnung/components"; const validationSchema = z .object({ @@ -28,7 +29,19 @@ export const ConfirmPasswordRecovery: React.FC = () => { useTitle(t("auth.confirmPasswordRecovery.tabTitle")); - const handleSubmit = (values: FormSchema, { setSubmitting, resetForm }: FormikHelpers) => { + const { + control, + handleSubmit, + reset: resetForm, + } = useForm({ + resolver: zodResolver(validationSchema), + defaultValues: { + password: "", + password2: "", + }, + }); + + const onSubmit = (values: FormSchema) => { if (!token) { return; } @@ -38,13 +51,11 @@ export const ConfirmPasswordRecovery: React.FC = () => { .then(() => { setStatus("success"); setError(null); - setSubmitting(false); resetForm(); }) .catch((err) => { setStatus("error"); setError(err.toString()); - setSubmitting(false); }); }; @@ -78,66 +89,40 @@ export const ConfirmPasswordRecovery: React.FC = () => { ) : ( - - {({ - values, - handleChange, - handleBlur, - isSubmitting, - errors, - touched, - }: FormikProps) => ( -
- + + + - + - {isSubmitting && } - - - )} -
+ + + )} diff --git a/frontend/apps/web/src/pages/auth/ConfirmRegistration.tsx b/frontend/apps/web/src/pages/auth/ConfirmRegistration.tsx index 56e4992c..f71fe4c5 100644 --- a/frontend/apps/web/src/pages/auth/ConfirmRegistration.tsx +++ b/frontend/apps/web/src/pages/auth/ConfirmRegistration.tsx @@ -1,7 +1,7 @@ import { Alert, Button, Container, Link, Typography } from "@mui/material"; import React, { useState } from "react"; import { Link as RouterLink, useParams } from "react-router-dom"; -import { Loading } from "@/components/style/Loading"; +import { Loading } from "@abrechnung/components"; import { MobilePaper } from "@/components/style"; import { api } from "@/core/api"; import { useTitle } from "@/core/utils"; diff --git a/frontend/apps/web/src/pages/auth/Login.tsx b/frontend/apps/web/src/pages/auth/Login.tsx index 2d9c6bc2..1fa84d7d 100644 --- a/frontend/apps/web/src/pages/auth/Login.tsx +++ b/frontend/apps/web/src/pages/auth/Login.tsx @@ -1,27 +1,17 @@ import React, { useEffect } from "react"; import { Link as RouterLink, useNavigate } from "react-router-dom"; -import { Form, Formik, FormikHelpers } from "formik"; import { api } from "@/core/api"; import { toast } from "react-toastify"; import { useQuery, useTitle } from "@/core/utils"; -import { - Avatar, - Box, - Button, - Container, - CssBaseline, - Grid, - LinearProgress, - Link, - TextField, - Typography, -} from "@mui/material"; +import { Avatar, Box, Button, Container, CssBaseline, Grid, Link, Typography } from "@mui/material"; import { LockOutlined } from "@mui/icons-material"; import { z } from "zod"; import { useAppDispatch, useAppSelector } from "@/store"; import { selectIsAuthenticated, login } from "@abrechnung/redux"; -import { toFormikValidationSchema } from "@abrechnung/utils"; import { useTranslation } from "react-i18next"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { FormTextField } from "@abrechnung/components"; const validationSchema = z.object({ username: z.string({ required_error: "username is required" }), @@ -48,17 +38,23 @@ export const Login: React.FC = () => { } }, [isLoggedIn, navigate, query]); - const handleSubmit = (values: FormValues, { setSubmitting }: FormikHelpers) => { + const { control, handleSubmit } = useForm({ + resolver: zodResolver(validationSchema), + defaultValues: { + password: "", + username: "", + }, + }); + + const onSubmit = (values: FormValues) => { const sessionName = navigator.appVersion + " " + navigator.userAgent + " " + navigator.appName; dispatch(login({ username: values.username, password: values.password, sessionName, api })) .unwrap() .then((res) => { toast.success(t("auth.login.loginSuccess")); - setSubmitting(false); }) .catch((err) => { toast.error(err.message); - setSubmitting(false); }); }; @@ -72,69 +68,49 @@ export const Login: React.FC = () => { {t("auth.login.header")} - - {({ values, handleBlur, handleChange, handleSubmit, isSubmitting }) => ( -
- - + + + - + - {isSubmitting && } - - - - - {t("auth.login.noAccountRegister")} - - - - - - - {t("auth.login.forgotPassword")} - - - - - )} -
+ + + + + {t("auth.login.noAccountRegister")} + + + + + + + {t("auth.login.forgotPassword")} + + + + ); diff --git a/frontend/apps/web/src/pages/auth/Logout.tsx b/frontend/apps/web/src/pages/auth/Logout.tsx index 4a220e95..a9fbd027 100644 --- a/frontend/apps/web/src/pages/auth/Logout.tsx +++ b/frontend/apps/web/src/pages/auth/Logout.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from "react"; -import { Loading } from "@/components/style/Loading"; +import { Loading } from "@abrechnung/components"; import { useAppDispatch, useAppSelector } from "@/store"; import { logout, selectIsAuthenticated } from "@abrechnung/redux"; import { api } from "@/core/api"; diff --git a/frontend/apps/web/src/pages/auth/Register.tsx b/frontend/apps/web/src/pages/auth/Register.tsx index d56473d8..8f69b949 100644 --- a/frontend/apps/web/src/pages/auth/Register.tsx +++ b/frontend/apps/web/src/pages/auth/Register.tsx @@ -1,29 +1,18 @@ import { selectIsAuthenticated } from "@abrechnung/redux"; import { LockOutlined } from "@mui/icons-material"; -import { - Avatar, - Box, - Button, - Container, - CssBaseline, - Grid, - LinearProgress, - Link, - TextField, - Typography, -} from "@mui/material"; -import { Form, Formik, FormikHelpers } from "formik"; +import { Avatar, Box, Button, Container, CssBaseline, Grid, Link, Typography } from "@mui/material"; import React, { useEffect, useState } from "react"; import { Link as RouterLink, useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; import { z } from "zod"; -import { Loading } from "@/components/style/Loading"; +import { FormTextField, Loading } from "@abrechnung/components"; import { api } from "@/core/api"; import { useQuery, useTitle } from "@/core/utils"; import { useAppSelector } from "@/store"; -import { toFormikValidationSchema } from "@abrechnung/utils"; import { useTranslation } from "react-i18next"; import i18n from "@/i18n"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; const validationSchema = z .object({ @@ -60,10 +49,19 @@ export const Register: React.FC = () => { } }, [loggedIn, navigate, query]); - const handleSubmit = (values: FormValues, { setSubmitting }: FormikHelpers) => { + const { control, handleSubmit } = useForm({ + resolver: zodResolver(validationSchema), + defaultValues: { + username: "", + email: "", + password: "", + password2: "", + }, + }); + + const onSubmit = (values: FormValues) => { // extract a potential invite token (which should be a uuid) from the query args let inviteToken = undefined; - console.log(query.get("next")); if (query.get("next") !== null && query.get("next") !== undefined) { const re = /\/invite\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/; const m = query.get("next")?.match(re); @@ -85,12 +83,10 @@ export const Register: React.FC = () => { toast.success(t("auth.register.registrationSuccess"), { autoClose: 20000, }); - setSubmitting(false); navigate(`/login${queryArgsForward}`); }) .catch((err) => { toast.error(err); - setSubmitting(false); }); }; @@ -114,91 +110,62 @@ export const Register: React.FC = () => { {t("auth.register.header")} - - {({ values, handleBlur, handleChange, handleSubmit, isSubmitting }) => ( -
- - + + + - + - + - {isSubmitting && } - - - - - {t("auth.register.alreadyHasAccount")} - - - - - )} -
+ + + + + {t("auth.register.alreadyHasAccount")} + + + + ); diff --git a/frontend/apps/web/src/pages/auth/RequestPasswordRecovery.tsx b/frontend/apps/web/src/pages/auth/RequestPasswordRecovery.tsx index 7928a494..4b1a1cb3 100644 --- a/frontend/apps/web/src/pages/auth/RequestPasswordRecovery.tsx +++ b/frontend/apps/web/src/pages/auth/RequestPasswordRecovery.tsx @@ -1,7 +1,5 @@ import { selectIsAuthenticated } from "@abrechnung/redux"; -import { toFormikValidationSchema } from "@abrechnung/utils"; -import { Alert, Box, Button, Container, CssBaseline, LinearProgress, TextField, Typography } from "@mui/material"; -import { Form, Formik, FormikHelpers, FormikProps } from "formik"; +import { Alert, Box, Button, Container, CssBaseline, Typography } from "@mui/material"; import React, { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { z } from "zod"; @@ -9,6 +7,9 @@ import { api } from "@/core/api"; import { useAppSelector } from "@/store"; import { useTranslation } from "react-i18next"; import { useTitle } from "@/core/utils"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { FormTextField } from "@abrechnung/components"; const validationSchema = z.object({ email: z.string({ required_error: "email is required" }).email("please enter a valid email address"), @@ -30,19 +31,26 @@ export const RequestPasswordRecovery: React.FC = () => { } }, [isLoggedIn, navigate]); - const handleSubmit = (values: FormSchema, { setSubmitting, resetForm }: FormikHelpers) => { + const { + control, + handleSubmit, + reset: resetForm, + } = useForm({ + resolver: zodResolver(validationSchema), + defaultValues: { email: "" }, + }); + + const onSubmit = (values: FormSchema) => { api.client.auth .recoverPassword({ requestBody: { email: values.email } }) .then(() => { setStatus("success"); setError(null); - setSubmitting(false); resetForm(); }) .catch((err) => { setStatus("error"); setError(err.toString()); - setSubmitting(false); }); }; @@ -73,49 +81,21 @@ export const RequestPasswordRecovery: React.FC = () => { {t("auth.recoverPassword.emailSent")} ) : ( - - {({ - values, - handleChange, - handleBlur, - handleSubmit, - isSubmitting, - touched, - errors, - }: FormikProps) => ( -
- - {isSubmitting && } - - - )} -
+
+ + + )} diff --git a/frontend/apps/web/src/pages/groups/Group.tsx b/frontend/apps/web/src/pages/groups/Group.tsx index 90639e49..610eb681 100644 --- a/frontend/apps/web/src/pages/groups/Group.tsx +++ b/frontend/apps/web/src/pages/groups/Group.tsx @@ -11,7 +11,7 @@ import React, { Suspense } from "react"; import { Navigate, Route, Routes, useParams } from "react-router-dom"; import { toast } from "react-toastify"; import { Balances } from "../accounts/Balances"; -import { Loading } from "@/components/style/Loading"; +import { Loading } from "@abrechnung/components"; import { api, ws } from "@/core/api"; import { useAppDispatch, useAppSelector } from "@/store"; import { AccountDetail } from "../accounts/AccountDetail"; diff --git a/frontend/apps/web/src/pages/groups/GroupActivity.tsx b/frontend/apps/web/src/pages/groups/GroupActivity.tsx index d0a6d438..381334c2 100644 --- a/frontend/apps/web/src/pages/groups/GroupActivity.tsx +++ b/frontend/apps/web/src/pages/groups/GroupActivity.tsx @@ -10,7 +10,7 @@ import { import { List, ListItem, ListItemText, Typography } from "@mui/material"; import { DateTime } from "luxon"; import React, { useEffect } from "react"; -import { Loading } from "@/components/style/Loading"; +import { Loading } from "@abrechnung/components"; import { MobilePaper } from "@/components/style"; import { api, ws } from "@/core/api"; import { useTitle } from "@/core/utils"; diff --git a/frontend/apps/web/src/pages/groups/GroupInvite.tsx b/frontend/apps/web/src/pages/groups/GroupInvite.tsx index 86765ce1..9297dc05 100644 --- a/frontend/apps/web/src/pages/groups/GroupInvite.tsx +++ b/frontend/apps/web/src/pages/groups/GroupInvite.tsx @@ -1,4 +1,5 @@ -import { MobilePaper, Loading } from "@/components/style"; +import { MobilePaper } from "@/components/style"; +import { Loading } from "@abrechnung/components"; import { api } from "@/core/api"; import { useTitle } from "@/core/utils"; import { GroupPreview } from "@abrechnung/api"; diff --git a/frontend/apps/web/src/pages/groups/settings/GroupInvites.tsx b/frontend/apps/web/src/pages/groups/settings/GroupInvites.tsx index 4e6d322f..09979286 100644 --- a/frontend/apps/web/src/pages/groups/settings/GroupInvites.tsx +++ b/frontend/apps/web/src/pages/groups/settings/GroupInvites.tsx @@ -15,7 +15,7 @@ import { DateTime } from "luxon"; import React, { useEffect, useState } from "react"; import { toast } from "react-toastify"; import { InviteLinkCreate } from "@/components/groups/InviteLinkCreate"; -import { Loading } from "@/components/style/Loading"; +import { Loading } from "@abrechnung/components"; import { api, ws } from "@/core/api"; import { useTitle } from "@/core/utils"; import { useAppDispatch, useAppSelector } from "@/store"; diff --git a/frontend/apps/web/src/pages/groups/settings/GroupMemberList.tsx b/frontend/apps/web/src/pages/groups/settings/GroupMemberList.tsx index bcb5bae1..fc5b22a0 100644 --- a/frontend/apps/web/src/pages/groups/settings/GroupMemberList.tsx +++ b/frontend/apps/web/src/pages/groups/settings/GroupMemberList.tsx @@ -8,21 +8,17 @@ import { import { Edit } from "@mui/icons-material"; import { Button, - Checkbox, Chip, Dialog, DialogActions, DialogContent, DialogTitle, - FormControlLabel, IconButton, - LinearProgress, List, ListItem, ListItemSecondaryAction, ListItemText, } from "@mui/material"; -import { Form, Formik, FormikHelpers } from "formik"; import { DateTime } from "luxon"; import React, { useState } from "react"; import { toast } from "react-toastify"; @@ -31,6 +27,8 @@ import { useTitle } from "@/core/utils"; import { useAppDispatch, useAppSelector } from "@/store"; import { useTranslation } from "react-i18next"; import { Navigate } from "react-router-dom"; +import { useForm } from "react-hook-form"; +import { FormCheckbox } from "@abrechnung/components"; interface GroupMemberListProps { group: Group; @@ -42,18 +40,39 @@ type FormValues = { canWrite: boolean; }; -export const GroupMemberList: React.FC = ({ group }) => { +const EditMemberDialog: React.FC<{ + group: Group; + memberToEdit: GroupMember | undefined; + setMemberToEdit: (member: GroupMember | undefined) => void; +}> = ({ group, memberToEdit, setMemberToEdit }) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const currentUserId = useAppSelector(selectCurrentUserId); - const members = useAppSelector((state) => selectGroupMembers(state, group.id)); - const permissions = useCurrentUserPermissions(group.id); - const [memberToEdit, setMemberToEdit] = useState(undefined); + const closeEditMemberModal = () => { + setMemberToEdit(undefined); + }; - useTitle(t("groups.memberList.tabTitle", "", { groupName: group?.name })); + const { + control, + handleSubmit, + reset: resetForm, + } = useForm({ + defaultValues: { + userId: memberToEdit?.user_id ?? -1, + isOwner: memberToEdit?.is_owner ?? false, + canWrite: memberToEdit?.can_write ?? false, + }, + }); - const handleEditMemberSubmit = (values: FormValues, { setSubmitting }: FormikHelpers) => { + React.useEffect(() => { + resetForm({ + userId: memberToEdit?.user_id ?? -1, + isOwner: memberToEdit?.is_owner ?? false, + canWrite: memberToEdit?.can_write ?? false, + }); + }, [memberToEdit, resetForm]); + + const handleEditMemberSubmit = (values: FormValues) => { dispatch( updateGroupMemberPrivileges({ groupId: group.id, @@ -64,16 +83,46 @@ export const GroupMemberList: React.FC = ({ group }) => { ) .unwrap() .then(() => { - setSubmitting(false); - setMemberToEdit(undefined); + closeEditMemberModal(); toast.success("Successfully updated group member permissions"); }) .catch((err) => { - setSubmitting(false); toast.error(err); }); }; + return ( + + Edit Group Member + +
+ + + + + + + + +
+
+ ); +}; + +export const GroupMemberList: React.FC = ({ group }) => { + const { t } = useTranslation(); + const currentUserId = useAppSelector(selectCurrentUserId); + const members = useAppSelector((state) => selectGroupMembers(state, group.id)); + const permissions = useCurrentUserPermissions(group.id); + + const [memberToEdit, setMemberToEdit] = useState(undefined); + + useTitle(t("groups.memberList.tabTitle", "", { groupName: group?.name })); + const getMemberUsername = (member_id: number) => { const member = members.find((member) => member.user_id === member_id); if (member === undefined) { @@ -82,10 +131,6 @@ export const GroupMemberList: React.FC = ({ group }) => { return member.username; }; - const closeEditMemberModal = () => { - setMemberToEdit(undefined); - }; - const openEditMemberModal = (userID: number) => { const user = members.find((member) => member.user_id === userID); // TODO: maybe deal with disappearing users in the list @@ -170,57 +215,7 @@ export const GroupMemberList: React.FC = ({ group }) => { )) )} - - Edit Group Member - - - {({ values, handleBlur, isSubmitting, setFieldValue }) => ( -
- setFieldValue("canWrite", evt.target.checked)} - checked={values.canWrite} - /> - } - label={t("groups.memberList.canWrite")} - /> - setFieldValue("isOwner", evt.target.checked)} - checked={values.isOwner} - /> - } - label={t("groups.memberList.isOwner")} - /> - - {isSubmitting && } - - - - - - )} -
-
-
+ ); }; diff --git a/frontend/apps/web/src/pages/groups/settings/SettingsForm.tsx b/frontend/apps/web/src/pages/groups/settings/SettingsForm.tsx index fc7a774f..a2da78c9 100644 --- a/frontend/apps/web/src/pages/groups/settings/SettingsForm.tsx +++ b/frontend/apps/web/src/pages/groups/settings/SettingsForm.tsx @@ -1,4 +1,4 @@ -import { DisabledFormControlLabel } from "@/components/style/DisabledTextField"; +import { DisabledFormControlLabel, DisabledFormTextField } from "@abrechnung/components"; import { api } from "@/core/api"; import { useAppDispatch } from "@/store"; import { Group } from "@abrechnung/api"; @@ -11,7 +11,6 @@ import { toast } from "react-toastify"; import { z } from "zod"; import { Controller, SubmitHandler, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { DisabledFormTextField } from "@/components"; type SettingsFormProps = { group: Group; diff --git a/frontend/apps/web/src/pages/profile/ChangeEmail.tsx b/frontend/apps/web/src/pages/profile/ChangeEmail.tsx index 3f6e31c1..13966d60 100644 --- a/frontend/apps/web/src/pages/profile/ChangeEmail.tsx +++ b/frontend/apps/web/src/pages/profile/ChangeEmail.tsx @@ -1,6 +1,4 @@ -import { toFormikValidationSchema } from "@abrechnung/utils"; -import { Button, LinearProgress, TextField, Typography } from "@mui/material"; -import { Form, Formik, FormikHelpers, FormikProps } from "formik"; +import { Button, Typography } from "@mui/material"; import React from "react"; import { useTranslation } from "react-i18next"; import { toast } from "react-toastify"; @@ -8,6 +6,9 @@ import { z } from "zod"; import { MobilePaper } from "@/components/style"; import { api } from "@/core/api"; import { useTitle } from "@/core/utils"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { FormTextField } from "@abrechnung/components"; const validationSchema = z.object({ password: z.string({ required_error: "password is required" }), @@ -19,16 +20,26 @@ export const ChangeEmail: React.FC = () => { const { t } = useTranslation(); useTitle(t("profile.changeEmail.tabTitle")); - const handleSubmit = (values: FormSchema, { setSubmitting, resetForm }: FormikHelpers) => { + const { + control, + handleSubmit, + reset: resetForm, + } = useForm({ + resolver: zodResolver(validationSchema), + defaultValues: { + password: "", + newEmail: "", + }, + }); + + const onSubmit = (values: FormSchema) => { api.client.auth .changeEmail({ requestBody: { password: values.password, email: values.newEmail } }) .then(() => { - setSubmitting(false); toast.success(t("profile.changeEmail.success")); resetForm(); }) .catch((error) => { - setSubmitting(false); toast.error(error.toString()); }); }; @@ -38,49 +49,32 @@ export const ChangeEmail: React.FC = () => { {t("profile.changeEmail.pageTitle")} - - {({ values, handleChange, handleBlur, isSubmitting, errors, touched }: FormikProps) => ( -
- + + - + - {isSubmitting && } - - - )} -
+ + ); }; diff --git a/frontend/apps/web/src/pages/profile/ChangePassword.tsx b/frontend/apps/web/src/pages/profile/ChangePassword.tsx index 686873a1..73bdf5f5 100644 --- a/frontend/apps/web/src/pages/profile/ChangePassword.tsx +++ b/frontend/apps/web/src/pages/profile/ChangePassword.tsx @@ -1,6 +1,4 @@ -import { toFormikValidationSchema } from "@abrechnung/utils"; -import { Button, LinearProgress, TextField, Typography } from "@mui/material"; -import { Form, Formik, FormikHelpers, FormikProps } from "formik"; +import { Button, Typography } from "@mui/material"; import React from "react"; import { useTranslation } from "react-i18next"; import { toast } from "react-toastify"; @@ -9,6 +7,9 @@ import { MobilePaper } from "@/components/style"; import { api } from "@/core/api"; import { useTitle } from "@/core/utils"; import i18n from "@/i18n"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { FormTextField } from "@abrechnung/components"; const validationSchema = z .object({ @@ -26,16 +27,27 @@ export const ChangePassword: React.FC = () => { const { t } = useTranslation(); useTitle(t("profile.changePassword.tabTitle")); - const handleSubmit = (values: FormSchema, { setSubmitting, resetForm }: FormikHelpers) => { + const { + control, + handleSubmit, + reset: resetForm, + } = useForm({ + resolver: zodResolver(validationSchema), + defaultValues: { + password: "", + newPassword: "", + newPassword2: "", + }, + }); + + const onSubmit = (values: FormSchema) => { api.client.auth .changePassword({ requestBody: { old_password: values.password, new_password: values.newPassword } }) .then(() => { - setSubmitting(false); toast.success(t("profile.changePassword.success")); resetForm(); }) .catch((error) => { - setSubmitting(false); toast.error(error.toString()); }); }; @@ -45,67 +57,42 @@ export const ChangePassword: React.FC = () => { {t("profile.changePassword.pageTitle")} - - {({ values, handleChange, handleBlur, isSubmitting, errors, touched }: FormikProps) => ( -
- + + - + - + - {isSubmitting && } - - - )} -
+ + ); }; diff --git a/frontend/apps/web/src/pages/profile/Profile.tsx b/frontend/apps/web/src/pages/profile/Profile.tsx index d4da2096..9939a9aa 100644 --- a/frontend/apps/web/src/pages/profile/Profile.tsx +++ b/frontend/apps/web/src/pages/profile/Profile.tsx @@ -1,4 +1,5 @@ -import { MobilePaper, Loading } from "@/components/style"; +import { MobilePaper } from "@/components/style"; +import { Loading } from "@abrechnung/components"; import { useTitle } from "@/core/utils"; import { useAppSelector } from "@/store"; import { selectProfile } from "@abrechnung/redux"; diff --git a/frontend/apps/web/src/pages/profile/SessionList.tsx b/frontend/apps/web/src/pages/profile/SessionList.tsx index 9ae7eb66..cfce853e 100644 --- a/frontend/apps/web/src/pages/profile/SessionList.tsx +++ b/frontend/apps/web/src/pages/profile/SessionList.tsx @@ -18,7 +18,8 @@ import { import { DateTime } from "luxon"; import React, { useState } from "react"; import { toast } from "react-toastify"; -import { MobilePaper, Loading } from "@/components/style"; +import { MobilePaper } from "@/components/style"; +import { Loading } from "@abrechnung/components"; import { api } from "@/core/api"; import { useTitle } from "@/core/utils"; import { useAppSelector } from "@/store"; diff --git a/frontend/apps/web/src/pages/transactions/TransactionDetail/FileGallery.tsx b/frontend/apps/web/src/pages/transactions/TransactionDetail/FileGallery.tsx index f38372ed..9400b903 100644 --- a/frontend/apps/web/src/pages/transactions/TransactionDetail/FileGallery.tsx +++ b/frontend/apps/web/src/pages/transactions/TransactionDetail/FileGallery.tsx @@ -1,4 +1,4 @@ -import { Loading } from "@/components/style"; +import { Loading } from "@abrechnung/components"; import { api } from "@/core/api"; import { useAppDispatch, useAppSelector } from "@/store"; import { FileAttachment as BackendFileAttachment, NewFile } from "@abrechnung/api"; diff --git a/frontend/apps/web/src/pages/transactions/TransactionDetail/TransactionDetail.tsx b/frontend/apps/web/src/pages/transactions/TransactionDetail/TransactionDetail.tsx index 527d265a..82bf88c8 100644 --- a/frontend/apps/web/src/pages/transactions/TransactionDetail/TransactionDetail.tsx +++ b/frontend/apps/web/src/pages/transactions/TransactionDetail/TransactionDetail.tsx @@ -1,4 +1,5 @@ -import { MobilePaper, Loading } from "@/components/style"; +import { MobilePaper } from "@/components/style"; +import { Loading } from "@abrechnung/components"; import { api } from "@/core/api"; import { useQuery, useTitle } from "@/core/utils"; import { useAppDispatch, useAppSelector } from "@/store"; diff --git a/frontend/apps/web/src/pages/transactions/TransactionDetail/TransactionMetadata.tsx b/frontend/apps/web/src/pages/transactions/TransactionDetail/TransactionMetadata.tsx index 8bba13e4..bb7059a9 100644 --- a/frontend/apps/web/src/pages/transactions/TransactionDetail/TransactionMetadata.tsx +++ b/frontend/apps/web/src/pages/transactions/TransactionDetail/TransactionMetadata.tsx @@ -1,6 +1,5 @@ import { AccountSelect } from "@/components/AccountSelect"; -import { DateInput } from "@/components/DateInput"; -import { NumericInput } from "@/components/NumericInput"; +import { DateInput, NumericInput } from "@abrechnung/components"; import { ShareSelect } from "@/components/ShareSelect"; import { TagSelector } from "@/components/TagSelector"; import { TextInput } from "@/components/TextInput"; diff --git a/frontend/apps/web/src/pages/transactions/TransactionDetail/purchase/PositionTableRow.tsx b/frontend/apps/web/src/pages/transactions/TransactionDetail/purchase/PositionTableRow.tsx index 7caf99b6..275f4eb7 100644 --- a/frontend/apps/web/src/pages/transactions/TransactionDetail/purchase/PositionTableRow.tsx +++ b/frontend/apps/web/src/pages/transactions/TransactionDetail/purchase/PositionTableRow.tsx @@ -1,4 +1,4 @@ -import { NumericInput } from "@/components/NumericInput"; +import { NumericInput } from "@abrechnung/components"; import { TextInput } from "@/components/TextInput"; import { Account, TransactionPosition } from "@abrechnung/types"; import { ContentCopy, Delete } from "@mui/icons-material"; diff --git a/frontend/apps/web/tsconfig.json b/frontend/apps/web/tsconfig.json index fd741ce6..07ea87b8 100644 --- a/frontend/apps/web/tsconfig.json +++ b/frontend/apps/web/tsconfig.json @@ -11,6 +11,7 @@ "@abrechnung/types": ["../../libs/types/src/index.ts"], "@abrechnung/utils": ["../../libs/utils/src/index.ts"], "@abrechnung/translations": ["../../libs/translations/src/index.ts"], + "@abrechnung/components": ["../../libs/components/src/index.ts"], "@/*": ["src/*"] } }, diff --git a/frontend/libs/components/.babelrc b/frontend/libs/components/.babelrc new file mode 100644 index 00000000..f7b3a9be --- /dev/null +++ b/frontend/libs/components/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/frontend/libs/components/.eslintrc.json b/frontend/libs/components/.eslintrc.json new file mode 100644 index 00000000..d400a254 --- /dev/null +++ b/frontend/libs/components/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nx/react", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/frontend/libs/components/README.md b/frontend/libs/components/README.md new file mode 100644 index 00000000..0f556914 --- /dev/null +++ b/frontend/libs/components/README.md @@ -0,0 +1,7 @@ +# components + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test components` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/frontend/libs/components/jest.config.ts b/frontend/libs/components/jest.config.ts new file mode 100644 index 00000000..bd593385 --- /dev/null +++ b/frontend/libs/components/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: "components", + preset: "../../jest.preset.js", + transform: { + "^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "@nx/react/plugins/jest", + "^.+\\.[tj]sx?$": ["babel-jest", { presets: ["@nx/react/babel"] }], + }, + moduleFileExtensions: ["ts", "tsx", "js", "jsx"], + coverageDirectory: "../../coverage/libs/components", +}; diff --git a/frontend/libs/components/project.json b/frontend/libs/components/project.json new file mode 100644 index 00000000..6bb7f1c9 --- /dev/null +++ b/frontend/libs/components/project.json @@ -0,0 +1,16 @@ +{ + "name": "components", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/components/src", + "projectType": "library", + "tags": [], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/components/jest.config.ts" + } + } + } +} diff --git a/frontend/libs/components/src/index.ts b/frontend/libs/components/src/index.ts new file mode 100644 index 00000000..8cd5167d --- /dev/null +++ b/frontend/libs/components/src/index.ts @@ -0,0 +1 @@ +export * from "./lib"; diff --git a/frontend/apps/web/src/components/DateInput.tsx b/frontend/libs/components/src/lib/DateInput.tsx similarity index 95% rename from frontend/apps/web/src/components/DateInput.tsx rename to frontend/libs/components/src/lib/DateInput.tsx index b7f3a4e3..da169817 100644 --- a/frontend/apps/web/src/components/DateInput.tsx +++ b/frontend/libs/components/src/lib/DateInput.tsx @@ -1,7 +1,7 @@ import { DatePicker } from "@mui/x-date-pickers"; import { DateTime } from "luxon"; import * as React from "react"; -import { DisabledTextField } from "./style/DisabledTextField"; +import { DisabledTextField } from "./DisabledTextField"; import { useTranslation } from "react-i18next"; interface Props { diff --git a/frontend/apps/web/src/components/DisabledFormTextField.tsx b/frontend/libs/components/src/lib/DisabledFormTextField.tsx similarity index 93% rename from frontend/apps/web/src/components/DisabledFormTextField.tsx rename to frontend/libs/components/src/lib/DisabledFormTextField.tsx index 6b9b87d5..b8459bb7 100644 --- a/frontend/apps/web/src/components/DisabledFormTextField.tsx +++ b/frontend/libs/components/src/lib/DisabledFormTextField.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { Control, Controller } from "react-hook-form"; import { TextFieldProps } from "@mui/material"; -import { DisabledTextField } from "./style"; +import { DisabledTextField } from "./DisabledTextField"; export type DisabledFormTextFieldProps = Omit & { name: string; diff --git a/frontend/apps/web/src/components/style/DisabledTextField.tsx b/frontend/libs/components/src/lib/DisabledTextField.tsx similarity index 100% rename from frontend/apps/web/src/components/style/DisabledTextField.tsx rename to frontend/libs/components/src/lib/DisabledTextField.tsx diff --git a/frontend/libs/components/src/lib/FormCheckbox.tsx b/frontend/libs/components/src/lib/FormCheckbox.tsx new file mode 100644 index 00000000..8bd82b06 --- /dev/null +++ b/frontend/libs/components/src/lib/FormCheckbox.tsx @@ -0,0 +1,27 @@ +import * as React from "react"; +import { Control, Controller } from "react-hook-form"; +import { Checkbox, FormControl, FormControlLabel, FormHelperText, CheckboxProps } from "@mui/material"; + +export type FormCheckboxProps = Omit & { + label?: string; + name: string; + control: Control; +}; + +export const FormCheckbox = ({ name, label, control, sx, ...props }: FormCheckboxProps) => { + return ( + ( + + } + /> + {error && error.message} + + )} + /> + ); +}; diff --git a/frontend/apps/web/src/components/FormTextField.tsx b/frontend/libs/components/src/lib/FormTextField.tsx similarity index 100% rename from frontend/apps/web/src/components/FormTextField.tsx rename to frontend/libs/components/src/lib/FormTextField.tsx diff --git a/frontend/apps/web/src/components/style/Loading.tsx b/frontend/libs/components/src/lib/Loading.tsx similarity index 100% rename from frontend/apps/web/src/components/style/Loading.tsx rename to frontend/libs/components/src/lib/Loading.tsx diff --git a/frontend/apps/web/src/components/NumericInput.tsx b/frontend/libs/components/src/lib/NumericInput.tsx similarity index 96% rename from frontend/apps/web/src/components/NumericInput.tsx rename to frontend/libs/components/src/lib/NumericInput.tsx index a98cc779..53b653b9 100644 --- a/frontend/apps/web/src/components/NumericInput.tsx +++ b/frontend/libs/components/src/lib/NumericInput.tsx @@ -1,6 +1,6 @@ import * as React from "react"; import { TextFieldProps } from "@mui/material"; -import { DisabledTextField } from "./style/DisabledTextField"; +import { DisabledTextField } from "@abrechnung/components"; import { parseAbrechnungFloat } from "@abrechnung/utils"; export type NumericInputProps = { diff --git a/frontend/libs/components/src/lib/index.ts b/frontend/libs/components/src/lib/index.ts new file mode 100644 index 00000000..39d281ef --- /dev/null +++ b/frontend/libs/components/src/lib/index.ts @@ -0,0 +1,7 @@ +export * from "./DateInput"; +export * from "./DisabledFormTextField"; +export * from "./NumericInput"; +export * from "./FormTextField"; +export * from "./DisabledTextField"; +export * from "./Loading"; +export * from "./FormCheckbox"; diff --git a/frontend/libs/components/tsconfig.json b/frontend/libs/components/tsconfig.json new file mode 100644 index 00000000..c699a232 --- /dev/null +++ b/frontend/libs/components/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../tsconfig.base.json" +} diff --git a/frontend/libs/components/tsconfig.lib.json b/frontend/libs/components/tsconfig.lib.json new file mode 100644 index 00000000..fab4be90 --- /dev/null +++ b/frontend/libs/components/tsconfig.lib.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": ["node", "@nx/react/typings/cssmodule.d.ts", "@nx/react/typings/image.d.ts"] + }, + "exclude": [ + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "src/**/*.spec.tsx", + "src/**/*.test.tsx", + "src/**/*.spec.js", + "src/**/*.test.js", + "src/**/*.spec.jsx", + "src/**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/frontend/libs/components/tsconfig.spec.json b/frontend/libs/components/tsconfig.spec.json new file mode 100644 index 00000000..959bad4a --- /dev/null +++ b/frontend/libs/components/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/frontend/nx.json b/frontend/nx.json index 9185185a..7cccaabc 100644 --- a/frontend/nx.json +++ b/frontend/nx.json @@ -55,9 +55,18 @@ }, "library": { "style": "css", - "linter": "eslint" + "linter": "eslint", + "unitTestRunner": "jest" } } }, - "defaultProject": "web" + "defaultProject": "web", + "plugins": [ + { + "plugin": "@nx/eslint/plugin", + "options": { + "targetName": "eslint:lint" + } + } + ] } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d537946c..7f945bf5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -43,8 +43,8 @@ "localforage": "^1.10.0", "luxon": "^3.5.0", "multer": "^1.4.5-lts.1", - "react": "18.2.0", - "react-dom": "18.2.0", + "react": "18.3.1", + "react-dom": "18.3.1", "react-hook-form": "^7.53.0", "react-i18next": "^15.0.0", "react-native": "0.74.5", @@ -108,8 +108,8 @@ "@types/jest": "29.5.12", "@types/luxon": "^3.4.2", "@types/node": "22.1.0", - "@types/react": "18.2.61", - "@types/react-dom": "18.2.18", + "@types/react": "18.3.1", + "@types/react-dom": "18.3.0", "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "6.21.0", "@typescript-eslint/parser": "6.21.0", @@ -280,6 +280,7 @@ }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { "version": "7.24.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.24.7", @@ -540,6 +541,7 @@ }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { "version": "7.25.3", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.8", @@ -554,6 +556,7 @@ }, "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { "version": "7.25.0", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.8" @@ -567,6 +570,7 @@ }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.25.0", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.8" @@ -580,6 +584,7 @@ }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { "version": "7.24.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.7", @@ -595,6 +600,7 @@ }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { "version": "7.25.0", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.8", @@ -772,6 +778,7 @@ }, "node_modules/@babel/plugin-proposal-private-property-in-object": { "version": "7.21.0-placeholder-for-preset-env.2", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -803,6 +810,7 @@ }, "node_modules/@babel/plugin-syntax-class-properties": { "version": "7.12.13", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" @@ -813,6 +821,7 @@ }, "node_modules/@babel/plugin-syntax-class-static-block": { "version": "7.14.5", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" @@ -863,6 +872,7 @@ }, "node_modules/@babel/plugin-syntax-export-namespace-from": { "version": "7.8.3", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.3" @@ -886,6 +896,7 @@ }, "node_modules/@babel/plugin-syntax-import-assertions": { "version": "7.24.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.7" @@ -899,6 +910,7 @@ }, "node_modules/@babel/plugin-syntax-import-attributes": { "version": "7.24.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.7" @@ -912,6 +924,7 @@ }, "node_modules/@babel/plugin-syntax-import-meta": { "version": "7.10.4", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" @@ -922,6 +935,7 @@ }, "node_modules/@babel/plugin-syntax-json-strings": { "version": "7.8.3", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -1018,6 +1032,7 @@ }, "node_modules/@babel/plugin-syntax-top-level-await": { "version": "7.14.5", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" @@ -1044,6 +1059,7 @@ }, "node_modules/@babel/plugin-syntax-unicode-sets-regex": { "version": "7.18.6", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", @@ -1071,6 +1087,7 @@ }, "node_modules/@babel/plugin-transform-async-generator-functions": { "version": "7.25.0", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.8", @@ -1102,6 +1119,7 @@ }, "node_modules/@babel/plugin-transform-block-scoped-functions": { "version": "7.24.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.7" @@ -1142,6 +1160,7 @@ }, "node_modules/@babel/plugin-transform-class-static-block": { "version": "7.24.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.24.7", @@ -1202,6 +1221,7 @@ }, "node_modules/@babel/plugin-transform-dotall-regex": { "version": "7.24.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.24.7", @@ -1216,6 +1236,7 @@ }, "node_modules/@babel/plugin-transform-duplicate-keys": { "version": "7.24.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.7" @@ -1229,6 +1250,7 @@ }, "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { "version": "7.25.0", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.0", @@ -1243,6 +1265,7 @@ }, "node_modules/@babel/plugin-transform-dynamic-import": { "version": "7.24.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.7", @@ -1257,6 +1280,7 @@ }, "node_modules/@babel/plugin-transform-exponentiation-operator": { "version": "7.24.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7", @@ -1271,6 +1295,7 @@ }, "node_modules/@babel/plugin-transform-export-namespace-from": { "version": "7.24.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.7", @@ -1299,6 +1324,7 @@ }, "node_modules/@babel/plugin-transform-for-of": { "version": "7.24.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.7", @@ -1328,6 +1354,7 @@ }, "node_modules/@babel/plugin-transform-json-strings": { "version": "7.24.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.7", @@ -1355,6 +1382,7 @@ }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { "version": "7.24.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.7", @@ -1369,6 +1397,7 @@ }, "node_modules/@babel/plugin-transform-member-expression-literals": { "version": "7.24.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.7" @@ -1382,6 +1411,7 @@ }, "node_modules/@babel/plugin-transform-modules-amd": { "version": "7.24.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.24.7", @@ -1411,6 +1441,7 @@ }, "node_modules/@babel/plugin-transform-modules-systemjs": { "version": "7.25.0", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.25.0", @@ -1427,6 +1458,7 @@ }, "node_modules/@babel/plugin-transform-modules-umd": { "version": "7.24.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.24.7", @@ -1455,6 +1487,7 @@ }, "node_modules/@babel/plugin-transform-new-target": { "version": "7.24.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.7" @@ -1482,6 +1515,7 @@ }, "node_modules/@babel/plugin-transform-numeric-separator": { "version": "7.24.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.7", @@ -1496,6 +1530,7 @@ }, "node_modules/@babel/plugin-transform-object-rest-spread": { "version": "7.24.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.24.7", @@ -1512,6 +1547,7 @@ }, "node_modules/@babel/plugin-transform-object-super": { "version": "7.24.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.7", @@ -1526,6 +1562,7 @@ }, "node_modules/@babel/plugin-transform-optional-catch-binding": { "version": "7.24.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.7", @@ -1598,6 +1635,7 @@ }, "node_modules/@babel/plugin-transform-property-literals": { "version": "7.24.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.7" @@ -1710,6 +1748,7 @@ }, "node_modules/@babel/plugin-transform-regenerator": { "version": "7.24.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.7", @@ -1724,6 +1763,7 @@ }, "node_modules/@babel/plugin-transform-reserved-words": { "version": "7.24.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.7" @@ -1808,6 +1848,7 @@ }, "node_modules/@babel/plugin-transform-typeof-symbol": { "version": "7.24.8", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.8" @@ -1838,6 +1879,7 @@ }, "node_modules/@babel/plugin-transform-unicode-escapes": { "version": "7.24.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.7" @@ -1851,6 +1893,7 @@ }, "node_modules/@babel/plugin-transform-unicode-property-regex": { "version": "7.24.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.24.7", @@ -1879,6 +1922,7 @@ }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { "version": "7.24.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.24.7", @@ -1893,6 +1937,7 @@ }, "node_modules/@babel/preset-env": { "version": "7.25.3", + "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.25.2", @@ -2005,6 +2050,7 @@ }, "node_modules/@babel/preset-modules": { "version": "0.1.6-no-external-plugins", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", @@ -11235,155 +11281,6 @@ "node": ">=10" } }, - "node_modules/@testing-library/dom": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", - "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@testing-library/dom/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@testing-library/dom/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@testing-library/dom/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@testing-library/dom/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@testing-library/dom/node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@testing-library/dom/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@testing-library/dom/node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@testing-library/dom/node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@testing-library/dom/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@testing-library/dom/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@testing-library/jest-dom": { "version": "6.4.8", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.8.tgz", @@ -11697,14 +11594,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/@types/babel__core": { "version": "7.20.5", "dev": true, @@ -12122,18 +12011,19 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.2.61", - "license": "MIT", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.1.tgz", + "integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==", "dependencies": { "@types/prop-types": "*", - "@types/scheduler": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "18.2.18", + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", "dev": true, - "license": "MIT", "dependencies": { "@types/react": "*" } @@ -12158,10 +12048,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/scheduler": { - "version": "0.23.0", - "license": "MIT" - }, "node_modules/@types/semver": { "version": "7.5.8", "dev": true, @@ -18092,6 +17978,7 @@ }, "node_modules/esutils": { "version": "2.0.3", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -18195,186 +18082,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/expo-modules-autolinking": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-1.11.1.tgz", - "integrity": "sha512-2dy3lTz76adOl7QUvbreMCrXyzUiF8lygI7iFJLjgIQIVH+43KnFWE5zBumpPbkiaq0f0uaFpN9U0RGQbnKiMw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "chalk": "^4.1.0", - "commander": "^7.2.0", - "fast-glob": "^3.2.5", - "find-up": "^5.0.0", - "fs-extra": "^9.1.0" - }, - "bin": { - "expo-modules-autolinking": "bin/expo-modules-autolinking.js" - } - }, - "node_modules/expo-modules-autolinking/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/expo-modules-autolinking/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/expo-modules-autolinking/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/expo-modules-autolinking/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/expo-modules-autolinking/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/expo-modules-autolinking/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/expo-modules-autolinking/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/expo-modules-autolinking/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/expo-modules-autolinking/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/expo-modules-autolinking/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/expo-modules-autolinking/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/express": { "version": "4.19.2", "dev": true, @@ -24153,17 +23860,6 @@ "node": ">=12" } }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "lz-string": "bin/bin.js" - } - }, "node_modules/make-dir": { "version": "4.0.0", "dev": true, @@ -24946,6 +24642,7 @@ }, "node_modules/moment": { "version": "2.30.1", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -27378,8 +27075,9 @@ } }, "node_modules/react": { - "version": "18.2.0", - "license": "MIT", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dependencies": { "loose-envify": "^1.1.0" }, @@ -27396,14 +27094,15 @@ } }, "node_modules/react-dom": { - "version": "18.2.0", - "license": "MIT", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dependencies": { "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" + "scheduler": "^0.23.2" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.3.1" } }, "node_modules/react-fast-compare": { @@ -28455,6 +28154,7 @@ }, "node_modules/regenerator-transform": { "version": "0.15.2", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.4" @@ -31085,7 +30785,7 @@ }, "node_modules/typescript": { "version": "5.5.4", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/frontend/tsconfig.base.json b/frontend/tsconfig.base.json index 81db8675..0b03e5a3 100644 --- a/frontend/tsconfig.base.json +++ b/frontend/tsconfig.base.json @@ -25,6 +25,7 @@ "baseUrl": ".", "paths": { "@abrechnung/api": ["libs/api/src/index.ts"], + "@abrechnung/components": ["libs/components/src/index.ts"], "@abrechnung/core": ["libs/core/src/index.ts"], "@abrechnung/redux": ["libs/redux/src/index.ts"], "@abrechnung/translations": ["libs/translations/src/index.ts"], From deba396a00bce9b21597b75c33011319187ae088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Sun, 22 Sep 2024 20:12:05 +0200 Subject: [PATCH 2/2] refactor(frontend): remove formik from app part --- .../mobile/src/components/DateTimeInput.tsx | 4 +- .../mobile/src/components/FormCheckbox.tsx | 27 + .../src/components/FormDateTimeInput.tsx | 30 + .../src/components/FormNumericInput.tsx | 24 + .../mobile/src/components/FormTagSelect.tsx | 24 + .../mobile/src/components/FormTextInput.tsx | 23 + .../components/FormTransactionShareInput.tsx | 24 + .../mobile/src/components/NumericInput.tsx | 4 +- frontend/apps/mobile/src/components/index.ts | 6 + .../src/components/tag-select/TagSelect.tsx | 4 +- .../TransactionShareInput.tsx | 6 +- frontend/apps/mobile/src/screens/AddGroup.tsx | 100 ++- frontend/apps/mobile/src/screens/Login.tsx | 140 ++-- frontend/apps/mobile/src/screens/Register.tsx | 166 ++--- .../src/screens/groups/AccountDetail.tsx | 2 +- .../mobile/src/screens/groups/AccountEdit.tsx | 143 ++-- .../src/screens/groups/TransactionDetail.tsx | 156 ++--- .../libs/components/src/lib/FormTextField.tsx | 3 +- frontend/libs/types/src/lib/transactions.ts | 12 + frontend/libs/utils/src/lib/validators.ts | 46 -- frontend/package-lock.json | 612 +++++++++++++----- frontend/package.json | 1 - 22 files changed, 925 insertions(+), 632 deletions(-) create mode 100644 frontend/apps/mobile/src/components/FormCheckbox.tsx create mode 100644 frontend/apps/mobile/src/components/FormDateTimeInput.tsx create mode 100644 frontend/apps/mobile/src/components/FormNumericInput.tsx create mode 100644 frontend/apps/mobile/src/components/FormTagSelect.tsx create mode 100644 frontend/apps/mobile/src/components/FormTextInput.tsx create mode 100644 frontend/apps/mobile/src/components/FormTransactionShareInput.tsx diff --git a/frontend/apps/mobile/src/components/DateTimeInput.tsx b/frontend/apps/mobile/src/components/DateTimeInput.tsx index cead8846..01e2dee9 100644 --- a/frontend/apps/mobile/src/components/DateTimeInput.tsx +++ b/frontend/apps/mobile/src/components/DateTimeInput.tsx @@ -3,7 +3,7 @@ import { DateTimePickerAndroid, DateTimePickerEvent } from "@react-native-commun import React, { useEffect, useState } from "react"; import { HelperText, TextInput } from "react-native-paper"; -interface Props +export interface DateTimeInputProps extends Omit, "onChange" | "value" | "disabled" | "editable" | "mode"> { value: Date | null; onChange: (newValue: Date) => void; @@ -12,7 +12,7 @@ interface Props editable?: boolean; } -export const DateTimeInput: React.FC = ({ +export const DateTimeInput: React.FC = ({ value, onChange, mode = "date", diff --git a/frontend/apps/mobile/src/components/FormCheckbox.tsx b/frontend/apps/mobile/src/components/FormCheckbox.tsx new file mode 100644 index 00000000..e8ba06e9 --- /dev/null +++ b/frontend/apps/mobile/src/components/FormCheckbox.tsx @@ -0,0 +1,27 @@ +import * as React from "react"; +import { Control, Controller } from "react-hook-form"; +import { Checkbox, HelperText, CheckboxItemProps } from "react-native-paper"; + +export type FormCheckboxProps = Omit & { + name: string; + control: Control; +}; + +export const FormCheckbox: React.FC = ({ name, control, ...props }) => { + return ( + ( + <> + onChange(!value)} + {...props} + /> + {error && {error.message}} + + )} + /> + ); +}; diff --git a/frontend/apps/mobile/src/components/FormDateTimeInput.tsx b/frontend/apps/mobile/src/components/FormDateTimeInput.tsx new file mode 100644 index 00000000..debc5ef7 --- /dev/null +++ b/frontend/apps/mobile/src/components/FormDateTimeInput.tsx @@ -0,0 +1,30 @@ +import * as React from "react"; +import { Control, Controller } from "react-hook-form"; +import { HelperText } from "react-native-paper"; +import DateTimeInput, { DateTimeInputProps } from "./DateTimeInput"; +import { fromISOStringNullable, toISODateStringNullable } from "@abrechnung/utils"; + +export type FormDateTimeInputProps = Omit & { + name: string; + control: Control; +}; + +export const FormDateTimeInput: React.FC = ({ name, control, ...props }) => { + return ( + ( + <> + onChange(toISODateStringNullable(val))} + error={!!error} + {...props} + /> + {error && {error.message}} + + )} + /> + ); +}; diff --git a/frontend/apps/mobile/src/components/FormNumericInput.tsx b/frontend/apps/mobile/src/components/FormNumericInput.tsx new file mode 100644 index 00000000..23b7c065 --- /dev/null +++ b/frontend/apps/mobile/src/components/FormNumericInput.tsx @@ -0,0 +1,24 @@ +import * as React from "react"; +import { Control, Controller } from "react-hook-form"; +import { HelperText } from "react-native-paper"; +import { NumericInput, NumericInputProps } from "./NumericInput"; + +export type FormNumericInput = Omit & { + name: string; + control: Control; +}; + +export const FormNumericInput: React.FC = ({ name, control, ...props }) => { + return ( + ( + <> + + {error && {error.message}} + + )} + /> + ); +}; diff --git a/frontend/apps/mobile/src/components/FormTagSelect.tsx b/frontend/apps/mobile/src/components/FormTagSelect.tsx new file mode 100644 index 00000000..6a4c165d --- /dev/null +++ b/frontend/apps/mobile/src/components/FormTagSelect.tsx @@ -0,0 +1,24 @@ +import * as React from "react"; +import { Control, Controller } from "react-hook-form"; +import { HelperText } from "react-native-paper"; +import { TagSelect, TagSelectProps } from "./tag-select"; + +export type FormTagSelect = Omit & { + name: string; + control: Control; +}; + +export const FormTagSelect: React.FC = ({ name, control, ...props }) => { + return ( + ( + <> + + {error && {error.message}} + + )} + /> + ); +}; diff --git a/frontend/apps/mobile/src/components/FormTextInput.tsx b/frontend/apps/mobile/src/components/FormTextInput.tsx new file mode 100644 index 00000000..cf97c5c7 --- /dev/null +++ b/frontend/apps/mobile/src/components/FormTextInput.tsx @@ -0,0 +1,23 @@ +import * as React from "react"; +import { Control, Controller } from "react-hook-form"; +import { HelperText, TextInput, TextInputProps } from "react-native-paper"; + +export type FormTextInputProps = Omit & { + name: string; + control: Control; +}; + +export const FormTextInput: React.FC = ({ name, control, ...props }) => { + return ( + ( + <> + + {error && {error.message}} + + )} + /> + ); +}; diff --git a/frontend/apps/mobile/src/components/FormTransactionShareInput.tsx b/frontend/apps/mobile/src/components/FormTransactionShareInput.tsx new file mode 100644 index 00000000..bdbde1f0 --- /dev/null +++ b/frontend/apps/mobile/src/components/FormTransactionShareInput.tsx @@ -0,0 +1,24 @@ +import * as React from "react"; +import { Control, Controller } from "react-hook-form"; +import { HelperText } from "react-native-paper"; +import { TransactionShareInput, TransactionShareInputProps } from "./transaction-shares/TransactionShareInput"; + +export type FormTransactionShareInputProps = Omit & { + name: string; + control: Control; +}; + +export const FormTransactionShareInput: React.FC = ({ name, control, ...props }) => { + return ( + ( + <> + + {error && {error.message}} + + )} + /> + ); +}; diff --git a/frontend/apps/mobile/src/components/NumericInput.tsx b/frontend/apps/mobile/src/components/NumericInput.tsx index 7acca46e..0477ef54 100644 --- a/frontend/apps/mobile/src/components/NumericInput.tsx +++ b/frontend/apps/mobile/src/components/NumericInput.tsx @@ -2,12 +2,12 @@ import React from "react"; import { TextInput } from "react-native-paper"; import { parseAbrechnungFloat } from "@abrechnung/utils"; -type Props = Omit, "onChange" | "value"> & { +export type NumericInputProps = Omit, "onChange" | "value"> & { value: number; onChange: (newValue: number) => void; }; -export const NumericInput: React.FC = ({ value, onChange, ...props }) => { +export const NumericInput: React.FC = ({ value, onChange, ...props }) => { const [internalValue, setInternalValue] = React.useState(""); const { editable } = props; diff --git a/frontend/apps/mobile/src/components/index.ts b/frontend/apps/mobile/src/components/index.ts index f41c4493..b0d1cc01 100644 --- a/frontend/apps/mobile/src/components/index.ts +++ b/frontend/apps/mobile/src/components/index.ts @@ -3,3 +3,9 @@ export * from "./NumericInput"; export * from "./DateTimeInput"; export * from "./CurrencySelect"; export * from "./tag-select"; +export * from "./FormTextInput"; +export * from "./FormCheckbox"; +export * from "./FormTransactionShareInput"; +export * from "./FormTagSelect"; +export * from "./FormNumericInput"; +export * from "./FormDateTimeInput"; diff --git a/frontend/apps/mobile/src/components/tag-select/TagSelect.tsx b/frontend/apps/mobile/src/components/tag-select/TagSelect.tsx index 1b1a1264..c43350e4 100644 --- a/frontend/apps/mobile/src/components/tag-select/TagSelect.tsx +++ b/frontend/apps/mobile/src/components/tag-select/TagSelect.tsx @@ -3,7 +3,7 @@ import { TouchableHighlight, View } from "react-native"; import { Portal, Text, useTheme } from "react-native-paper"; import { TagSelectDialog } from "./TagSelectDialog"; -interface Props { +export interface TagSelectProps { groupId: number; label: string; value: string[]; @@ -11,7 +11,7 @@ interface Props { onChange: (newValue: string[]) => void; } -export const TagSelect: React.FC = ({ groupId, label, value, onChange, disabled }) => { +export const TagSelect: React.FC = ({ groupId, label, value, onChange, disabled }) => { const theme = useTheme(); const [showDialog, setShowDialog] = useState(false); diff --git a/frontend/apps/mobile/src/components/transaction-shares/TransactionShareInput.tsx b/frontend/apps/mobile/src/components/transaction-shares/TransactionShareInput.tsx index a3cf0599..6a7083f3 100644 --- a/frontend/apps/mobile/src/components/transaction-shares/TransactionShareInput.tsx +++ b/frontend/apps/mobile/src/components/transaction-shares/TransactionShareInput.tsx @@ -7,7 +7,7 @@ import { TransactionShare } from "@abrechnung/types"; import { useAppSelector } from "../../store"; import { selectGroupAccounts } from "@abrechnung/redux"; -interface Props { +export interface TransactionShareInputProps { groupId: number; title: string; multiSelect: boolean; @@ -19,7 +19,7 @@ interface Props { excludedAccounts?: number[]; } -export const TransactionShareInput: React.FC = ({ +export const TransactionShareInput: React.FC = ({ groupId, title, multiSelect, @@ -103,5 +103,3 @@ export const TransactionShareInput: React.FC = ({ ); }; - -export default TransactionShareInput; diff --git a/frontend/apps/mobile/src/screens/AddGroup.tsx b/frontend/apps/mobile/src/screens/AddGroup.tsx index 72e9c55c..6aedd9aa 100644 --- a/frontend/apps/mobile/src/screens/AddGroup.tsx +++ b/frontend/apps/mobile/src/screens/AddGroup.tsx @@ -1,47 +1,57 @@ import { components } from "@abrechnung/api"; import { createGroup } from "@abrechnung/redux"; -import { toFormikValidationSchema } from "@abrechnung/utils"; -import { useFormik } from "formik"; import React from "react"; import { StyleSheet, View } from "react-native"; -import { Button, Checkbox, HelperText, ProgressBar, TextInput, useTheme } from "react-native-paper"; +import { Button, HelperText, useTheme } from "react-native-paper"; import { CurrencySelect } from "../components/CurrencySelect"; import { useApi } from "../core/ApiProvider"; import { GroupStackScreenProps } from "../navigation/types"; import { useAppDispatch } from "../store"; +import { z } from "zod"; +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { FormCheckbox, FormTextInput } from "../components"; +import { notify } from "../notifications"; + +type FormSchema = z.infer; export const AddGroup: React.FC> = ({ navigation }) => { const theme = useTheme(); const dispatch = useAppDispatch(); const { api } = useApi(); - const formik = useFormik({ - initialValues: { + const { + control, + handleSubmit, + reset: resetForm, + } = useForm({ + resolver: zodResolver(components.schemas.GroupPayload), + defaultValues: { name: "", description: "", currency_symbol: "€", terms: "", add_user_account_on_join: false, }, - validationSchema: toFormikValidationSchema(components.schemas.GroupPayload), - onSubmit: (values, { setSubmitting }) => { - setSubmitting(true); + }); + const onSubmit = React.useCallback( + (values: FormSchema) => { dispatch(createGroup({ api, group: values })) .unwrap() .then(() => { - setSubmitting(false); navigation.goBack(); }) .catch(() => { - setSubmitting(false); + notify({ text: "An error occured during group creation" }); }); }, - }); + [dispatch, navigation, api] + ); const cancel = React.useCallback(() => { - formik.resetForm(); + resetForm(); navigation.goBack(); - }, [formik, navigation]); + }, [resetForm, navigation]); React.useLayoutEffect(() => { navigation.setOptions({ @@ -52,63 +62,33 @@ export const AddGroup: React.FC> = ({ navigati - + ); }, } as any); - }, [theme, navigation, formik, cancel]); + }, [theme, navigation, handleSubmit, onSubmit, cancel]); return ( - {formik.isSubmitting ? : null} - formik.setFieldValue("name", val)} - error={formik.touched.name && !!formik.errors.name} - /> - {formik.touched.name && !!formik.errors.name ? ( - {formik.errors.name} - ) : null} - formik.setFieldValue("description", val)} - error={formik.touched.description && !!formik.errors.description} - /> - {formik.touched.description && !!formik.errors.description ? ( - {formik.errors.description} - ) : null} - formik.setFieldValue("terms", val)} - error={formik.touched.terms && !!formik.errors.terms} - /> - {formik.touched.terms && !!formik.errors.terms ? ( - {formik.errors.terms} - ) : null} - formik.setFieldValue("currency_symbol", val)} - // error={formik.touched.description && !!formik.errors.currency_symbol} + + + + ( + <> + + {error && {error.message}} + + )} /> - {formik.touched.currency_symbol && !!formik.errors.description ? ( - {formik.errors.currency_symbol} - ) : null} - - formik.setFieldValue("add_user_account_on_join", !formik.values.add_user_account_on_join) - } /> ); diff --git a/frontend/apps/mobile/src/screens/Login.tsx b/frontend/apps/mobile/src/screens/Login.tsx index b3029dca..aa5699c7 100644 --- a/frontend/apps/mobile/src/screens/Login.tsx +++ b/frontend/apps/mobile/src/screens/Login.tsx @@ -1,10 +1,8 @@ import { login } from "@abrechnung/redux"; -import { toFormikValidationSchema } from "@abrechnung/utils"; import { SerializedError } from "@reduxjs/toolkit"; -import { Formik, FormikHelpers } from "formik"; import React, { useState } from "react"; import { StyleSheet, TouchableOpacity, View } from "react-native"; -import { Appbar, Button, HelperText, ProgressBar, Text, TextInput, useTheme } from "react-native-paper"; +import { Appbar, Button, Text, TextInput, useTheme } from "react-native-paper"; import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons"; import { z } from "zod"; import { useInitApi } from "../core/ApiProvider"; @@ -13,6 +11,9 @@ import { notify } from "../notifications"; import { useAppDispatch } from "../store"; import { useTranslation } from "react-i18next"; import LogoSvg from "../assets/logo.svg"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { FormTextInput } from "../components"; const validationSchema = z.object({ server: z.string({ required_error: "server is required" }).url({ message: "invalid server url" }), @@ -39,7 +40,12 @@ export const LoginScreen: React.FC> = ({ navigati setShowPassword((oldVal) => !oldVal); }; - const handleSubmit = (values: FormSchema, { setSubmitting }: FormikHelpers) => { + const { control, handleSubmit } = useForm({ + resolver: zodResolver(validationSchema), + defaultValues: initialValues, + }); + + const onSubmit = (values: FormSchema) => { const { api } = initApi(values.server); dispatch( login({ @@ -50,15 +56,11 @@ export const LoginScreen: React.FC> = ({ navigati }) ) .unwrap() - .then(() => { - setSubmitting(false); - }) .catch((err: SerializedError) => { console.log("error on login", err); if (err.message) { notify({ text: err.message }); } - setSubmitting(false); }); }; @@ -70,84 +72,62 @@ export const LoginScreen: React.FC> = ({ navigati - - {({ values, touched, handleSubmit, handleBlur, isSubmitting, errors, setFieldValue }) => ( - - handleBlur("server")} - onChangeText={(val) => setFieldValue("server", val)} - error={touched.server && !!errors.server} - /> - {touched.server && !!errors.server && {errors.server}} + + - handleBlur("username")} - onChangeText={(val) => setFieldValue("username", val)} - error={touched.username && !!errors.username} - /> - {touched.username && !!errors.username && ( - {errors.username} - )} + - handleBlur("password")} - onChangeText={(val) => setFieldValue("password", val)} - error={touched.password && !!errors.password} - secureTextEntry={!showPassword} - right={ - ( - - )} + ( + - } + )} /> - {touched.password && !!errors.password && ( - {errors.password} - )} + } + /> - {isSubmitting ? : null} - + - - Don’t have an account? - navigation.navigate("Register")}> - Sign up - - - - )} - + + Don’t have an account? + navigation.navigate("Register")}> + Sign up + + + ); }; diff --git a/frontend/apps/mobile/src/screens/Register.tsx b/frontend/apps/mobile/src/screens/Register.tsx index e9e5a11b..99297f20 100644 --- a/frontend/apps/mobile/src/screens/Register.tsx +++ b/frontend/apps/mobile/src/screens/Register.tsx @@ -1,9 +1,7 @@ import { selectIsAuthenticated } from "@abrechnung/redux"; -import { toFormikValidationSchema } from "@abrechnung/utils"; -import { Formik, FormikHelpers } from "formik"; import React from "react"; import { StyleSheet, TouchableOpacity, View } from "react-native"; -import { Appbar, Button, HelperText, ProgressBar, Text, TextInput, useTheme } from "react-native-paper"; +import { Appbar, Button, Text, useTheme } from "react-native-paper"; import { z } from "zod"; import { useInitApi } from "../core/ApiProvider"; import { RootDrawerScreenProps } from "../navigation/types"; @@ -11,6 +9,9 @@ import { notify } from "../notifications"; import { useAppSelector } from "../store"; import { useTranslation } from "react-i18next"; import { ApiError } from "@abrechnung/api"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { FormTextInput } from "../components"; const validationSchema = z .object({ @@ -46,18 +47,21 @@ export const RegisterScreen: React.FC> = ({ na } }, [loggedIn, navigation]); - const handleSubmit = (values: FormSchema, { setSubmitting }: FormikHelpers) => { + const { control, handleSubmit } = useForm({ + resolver: zodResolver(validationSchema), + defaultValues: initialValues, + }); + + const onSubmit = (values: FormSchema) => { const { api: newApi } = initApi(values.server); newApi.client.auth .register({ requestBody: { username: values.username, email: values.email, password: values.password } }) .then(() => { notify({ text: `Registered successfully, please confirm your email before logging in...` }); - setSubmitting(false); navigation.navigate("Login"); }) .catch((err: ApiError) => { notify({ text: `${err.body.msg}` }); - setSubmitting(false); }); }; @@ -66,102 +70,72 @@ export const RegisterScreen: React.FC> = ({ na - - {({ values, handleBlur, setFieldValue, touched, handleSubmit, isSubmitting, errors }) => ( - - handleBlur("server")} - onChangeText={(val) => setFieldValue("server", val)} - error={touched.server && !!errors.server} - /> - {touched.server && !!errors.server && {errors.server}} + + - handleBlur("username")} - onChangeText={(val) => setFieldValue("username", val)} - value={values.username} - /> - {touched.username && !!errors.username && ( - {errors.username} - )} + - handleBlur("email")} - onChangeText={(val) => setFieldValue("email", val)} - value={values.email} - /> - {touched.email && !!errors.email && {errors.email}} + - handleBlur("password")} - onChangeText={(val) => setFieldValue("password", val)} - value={values.password} - /> - {touched.password && !!errors.password && ( - {errors.password} - )} + - handleBlur("password2")} - onChangeText={(val) => setFieldValue("password2", val)} - value={values.password2} - /> - {touched.password2 && !!errors.password2 && ( - {errors.password2} - )} + - {isSubmitting ? : null} - + - - Already have an account? - navigation.navigate("Login")}> - Sign in - - - - )} - + + Already have an account? + navigation.navigate("Login")}> + Sign in + + + ); }; diff --git a/frontend/apps/mobile/src/screens/groups/AccountDetail.tsx b/frontend/apps/mobile/src/screens/groups/AccountDetail.tsx index 7cea11a9..22426f72 100644 --- a/frontend/apps/mobile/src/screens/groups/AccountDetail.tsx +++ b/frontend/apps/mobile/src/screens/groups/AccountDetail.tsx @@ -23,7 +23,7 @@ import { Text, useTheme, } from "react-native-paper"; -import TransactionShareInput from "../../components/transaction-shares/TransactionShareInput"; +import { TransactionShareInput } from "../../components/transaction-shares/TransactionShareInput"; import { clearingAccountIcon, getTransactionIcon } from "../../constants/Icons"; import { useApi } from "../../core/ApiProvider"; import { GroupStackScreenProps } from "../../navigation/types"; diff --git a/frontend/apps/mobile/src/screens/groups/AccountEdit.tsx b/frontend/apps/mobile/src/screens/groups/AccountEdit.tsx index c182379f..f418af2c 100644 --- a/frontend/apps/mobile/src/screens/groups/AccountEdit.tsx +++ b/frontend/apps/mobile/src/screens/groups/AccountEdit.tsx @@ -7,18 +7,26 @@ import { wipAccountUpdated, } from "@abrechnung/redux"; import { AccountValidator } from "@abrechnung/types"; -import { fromISOStringNullable, toFormikValidationSchema, toISODateStringNullable } from "@abrechnung/utils"; import { useFocusEffect } from "@react-navigation/native"; -import { useFormik } from "formik"; import React, { useEffect, useLayoutEffect } from "react"; import { BackHandler, ScrollView, StyleSheet } from "react-native"; -import { Button, Dialog, HelperText, IconButton, Portal, ProgressBar, TextInput, useTheme } from "react-native-paper"; -import { TransactionShareInput } from "../../components/transaction-shares/TransactionShareInput"; +import { Button, Dialog, IconButton, Portal, useTheme } from "react-native-paper"; import { useApi } from "../../core/ApiProvider"; import { GroupStackScreenProps } from "../../navigation/types"; import { notify } from "../../notifications"; import { useAppDispatch } from "../../store"; -import { LoadingIndicator, TagSelect, DateTimeInput } from "../../components"; +import { + LoadingIndicator, + FormTextInput, + FormTransactionShareInput, + FormTagSelect, + FormDateTimeInput, +} from "../../components"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; + +type FormSchema = z.infer; export const AccountEdit: React.FC> = ({ route, navigation }) => { const theme = useTheme(); @@ -73,8 +81,15 @@ export const AccountEdit: React.FC> = ({ ro } }, [navigation, accountId, isGroupWritable, groupId]); - const formik = useFormik({ - initialValues: + const { + control, + handleSubmit, + reset: resetForm, + watch, + getValues, + } = useForm({ + resolver: zodResolver(AccountValidator), + defaultValues: account === undefined ? {} : account.type === "clearing" @@ -92,44 +107,38 @@ export const AccountEdit: React.FC> = ({ ro description: account.description, owning_user_id: account.owning_user_id, }, - validationSchema: toFormikValidationSchema(AccountValidator), - onSubmit: (values, { setSubmitting }) => { + }); + + const onSubmit = React.useCallback( + (values: FormSchema) => { if (!account) { return; } - setSubmitting(true); dispatch(wipAccountUpdated({ ...account, ...values })); dispatch(saveAccount({ groupId: groupId, accountId: account.id, api })) .unwrap() .then(({ account }) => { - setSubmitting(false); navigation.replace("AccountDetail", { accountId: account.id, groupId: groupId, }); }) .catch(() => { - setSubmitting(false); + notify({ text: "Error saving account" }); }); }, - enableReinitialize: true, - }); - - const onUpdate = React.useCallback(() => { - if (account) { - dispatch(wipAccountUpdated({ ...account, ...formik.values })); - } - }, [dispatch, account, formik]); + [dispatch, account, navigation, groupId, api] + ); - const updateWipAccount = React.useCallback( - (values: Partial) => { + React.useEffect(() => { + const { unsubscribe } = watch(() => { if (account) { - dispatch(wipAccountUpdated({ ...account, ...values })); + dispatch(wipAccountUpdated({ ...account, ...getValues() })); } - }, - [dispatch, account, formik] - ); + }); + return unsubscribe; + }, [dispatch, account, getValues, watch]); const cancelEdit = React.useCallback(() => { if (!account) { @@ -137,27 +146,27 @@ export const AccountEdit: React.FC> = ({ ro } dispatch(discardAccountChange({ groupId, accountId: account.id })); - formik.resetForm(); + resetForm(); navigation.pop(); - }, [dispatch, groupId, account, navigation, formik]); + }, [dispatch, groupId, account, navigation, resetForm]); useLayoutEffect(() => { navigation.setOptions({ onGoBack: onGoBack, - headerTitle: formik.values?.name ?? account?.name ?? "", + headerTitle: account?.name ?? "", headerRight: () => { return ( <> - + ); }, } as any); - }, [theme, account, navigation, formik, cancelEdit, onGoBack]); + }, [theme, account, navigation, handleSubmit, onSubmit, cancelEdit, onGoBack]); if (account == null) { return ; @@ -165,76 +174,28 @@ export const AccountEdit: React.FC> = ({ ro return ( - {formik.isSubmitting ? : null} - formik.setFieldValue("name", val)} - onBlur={onUpdate} - error={formik.touched.name && !!formik.errors.name} - /> - {formik.touched.name && !!formik.errors.name ? ( - {formik.errors.name} - ) : null} - formik.setFieldValue("description", val)} - error={formik.touched.description && !!formik.errors.description} - /> - {formik.touched.description && !!formik.errors.description ? ( - {formik.errors.description} - ) : null} - {account.type === "personal" && formik.touched.owning_user_id && !!formik.errors.owning_user_id && ( - {formik.errors.owning_user_id} - )} - {formik.values.type === "clearing" && ( + + + {account.type === "clearing" && ( <> - formik.setFieldValue("dateInfo", toISODateStringNullable(val))} - onBlur={onUpdate} - error={formik.touched.date_info && !!formik.errors.date_info} - /> - {formik.touched.date_info && !!formik.errors.date_info && ( - {formik.errors.date_info} - )} - { - updateWipAccount({ tags: val }); - }} /> - {formik.touched.tags && !!formik.errors.tags && ( - {formik.errors.tags} - )} - + { - updateWipAccount({ clearing_shares: newValue }); - }} + disabled={false} + excludedAccounts={[account.id]} enableAdvanced={true} multiSelect={true} - excludedAccounts={[account.id]} - error={formik.touched.clearing_shares && !!formik.errors.clearing_shares} /> - {formik.touched.clearing_shares && !!formik.errors.clearing_shares && ( - {formik.errors.clearing_shares as string} - )} )} diff --git a/frontend/apps/mobile/src/screens/groups/TransactionDetail.tsx b/frontend/apps/mobile/src/screens/groups/TransactionDetail.tsx index f32848a8..9395ea9e 100644 --- a/frontend/apps/mobile/src/screens/groups/TransactionDetail.tsx +++ b/frontend/apps/mobile/src/screens/groups/TransactionDetail.tsx @@ -9,9 +9,7 @@ import { wipTransactionUpdated, } from "@abrechnung/redux"; import { TransactionPosition, TransactionValidator } from "@abrechnung/types"; -import { fromISOStringNullable, toFormikValidationSchema, toISODateString } from "@abrechnung/utils"; import { useFocusEffect } from "@react-navigation/native"; -import { useFormik } from "formik"; import * as React from "react"; import { useEffect, useLayoutEffect } from "react"; import { BackHandler, ScrollView, StyleSheet, View } from "react-native"; @@ -20,24 +18,33 @@ import { Chip, Dialog, Divider, - HelperText, IconButton, List, Portal, - ProgressBar, Surface, Text, TextInput, useTheme, } from "react-native-paper"; -import { DateTimeInput, LoadingIndicator, NumericInput, TagSelect } from "../../components"; +import { + FormDateTimeInput, + FormNumericInput, + FormTagSelect, + FormTextInput, + FormTransactionShareInput, + LoadingIndicator, +} from "../../components"; import { PositionListItem } from "../../components/PositionListItem"; -import { TransactionShareInput } from "../../components/transaction-shares/TransactionShareInput"; import { useApi } from "../../core/ApiProvider"; import { GroupStackScreenProps } from "../../navigation/types"; import { notify } from "../../notifications"; import { useAppDispatch, useAppSelector } from "../../store"; import { SerializedError } from "@reduxjs/toolkit"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +type FormSchema = z.infer; export const TransactionDetail: React.FC> = ({ route, navigation }) => { const theme = useTheme(); @@ -98,8 +105,15 @@ export const TransactionDetail: React.FC({ + resolver: zodResolver(TransactionValidator), + defaultValues: transaction === undefined ? {} : { @@ -114,13 +128,16 @@ export const TransactionDetail: React.FC { + }); + const value = watch("value"); + const currencySymbol = watch("currency_symbol"); + + const onSubmit = React.useCallback( + (values: FormSchema) => { if (!transaction) { return; } - setSubmitting(true); dispatch(wipTransactionUpdated({ ...transaction, ...values })); dispatch(saveTransaction({ api, transactionId, groupId })) .unwrap() @@ -130,21 +147,22 @@ export const TransactionDetail: React.FC { notify({ text: `Error while saving transaction: ${e.message}` }); - setSubmitting(false); }); }, - enableReinitialize: true, - }); + [dispatch, groupId, navigation, transaction, api, transactionId] + ); - const onUpdate = React.useCallback(() => { - if (transaction) { - dispatch(wipTransactionUpdated({ ...transaction, ...formik.values })); - } - }, [dispatch, transaction, formik]); + React.useEffect(() => { + const { unsubscribe } = watch(() => { + if (transaction) { + dispatch(wipTransactionUpdated({ ...transaction, ...getValues() })); + } + }); + return unsubscribe; + }, [dispatch, transaction, getValues, watch]); const edit = React.useCallback(() => { navigation.navigate("TransactionDetail", { @@ -156,6 +174,7 @@ export const TransactionDetail: React.FC { dispatch(discardTransactionChange({ transactionId, groupId })); + resetForm(); if (transactionId < 0) { navigation.navigate("BottomTabNavigator", { screen: "TransactionList", @@ -168,12 +187,12 @@ export const TransactionDetail: React.FC { navigation.setOptions({ onGoBack: onGoBack, - headerTitle: formik.values?.name ?? transaction?.name ?? "", + headerTitle: transaction?.name ?? "", headerRight: () => { if (!isGroupWritable) { return null; @@ -184,7 +203,7 @@ export const TransactionDetail: React.FC Cancel - + ); @@ -202,7 +221,8 @@ export const TransactionDetail: React.FC - {formik.isSubmitting && } - formik.setFieldValue("name", val)} - onBlur={onUpdate} - style={inputStyles} - error={formik.touched.name && !!formik.errors.name} - /> - {formik.touched.name && !!formik.errors.name && {formik.errors.name}} - + formik.setFieldValue("description", val)} - onBlur={onUpdate} style={inputStyles} - error={formik.touched.description && !!formik.errors.description} + name="description" + control={control} /> - {formik.touched.description && !!formik.errors.description && ( - {formik.errors.description} - )} - { - formik.setFieldValue("billed_at", toISODateString(val)); - onUpdate(); - }} - error={formik.touched.billed_at && !!formik.errors.billed_at} + name="billed_at" + control={control} /> - {formik.touched.billed_at && !!formik.errors.billed_at && ( - {formik.errors.billed_at} - )} - formik.setFieldValue("value", val)} - onBlur={onUpdate} style={inputStyles} - right={} - error={formik.touched.value && !!formik.errors.value} + right={} /> - {formik.touched.value && !!formik.errors.value && ( - {formik.errors.value} - )} {editing ? ( - <> - { - formik.setFieldValue("tags", val); - onUpdate(); - }} - /> - {formik.touched.tags && !!formik.errors.tags && ( - {formik.errors.tags} - )} - + ) : ( )} - formik.setFieldValue("creditor_shares", val)} enableAdvanced={false} multiSelect={false} - error={formik.touched.creditor_shares && !!formik.errors.creditor_shares} /> - {formik.touched.creditor_shares && !!formik.errors.creditor_shares && ( - {formik.errors.creditor_shares as string} - )} - formik.setFieldValue("debitor_shares", val)} enableAdvanced={transaction.type === "purchase"} multiSelect={transaction.type === "purchase"} - error={formik.touched.debitor_shares && !!formik.errors.debitor_shares} /> - {formik.touched.debitor_shares && !!formik.errors.debitor_shares && ( - {formik.errors.debitor_shares as string} - )} {transaction.type === "purchase" && !showPositions && editing && !hasPositions ? (