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 ce971cf8..6aedd9aa 100644 --- a/frontend/apps/mobile/src/screens/AddGroup.tsx +++ b/frontend/apps/mobile/src/screens/AddGroup.tsx @@ -1,48 +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 { StackNavigationOptions } from "@react-navigation/stack"; +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({ @@ -53,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 ? ( - - - - )} - + + + + + + ); 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 85% rename from frontend/apps/web/src/components/FormTextField.tsx rename to frontend/libs/components/src/lib/FormTextField.tsx index 3270813d..c8ddff9f 100644 --- a/frontend/apps/web/src/components/FormTextField.tsx +++ b/frontend/libs/components/src/lib/FormTextField.tsx @@ -12,10 +12,11 @@ export const FormTextField = ({ name, control, ...props }: FormTextFieldProps) = ( + render={({ field: { onChange, onBlur, value }, fieldState: { error } }) => ( Object.keys(shares).length !== 1, "somebody has payed for this"), + debitor_shares: z.record(z.number()).refine((shares) => Object.keys(shares).length > 0, "select at least one"), + }) + .merge(BaseTransactionValidator) + .passthrough(); + +export const MimoValidator = z .object({ creditor_shares: z .record(z.number()) @@ -49,6 +60,7 @@ export const TransferValidator = z export const TransactionValidator = z.discriminatedUnion("type", [ PurchaseValidator.merge(z.object({ type: z.literal("purchase") })), TransferValidator.merge(z.object({ type: z.literal("transfer") })), + MimoValidator.merge(z.object({ type: z.literal("mimo") })), ]); export const PositionValidator = z diff --git a/frontend/libs/utils/src/lib/validators.ts b/frontend/libs/utils/src/lib/validators.ts index d08fb674..c9934f21 100644 --- a/frontend/libs/utils/src/lib/validators.ts +++ b/frontend/libs/utils/src/lib/validators.ts @@ -42,49 +42,3 @@ export class ValidationError extends Error { super(message); } } - -function createValidationError(e: z.ZodError) { - const error = new ValidationError(e.message); - error.inner = e.errors.map((err) => ({ - message: err.message, - path: err.path.join("."), - })); - - return error; -} - -function createValidationErrorMap(e: z.ZodError) { - return e.errors.map((err) => err.message); -} - -/** - * Wrap your zod schema in this function when providing it to Formik's validation schema prop - * @param schema The zod schema - * @returns An object containing the `validate` method expected by Formik - */ -export function toFormikValidationSchema( - schema: z.ZodSchema, - params?: Partial -): { validate: (obj: T) => Promise } { - return { - async validate(obj: T) { - try { - await schema.parseAsync(obj, params); - } catch (err: unknown) { - throw createValidationError(err as z.ZodError); - } - }, - }; -} - -export function toFormikValidate(schema: z.ZodSchema): (obj: T) => Promise> { - return async (obj: T) => { - try { - await schema.parseAsync(obj); - return {}; - } catch (err: unknown) { - console.log("validation error", err, obj); - return createValidationErrorMap(err as z.ZodError); - } - }; -} 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..a8083c0a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -37,7 +37,6 @@ "cookie-parser": "^1.4.6", "core-js": "^3.38.0", "deepmerge": "^4.3.1", - "formik": "^2.4.6", "i18next": "^23.12.2", "i18next-browser-languagedetector": "^8.0.0", "localforage": "^1.10.0", @@ -4326,23 +4325,26 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.6.5", - "license": "MIT", + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", + "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", "dependencies": { - "@floating-ui/utils": "^0.2.5" + "@floating-ui/utils": "^0.2.8" } }, "node_modules/@floating-ui/dom": { - "version": "1.6.8", - "license": "MIT", + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.11.tgz", + "integrity": "sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ==", "dependencies": { "@floating-ui/core": "^1.6.0", - "@floating-ui/utils": "^0.2.5" + "@floating-ui/utils": "^0.2.8" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.1", - "license": "MIT", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", "dependencies": { "@floating-ui/dom": "^1.0.0" }, @@ -4352,8 +4354,9 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.5", - "license": "MIT" + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", + "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" }, "node_modules/@hapi/hoek": { "version": "9.3.0", @@ -5578,7 +5581,8 @@ }, "node_modules/@mui/base": { "version": "5.0.0-beta.40", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", + "integrity": "sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==", "dependencies": { "@babel/runtime": "^7.23.9", "@floating-ui/react-dom": "^2.0.8", @@ -6000,6 +6004,21 @@ "react": ">= 16.14.0 < 19.0.0" } }, + "node_modules/@nivo/annotations/node_modules/@react-spring/web": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.4.tgz", + "integrity": "sha512-UMvCZp7I5HCVIleSa4BwbNxynqvj+mJjG2m20VO2yPoi2pnCYANy58flvz9v/YcXTAvsmL655FV3pm5fbr6akA==", + "dependencies": { + "@react-spring/animated": "~9.7.4", + "@react-spring/core": "~9.7.4", + "@react-spring/shared": "~9.7.4", + "@react-spring/types": "~9.7.4" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@nivo/axes": { "version": "0.87.0", "resolved": "https://registry.npmjs.org/@nivo/axes/-/axes-0.87.0.tgz", @@ -6018,6 +6037,21 @@ "react": ">= 16.14.0 < 19.0.0" } }, + "node_modules/@nivo/axes/node_modules/@react-spring/web": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.4.tgz", + "integrity": "sha512-UMvCZp7I5HCVIleSa4BwbNxynqvj+mJjG2m20VO2yPoi2pnCYANy58flvz9v/YcXTAvsmL655FV3pm5fbr6akA==", + "dependencies": { + "@react-spring/animated": "~9.7.4", + "@react-spring/core": "~9.7.4", + "@react-spring/shared": "~9.7.4", + "@react-spring/types": "~9.7.4" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@nivo/colors": { "version": "0.87.0", "resolved": "https://registry.npmjs.org/@nivo/colors/-/colors-0.87.0.tgz", @@ -6066,6 +6100,21 @@ "react": ">= 16.14.0 < 19.0.0" } }, + "node_modules/@nivo/core/node_modules/@react-spring/web": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.4.tgz", + "integrity": "sha512-UMvCZp7I5HCVIleSa4BwbNxynqvj+mJjG2m20VO2yPoi2pnCYANy58flvz9v/YcXTAvsmL655FV3pm5fbr6akA==", + "dependencies": { + "@react-spring/animated": "~9.7.4", + "@react-spring/core": "~9.7.4", + "@react-spring/shared": "~9.7.4", + "@react-spring/types": "~9.7.4" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@nivo/legends": { "version": "0.87.0", "resolved": "https://registry.npmjs.org/@nivo/legends/-/legends-0.87.0.tgz", @@ -6102,6 +6151,21 @@ "react": ">= 16.14.0 < 19.0.0" } }, + "node_modules/@nivo/line/node_modules/@react-spring/web": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.4.tgz", + "integrity": "sha512-UMvCZp7I5HCVIleSa4BwbNxynqvj+mJjG2m20VO2yPoi2pnCYANy58flvz9v/YcXTAvsmL655FV3pm5fbr6akA==", + "dependencies": { + "@react-spring/animated": "~9.7.4", + "@react-spring/core": "~9.7.4", + "@react-spring/shared": "~9.7.4", + "@react-spring/types": "~9.7.4" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@nivo/scales": { "version": "0.87.0", "resolved": "https://registry.npmjs.org/@nivo/scales/-/scales-0.87.0.tgz", @@ -6136,6 +6200,21 @@ "react": ">= 16.14.0 < 19.0.0" } }, + "node_modules/@nivo/tooltip/node_modules/@react-spring/web": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.4.tgz", + "integrity": "sha512-UMvCZp7I5HCVIleSa4BwbNxynqvj+mJjG2m20VO2yPoi2pnCYANy58flvz9v/YcXTAvsmL655FV3pm5fbr6akA==", + "dependencies": { + "@react-spring/animated": "~9.7.4", + "@react-spring/core": "~9.7.4", + "@react-spring/shared": "~9.7.4", + "@react-spring/types": "~9.7.4" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@nivo/voronoi": { "version": "0.87.0", "resolved": "https://registry.npmjs.org/@nivo/voronoi/-/voronoi-0.87.0.tgz", @@ -10302,7 +10381,8 @@ }, "node_modules/@react-spring/animated": { "version": "9.7.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.4.tgz", + "integrity": "sha512-7As+8Pty2QlemJ9O5ecsuPKjmO0NKvmVkRR1n6mEotFgWar8FKuQt2xgxz3RTgxcccghpx1YdS1FCdElQNexmQ==", "dependencies": { "@react-spring/shared": "~9.7.4", "@react-spring/types": "~9.7.4" @@ -10313,7 +10393,8 @@ }, "node_modules/@react-spring/core": { "version": "9.7.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.4.tgz", + "integrity": "sha512-GzjA44niEJBFUe9jN3zubRDDDP2E4tBlhNlSIkTChiNf9p4ZQlgXBg50qbXfSXHQPHak/ExYxwhipKVsQ/sUTw==", "dependencies": { "@react-spring/animated": "~9.7.4", "@react-spring/shared": "~9.7.4", @@ -10329,11 +10410,13 @@ }, "node_modules/@react-spring/rafz": { "version": "9.7.4", - "license": "MIT" + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.4.tgz", + "integrity": "sha512-mqDI6rW0Ca8IdryOMiXRhMtVGiEGLIO89vIOyFQXRIwwIMX30HLya24g9z4olDvFyeDW3+kibiKwtZnA4xhldA==" }, "node_modules/@react-spring/shared": { "version": "9.7.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.4.tgz", + "integrity": "sha512-bEPI7cQp94dOtCFSEYpxvLxj0+xQfB5r9Ru1h8OMycsIq7zFZon1G0sHrBLaLQIWeMCllc4tVDYRTLIRv70C8w==", "dependencies": { "@react-spring/rafz": "~9.7.4", "@react-spring/types": "~9.7.4" @@ -10344,21 +10427,8 @@ }, "node_modules/@react-spring/types": { "version": "9.7.4", - "license": "MIT" - }, - "node_modules/@react-spring/web": { - "version": "9.7.4", - "license": "MIT", - "dependencies": { - "@react-spring/animated": "~9.7.4", - "@react-spring/core": "~9.7.4", - "@react-spring/shared": "~9.7.4", - "@react-spring/types": "~9.7.4" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.4.tgz", + "integrity": "sha512-iQVztO09ZVfsletMiY+DpT/JRiBntdsdJ4uqk3UJFhrhS8mIC9ZOZbmfGSRs/kdbNPQkVyzucceDicQ/3Mlj9g==" }, "node_modules/@reduxjs/toolkit": { "version": "2.2.7", @@ -11240,7 +11310,6 @@ "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", @@ -11261,7 +11330,6 @@ "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" @@ -11278,7 +11346,6 @@ "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", @@ -11296,7 +11363,6 @@ "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" @@ -11310,7 +11376,6 @@ "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": { @@ -11318,7 +11383,6 @@ "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": { @@ -11326,7 +11390,6 @@ "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" @@ -11337,7 +11400,6 @@ "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", @@ -11353,7 +11415,6 @@ "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" @@ -11367,7 +11428,6 @@ "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": { @@ -11375,7 +11435,6 @@ "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" @@ -11702,7 +11761,6 @@ "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": { @@ -11961,14 +12019,6 @@ "version": "2.0.45", "license": "MIT" }, - "node_modules/@types/hoist-non-react-statics": { - "version": "3.3.5", - "license": "MIT", - "dependencies": { - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0" - } - }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "dev": true, @@ -12123,7 +12173,8 @@ }, "node_modules/@types/react": { "version": "18.2.61", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.61.tgz", + "integrity": "sha512-NURTN0qNnJa7O/k4XUkEW2yfygA+NxS0V5h1+kp9jPwhzZy95q3ADoGMP0+JypMhrZBTTgjKAUlTctde1zzeQA==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -12132,8 +12183,9 @@ }, "node_modules/@types/react-dom": { "version": "18.2.18", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.18.tgz", + "integrity": "sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==", "dev": true, - "license": "MIT", "dependencies": { "@types/react": "*" } @@ -12160,7 +12212,8 @@ }, "node_modules/@types/scheduler": { "version": "0.23.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==" }, "node_modules/@types/semver": { "version": "7.5.8", @@ -16263,7 +16316,8 @@ }, "node_modules/dom-helpers": { "version": "5.2.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" @@ -18196,18 +18250,19 @@ } }, "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==", + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-1.11.2.tgz", + "integrity": "sha512-fdcaNO8ucHA3yLNY52ZUENBcAG7KEx8QyMmnVNavO1JVBGRMZG8JyVcbrhYQDtVtpxkbai5YzwvLutINvbDZDQ==", "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" + "fs-extra": "^9.1.0", + "require-from-string": "^2.0.2", + "resolve-from": "^5.0.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" @@ -18218,7 +18273,6 @@ "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" @@ -18235,7 +18289,6 @@ "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", @@ -18253,7 +18306,6 @@ "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" @@ -18267,7 +18319,6 @@ "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": { @@ -18275,7 +18326,6 @@ "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" @@ -18286,7 +18336,6 @@ "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", @@ -18304,7 +18353,6 @@ "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", @@ -18321,7 +18369,6 @@ "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" @@ -18332,7 +18379,6 @@ "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" @@ -18349,7 +18395,6 @@ "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" @@ -18366,7 +18411,6 @@ "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" @@ -18515,7 +18559,8 @@ }, "node_modules/fast-equals": { "version": "5.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz", + "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==", "engines": { "node": ">=6.0.0" } @@ -19355,36 +19400,6 @@ "node": ">= 6" } }, - "node_modules/formik": { - "version": "2.4.6", - "funding": [ - { - "type": "individual", - "url": "https://opencollective.com/formik" - } - ], - "license": "Apache-2.0", - "dependencies": { - "@types/hoist-non-react-statics": "^3.3.1", - "deepmerge": "^2.1.1", - "hoist-non-react-statics": "^3.3.0", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", - "react-fast-compare": "^2.0.1", - "tiny-warning": "^1.0.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/formik/node_modules/deepmerge": { - "version": "2.2.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/forwarded": { "version": "0.2.0", "dev": true, @@ -23870,10 +23885,6 @@ "version": "4.17.21", "license": "MIT" }, - "node_modules/lodash-es": { - "version": "4.17.21", - "license": "MIT" - }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "dev": true, @@ -24158,7 +24169,6 @@ "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" @@ -27379,7 +27389,8 @@ }, "node_modules/react": { "version": "18.2.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", "dependencies": { "loose-envify": "^1.1.0" }, @@ -27397,7 +27408,8 @@ }, "node_modules/react-dom": { "version": "18.2.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -27406,10 +27418,6 @@ "react": "^18.2.0" } }, - "node_modules/react-fast-compare": { - "version": "2.0.4", - "license": "MIT" - }, "node_modules/react-freeze": { "version": "1.0.4", "license": "MIT", @@ -28199,7 +28207,8 @@ }, "node_modules/react-smooth": { "version": "4.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.1.tgz", + "integrity": "sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==", "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", @@ -28236,7 +28245,8 @@ }, "node_modules/react-transition-group": { "version": "4.4.5", - "license": "BSD-3-Clause", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", @@ -30473,10 +30483,6 @@ "version": "1.3.3", "license": "MIT" }, - "node_modules/tiny-warning": { - "version": "1.0.3", - "license": "MIT" - }, "node_modules/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index 507df8a0..f4b81850 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -37,7 +37,6 @@ "cookie-parser": "^1.4.6", "core-js": "^3.38.0", "deepmerge": "^4.3.1", - "formik": "^2.4.6", "i18next": "^23.12.2", "i18next-browser-languagedetector": "^8.0.0", "localforage": "^1.10.0", 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"],