From 10a816510d0c8d584b04c69a096f904ac00dc426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Wed, 4 Sep 2024 20:47:47 +0200 Subject: [PATCH] refactor: completely get rid of proxy-memoize --- .vscode/tasks.json | 13 ++ frontend/apps/mobile/project.json | 8 + .../mobile/src/@types/react-navigation.d.ts | 17 ++ frontend/apps/mobile/src/App.tsx | 17 +- .../mobile/src/components/GroupListItem.tsx | 6 +- .../mobile/src/components/PositionDialog.tsx | 28 +-- .../mobile/src/components/style/Searchbar.tsx | 13 +- .../components/tag-select/TagSelectDialog.tsx | 2 +- .../transaction-shares/ShareSelect.tsx | 4 +- .../TransactionShareDialog.tsx | 27 +-- .../TransactionShareInput.tsx | 4 +- .../mobile/src/navigation/DrawerContent.tsx | 21 +- .../apps/mobile/src/navigation/Header.tsx | 62 ++++-- .../src/navigation/LinkingConfiguration.ts | 4 +- .../apps/mobile/src/navigation/Navigation.tsx | 24 +-- frontend/apps/mobile/src/navigation/types.tsx | 27 +-- frontend/apps/mobile/src/screens/AddGroup.tsx | 3 +- .../apps/mobile/src/screens/GroupList.tsx | 4 +- .../mobile/src/screens/PreferencesScreen.tsx | 4 +- .../apps/mobile/src/screens/ProfileScreen.tsx | 4 +- frontend/apps/mobile/src/screens/Register.tsx | 6 +- .../TransactionList/TransactionList.tsx | 32 +-- .../TransactionList/TransactionListItem.tsx | 9 +- .../src/screens/groups/AccountDetail.tsx | 40 ++-- .../mobile/src/screens/groups/AccountEdit.tsx | 21 +- .../mobile/src/screens/groups/AccountList.tsx | 40 ++-- .../src/screens/groups/TransactionDetail.tsx | 31 ++- frontend/apps/mobile/src/store/index.ts | 1 - frontend/apps/mobile/src/store/selectors.ts | 8 - .../apps/mobile/src/store/settingsSlice.ts | 9 +- frontend/apps/mobile/src/store/uiSlice.ts | 13 +- frontend/apps/mobile/tsconfig.json | 1 - frontend/apps/web/src/app/app.tsx | 8 +- .../AuthenticatedLayout.tsx | 4 +- .../authenticated-layout/SidebarGroupList.tsx | 6 +- .../UnauthenticatedLayout.tsx | 4 +- .../apps/web/src/components/AccountSelect.tsx | 7 +- .../apps/web/src/components/RequireAuth.tsx | 4 +- .../apps/web/src/components/ShareSelect.tsx | 7 +- .../apps/web/src/components/TagSelector.tsx | 2 +- .../accounts/AccountClearingListEntry.tsx | 10 +- .../accounts/AccountTransactionList.tsx | 12 +- .../accounts/AccountTransactionListEntry.tsx | 12 +- .../accounts/BalanceHistoryGraph.tsx | 129 ++++++------ .../src/components/accounts/BalanceTable.tsx | 15 +- .../accounts/ClearingAccountDetail.tsx | 10 +- .../components/groups/GroupMemberSelect.tsx | 8 +- .../accounts/AccountDetail/AccountDetail.tsx | 9 +- .../accounts/AccountDetail/AccountInfo.tsx | 16 +- .../apps/web/src/pages/accounts/Balances.tsx | 21 +- .../ClearingAccountList.tsx | 22 +- .../ClearingAccountListItem.tsx | 12 +- .../PersonalAccountList.tsx | 26 +-- .../PersonalAccountListItem.tsx | 12 +- .../pages/accounts/SettlementPlanDisplay.tsx | 14 +- frontend/apps/web/src/pages/auth/Login.tsx | 4 +- frontend/apps/web/src/pages/auth/Logout.tsx | 4 +- frontend/apps/web/src/pages/auth/Register.tsx | 4 +- .../pages/auth/RequestPasswordRecovery.tsx | 4 +- frontend/apps/web/src/pages/groups/Group.tsx | 21 +- .../web/src/pages/groups/GroupInvites.tsx | 24 +-- .../apps/web/src/pages/groups/GroupList.tsx | 6 +- .../apps/web/src/pages/groups/GroupLog.tsx | 14 +- .../web/src/pages/groups/GroupMemberList.tsx | 16 +- .../web/src/pages/groups/GroupSettings.tsx | 26 +-- .../apps/web/src/pages/profile/Profile.tsx | 4 +- .../web/src/pages/profile/SessionList.tsx | 4 +- .../apps/web/src/pages/profile/Settings.tsx | 12 +- .../TransactionDetail/FileGallery.tsx | 6 +- .../TransactionDetail/TransactionActions.tsx | 7 +- .../TransactionDetail/TransactionDetail.tsx | 16 +- .../TransactionDetail/TransactionMetadata.tsx | 14 +- .../purchase/TransactionPositions.tsx | 20 +- .../TransactionList/TransactionList.tsx | 30 ++- .../TransactionList/TransactionListItem.tsx | 12 +- frontend/apps/web/src/store/index.ts | 1 - frontend/apps/web/src/store/selectors.ts | 7 - frontend/apps/web/src/store/settingsSlice.ts | 6 +- frontend/apps/web/tsconfig.app.json | 12 +- frontend/apps/web/tsconfig.json | 9 - frontend/libs/api/package.json | 5 - frontend/libs/core/package.json | 4 - frontend/libs/redux/package.json | 4 - .../redux/src/lib/accounts/accountSlice.ts | 188 +++++++++--------- frontend/libs/redux/src/lib/accounts/hooks.ts | 0 frontend/libs/redux/src/lib/auth/authSlice.ts | 41 ++-- .../libs/redux/src/lib/groups/groupSlice.ts | 153 ++++++-------- frontend/libs/redux/src/lib/selectors.ts | 140 ++++++------- .../src/lib/transactions/transactionSlice.ts | 173 +++++++--------- frontend/libs/types/package.json | 5 - frontend/libs/utils/package.json | 5 - frontend/package-lock.json | 37 ++-- frontend/package.json | 2 +- frontend/tsconfig.base.json | 8 + 94 files changed, 830 insertions(+), 1091 deletions(-) create mode 100644 .vscode/tasks.json create mode 100644 frontend/apps/mobile/src/@types/react-navigation.d.ts delete mode 100644 frontend/apps/mobile/src/store/selectors.ts delete mode 100644 frontend/apps/web/src/store/selectors.ts delete mode 100644 frontend/libs/api/package.json delete mode 100644 frontend/libs/core/package.json delete mode 100644 frontend/libs/redux/package.json create mode 100644 frontend/libs/redux/src/lib/accounts/hooks.ts delete mode 100644 frontend/libs/types/package.json delete mode 100644 frontend/libs/utils/package.json diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..a2907f61 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,13 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Abrechnung API", + "type": "shell", + "command": ".venv/bin/abrechnung -c config.yaml -vvv api", + "problemMatcher": [] + } + ] +} diff --git a/frontend/apps/mobile/project.json b/frontend/apps/mobile/project.json index 010792e2..3c9a43e3 100644 --- a/frontend/apps/mobile/project.json +++ b/frontend/apps/mobile/project.json @@ -59,6 +59,14 @@ "dependsOn": ["ensure-symlink", "sync-deps", "pod-install", "collect-assets"], "options": {} }, + "tsc-watch": { + "executor": "nx:run-commands", + "options": { + "commands": ["npx tsc -p tsconfig.app.json --noEmit --watch"], + "cwd": "apps/mobile", + "parallel": false + } + }, "pod-install": { "executor": "@nx/react-native:pod-install", "options": {} diff --git a/frontend/apps/mobile/src/@types/react-navigation.d.ts b/frontend/apps/mobile/src/@types/react-navigation.d.ts new file mode 100644 index 00000000..239a3437 --- /dev/null +++ b/frontend/apps/mobile/src/@types/react-navigation.d.ts @@ -0,0 +1,17 @@ +import type { RootDrawerParamList } from "../navigation/types"; +// import type { StackNavigationOptions as OriginalStackNavigationOptions } from "@react-navigation/stack"; +// import type { DrawerNavigationOptions as OriginalDrawerNavigationOptions } from "@react-navigation/drawer"; + +declare global { + namespace ReactNavigation { + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface RootParamList extends RootDrawerParamList {} + + // interface StackNavigationOptions extends OriginalStackNavigationOptions { + // onGoBack?: (() => void) | (() => Promise); + // } + // interface DrawerNavigationOptions extends OriginalDrawerNavigationOptions { + // onGoBack?: (() => void) | (() => Promise); + // } + } +} diff --git a/frontend/apps/mobile/src/App.tsx b/frontend/apps/mobile/src/App.tsx index 6e5ef693..0d4a8d1e 100644 --- a/frontend/apps/mobile/src/App.tsx +++ b/frontend/apps/mobile/src/App.tsx @@ -19,14 +19,7 @@ import { useColorScheme } from "./hooks/useColorScheme"; import { Navigation } from "./navigation"; import { NotificationProvider } from "./notifications"; import { SplashScreen } from "./screens/SplashScreen"; -import { - selectAuthSlice, - selectSettingsSlice, - selectTheme, - setGlobalInfo, - useAppDispatch, - useAppSelector, -} from "./store"; +import { selectTheme, setGlobalInfo, useAppDispatch, useAppSelector } from "./store"; import { CustomDarkTheme, CustomLightTheme } from "./theme"; export const App: React.FC = () => { @@ -34,12 +27,12 @@ export const App: React.FC = () => { const dispatch = useAppDispatch(); const [api, setApi] = React.useState<{ api: Api; websocket: AbrechnungWebSocket } | undefined>(undefined); - const accessToken = useAppSelector((state) => selectAccessToken({ state: selectAuthSlice(state) })); - const baseUrl = useAppSelector((state) => selectBaseUrl({ state: selectAuthSlice(state) })); + const accessToken = useAppSelector(selectAccessToken); + const baseUrl = useAppSelector(selectBaseUrl); const isAuthenticated = accessToken !== undefined; - const userId = useAppSelector((state) => selectCurrentUserId({ state: selectAuthSlice(state) })); + const userId = useAppSelector(selectCurrentUserId); const groupStoreStatus = useAppSelector((state) => state.groups.status); - const themeMode = useAppSelector((state) => selectTheme({ state: selectSettingsSlice(state) })); + const themeMode = useAppSelector(selectTheme); const useDarkTheme = themeMode === "system" ? colorScheme === "dark" : themeMode === "dark"; const theme = useDarkTheme ? CustomDarkTheme : CustomLightTheme; diff --git a/frontend/apps/mobile/src/components/GroupListItem.tsx b/frontend/apps/mobile/src/components/GroupListItem.tsx index 95ce9eeb..a064e967 100644 --- a/frontend/apps/mobile/src/components/GroupListItem.tsx +++ b/frontend/apps/mobile/src/components/GroupListItem.tsx @@ -1,11 +1,11 @@ -import { selectGroupById } from "@abrechnung/redux"; import { useNavigation } from "@react-navigation/native"; import React from "react"; import { List } from "react-native-paper"; import { useApi } from "../core/ApiProvider"; -import { changeActiveGroup, selectGroupSlice, useAppDispatch, useAppSelector } from "../store"; +import { changeActiveGroup, useAppDispatch } from "../store"; import { RootDrawerParamList } from "../navigation/types"; import { DrawerNavigationProp } from "@react-navigation/drawer"; +import { useGroup } from "@abrechnung/redux"; interface Props { groupId: number; @@ -15,7 +15,7 @@ export const GroupListItem: React.FC = ({ groupId }) => { const navigation = useNavigation>(); const dispatch = useAppDispatch(); const { api } = useApi(); - const group = useAppSelector((state) => selectGroupById({ state: selectGroupSlice(state), groupId })); + const group = useGroup(groupId); if (group === undefined) { return null; diff --git a/frontend/apps/mobile/src/components/PositionDialog.tsx b/frontend/apps/mobile/src/components/PositionDialog.tsx index b1fe44a5..76219e15 100644 --- a/frontend/apps/mobile/src/components/PositionDialog.tsx +++ b/frontend/apps/mobile/src/components/PositionDialog.tsx @@ -1,6 +1,5 @@ -import { selectSortedAccounts, wipPositionUpdated } from "@abrechnung/redux"; +import { useSortedAccounts, wipPositionUpdated } from "@abrechnung/redux"; import { TransactionPosition, PositionValidator, PositionValidationErrors } from "@abrechnung/types"; -import memoize from "proxy-memoize"; import React, { useCallback, useEffect, useState } from "react"; import { ScrollView, StyleSheet } from "react-native"; import { @@ -15,7 +14,7 @@ import { TextInput, useTheme, } from "react-native-paper"; -import { RootState, selectAccountSlice, useAppDispatch, useAppSelector } from "../store"; +import { useAppDispatch } from "../store"; import { NumericInput } from "./NumericInput"; import { KeyboardAvoidingDialog } from "./style/KeyboardAvoidingDialog"; @@ -59,22 +58,13 @@ export const PositionDialog: React.FC = ({ const [localEditingState, setLocalEditingState] = useState(initialEditingState); const [searchTerm, setSearchTerm] = useState(""); - const selector = React.useCallback( - memoize((state: RootState) => { - const sorted = selectSortedAccounts({ - state: selectAccountSlice(state), - groupId, - sortMode: "name", - searchTerm, - }); - if (!editing) { - return sorted.filter((acc) => (localEditingState.usages[acc.id] ?? 0) > 0); - } - return sorted; - }), - [groupId, searchTerm, editing, localEditingState] - ); - const accounts = useAppSelector(selector); + const sortedAccounts = useSortedAccounts(groupId, "name", undefined, searchTerm); + const accounts = React.useMemo(() => { + if (!editing) { + return sortedAccounts.filter((acc) => (localEditingState.usages[acc.id] ?? 0) > 0); + } + return sortedAccounts; + }, [sortedAccounts, localEditingState, editing]); const [errors, setErrors] = useState(emptyFormErrors); const toggleShare = (accountID: number) => { diff --git a/frontend/apps/mobile/src/components/style/Searchbar.tsx b/frontend/apps/mobile/src/components/style/Searchbar.tsx index 278b823d..4fb035bd 100644 --- a/frontend/apps/mobile/src/components/style/Searchbar.tsx +++ b/frontend/apps/mobile/src/components/style/Searchbar.tsx @@ -155,16 +155,13 @@ const Searchbar = React.forwardRef( } }; - const { colors, roundness, dark, isV3 } = theme; - const textColor = isV3 ? theme.colors.onSurface : theme.colors.text; + const { colors, roundness, dark } = theme; + const textColor = theme.colors.onSurface; const iconColor = customIconColor || (dark ? textColor : color(textColor).alpha(0.54).rgb().string()); const rippleColor = color(textColor).alpha(0.32).rgb().string(); return ( - + ( styles.input, { color: textColor, - ...(theme.isV3 ? theme.fonts.default : theme.fonts.regular), + ...theme.fonts.default, ...Platform.select({ web: { outline: "none" } }), }, inputStyle, ]} placeholder={placeholder || ""} - placeholderTextColor={theme.isV3 ? theme.colors.onSurface : theme.colors?.placeholder} + placeholderTextColor={theme.colors.onSurface} selectionColor={colors?.primary} underlineColorAndroid="transparent" returnKeyType="search" diff --git a/frontend/apps/mobile/src/components/tag-select/TagSelectDialog.tsx b/frontend/apps/mobile/src/components/tag-select/TagSelectDialog.tsx index bcccc8bc..30424819 100644 --- a/frontend/apps/mobile/src/components/tag-select/TagSelectDialog.tsx +++ b/frontend/apps/mobile/src/components/tag-select/TagSelectDialog.tsx @@ -30,7 +30,7 @@ const EMPTY_LIST: string[] = []; export const TagSelectDialog: React.FC = ({ groupId, value, onChange, showDialog, onHideDialog, title }) => { const theme = useTheme(); - const usedTags = useAppSelector((state) => selectTagsInGroup({ state, groupId })); + const usedTags = useAppSelector((state) => selectTagsInGroup(state, groupId)); const [tags, setTags] = useState(EMPTY_LIST); const [searchTerm, setSearchTerm] = useState(""); diff --git a/frontend/apps/mobile/src/components/transaction-shares/ShareSelect.tsx b/frontend/apps/mobile/src/components/transaction-shares/ShareSelect.tsx index d4516b10..2460257e 100644 --- a/frontend/apps/mobile/src/components/transaction-shares/ShareSelect.tsx +++ b/frontend/apps/mobile/src/components/transaction-shares/ShareSelect.tsx @@ -4,7 +4,7 @@ import { ScrollView, View } from "react-native"; import { useEffect, useState } from "react"; import { createComparator, lambdaComparator } from "@abrechnung/utils"; import { Account, TransactionShare } from "@abrechnung/types"; -import { useAppSelector, selectAccountSlice } from "../../store"; +import { useAppSelector } from "../../store"; import { selectGroupAccounts } from "@abrechnung/redux"; interface Props { @@ -28,7 +28,7 @@ export const ShareSelect: React.FC = ({ }) => { const [shares, setShares] = useState({}); const [searchTerm, setSearchTerm] = useState(""); - const accounts = useAppSelector((state) => selectGroupAccounts({ state: selectAccountSlice(state), groupId })); + const accounts = useAppSelector((state) => selectGroupAccounts(state, groupId)); const [filteredAccounts, setFilteredAccounts] = useState([]); const toggleShare = (accountID: number) => { diff --git a/frontend/apps/mobile/src/components/transaction-shares/TransactionShareDialog.tsx b/frontend/apps/mobile/src/components/transaction-shares/TransactionShareDialog.tsx index 82a8b22d..4677a797 100644 --- a/frontend/apps/mobile/src/components/transaction-shares/TransactionShareDialog.tsx +++ b/frontend/apps/mobile/src/components/transaction-shares/TransactionShareDialog.tsx @@ -1,12 +1,10 @@ -import { selectSortedAccounts } from "@abrechnung/redux"; +import { useSortedAccounts } from "@abrechnung/redux"; import { TransactionShare } from "@abrechnung/types"; -import memoize from "proxy-memoize"; import * as React from "react"; import { useState } from "react"; import { ScrollView } from "react-native"; import { Button, Checkbox, Dialog, List, Searchbar } from "react-native-paper"; import { getAccountIcon } from "../../constants/Icons"; -import { RootState, selectAccountSlice, useAppSelector } from "../../store"; import { KeyboardAvoidingDialog } from "../style/KeyboardAvoidingDialog"; interface Props { @@ -34,22 +32,13 @@ export const TransactionShareDialog: React.FC = ({ excludedAccounts = [], }) => { const [searchTerm, setSearchTerm] = useState(""); - const selector = React.useCallback( - memoize((state: RootState) => { - const sorted = selectSortedAccounts({ - state: selectAccountSlice(state), - groupId, - sortMode: "name", - searchTerm, - }); - if (disabled) { - return sorted.filter((acc) => (value[acc.id] ?? 0) > 0 && !excludedAccounts.includes(acc.id)); - } - return sorted.filter((acc) => !excludedAccounts.includes(acc.id)); - }), - [groupId, searchTerm, disabled, value, excludedAccounts] - ); - const accounts = useAppSelector(selector); + const sortedAccounts = useSortedAccounts(groupId, "name", undefined, searchTerm); + const accounts = React.useMemo(() => { + if (disabled) { + return sortedAccounts.filter((acc) => (value[acc.id] ?? 0) > 0 && !excludedAccounts.includes(acc.id)); + } + return sortedAccounts.filter((acc) => !excludedAccounts.includes(acc.id)); + }, [sortedAccounts, excludedAccounts, value, disabled]); const toggleShare = (account_id: number) => { if (!onChange) { diff --git a/frontend/apps/mobile/src/components/transaction-shares/TransactionShareInput.tsx b/frontend/apps/mobile/src/components/transaction-shares/TransactionShareInput.tsx index 2e3bf59b..a3cf0599 100644 --- a/frontend/apps/mobile/src/components/transaction-shares/TransactionShareInput.tsx +++ b/frontend/apps/mobile/src/components/transaction-shares/TransactionShareInput.tsx @@ -4,7 +4,7 @@ import { TransactionShareDialog } from "./TransactionShareDialog"; import { useEffect, useState } from "react"; import { TouchableHighlight, View } from "react-native"; import { TransactionShare } from "@abrechnung/types"; -import { useAppSelector, selectAccountSlice } from "../../store"; +import { useAppSelector } from "../../store"; import { selectGroupAccounts } from "@abrechnung/redux"; interface Props { @@ -33,7 +33,7 @@ export const TransactionShareInput: React.FC = ({ const [showDialog, setShowDialog] = useState(false); const [stringifiedValue, setStringifiedValue] = useState(""); const theme = useTheme(); - const accounts = useAppSelector((state) => selectGroupAccounts({ state: selectAccountSlice(state), groupId })); + const accounts = useAppSelector((state) => selectGroupAccounts(state, groupId)); useEffect(() => { if (!value) { diff --git a/frontend/apps/mobile/src/navigation/DrawerContent.tsx b/frontend/apps/mobile/src/navigation/DrawerContent.tsx index fb6b78f9..2627090e 100644 --- a/frontend/apps/mobile/src/navigation/DrawerContent.tsx +++ b/frontend/apps/mobile/src/navigation/DrawerContent.tsx @@ -1,30 +1,21 @@ import { selectGroups } from "@abrechnung/redux"; -import { DrawerContentScrollView, DrawerNavigationProp } from "@react-navigation/drawer"; +import { DrawerContentComponentProps, DrawerContentScrollView, DrawerNavigationProp } from "@react-navigation/drawer"; import { useNavigation } from "@react-navigation/native"; import * as React from "react"; -import { ScrollView, ScrollViewProps, StyleSheet, View } from "react-native"; +import { StyleSheet, View } from "react-native"; import { ActivityIndicator, Drawer, IconButton, useTheme } from "react-native-paper"; import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons"; import { useOptionalApi } from "../core/ApiProvider"; -import { - changeActiveGroup, - selectActiveGroupId, - selectGroupSlice, - selectUiSlice, - useAppDispatch, - useAppSelector, -} from "../store"; +import { changeActiveGroup, selectActiveGroupId, useAppDispatch, useAppSelector } from "../store"; import { RootDrawerParamList } from "./types"; -type Props = React.ForwardRefExoticComponent>; - -export const DrawerContent: React.FC = (props) => { +export const DrawerContent: React.FC = (props) => { const theme = useTheme(); const { api } = useOptionalApi(); const dispatch = useAppDispatch(); const navigation = useNavigation>(); - const activeGroupID = useAppSelector((state) => selectActiveGroupId({ state: selectUiSlice(state) })); - const groups = useAppSelector((state) => selectGroups({ state: selectGroupSlice(state) })); + const activeGroupID = useAppSelector(selectActiveGroupId); + const groups = useAppSelector(selectGroups); if (!api) { return null; diff --git a/frontend/apps/mobile/src/navigation/Header.tsx b/frontend/apps/mobile/src/navigation/Header.tsx index 277abccf..0a6917c4 100644 --- a/frontend/apps/mobile/src/navigation/Header.tsx +++ b/frontend/apps/mobile/src/navigation/Header.tsx @@ -1,4 +1,4 @@ -import { DrawerNavigationProp } from "@react-navigation/drawer"; +import { DrawerHeaderProps, DrawerNavigationProp } from "@react-navigation/drawer"; import { HeaderTitleProps } from "@react-navigation/elements"; import { MaterialTopTabNavigationProp } from "@react-navigation/material-top-tabs"; import { Route } from "@react-navigation/native"; @@ -8,8 +8,6 @@ import { Appbar, Banner, useTheme } from "react-native-paper"; import { selectGlobalInfo, useAppSelector } from "../store"; import { GroupStackParamList, GroupTabParamList, RootDrawerParamList } from "./types"; -type Props = StackHeaderProps; - export interface HeaderOptions { title?: string; headerTitle?: string | ((props: HeaderTitleProps) => React.ReactNode); @@ -18,7 +16,22 @@ export interface HeaderOptions { headerRight?: (props: Record) => React.ReactNode; } -export interface HeaderProps { +// export interface HeaderProps { +// back?: { +// /** +// * Title of the previous screen. +// */ +// title: string; +// }; +// options: HeaderOptions; +// navigation: +// | DrawerNavigationProp +// | MaterialTopTabNavigationProp +// | StackNavigationProp; +// route: Route; +// } + +export type HeaderProps = (StackHeaderProps | DrawerHeaderProps) & { back?: { /** * Title of the previous screen. @@ -26,29 +39,34 @@ export interface HeaderProps { title: string; }; options: HeaderOptions; - navigation: - | DrawerNavigationProp - | MaterialTopTabNavigationProp - | StackNavigationProp; - route: Route; -} +}; -export const Header: React.FC = ({ navigation, route, options, back, ...props }) => { +export const Header: React.FC = ({ navigation, route, options, back }) => { const theme = useTheme(); - const title = - options.headerTitle !== undefined - ? options.headerTitle - : options.title !== undefined - ? options.title - : route.name; const showTitle = options.titleShown ?? true; - const globalInfo = useAppSelector((state) => selectGlobalInfo({ state: state.ui })); + const globalInfo = useAppSelector(selectGlobalInfo); + + const title = React.useMemo(() => { + const actualTitle = + options.headerTitle !== undefined + ? options.headerTitle + : options.title !== undefined + ? options.title + : route.name; + + if (typeof actualTitle === "function") { + return actualTitle({ children: "" }); + } + return actualTitle; + }, [options, route.name]); const openDrawer = () => { - if (navigation.openDrawer !== undefined) { - navigation.openDrawer(); - } else if (navigation.getParent("Drawer") !== undefined) { - navigation.getParent("Drawer").openDrawer(); + if ((navigation as any).openDrawer !== undefined) { + (navigation as any).openDrawer(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } else if ((navigation as any).getParent("Drawer") !== undefined) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (navigation as any).getParent("Drawer").openDrawer(); } else { console.error("cannot open drawer, unexpected location in navigation tree"); } diff --git a/frontend/apps/mobile/src/navigation/LinkingConfiguration.ts b/frontend/apps/mobile/src/navigation/LinkingConfiguration.ts index 0f8f3637..d0ceda94 100644 --- a/frontend/apps/mobile/src/navigation/LinkingConfiguration.ts +++ b/frontend/apps/mobile/src/navigation/LinkingConfiguration.ts @@ -12,7 +12,7 @@ import { RootDrawerParamList } from "./types"; // const prefix = Linking.createURL("/"); export const linkingOptions: LinkingOptions = { - // prefixes: [prefix], + prefixes: [], config: { screens: { GroupList: "groups", @@ -21,7 +21,7 @@ export const linkingOptions: LinkingOptions = { initialRouteName: "BottomTabNavigator", screens: { BottomTabNavigator: { - initialRouteName: "TransactionList", + // initialRouteName: "TransactionList", screens: { TransactionList: "transactions", AccountList: "personal-accounts", diff --git a/frontend/apps/mobile/src/navigation/Navigation.tsx b/frontend/apps/mobile/src/navigation/Navigation.tsx index d8453307..e6c65e94 100644 --- a/frontend/apps/mobile/src/navigation/Navigation.tsx +++ b/frontend/apps/mobile/src/navigation/Navigation.tsx @@ -1,5 +1,5 @@ import { fetchGroupDependencies, selectGroups, selectIsAuthenticated, subscribe, unsubscribe } from "@abrechnung/redux"; -import { createDrawerNavigator } from "@react-navigation/drawer"; +import { createDrawerNavigator, DrawerNavigationOptions } from "@react-navigation/drawer"; import { createMaterialTopTabNavigator } from "@react-navigation/material-top-tabs"; import { NavigationContainer } from "@react-navigation/native"; import { createStackNavigator } from "@react-navigation/stack"; @@ -22,15 +22,7 @@ import { AccountDetail } from "../screens/groups/AccountDetail"; import { AccountEdit } from "../screens/groups/AccountEdit"; import { AccountList } from "../screens/groups/AccountList"; import { TransactionDetail } from "../screens/groups/TransactionDetail"; -import { - changeActiveGroup, - selectActiveGroupId, - selectAuthSlice, - selectGroupSlice, - selectUiSlice, - useAppDispatch, - useAppSelector, -} from "../store"; +import { changeActiveGroup, selectActiveGroupId, useAppDispatch, useAppSelector } from "../store"; import { Theme } from "../theme"; import { DrawerContent } from "./DrawerContent"; import { Header } from "./Header"; @@ -50,9 +42,9 @@ const Drawer = createDrawerNavigator(); const RootNavigator: React.FC = () => { const { api, websocket } = useOptionalApi(); const dispatch = useAppDispatch(); - const activeGroupId = useAppSelector((state) => selectActiveGroupId({ state: selectUiSlice(state) })); - const groups = useAppSelector((state) => selectGroups({ state: selectGroupSlice(state) })); - const isAuthenticated = useAppSelector((state) => selectIsAuthenticated({ state: selectAuthSlice(state) })); + const activeGroupId = useAppSelector(selectActiveGroupId); + const groups = useAppSelector(selectGroups); + const isAuthenticated = useAppSelector(selectIsAuthenticated); useEffect(() => { if (!isAuthenticated || !api) { @@ -91,7 +83,7 @@ const RootNavigator: React.FC = () => { }; }, [websocket, isAuthenticated, activeGroupId, dispatch]); - const propsWithHeader = { + const propsWithHeader: DrawerNavigationOptions = { headerShown: true, header: (props) =>
, }; @@ -146,7 +138,7 @@ const RootNavigator: React.FC = () => { const GroupStack = createStackNavigator(); const GroupStackNavigator = () => { - const groupId = useAppSelector((state) => selectActiveGroupId({ state: selectUiSlice(state) })); + const groupId = useAppSelector(selectActiveGroupId); return ( { const BottomTab = createMaterialTopTabNavigator(); const BottomTabNavigator: React.FC = () => { - const activeGroupID = useAppSelector((state) => selectActiveGroupId({ state: selectUiSlice(state) })); + const activeGroupID = useAppSelector(selectActiveGroupId); return ( diff --git a/frontend/apps/mobile/src/navigation/types.tsx b/frontend/apps/mobile/src/navigation/types.tsx index 35fc9749..6aab855d 100644 --- a/frontend/apps/mobile/src/navigation/types.tsx +++ b/frontend/apps/mobile/src/navigation/types.tsx @@ -19,6 +19,14 @@ export type RootDrawerParamList = { NotFound: undefined; }; +export type RootDrawerScreenProps = DrawerScreenProps< + RootDrawerParamList, + Screen +>; + +export type RootDrawerNavigationProp = + RootDrawerScreenProps["navigation"]; + export type GroupStackParamList = { BottomTabNavigator: NavigatorScreenParams; TransactionDetail: { @@ -31,19 +39,6 @@ export type GroupStackParamList = { AddGroup: undefined; }; -export type GroupTabParamList = { - TransactionList: { groupId: number }; - AccountList: { groupId: number }; - ClearingAccountList: { groupId: number }; -}; - -export type RootDrawerScreenProps = DrawerScreenProps< - RootDrawerParamList, - Screen ->; -export type RootDrawerNavigationProp = - RootDrawerScreenProps["navigation"]; - export type GroupStackScreenProps = CompositeScreenProps< StackScreenProps, RootDrawerScreenProps @@ -51,6 +46,12 @@ export type GroupStackScreenProps = Co export type GroupStackNavigationProp = GroupStackScreenProps["navigation"]; +export type GroupTabParamList = { + TransactionList: { groupId: number }; + AccountList: { groupId: number }; + ClearingAccountList: { groupId: number }; +}; + export type GroupTabScreenProps = CompositeScreenProps< MaterialTopTabScreenProps, CompositeScreenProps< diff --git a/frontend/apps/mobile/src/screens/AddGroup.tsx b/frontend/apps/mobile/src/screens/AddGroup.tsx index 844eb434..ce971cf8 100644 --- a/frontend/apps/mobile/src/screens/AddGroup.tsx +++ b/frontend/apps/mobile/src/screens/AddGroup.tsx @@ -9,6 +9,7 @@ import { CurrencySelect } from "../components/CurrencySelect"; import { useApi } from "../core/ApiProvider"; import { GroupStackScreenProps } from "../navigation/types"; import { useAppDispatch } from "../store"; +import { StackNavigationOptions } from "@react-navigation/stack"; export const AddGroup: React.FC> = ({ navigation }) => { const theme = useTheme(); @@ -56,7 +57,7 @@ export const AddGroup: React.FC> = ({ navigati ); }, - }); + } as any); }, [theme, navigation, formik, cancel]); return ( diff --git a/frontend/apps/mobile/src/screens/GroupList.tsx b/frontend/apps/mobile/src/screens/GroupList.tsx index 1a7cc0ec..a93c2a50 100644 --- a/frontend/apps/mobile/src/screens/GroupList.tsx +++ b/frontend/apps/mobile/src/screens/GroupList.tsx @@ -6,13 +6,13 @@ import { GroupListItem } from "../components/GroupListItem"; import LoadingIndicator from "../components/LoadingIndicator"; import { useApi } from "../core/ApiProvider"; import { RootDrawerScreenProps } from "../navigation/types"; -import { selectGroupSlice, useAppDispatch, useAppSelector } from "../store"; +import { useAppDispatch, useAppSelector } from "../store"; export const GroupList: React.FC> = ({ navigation, route }) => { const dispatch = useAppDispatch(); const [refreshing, setRefreshing] = React.useState(false); const groupStatus = useAppSelector((state) => state.groups.status); - const groupIds = useAppSelector((state) => selectGroupIds({ state: selectGroupSlice(state) })); + const groupIds = useAppSelector(selectGroupIds); const { api } = useApi(); diff --git a/frontend/apps/mobile/src/screens/PreferencesScreen.tsx b/frontend/apps/mobile/src/screens/PreferencesScreen.tsx index 634817b9..319af9a5 100644 --- a/frontend/apps/mobile/src/screens/PreferencesScreen.tsx +++ b/frontend/apps/mobile/src/screens/PreferencesScreen.tsx @@ -4,7 +4,7 @@ import { View } from "react-native"; import { Dialog, Divider, List, Portal, RadioButton, useTheme } from "react-native-paper"; import { useApi } from "../core/ApiProvider"; import { RootDrawerScreenProps } from "../navigation/types"; -import { ThemeMode, selectSettingsSlice, selectTheme, themeChanged, useAppDispatch, useAppSelector } from "../store"; +import { ThemeMode, selectTheme, themeChanged, useAppDispatch, useAppSelector } from "../store"; const themeModes: ThemeMode[] = ["system", "dark", "light"]; @@ -12,7 +12,7 @@ export const PreferencesScreen: React.FC> = const dispatch = useAppDispatch(); const theme = useTheme(); const { api } = useApi(); - const themeMode = useAppSelector((state) => selectTheme({ state: selectSettingsSlice(state) })); + const themeMode = useAppSelector((state) => selectTheme(state)); const [themeSelectOpen, setThemeSelectOpen] = React.useState(false); const onLogout = () => { diff --git a/frontend/apps/mobile/src/screens/ProfileScreen.tsx b/frontend/apps/mobile/src/screens/ProfileScreen.tsx index 9b6bc83e..938f41d6 100644 --- a/frontend/apps/mobile/src/screens/ProfileScreen.tsx +++ b/frontend/apps/mobile/src/screens/ProfileScreen.tsx @@ -3,10 +3,10 @@ import * as React from "react"; import { ScrollView, View } from "react-native"; import { Banner, Divider, List } from "react-native-paper"; import { RootDrawerScreenProps } from "../navigation/types"; -import { selectAuthSlice, useAppSelector } from "../store"; +import { useAppSelector } from "../store"; export const ProfileScreen: React.FC> = () => { - const profile = useAppSelector((state) => selectProfile({ state: selectAuthSlice(state) })); + const profile = useAppSelector(selectProfile); if (!profile) { return ( diff --git a/frontend/apps/mobile/src/screens/Register.tsx b/frontend/apps/mobile/src/screens/Register.tsx index 459edebd..e9e5a11b 100644 --- a/frontend/apps/mobile/src/screens/Register.tsx +++ b/frontend/apps/mobile/src/screens/Register.tsx @@ -8,7 +8,7 @@ import { z } from "zod"; import { useInitApi } from "../core/ApiProvider"; import { RootDrawerScreenProps } from "../navigation/types"; import { notify } from "../notifications"; -import { selectAuthSlice, useAppSelector } from "../store"; +import { useAppSelector } from "../store"; import { useTranslation } from "react-i18next"; import { ApiError } from "@abrechnung/api"; @@ -37,12 +37,12 @@ const initialValues: FormSchema = { export const RegisterScreen: React.FC> = ({ navigation }) => { const { t } = useTranslation(); const theme = useTheme(); - const loggedIn = useAppSelector((state) => selectIsAuthenticated({ state: selectAuthSlice(state) })); + const loggedIn = useAppSelector(selectIsAuthenticated); const initApi = useInitApi(); React.useEffect(() => { if (loggedIn) { - navigation.navigate("GroupStackNavigator"); + navigation.navigate("GroupList"); } }, [loggedIn, navigation]); diff --git a/frontend/apps/mobile/src/screens/TransactionList/TransactionList.tsx b/frontend/apps/mobile/src/screens/TransactionList/TransactionList.tsx index 3a050102..4d6a0f63 100644 --- a/frontend/apps/mobile/src/screens/TransactionList/TransactionList.tsx +++ b/frontend/apps/mobile/src/screens/TransactionList/TransactionList.tsx @@ -3,10 +3,10 @@ import { TransactionSortMode } from "@abrechnung/core"; import { createTransaction, fetchTransactions, - selectCurrentUserPermissions, - selectGroupById, selectGroupTransactionsStatus, - selectSortedTransactions, + useCurrentUserPermissions, + useGroup, + useSortedTransactions, } from "@abrechnung/redux"; import { Transaction, TransactionType } from "@abrechnung/types"; import { useIsFocused } from "@react-navigation/native"; @@ -19,14 +19,7 @@ import Searchbar from "../../components/style/Searchbar"; import { purchaseIcon, transferIcon } from "../../constants/Icons"; import { useApi } from "../../core/ApiProvider"; import { GroupTabScreenProps } from "../../navigation/types"; -import { - selectActiveGroupId, - selectGroupSlice, - selectTransactionSlice, - selectUiSlice, - useAppDispatch, - useAppSelector, -} from "../../store"; +import { selectActiveGroupId, useAppDispatch, useAppSelector } from "../../store"; import TransactionListItem from "./TransactionListItem"; type Props = GroupTabScreenProps<"TransactionList">; @@ -36,17 +29,14 @@ export const TransactionList: React.FC = ({ navigation }) => { const dispatch = useAppDispatch(); const { api } = useApi(); - const groupId = useAppSelector((state) => selectActiveGroupId({ state: selectUiSlice(state) })) as number; // TODO: proper typing - const group = useAppSelector((state) => selectGroupById({ state: selectGroupSlice(state), groupId })); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const groupId = useAppSelector((state) => selectActiveGroupId(state))!; + const group = useGroup(groupId); const [search, setSearch] = useState(""); const [sortMode, setSortMode] = useState("last_changed"); - const transactions = useAppSelector((state) => - selectSortedTransactions({ state: state, groupId, searchTerm: search, sortMode }) - ); - const transactionStatus = useAppSelector((state) => - selectGroupTransactionsStatus({ state: selectTransactionSlice(state), groupId }) - ); - const permissions = useAppSelector((state) => selectCurrentUserPermissions({ state: state, groupId })); + const transactions = useSortedTransactions(groupId, sortMode, search); + const transactionStatus = useAppSelector((state) => selectGroupTransactionsStatus(state, groupId)); + const permissions = useCurrentUserPermissions(groupId); const [refreshing, setRefreshing] = useState(false); const [isFapOpen, setFabOpen] = useState(false); @@ -154,7 +144,7 @@ export const TransactionList: React.FC = ({ navigation }) => { onRefresh={onRefresh} refreshing={refreshing} ListFooterComponent={ - permissions?.canWrite ? ( + permissions?.can_write ? ( = ({ groupId, transactionId }) => { - const navigation = useNavigation(); + const navigation = useNavigation>(); const theme = useTheme(); - const transaction = useAppSelector((state) => - selectTransactionById({ state: selectTransactionSlice(state), groupId, transactionId }) - ); + const transaction = useAppSelector((state) => selectTransactionById(state, groupId, transactionId)); if (transaction === undefined) { return null; diff --git a/frontend/apps/mobile/src/screens/groups/AccountDetail.tsx b/frontend/apps/mobile/src/screens/groups/AccountDetail.tsx index cd22b8a5..8ea34496 100644 --- a/frontend/apps/mobile/src/screens/groups/AccountDetail.tsx +++ b/frontend/apps/mobile/src/screens/groups/AccountDetail.tsx @@ -1,11 +1,11 @@ import { deleteAccount, selectAccountBalances, - selectAccountById, - selectClearingAccountsInvolvingAccounts, - selectCurrentUserPermissions, - selectGroupCurrencySymbol, selectTransactionsInvolvingAccount, + useAccount, + useClearingAccountsInvolvingAccount, + useCurrentUserPermissions, + useGroupCurrencySymbol, } from "@abrechnung/redux"; import { Account, AccountBalance, Transaction, TransactionShare } from "@abrechnung/types"; import { fromISOString } from "@abrechnung/utils"; @@ -28,13 +28,7 @@ import { clearingAccountIcon, getTransactionIcon } from "../../constants/Icons"; import { useApi } from "../../core/ApiProvider"; import { GroupStackScreenProps } from "../../navigation/types"; import { notify } from "../../notifications"; -import { - selectAccountSlice, - selectGroupSlice, - selectTransactionSlice, - useAppDispatch, - useAppSelector, -} from "../../store"; +import { useAppDispatch, useAppSelector } from "../../store"; import { successColor } from "../../theme"; type ArrayAccountsAndTransactions = Array; @@ -46,26 +40,18 @@ export const AccountDetail: React.FC> = ( const { groupId, accountId } = route.params; - const account = useAppSelector((state) => - selectAccountById({ state: selectAccountSlice(state), groupId, accountId }) - ); - const accountBalances = useAppSelector((state) => selectAccountBalances({ state, groupId })); - const transactions = useAppSelector((state) => - selectTransactionsInvolvingAccount({ state: selectTransactionSlice(state), groupId, accountId }) - ); + const account = useAccount(groupId, accountId); + const accountBalances = useAppSelector((state) => selectAccountBalances(state, groupId)); + const transactions = useAppSelector((state) => selectTransactionsInvolvingAccount(state, groupId, accountId)); - const clearingAccounts = useAppSelector((state) => - selectClearingAccountsInvolvingAccounts({ state: selectAccountSlice(state), groupId, accountId }) - ); + const clearingAccounts = useClearingAccountsInvolvingAccount(groupId, accountId); const combinedList: ArrayAccountsAndTransactions = (transactions as ArrayAccountsAndTransactions) .concat(clearingAccounts) .sort((f1, f2) => fromISOString(f2.last_changed).getTime() - fromISOString(f1.last_changed).getTime()); - const currency_symbol = useAppSelector((state) => - selectGroupCurrencySymbol({ state: selectGroupSlice(state), groupId }) - ); - const permissions = useAppSelector((state) => selectCurrentUserPermissions({ state: state, groupId })); + const currency_symbol = useGroupCurrencySymbol(groupId); + const permissions = useCurrentUserPermissions(groupId); const [confirmDeleteModalOpen, setConfirmDeleteModalOpen] = React.useState(false); @@ -94,7 +80,7 @@ export const AccountDetail: React.FC> = ( navigation.setOptions({ headerTitle: account?.name || "", headerRight: () => { - if (permissions === undefined || !permissions.canWrite) { + if (permissions === undefined || !permissions.can_write) { return null; } return ( @@ -244,7 +230,7 @@ const styles = StyleSheet.create({ }, shareContainer: { padding: 16, - borderBottomStyle: "solid", + borderStyle: "solid", borderBottomColor: "#bebebe", borderBottomWidth: 1, borderTopRightRadius: 4, diff --git a/frontend/apps/mobile/src/screens/groups/AccountEdit.tsx b/frontend/apps/mobile/src/screens/groups/AccountEdit.tsx index e5926dd0..f491c7fd 100644 --- a/frontend/apps/mobile/src/screens/groups/AccountEdit.tsx +++ b/frontend/apps/mobile/src/screens/groups/AccountEdit.tsx @@ -2,8 +2,8 @@ import { deleteAccount, discardAccountChange, saveAccount, - selectAccountById, - selectCurrentUserPermissions, + useAccount, + useCurrentUserPermissions, wipAccountUpdated, } from "@abrechnung/redux"; import { AccountValidator } from "@abrechnung/types"; @@ -17,7 +17,7 @@ import { TransactionShareInput } from "../../components/transaction-shares/Trans import { useApi } from "../../core/ApiProvider"; import { GroupStackScreenProps } from "../../navigation/types"; import { notify } from "../../notifications"; -import { selectAccountSlice, useAppDispatch, useAppSelector } from "../../store"; +import { useAppDispatch } from "../../store"; import { LoadingIndicator, TagSelect, DateTimeInput } from "../../components"; export const AccountEdit: React.FC> = ({ route, navigation }) => { @@ -27,15 +27,14 @@ export const AccountEdit: React.FC> = ({ ro const { groupId, accountId } = route.params; - const account = useAppSelector((state) => - selectAccountById({ state: selectAccountSlice(state), groupId, accountId }) - ); - const permissions = useAppSelector((state) => selectCurrentUserPermissions({ state: state, groupId })); + const account = useAccount(groupId, accountId); + const permissions = useCurrentUserPermissions(groupId); const onGoBack = React.useCallback(async () => { if (account) { return dispatch(discardAccountChange({ groupId, accountId: account.id })); } + return; }, [dispatch, account, groupId]); const [confirmDeleteModalOpen, setConfirmDeleteModalOpen] = React.useState(false); @@ -69,7 +68,7 @@ export const AccountEdit: React.FC> = ({ ro ); useEffect(() => { - if (permissions === undefined || !permissions.canWrite) { + if (permissions === undefined || !permissions.can_write) { navigation.replace("AccountDetail", { accountId, groupId }); } }, [navigation, accountId, permissions, groupId]); @@ -157,7 +156,7 @@ export const AccountEdit: React.FC> = ({ ro ); }, - }); + } as any); }, [theme, account, navigation, formik, cancelEdit, onGoBack]); if (account == null) { @@ -234,7 +233,7 @@ export const AccountEdit: React.FC> = ({ ro error={formik.touched.clearing_shares && !!formik.errors.clearing_shares} /> {formik.touched.clearing_shares && !!formik.errors.clearing_shares && ( - {formik.errors.clearing_shares} + {formik.errors.clearing_shares as string} )} )} @@ -264,7 +263,7 @@ const styles = StyleSheet.create({ }, shareContainer: { padding: 16, - borderBottomStyle: "solid", + borderStyle: "solid", borderBottomColor: "#bebebe", borderBottomWidth: 1, borderTopRightRadius: 4, diff --git a/frontend/apps/mobile/src/screens/groups/AccountList.tsx b/frontend/apps/mobile/src/screens/groups/AccountList.tsx index ece47dc2..6e5c60a9 100644 --- a/frontend/apps/mobile/src/screens/groups/AccountList.tsx +++ b/frontend/apps/mobile/src/screens/groups/AccountList.tsx @@ -4,10 +4,10 @@ import { createAccount, fetchAccounts, selectAccountBalances, - selectCurrentUserPermissions, selectGroupAccountsStatus, - selectGroupById, - selectSortedAccounts, + useCurrentUserPermissions, + useGroup, + useSortedAccounts, } from "@abrechnung/redux"; import { Account, AccountBalance } from "@abrechnung/types"; import { useIsFocused } from "@react-navigation/native"; @@ -21,14 +21,7 @@ import Searchbar from "../../components/style/Searchbar"; import { getAccountIcon } from "../../constants/Icons"; import { useApi } from "../../core/ApiProvider"; import { GroupTabScreenProps } from "../../navigation/types"; -import { - selectAccountSlice, - selectActiveGroupId, - selectGroupSlice, - selectUiSlice, - useAppDispatch, - useAppSelector, -} from "../../store"; +import { selectActiveGroupId, useAppDispatch, useAppSelector } from "../../store"; import { successColor } from "../../theme"; type Props = GroupTabScreenProps<"AccountList" | "ClearingAccountList">; @@ -39,25 +32,16 @@ export const AccountList: React.FC = ({ route, navigation }) => { const { api } = useApi(); const accountType: AccountType = route.name === "AccountList" ? "personal" : "clearing"; - const groupId = useAppSelector((state) => selectActiveGroupId({ state: selectUiSlice(state) })) as number; // TODO: proper typing - const group = useAppSelector((state) => selectGroupById({ state: selectGroupSlice(state), groupId })); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const groupId = useAppSelector((state) => selectActiveGroupId(state))!; + const group = useGroup(groupId); const [search, setSearch] = useState(""); const [sortMode, setSortMode] = useState("name"); - const accounts = useAppSelector((state) => - selectSortedAccounts({ - state: selectAccountSlice(state), - groupId, - type: accountType, - sortMode, - searchTerm: search, - }) - ); - const accountBalances = useAppSelector((state) => selectAccountBalances({ state, groupId })); - const permissions = useAppSelector((state) => selectCurrentUserPermissions({ state: state, groupId })); + const accounts = useSortedAccounts(groupId, sortMode, accountType, search); + const accountBalances = useAppSelector((state) => selectAccountBalances(state, groupId)); + const permissions = useCurrentUserPermissions(groupId); const currency_symbol = group?.currency_symbol; - const accountStatus = useAppSelector((state) => - selectGroupAccountsStatus({ state: selectAccountSlice(state), groupId }) - ); + const accountStatus = useAppSelector((state) => selectGroupAccountsStatus(state, groupId)); const [isMenuOpen, setMenuOpen] = useState(false); const [showSearchInput, setShowSearchInput] = useState(false); @@ -204,7 +188,7 @@ export const AccountList: React.FC = ({ route, navigation }) => { data={accounts} renderItem={renderItem} ListFooterComponent={ - permissions?.canWrite ? ( + permissions?.can_write ? ( diff --git a/frontend/apps/mobile/src/screens/groups/TransactionDetail.tsx b/frontend/apps/mobile/src/screens/groups/TransactionDetail.tsx index 6cb86719..b71b7401 100644 --- a/frontend/apps/mobile/src/screens/groups/TransactionDetail.tsx +++ b/frontend/apps/mobile/src/screens/groups/TransactionDetail.tsx @@ -2,9 +2,9 @@ import { deleteTransaction, discardTransactionChange, saveTransaction, - selectCurrentUserPermissions, - selectTransactionById, selectTransactionHasPositions, + useCurrentUserPermissions, + useTransaction, useWipTransactionPositions, wipTransactionUpdated, } from "@abrechnung/redux"; @@ -36,7 +36,7 @@ import { TransactionShareInput } from "../../components/transaction-shares/Trans import { useApi } from "../../core/ApiProvider"; import { GroupStackScreenProps } from "../../navigation/types"; import { notify } from "../../notifications"; -import { selectTransactionSlice, useAppDispatch, useAppSelector } from "../../store"; +import { useAppDispatch, useAppSelector } from "../../store"; import { SerializedError } from "@reduxjs/toolkit"; export const TransactionDetail: React.FC> = ({ route, navigation }) => { @@ -47,17 +47,14 @@ export const TransactionDetail: React.FC - selectTransactionById({ state: selectTransactionSlice(state), groupId, transactionId }) - )!; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const transaction = useTransaction(groupId, transactionId)!; const [showPositions, setShowPositions] = React.useState(false); - const hasPositions = useAppSelector((state) => - selectTransactionHasPositions({ state: selectTransactionSlice(state), groupId, transactionId }) - ); - const positions = useWipTransactionPositions(transaction); + const hasPositions = useAppSelector((state) => selectTransactionHasPositions(state, groupId, transactionId)); + const positions = useWipTransactionPositions(groupId, transaction.id); const totalPositionValue = positions.reduce((acc, curr) => acc + curr.price, 0); - const permissions = useAppSelector((state) => selectCurrentUserPermissions({ state: state, groupId })); + const permissions = useCurrentUserPermissions(groupId); const onGoBack = React.useCallback(async () => { if (editing && transaction != null) { @@ -96,7 +93,7 @@ export const TransactionDetail: React.FC { - if (editing && (permissions === undefined || !permissions.canWrite)) { + if (editing && (permissions === undefined || !permissions.can_write)) { navigation.replace("TransactionDetail", { transactionId, groupId, editing: false }); } }, [editing, permissions, transactionId, groupId, navigation]); @@ -178,7 +175,7 @@ export const TransactionDetail: React.FC { - if (permissions === undefined || !permissions.canWrite) { + if (permissions === undefined || !permissions.can_write) { return null; } if (editing) { @@ -199,7 +196,7 @@ export const TransactionDetail: React.FC ); }, - }); + } as any); }, [theme, editing, permissions, navigation, formik, onGoBack, cancelEdit, edit, transaction, onDeleteTransaction]); if (transaction == null) { @@ -335,7 +332,7 @@ export const TransactionDetail: React.FC {formik.touched.creditor_shares && !!formik.errors.creditor_shares && ( - {formik.errors.creditor_shares} + {formik.errors.creditor_shares as string} )} {formik.touched.debitor_shares && !!formik.errors.debitor_shares && ( - {formik.errors.debitor_shares} + {formik.errors.debitor_shares as string} )} {transaction.type === "purchase" && !showPositions && editing && !hasPositions ? ( @@ -415,7 +412,7 @@ const styles = StyleSheet.create({ }, shareContainer: { padding: 16, - borderBottomStyle: "solid", + borderStyle: "solid", borderBottomColor: "#bebebe", borderBottomWidth: 1, borderTopRightRadius: 4, diff --git a/frontend/apps/mobile/src/store/index.ts b/frontend/apps/mobile/src/store/index.ts index d59c8028..7f4463c8 100644 --- a/frontend/apps/mobile/src/store/index.ts +++ b/frontend/apps/mobile/src/store/index.ts @@ -1,4 +1,3 @@ export * from "./store"; -export * from "./selectors"; export * from "./settingsSlice"; export * from "./uiSlice"; diff --git a/frontend/apps/mobile/src/store/selectors.ts b/frontend/apps/mobile/src/store/selectors.ts deleted file mode 100644 index 191f0c6c..00000000 --- a/frontend/apps/mobile/src/store/selectors.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { RootState } from "./store"; - -export const selectGroupSlice = (state: RootState) => state.groups; -export const selectAccountSlice = (state: RootState) => state.accounts; -export const selectTransactionSlice = (state: RootState) => state.transactions; -export const selectAuthSlice = (state: RootState) => state.auth; -export const selectSettingsSlice = (state: RootState) => state.settings; -export const selectUiSlice = (state: RootState) => state.ui; diff --git a/frontend/apps/mobile/src/store/settingsSlice.ts b/frontend/apps/mobile/src/store/settingsSlice.ts index 3404776e..e304fbba 100644 --- a/frontend/apps/mobile/src/store/settingsSlice.ts +++ b/frontend/apps/mobile/src/store/settingsSlice.ts @@ -1,5 +1,5 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import memoize from "proxy-memoize"; +import type { RootState } from "./store"; export type ThemeMode = "light" | "dark" | "system"; @@ -12,10 +12,9 @@ const initialState: SettingsSliceState = { }; // selectors -export const selectTheme = memoize((args: { state: SettingsSliceState }): ThemeMode => { - const { state } = args; - return state.theme; -}); +export const selectTheme = (state: RootState): ThemeMode => { + return state.settings.theme; +}; const settingsSlice = createSlice({ name: "settings", diff --git a/frontend/apps/mobile/src/store/uiSlice.ts b/frontend/apps/mobile/src/store/uiSlice.ts index acee1a7a..ea3c0c2e 100644 --- a/frontend/apps/mobile/src/store/uiSlice.ts +++ b/frontend/apps/mobile/src/store/uiSlice.ts @@ -1,7 +1,6 @@ import { Api } from "@abrechnung/api"; import { fetchGroupDependencies } from "@abrechnung/redux"; import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"; -import memoize from "proxy-memoize"; import { notify } from "../notifications"; import { RootState } from "./store"; @@ -20,14 +19,12 @@ const initialState: UiSliceState = { }; // selectors -export const selectActiveGroupId = memoize((args: { state: UiSliceState }): number | undefined => { - const { state } = args; - return state.activeGroupId; -}); +export const selectActiveGroupId = (state: RootState): number | undefined => { + return state.ui.activeGroupId; +}; -export const selectGlobalInfo = (args: { state: UiSliceState }): GlobalInfo | undefined => { - const { state } = args; - return state.globalInfo; +export const selectGlobalInfo = (state: RootState): GlobalInfo | undefined => { + return state.ui.globalInfo; }; export const changeActiveGroup = createAsyncThunk< diff --git a/frontend/apps/mobile/tsconfig.json b/frontend/apps/mobile/tsconfig.json index 9c0d55bd..604b91ec 100644 --- a/frontend/apps/mobile/tsconfig.json +++ b/frontend/apps/mobile/tsconfig.json @@ -5,7 +5,6 @@ "jsx": "react-native", "lib": ["dom", "esnext"], "moduleResolution": "node", - "skipLibCheck": true, "resolveJsonModule": true, "declaration": true }, diff --git a/frontend/apps/web/src/app/app.tsx b/frontend/apps/web/src/app/app.tsx index 9b8a8eb6..eb56fd39 100644 --- a/frontend/apps/web/src/app/app.tsx +++ b/frontend/apps/web/src/app/app.tsx @@ -15,7 +15,7 @@ import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; import { Loading } from "@/components/style"; import { api, ws } from "../core/api"; -import { selectAuthSlice, selectSettingsSlice, selectTheme, useAppDispatch, useAppSelector } from "../store"; +import { selectTheme, useAppDispatch, useAppSelector } from "../store"; import { Router } from "./Router"; export const App = () => { @@ -23,10 +23,10 @@ export const App = () => { const dispatch = useAppDispatch(); const groupStoreStatus = useAppSelector((state) => state.groups.status); const [apiInitialized, setApiInitialized] = React.useState(false); - const accessToken = useAppSelector((state) => selectAccessToken({ state: selectAuthSlice(state) })); - const themeMode = useAppSelector((state) => selectTheme({ state: selectSettingsSlice(state) })); + const accessToken = useAppSelector(selectAccessToken); + const themeMode = useAppSelector(selectTheme); const isAuthenticated = accessToken !== undefined; - const userId = useAppSelector((state) => selectCurrentUserId({ state: selectAuthSlice(state) })); + const userId = useAppSelector(selectCurrentUserId); const useDarkMode: PaletteMode = themeMode === "browser" ? (darkModeSystem ? "dark" : "light") : themeMode; diff --git a/frontend/apps/web/src/app/authenticated-layout/AuthenticatedLayout.tsx b/frontend/apps/web/src/app/authenticated-layout/AuthenticatedLayout.tsx index ffcab152..40b07175 100644 --- a/frontend/apps/web/src/app/authenticated-layout/AuthenticatedLayout.tsx +++ b/frontend/apps/web/src/app/authenticated-layout/AuthenticatedLayout.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { selectIsAuthenticated } from "@abrechnung/redux"; import { Link as RouterLink, Navigate, Outlet, useLocation, useParams } from "react-router-dom"; -import { selectAuthSlice, useAppSelector } from "@/store"; +import { useAppSelector } from "@/store"; import { ListItemLink } from "@/components/style/ListItemLink"; import { SidebarGroupList } from "@/app/authenticated-layout/SidebarGroupList"; import { @@ -52,7 +52,7 @@ const AUTH_FALLBACK = "/login"; export const AuthenticatedLayout: React.FC = () => { const { t } = useTranslation(); - const authenticated = useAppSelector((state) => selectIsAuthenticated({ state: selectAuthSlice(state) })); + const authenticated = useAppSelector(selectIsAuthenticated); const location = useLocation(); const params = useParams(); const groupId = params["groupId"] ? Number(params["groupId"]) : undefined; diff --git a/frontend/apps/web/src/app/authenticated-layout/SidebarGroupList.tsx b/frontend/apps/web/src/app/authenticated-layout/SidebarGroupList.tsx index 190bdb1b..f81dd5db 100644 --- a/frontend/apps/web/src/app/authenticated-layout/SidebarGroupList.tsx +++ b/frontend/apps/web/src/app/authenticated-layout/SidebarGroupList.tsx @@ -1,6 +1,6 @@ import { GroupCreateModal } from "@/components/groups/GroupCreateModal"; import { ListItemLink } from "@/components/style"; -import { selectAuthSlice, selectGroupSlice, useAppSelector } from "@/store"; +import { useAppSelector } from "@/store"; import { selectGroups, selectIsGuestUser } from "@abrechnung/redux"; import { Add } from "@mui/icons-material"; import { Grid, IconButton, List, ListItem, ListItemText, Tooltip } from "@mui/material"; @@ -13,8 +13,8 @@ interface Props { export const SidebarGroupList: React.FC = ({ activeGroupId }) => { const { t } = useTranslation(); - const isGuest = useAppSelector((state) => selectIsGuestUser({ state: selectAuthSlice(state) })); - const groups = useAppSelector((state) => selectGroups({ state: selectGroupSlice(state) })); + const isGuest = useAppSelector(selectIsGuestUser); + const groups = useAppSelector((state) => selectGroups(state)); const [showGroupCreationModal, setShowGroupCreationModal] = useState(false); const openGroupCreateModal = () => { diff --git a/frontend/apps/web/src/app/unauthenticated-layout/UnauthenticatedLayout.tsx b/frontend/apps/web/src/app/unauthenticated-layout/UnauthenticatedLayout.tsx index 42dc621d..08dffe25 100644 --- a/frontend/apps/web/src/app/unauthenticated-layout/UnauthenticatedLayout.tsx +++ b/frontend/apps/web/src/app/unauthenticated-layout/UnauthenticatedLayout.tsx @@ -3,13 +3,13 @@ import { Link as RouterLink, Outlet, Navigate } from "react-router-dom"; import { AppBar, Box, Button, Container, CssBaseline, Toolbar, Typography } from "@mui/material"; import { Banner } from "@/components/style/Banner"; import { selectIsAuthenticated } from "@abrechnung/redux"; -import { useAppSelector, selectAuthSlice } from "../../store"; +import { useAppSelector } from "../../store"; import { LanguageSelect } from "@/components/LanguageSelect"; import { useTranslation } from "react-i18next"; export const UnauthenticatedLayout: React.FC = () => { const { t } = useTranslation(); - const authenticated = useAppSelector((state) => selectIsAuthenticated({ state: selectAuthSlice(state) })); + const authenticated = useAppSelector(selectIsAuthenticated); if (authenticated) { return ; diff --git a/frontend/apps/web/src/components/AccountSelect.tsx b/frontend/apps/web/src/components/AccountSelect.tsx index e419ef35..7e4b345c 100644 --- a/frontend/apps/web/src/components/AccountSelect.tsx +++ b/frontend/apps/web/src/components/AccountSelect.tsx @@ -1,9 +1,8 @@ -import { selectSortedAccounts } from "@abrechnung/redux"; +import { useSortedAccounts } from "@abrechnung/redux"; import { Account } from "@abrechnung/types"; import { Autocomplete, Box, Popper, TextField, TextFieldProps, Typography } from "@mui/material"; import { styled } from "@mui/material/styles"; import React from "react"; -import { selectAccountSlice, useAppSelector } from "@/store"; import { getAccountIcon } from "./style/AbrechnungIcons"; import { DisabledTextField } from "./style/DisabledTextField"; @@ -31,9 +30,7 @@ export const AccountSelect: React.FC = ({ noDisabledStyling = false, ...props }) => { - const accounts = useAppSelector((state) => - selectSortedAccounts({ state: selectAccountSlice(state), groupId, sortMode: "name" }) - ); + const accounts = useSortedAccounts(groupId, "name"); const [filteredAccounts, setFilteredAccounts] = React.useState([]); diff --git a/frontend/apps/web/src/components/RequireAuth.tsx b/frontend/apps/web/src/components/RequireAuth.tsx index fbd6a9ec..278eefd9 100644 --- a/frontend/apps/web/src/components/RequireAuth.tsx +++ b/frontend/apps/web/src/components/RequireAuth.tsx @@ -1,7 +1,7 @@ import { selectIsAuthenticated } from "@abrechnung/redux"; import React from "react"; import { Navigate, useLocation } from "react-router-dom"; -import { selectAuthSlice, useAppSelector } from "@/store"; +import { useAppSelector } from "@/store"; interface Props { authFallback?: string; @@ -9,7 +9,7 @@ interface Props { } export const RequireAuth: React.FC = ({ authFallback = "/login", children }) => { - const loggedIn = useAppSelector((state) => selectIsAuthenticated({ state: selectAuthSlice(state) })); + const loggedIn = useAppSelector(selectIsAuthenticated); const location = useLocation(); if (!loggedIn) { return ; diff --git a/frontend/apps/web/src/components/ShareSelect.tsx b/frontend/apps/web/src/components/ShareSelect.tsx index 37bef20f..b1741c25 100644 --- a/frontend/apps/web/src/components/ShareSelect.tsx +++ b/frontend/apps/web/src/components/ShareSelect.tsx @@ -1,4 +1,4 @@ -import { selectGroupAccounts } from "@abrechnung/redux"; +import { useGroupAccounts } from "@abrechnung/redux"; import { Account, TransactionShare } from "@abrechnung/types"; import { Clear as ClearIcon, Search as SearchIcon } from "@mui/icons-material"; import { @@ -24,7 +24,6 @@ import { } from "@mui/material"; import * as React from "react"; import { Link } from "react-router-dom"; -import { selectAccountSlice, useAppSelector } from "../store"; import { getAccountLink } from "../utils"; import { NumericInput } from "./NumericInput"; import { getAccountIcon } from "./style/AbrechnungIcons"; @@ -136,9 +135,7 @@ export const ShareSelect: React.FC = ({ const [showAdvanced, setShowAdvanced] = React.useState(false); const [searchValue, setSearchValue] = React.useState(""); - const unfilteredAccounts = useAppSelector((state) => - selectGroupAccounts({ state: selectAccountSlice(state), groupId }) - ); + const unfilteredAccounts = useGroupAccounts(groupId); const accounts = React.useMemo(() => { const sortFn = getAccountSortFunc("name"); return unfilteredAccounts diff --git a/frontend/apps/web/src/components/TagSelector.tsx b/frontend/apps/web/src/components/TagSelector.tsx index f6012191..f7cc76c0 100644 --- a/frontend/apps/web/src/components/TagSelector.tsx +++ b/frontend/apps/web/src/components/TagSelector.tsx @@ -30,7 +30,7 @@ export const TagSelector: React.FC = ({ const { t } = useTranslation(); const [addTagDialogOpen, setAddTagDialogOpen] = React.useState(false); - const possibleTags = useAppSelector((state) => selectTagsInGroup({ state, groupId })); + const possibleTags = useAppSelector((state) => selectTagsInGroup(state, groupId)); const handleChange = (event: React.ChangeEvent) => { if (!editable) { diff --git a/frontend/apps/web/src/components/accounts/AccountClearingListEntry.tsx b/frontend/apps/web/src/components/accounts/AccountClearingListEntry.tsx index c6c1e2fa..913274a1 100644 --- a/frontend/apps/web/src/components/accounts/AccountClearingListEntry.tsx +++ b/frontend/apps/web/src/components/accounts/AccountClearingListEntry.tsx @@ -1,9 +1,9 @@ -import { selectAccountBalances, selectGroupCurrencySymbol } from "@abrechnung/redux"; +import { selectAccountBalances, useGroupCurrencySymbol } from "@abrechnung/redux"; import { Box, ListItemAvatar, ListItemText, Tooltip, Typography } from "@mui/material"; import { DateTime } from "luxon"; import React from "react"; import { balanceColor } from "@/core/utils"; -import { selectGroupSlice, useAppSelector } from "@/store"; +import { useAppSelector } from "@/store"; import { getAccountLink } from "@/utils"; import { ListItemLink, ClearingAccountIcon } from "../style"; import { useTranslation } from "react-i18next"; @@ -19,10 +19,8 @@ interface Props { export const AccountClearingListEntry: React.FC = ({ groupId, accountId, clearingAccount }) => { const { t } = useTranslation(); const formatCurrency = useFormatCurrency(); - const balances = useAppSelector((state) => selectAccountBalances({ state, groupId })); - const currency_symbol = useAppSelector((state) => - selectGroupCurrencySymbol({ state: selectGroupSlice(state), groupId }) - ); + const balances = useAppSelector((state) => selectAccountBalances(state, groupId)); + const currency_symbol = useGroupCurrencySymbol(groupId); if (!currency_symbol) { return null; } diff --git a/frontend/apps/web/src/components/accounts/AccountTransactionList.tsx b/frontend/apps/web/src/components/accounts/AccountTransactionList.tsx index 7d0a4305..97439afb 100644 --- a/frontend/apps/web/src/components/accounts/AccountTransactionList.tsx +++ b/frontend/apps/web/src/components/accounts/AccountTransactionList.tsx @@ -1,14 +1,14 @@ import { createTransaction, - selectClearingAccountsInvolvingAccounts, selectTransactionsInvolvingAccount, + useClearingAccountsInvolvingAccount, } from "@abrechnung/redux"; import { Add as AddIcon } from "@mui/icons-material"; import { Account, Transaction } from "@abrechnung/types"; import { Alert, Box, IconButton, List, Tooltip, Typography } from "@mui/material"; import { DateTime } from "luxon"; import * as React from "react"; -import { selectAccountSlice, selectTransactionSlice, useAppDispatch, useAppSelector } from "@/store"; +import { useAppDispatch, useAppSelector } from "@/store"; import { AccountClearingListEntry } from "./AccountClearingListEntry"; import { AccountTransactionListEntry } from "./AccountTransactionListEntry"; import { useTranslation } from "react-i18next"; @@ -27,12 +27,8 @@ export const AccountTransactionList: React.FC = ({ groupId, account }) => const { t } = useTranslation(); const dispatch = useAppDispatch(); const navigate = useNavigate(); - const transactions = useAppSelector((state) => - selectTransactionsInvolvingAccount({ state: selectTransactionSlice(state), groupId, accountId: account.id }) - ); - const clearingAccounts = useAppSelector((state) => - selectClearingAccountsInvolvingAccounts({ state: selectAccountSlice(state), groupId, accountId: account.id }) - ); + const transactions = useAppSelector((state) => selectTransactionsInvolvingAccount(state, groupId, account.id)); + const clearingAccounts = useClearingAccountsInvolvingAccount(groupId, account.id); const combinedList: ArrayAccountsAndTransactions = (transactions as ArrayAccountsAndTransactions) .concat(clearingAccounts) diff --git a/frontend/apps/web/src/components/accounts/AccountTransactionListEntry.tsx b/frontend/apps/web/src/components/accounts/AccountTransactionListEntry.tsx index 09ff628d..b3963b5e 100644 --- a/frontend/apps/web/src/components/accounts/AccountTransactionListEntry.tsx +++ b/frontend/apps/web/src/components/accounts/AccountTransactionListEntry.tsx @@ -1,10 +1,10 @@ -import { selectGroupCurrencySymbol, selectTransactionBalanceEffect } from "@abrechnung/redux"; +import { selectTransactionBalanceEffect, useGroupCurrencySymbol } from "@abrechnung/redux"; import { HelpOutline } from "@mui/icons-material"; import { Chip, ListItemAvatar, ListItemText, Tooltip, Typography } from "@mui/material"; import { DateTime } from "luxon"; import React from "react"; import { balanceColor } from "@/core/utils"; -import { selectGroupSlice, selectTransactionSlice, useAppSelector } from "@/store"; +import { useAppSelector } from "@/store"; import { ListItemLink, PurchaseIcon, TransferIcon } from "../style"; import { useTranslation } from "react-i18next"; import { Transaction } from "@abrechnung/types"; @@ -17,12 +17,8 @@ interface Props { export const AccountTransactionListEntry: React.FC = ({ groupId, transaction, accountId }) => { const { t } = useTranslation(); - const balanceEffect = useAppSelector((state) => - selectTransactionBalanceEffect({ state: selectTransactionSlice(state), groupId, transactionId: transaction.id }) - ); - const currency_symbol = useAppSelector((state) => - selectGroupCurrencySymbol({ state: selectGroupSlice(state), groupId }) - ); + const balanceEffect = useAppSelector((state) => selectTransactionBalanceEffect(state, groupId, transaction.id)); + const currency_symbol = useGroupCurrencySymbol(groupId); return ( diff --git a/frontend/apps/web/src/components/accounts/BalanceHistoryGraph.tsx b/frontend/apps/web/src/components/accounts/BalanceHistoryGraph.tsx index 6311c40f..61992315 100644 --- a/frontend/apps/web/src/components/accounts/BalanceHistoryGraph.tsx +++ b/frontend/apps/web/src/components/accounts/BalanceHistoryGraph.tsx @@ -1,11 +1,11 @@ import { balanceColor } from "@/core/utils"; -import { RootState, selectAccountSlice, selectGroupSlice, selectTransactionSlice, useAppSelector } from "@/store"; +import { useAppSelector } from "@/store"; import { BalanceChangeOrigin } from "@abrechnung/core"; import { selectAccountBalanceHistory, selectAccountIdToAccountMap, - selectGroupCurrencySymbol, selectTransactionByIdMap, + useGroupCurrencySymbol, } from "@abrechnung/redux"; import { fromISOString, toISODateString } from "@abrechnung/utils"; import { Card, Divider, Theme, Typography, useTheme } from "@mui/material"; @@ -14,7 +14,6 @@ import { DateTime } from "luxon"; import React from "react"; import { useNavigate } from "react-router-dom"; import { ClearingAccountIcon, PurchaseIcon, TransferIcon } from "../style/AbrechnungIcons"; -import memoize from "proxy-memoize"; interface Props { groupId: number; @@ -25,87 +24,71 @@ export const BalanceHistoryGraph: React.FC = ({ groupId, accountId }) => const theme: Theme = useTheme(); const navigate = useNavigate(); - const currency_symbol = useAppSelector((state) => - selectGroupCurrencySymbol({ state: selectGroupSlice(state), groupId }) - ); - const transactionMap = useAppSelector((state) => - selectTransactionByIdMap({ state: selectTransactionSlice(state), groupId }) - ); - const accounts = useAppSelector((state) => - selectAccountIdToAccountMap({ state: selectAccountSlice(state), groupId }) - ); - - const balanceSelector = React.useCallback( - memoize((state: RootState) => { - const balanceHistory = selectAccountBalanceHistory({ state, groupId, accountId }); - const { hasNegativeEntries, hasPositiveEntries, max, min } = balanceHistory.reduce( - (acc, curr) => { - const neg = curr.balance < 0; - const pos = curr.balance >= 0; - return { - hasNegativeEntries: acc.hasNegativeEntries || neg, - hasPositiveEntries: acc.hasPositiveEntries || pos, - max: Math.max(curr.balance, acc.max), - min: Math.min(curr.balance, acc.min), - }; - }, - { hasNegativeEntries: false, hasPositiveEntries: false, max: -Infinity, min: Infinity } - ); + const currency_symbol = useGroupCurrencySymbol(groupId); + const transactionMap = useAppSelector((state) => selectTransactionByIdMap(state, groupId)); + const accounts = useAppSelector((state) => selectAccountIdToAccountMap(state, groupId)); + const balanceHistory = useAppSelector((state) => selectAccountBalanceHistory(state, groupId, accountId)); - const areaBaselineValue = - balanceHistory.length === 0 - ? undefined - : !hasNegativeEntries - ? min - : !hasPositiveEntries - ? max - : undefined; - - const graphData: Serie[] = []; - let lastPoint = balanceHistory[0]; - const makeSerie = (): { - id: string; - data: Array<{ x: Date; y: number; changeOrigin: BalanceChangeOrigin }>; - } => { + const { graphData, seriesColors, areaBaselineValue } = React.useMemo(() => { + const { hasNegativeEntries, hasPositiveEntries, max, min } = balanceHistory.reduce( + (acc, curr) => { + const neg = curr.balance < 0; + const pos = curr.balance >= 0; return { - id: `serie-${graphData.length}`, - data: [], + hasNegativeEntries: acc.hasNegativeEntries || neg, + hasPositiveEntries: acc.hasPositiveEntries || pos, + max: Math.max(curr.balance, acc.max), + min: Math.min(curr.balance, acc.min), }; + }, + { hasNegativeEntries: false, hasPositiveEntries: false, max: -Infinity, min: Infinity } + ); + + const areaBaselineValue = + balanceHistory.length === 0 ? undefined : !hasNegativeEntries ? min : !hasPositiveEntries ? max : undefined; + + const graphData: Serie[] = []; + let lastPoint = balanceHistory[0]; + const makeSerie = (): { + id: string; + data: Array<{ x: Date; y: number; changeOrigin: BalanceChangeOrigin }>; + } => { + return { + id: `serie-${graphData.length}`, + data: [], }; - let currentSeries = makeSerie(); - for (const entry of balanceHistory) { - if (lastPoint === undefined) { - break; - } - const hasDifferentSign = Math.sign(lastPoint.balance) !== Math.sign(entry.balance); + }; + let currentSeries = makeSerie(); + for (const entry of balanceHistory) { + if (lastPoint === undefined) { + break; + } + const hasDifferentSign = Math.sign(lastPoint.balance) !== Math.sign(entry.balance); + currentSeries.data.push({ + x: fromISOString(entry.date), + y: entry.balance, + changeOrigin: entry.changeOrigin, + }); + if (hasDifferentSign) { + graphData.push(currentSeries); + currentSeries = makeSerie(); currentSeries.data.push({ x: fromISOString(entry.date), y: entry.balance, changeOrigin: entry.changeOrigin, }); - if (hasDifferentSign) { - graphData.push(currentSeries); - currentSeries = makeSerie(); - currentSeries.data.push({ - x: fromISOString(entry.date), - y: entry.balance, - changeOrigin: entry.changeOrigin, - }); - } - lastPoint = entry; } - if (balanceHistory.length > 0) { - graphData.push(currentSeries); - } - const seriesColors: string[] = graphData.map((serie) => - Number(serie.data[0].y) >= 0 ? theme.palette.success.main : theme.palette.error.main - ); + lastPoint = entry; + } + if (balanceHistory.length > 0) { + graphData.push(currentSeries); + } + const seriesColors: string[] = graphData.map((serie) => + Number(serie.data[0].y) >= 0 ? theme.palette.success.main : theme.palette.error.main + ); - return { graphData, seriesColors, areaBaselineValue }; - }), - [groupId, accountId] - ); - const { graphData, seriesColors, areaBaselineValue } = useAppSelector(balanceSelector); + return { graphData, seriesColors, areaBaselineValue }; + }, [balanceHistory, theme]); const onClick: PointMouseHandler = (point, event) => { const changeOrigin: BalanceChangeOrigin = (point.data as any).changeOrigin; diff --git a/frontend/apps/web/src/components/accounts/BalanceTable.tsx b/frontend/apps/web/src/components/accounts/BalanceTable.tsx index 34b7c492..f80f70ad 100644 --- a/frontend/apps/web/src/components/accounts/BalanceTable.tsx +++ b/frontend/apps/web/src/components/accounts/BalanceTable.tsx @@ -1,5 +1,5 @@ -import { selectAccountSlice, useAppSelector } from "@/store"; -import { selectAccountBalances, selectSortedAccounts } from "@abrechnung/redux"; +import { useAppSelector } from "@/store"; +import { selectAccountBalances, useSortedAccounts } from "@abrechnung/redux"; import { DataGrid, GridColDef, GridToolbar } from "@mui/x-data-grid"; import React from "react"; import { renderCurrency } from "../style/datagrid/renderCurrency"; @@ -12,15 +12,8 @@ interface Props { export const BalanceTable: React.FC = ({ group }) => { const { t } = useTranslation(); - const personalAccounts = useAppSelector((state) => - selectSortedAccounts({ - state: selectAccountSlice(state), - groupId: group.id, - type: "personal", - sortMode: "name", - }) - ); - const balances = useAppSelector((state) => selectAccountBalances({ state, groupId: group.id })); + const personalAccounts = useSortedAccounts(group.id, "name", "personal"); + const balances = useAppSelector((state) => selectAccountBalances(state, group.id)); const tableData = personalAccounts.map((acc) => { return { diff --git a/frontend/apps/web/src/components/accounts/ClearingAccountDetail.tsx b/frontend/apps/web/src/components/accounts/ClearingAccountDetail.tsx index 90c61037..d1a8f5c3 100644 --- a/frontend/apps/web/src/components/accounts/ClearingAccountDetail.tsx +++ b/frontend/apps/web/src/components/accounts/ClearingAccountDetail.tsx @@ -1,7 +1,7 @@ -import { selectAccountBalances, selectGroupCurrencySymbol } from "@abrechnung/redux"; +import { selectAccountBalances, useGroupCurrencySymbol } from "@abrechnung/redux"; import { TableCell, Typography } from "@mui/material"; import React from "react"; -import { selectGroupSlice, useAppSelector } from "@/store"; +import { useAppSelector } from "@/store"; import { ShareSelect } from "../ShareSelect"; import { useTranslation } from "react-i18next"; import { useFormatCurrency } from "@/hooks"; @@ -15,10 +15,8 @@ interface Props { export const ClearingAccountDetail: React.FC = ({ groupId, account }) => { const { t } = useTranslation(); const formatCurrency = useFormatCurrency(); - const currency_symbol = useAppSelector((state) => - selectGroupCurrencySymbol({ state: selectGroupSlice(state), groupId }) - ); - const balances = useAppSelector((state) => selectAccountBalances({ state, groupId })); + const currency_symbol = useGroupCurrencySymbol(groupId); + const balances = useAppSelector((state) => selectAccountBalances(state, groupId)); if (!currency_symbol) { return null; } diff --git a/frontend/apps/web/src/components/groups/GroupMemberSelect.tsx b/frontend/apps/web/src/components/groups/GroupMemberSelect.tsx index 93107c05..cbe597a5 100644 --- a/frontend/apps/web/src/components/groups/GroupMemberSelect.tsx +++ b/frontend/apps/web/src/components/groups/GroupMemberSelect.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Autocomplete, Box, Popper, TextField, TextFieldProps, Typography } from "@mui/material"; import { DisabledTextField } from "../style/DisabledTextField"; import { styled } from "@mui/material/styles"; -import { selectGroupSlice, useAppSelector } from "@/store"; +import { useAppSelector } from "@/store"; import { selectGroupMemberIds, selectGroupMemberIdToUsername } from "@abrechnung/redux"; const StyledAutocompletePopper = styled(Popper)(({ theme }) => ({ @@ -27,10 +27,8 @@ export const GroupMemberSelect: React.FC = ({ className, ...props }) => { - const memberIds = useAppSelector((state) => selectGroupMemberIds({ state: selectGroupSlice(state), groupId })); - const memberIDToUsername = useAppSelector((state) => - selectGroupMemberIdToUsername({ state: selectGroupSlice(state), groupId }) - ); + const memberIds = useAppSelector((state) => selectGroupMemberIds(state, groupId)); + const memberIDToUsername = useAppSelector((state) => selectGroupMemberIdToUsername(state, groupId)); return ( = ({ groupId }) => { const params = useParams(); const accountId = Number(params["id"]); - const group = useAppSelector((state) => selectGroupById({ state: selectGroupSlice(state), groupId })); - const account = useAppSelector((state) => - selectAccountById({ state: selectAccountSlice(state), groupId, accountId }) - ); + const group = useGroup(groupId); + const account = useAccount(groupId, accountId); const query = useQuery(); useTitle( diff --git a/frontend/apps/web/src/pages/accounts/AccountDetail/AccountInfo.tsx b/frontend/apps/web/src/pages/accounts/AccountDetail/AccountInfo.tsx index 3abadaa3..757de454 100644 --- a/frontend/apps/web/src/pages/accounts/AccountDetail/AccountInfo.tsx +++ b/frontend/apps/web/src/pages/accounts/AccountDetail/AccountInfo.tsx @@ -5,15 +5,15 @@ import { TextInput } from "@/components/TextInput"; import { DeleteAccountModal } from "@/components/accounts/DeleteAccountModal"; import { api } from "@/core/api"; import { useFormatCurrency } from "@/hooks"; -import { selectGroupSlice, useAppDispatch, useAppSelector } from "@/store"; +import { useAppDispatch, useAppSelector } from "@/store"; import { getAccountLink, getAccountListLink } from "@/utils"; import { accountEditStarted, discardAccountChange, saveAccount, selectAccountBalances, - selectCurrentUserPermissions, - selectGroupCurrencySymbol, + useCurrentUserPermissions, + useGroupCurrencySymbol, wipAccountUpdated, } from "@abrechnung/redux"; import { Account, AccountValidator } from "@abrechnung/types"; @@ -38,11 +38,9 @@ export const AccountInfo: React.FC = ({ groupId, account }) => { const dispatch = useAppDispatch(); const navigate = useNavigate(); - const permissions = useAppSelector((state) => selectCurrentUserPermissions({ state: state, groupId })); - const currencySymbol = useAppSelector((state) => - selectGroupCurrencySymbol({ state: selectGroupSlice(state), groupId }) - ); - const balances = useAppSelector((state) => selectAccountBalances({ state, groupId })); + const permissions = useCurrentUserPermissions(groupId); + const currencySymbol = useGroupCurrencySymbol(groupId); + const balances = useAppSelector((state) => selectAccountBalances(state, groupId)); const [confirmDeleteDialogOpen, setConfirmDeleteDialogOpen] = React.useState(false); const [showProgress, setShowProgress] = React.useState(false); @@ -127,7 +125,7 @@ export const AccountInfo: React.FC = ({ groupId, account }) => { - {permissions.canWrite && ( + {permissions.can_write && ( <> {account.is_wip ? ( <> diff --git a/frontend/apps/web/src/pages/accounts/Balances.tsx b/frontend/apps/web/src/pages/accounts/Balances.tsx index 192c0140..5c399a3b 100644 --- a/frontend/apps/web/src/pages/accounts/Balances.tsx +++ b/frontend/apps/web/src/pages/accounts/Balances.tsx @@ -2,13 +2,8 @@ import { BalanceTable } from "@/components/accounts/BalanceTable"; import { MobilePaper, ListItemLink } from "@/components/style"; import { useTitle } from "@/core/utils"; import { useFormatCurrency } from "@/hooks"; -import { selectAccountSlice, selectGroupSlice, useAppSelector } from "@/store"; -import { - selectAccountBalances, - selectClearingAccounts, - selectGroupById, - selectSortedAccounts, -} from "@abrechnung/redux"; +import { useAppSelector } from "@/store"; +import { selectAccountBalances, useGroup, useGroupAccounts, useSortedAccounts } from "@abrechnung/redux"; import { TabContext, TabList, TabPanel } from "@mui/lab"; import { Alert, @@ -49,14 +44,10 @@ export const Balances: React.FC = ({ groupId }) => { const isSmallScreen = useMediaQuery(theme.breakpoints.down("sm")); const navigate = useNavigate(); - const group = useAppSelector((state) => selectGroupById({ state: selectGroupSlice(state), groupId })); - const personalAccounts = useAppSelector((state) => - selectSortedAccounts({ state: selectAccountSlice(state), groupId, sortMode: "name", type: "personal" }) - ); - const clearingAccounts = useAppSelector((state) => - selectClearingAccounts({ state: selectAccountSlice(state), groupId }) - ); - const balances = useAppSelector((state) => selectAccountBalances({ state, groupId })); + const group = useGroup(groupId); + const personalAccounts = useSortedAccounts(groupId, "name", "personal"); + const clearingAccounts = useGroupAccounts(groupId, "clearing"); + const balances = useAppSelector((state) => selectAccountBalances(state, groupId)); const [selectedTab, setSelectedTab] = useState("1"); diff --git a/frontend/apps/web/src/pages/accounts/ClearingAccountList/ClearingAccountList.tsx b/frontend/apps/web/src/pages/accounts/ClearingAccountList/ClearingAccountList.tsx index de62f716..36c134a9 100644 --- a/frontend/apps/web/src/pages/accounts/ClearingAccountList/ClearingAccountList.tsx +++ b/frontend/apps/web/src/pages/accounts/ClearingAccountList/ClearingAccountList.tsx @@ -1,5 +1,5 @@ import { AccountSortMode } from "@abrechnung/core"; -import { createAccount, selectCurrentUserPermissions, selectGroupById, selectSortedAccounts } from "@abrechnung/redux"; +import { createAccount, useCurrentUserPermissions, useGroup, useSortedAccounts } from "@abrechnung/redux"; import { Add as AddIcon, Clear as ClearIcon, Search as SearchIcon } from "@mui/icons-material"; import { Alert, @@ -27,7 +27,7 @@ import { TagSelector } from "@/components/TagSelector"; import { DeleteAccountModal } from "@/components/accounts/DeleteAccountModal"; import { MobilePaper } from "@/components/style"; import { useTitle } from "@/core/utils"; -import { selectAccountSlice, selectGroupSlice, useAppDispatch, useAppSelector } from "@/store"; +import { useAppDispatch } from "@/store"; import { getAccountLink } from "@/utils"; import { ClearingAccountListItem } from "./ClearingAccountListItem"; import { useTranslation } from "react-i18next"; @@ -44,25 +44,15 @@ export const ClearingAccountList: React.FC = ({ groupId }) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const navigate = useNavigate(); - const group = useAppSelector((state) => selectGroupById({ state: selectGroupSlice(state), groupId })); + const group = useGroup(groupId); const theme: Theme = useTheme(); const isSmallScreen = useMediaQuery(theme.breakpoints.down("md")); const [searchValue, setSearchValue] = useState(""); const [tagFilter, setTagFilter] = useState(emptyList); const [sortMode, setSortMode] = useState("last_changed"); - const permissions = useAppSelector((state) => selectCurrentUserPermissions({ state: state, groupId })); - const clearingAccounts = useAppSelector((state) => - selectSortedAccounts({ - state: selectAccountSlice(state), - groupId, - type: "clearing", - searchTerm: searchValue, - sortMode, - tags: tagFilter, - wipAtTop: true, - }) - ); + const permissions = useCurrentUserPermissions(groupId); + const clearingAccounts = useSortedAccounts(groupId, sortMode, "clearing", searchValue, true, tagFilter); const [currentPage, setCurrentPage] = useState(0); const shouldShowPagination = clearingAccounts.length > MAX_ITEMS_PER_PAGE; @@ -199,7 +189,7 @@ export const ClearingAccountList: React.FC = ({ groupId }) => { )} - {permissions.canWrite && ( + {permissions.can_write && ( <> = ({ groupId, account, set const dispatch = useAppDispatch(); const navigate = useNavigate(); - const permissions = useAppSelector((state) => selectCurrentUserPermissions({ state: state, groupId })); - const accounts = useAppSelector((state) => - selectAccountIdToAccountMap({ state: selectAccountSlice(state), groupId }) - ); + const permissions = useCurrentUserPermissions(groupId); + const accounts = useAppSelector((state) => selectAccountIdToAccountMap(state, groupId)); if (!permissions || account.type !== "clearing") { return null; @@ -77,7 +75,7 @@ export const ClearingAccountListItem: React.FC = ({ groupId, account, set } /> - {permissions.canWrite && ( + {permissions.can_write && ( diff --git a/frontend/apps/web/src/pages/accounts/PersonalAccountList/PersonalAccountList.tsx b/frontend/apps/web/src/pages/accounts/PersonalAccountList/PersonalAccountList.tsx index cda66d73..06b71552 100644 --- a/frontend/apps/web/src/pages/accounts/PersonalAccountList/PersonalAccountList.tsx +++ b/frontend/apps/web/src/pages/accounts/PersonalAccountList/PersonalAccountList.tsx @@ -1,14 +1,14 @@ import { DeleteAccountModal } from "@/components/accounts/DeleteAccountModal"; import { MobilePaper } from "@/components/style"; import { useTitle } from "@/core/utils"; -import { selectAccountSlice, selectAuthSlice, selectGroupSlice, useAppDispatch, useAppSelector } from "@/store"; +import { useAppDispatch, useAppSelector } from "@/store"; import { AccountSortMode } from "@abrechnung/core"; import { createAccount, selectCurrentUserId, - selectCurrentUserPermissions, - selectGroupById, - selectSortedAccounts, + useCurrentUserPermissions, + useGroup, + useSortedAccounts, } from "@abrechnung/redux"; import { Add as AddIcon, Clear as ClearIcon, Search as SearchIcon } from "@mui/icons-material"; import { @@ -48,25 +48,17 @@ export const PersonalAccountList: React.FC = ({ groupId }) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const navigate = useNavigate(); - const group = useAppSelector((state) => selectGroupById({ state: selectGroupSlice(state), groupId })); + const group = useGroup(groupId); const theme: Theme = useTheme(); const isSmallScreen = useMediaQuery(theme.breakpoints.down("md")); const [searchValue, setSearchValue] = useState(""); const [sortMode, setSortMode] = useState("name"); - const personalAccounts = useAppSelector((state) => - selectSortedAccounts({ - state: selectAccountSlice(state), - groupId, - type: "personal", - searchTerm: searchValue, - sortMode, - }) - ); + const personalAccounts = useSortedAccounts(groupId, sortMode, "personal", searchValue); - const permissions = useAppSelector((state) => selectCurrentUserPermissions({ state, groupId })); - const currentUserId = useAppSelector((state) => selectCurrentUserId({ state: selectAuthSlice(state) })); + const permissions = useCurrentUserPermissions(groupId); + const currentUserId = useAppSelector(selectCurrentUserId); const [currentPage, setCurrentPage] = useState(0); const shouldShowPagination = personalAccounts.length > MAX_ITEMS_PER_PAGE; @@ -193,7 +185,7 @@ export const PersonalAccountList: React.FC = ({ groupId }) => { )} - {permissions.canWrite && ( + {permissions.can_write && ( <> = ({ groupId, currentUserI const dispatch = useAppDispatch(); const navigate = useNavigate(); - const permissions = useAppSelector((state) => selectCurrentUserPermissions({ state: state, groupId })); - const memberIDToUsername = useAppSelector((state) => - selectGroupMemberIdToUsername({ state: selectGroupSlice(state), groupId }) - ); + const permissions = useCurrentUserPermissions(groupId); + const memberIDToUsername = useAppSelector((state) => selectGroupMemberIdToUsername(state, groupId)); if (!permissions || !account) { return ; @@ -75,7 +73,7 @@ export const PersonalAccountListItem: React.FC = ({ groupId, currentUserI secondary={account.description} /> - {permissions.canWrite && ( + {permissions.can_write && ( diff --git a/frontend/apps/web/src/pages/accounts/SettlementPlanDisplay.tsx b/frontend/apps/web/src/pages/accounts/SettlementPlanDisplay.tsx index 67c125f3..1dba0fc1 100644 --- a/frontend/apps/web/src/pages/accounts/SettlementPlanDisplay.tsx +++ b/frontend/apps/web/src/pages/accounts/SettlementPlanDisplay.tsx @@ -1,11 +1,11 @@ import { MobilePaper } from "@/components/style"; -import { selectAccountSlice, selectGroupSlice, useAppDispatch, useAppSelector } from "@/store"; +import { useAppDispatch, useAppSelector } from "@/store"; import { SettlementPlanItem } from "@abrechnung/core"; import { createTransaction, selectAccountIdToAccountMap, - selectGroupCurrencySymbol, selectSettlementPlan, + useGroupCurrencySymbol, } from "@abrechnung/redux"; import { Button, List, ListItem, ListItemSecondaryAction, ListItemText, Typography } from "@mui/material"; import * as React from "react"; @@ -22,13 +22,9 @@ export const SettlementPlanDisplay: React.FC = ({ groupId }) => { const formatCurrency = useFormatCurrency(); const dispatch = useAppDispatch(); const navigate = useNavigate(); - const settlementPlan = useAppSelector((state) => selectSettlementPlan({ state, groupId })); - const currencySymbol = useAppSelector((state) => - selectGroupCurrencySymbol({ state: selectGroupSlice(state), groupId }) - ); - const accountMap = useAppSelector((state) => - selectAccountIdToAccountMap({ state: selectAccountSlice(state), groupId }) - ); + const settlementPlan = useAppSelector((state) => selectSettlementPlan(state, groupId)); + const currencySymbol = useGroupCurrencySymbol(groupId); + const accountMap = useAppSelector((state) => selectAccountIdToAccountMap(state, groupId)); if (!currencySymbol) { return ; diff --git a/frontend/apps/web/src/pages/auth/Login.tsx b/frontend/apps/web/src/pages/auth/Login.tsx index 8bb016e4..2d9c6bc2 100644 --- a/frontend/apps/web/src/pages/auth/Login.tsx +++ b/frontend/apps/web/src/pages/auth/Login.tsx @@ -18,7 +18,7 @@ import { } from "@mui/material"; import { LockOutlined } from "@mui/icons-material"; import { z } from "zod"; -import { useAppDispatch, useAppSelector, selectAuthSlice } from "@/store"; +import { useAppDispatch, useAppSelector } from "@/store"; import { selectIsAuthenticated, login } from "@abrechnung/redux"; import { toFormikValidationSchema } from "@abrechnung/utils"; import { useTranslation } from "react-i18next"; @@ -32,7 +32,7 @@ type FormValues = z.infer; export const Login: React.FC = () => { const { t } = useTranslation(); - const isLoggedIn = useAppSelector((state) => selectIsAuthenticated({ state: selectAuthSlice(state) })); + const isLoggedIn = useAppSelector(selectIsAuthenticated); const dispatch = useAppDispatch(); const query = useQuery(); const navigate = useNavigate(); diff --git a/frontend/apps/web/src/pages/auth/Logout.tsx b/frontend/apps/web/src/pages/auth/Logout.tsx index 0d57992c..4a220e95 100644 --- a/frontend/apps/web/src/pages/auth/Logout.tsx +++ b/frontend/apps/web/src/pages/auth/Logout.tsx @@ -1,13 +1,13 @@ import React, { useEffect } from "react"; import { Loading } from "@/components/style/Loading"; -import { useAppDispatch, useAppSelector, selectAuthSlice } from "@/store"; +import { useAppDispatch, useAppSelector } from "@/store"; import { logout, selectIsAuthenticated } from "@abrechnung/redux"; import { api } from "@/core/api"; import { Navigate } from "react-router-dom"; export const Logout: React.FC = () => { const dispatch = useAppDispatch(); - const isAuthenticated = useAppSelector((state) => selectIsAuthenticated({ state: selectAuthSlice(state) })); + const isAuthenticated = useAppSelector(selectIsAuthenticated); useEffect(() => { if (isAuthenticated) { diff --git a/frontend/apps/web/src/pages/auth/Register.tsx b/frontend/apps/web/src/pages/auth/Register.tsx index ff0a7013..d56473d8 100644 --- a/frontend/apps/web/src/pages/auth/Register.tsx +++ b/frontend/apps/web/src/pages/auth/Register.tsx @@ -20,7 +20,7 @@ import { z } from "zod"; import { Loading } from "@/components/style/Loading"; import { api } from "@/core/api"; import { useQuery, useTitle } from "@/core/utils"; -import { selectAuthSlice, useAppSelector } from "@/store"; +import { useAppSelector } from "@/store"; import { toFormikValidationSchema } from "@abrechnung/utils"; import { useTranslation } from "react-i18next"; import i18n from "@/i18n"; @@ -41,7 +41,7 @@ type FormValues = z.infer; export const Register: React.FC = () => { const { t } = useTranslation(); - const loggedIn = useAppSelector((state) => selectIsAuthenticated({ state: selectAuthSlice(state) })); + const loggedIn = useAppSelector(selectIsAuthenticated); const [loading, setLoading] = useState(true); const query = useQuery(); const navigate = useNavigate(); diff --git a/frontend/apps/web/src/pages/auth/RequestPasswordRecovery.tsx b/frontend/apps/web/src/pages/auth/RequestPasswordRecovery.tsx index a8347041..7928a494 100644 --- a/frontend/apps/web/src/pages/auth/RequestPasswordRecovery.tsx +++ b/frontend/apps/web/src/pages/auth/RequestPasswordRecovery.tsx @@ -6,7 +6,7 @@ import React, { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { z } from "zod"; import { api } from "@/core/api"; -import { selectAuthSlice, useAppSelector } from "@/store"; +import { useAppSelector } from "@/store"; import { useTranslation } from "react-i18next"; import { useTitle } from "@/core/utils"; @@ -17,7 +17,7 @@ type FormSchema = z.infer; export const RequestPasswordRecovery: React.FC = () => { const { t } = useTranslation(); - const isLoggedIn = useAppSelector((state) => selectIsAuthenticated({ state: selectAuthSlice(state) })); + const isLoggedIn = useAppSelector(selectIsAuthenticated); const [status, setStatus] = useState("initial"); const [error, setError] = useState(null); const navigate = useNavigate(); diff --git a/frontend/apps/web/src/pages/groups/Group.tsx b/frontend/apps/web/src/pages/groups/Group.tsx index 84b0aca3..bab83cbc 100644 --- a/frontend/apps/web/src/pages/groups/Group.tsx +++ b/frontend/apps/web/src/pages/groups/Group.tsx @@ -1,12 +1,11 @@ import { fetchGroupDependencies, selectGroupAccountsStatus, - selectGroupById, - selectGroupExists, selectGroupMemberStatus, selectGroupTransactionsStatus, subscribe, unsubscribe, + useGroup, } from "@abrechnung/redux"; import React, { Suspense } from "react"; import { Navigate, Route, Routes, useParams } from "react-router-dom"; @@ -14,7 +13,7 @@ import { toast } from "react-toastify"; import { Balances } from "../accounts/Balances"; import { Loading } from "@/components/style/Loading"; import { api, ws } from "@/core/api"; -import { selectAccountSlice, selectGroupSlice, selectTransactionSlice, useAppDispatch, useAppSelector } from "@/store"; +import { useAppDispatch, useAppSelector } from "@/store"; import { AccountDetail } from "../accounts/AccountDetail"; import { PersonalAccountList } from "../accounts/PersonalAccountList"; import { ClearingAccountList } from "../accounts/ClearingAccountList"; @@ -31,17 +30,11 @@ export const Group: React.FC = () => { const params = useParams(); const dispatch = useAppDispatch(); const groupId = Number(params["groupId"]); - const group = useAppSelector((state) => selectGroupById({ state: selectGroupSlice(state), groupId })); - const groupExists = useAppSelector((state) => selectGroupExists({ state: selectGroupSlice(state), groupId })); - const transactionStatus = useAppSelector((state) => - selectGroupTransactionsStatus({ state: selectTransactionSlice(state), groupId }) - ); - const accountStatus = useAppSelector((state) => - selectGroupAccountsStatus({ state: selectAccountSlice(state), groupId }) - ); - const groupMemberStatus = useAppSelector((state) => - selectGroupMemberStatus({ state: selectGroupSlice(state), groupId }) - ); + const group = useGroup(groupId); + const groupExists = group !== undefined; + const transactionStatus = useAppSelector((state) => selectGroupTransactionsStatus(state, groupId)); + const accountStatus = useAppSelector((state) => selectGroupAccountsStatus(state, groupId)); + const groupMemberStatus = useAppSelector((state) => selectGroupMemberStatus(state, groupId)); React.useEffect(() => { if (!groupExists) { diff --git a/frontend/apps/web/src/pages/groups/GroupInvites.tsx b/frontend/apps/web/src/pages/groups/GroupInvites.tsx index be679bfe..b39dfe0d 100644 --- a/frontend/apps/web/src/pages/groups/GroupInvites.tsx +++ b/frontend/apps/web/src/pages/groups/GroupInvites.tsx @@ -1,14 +1,14 @@ import { deleteGroupInvite, fetchGroupInvites, - selectCurrentUserPermissions, - selectGroupById, selectGroupInviteStatus, selectGroupInvites, selectGroupMembers, selectIsGuestUser, subscribe, unsubscribe, + useCurrentUserPermissions, + useGroup, } from "@abrechnung/redux"; import { Add, ContentCopy, Delete } from "@mui/icons-material"; import { @@ -29,7 +29,7 @@ import { Loading } from "@/components/style/Loading"; import { MobilePaper } from "@/components/style"; import { api, ws } from "@/core/api"; import { useTitle } from "@/core/utils"; -import { selectAuthSlice, selectGroupSlice, useAppDispatch, useAppSelector } from "@/store"; +import { useAppDispatch, useAppSelector } from "@/store"; import { useTranslation } from "react-i18next"; import { Navigate } from "react-router-dom"; @@ -41,15 +41,13 @@ export const GroupInvites: React.FC = ({ groupId }) => { const { t } = useTranslation(); const [showModal, setShowModal] = useState(false); const dispatch = useAppDispatch(); - const group = useAppSelector((state) => selectGroupById({ state: selectGroupSlice(state), groupId })); - const invites = useAppSelector((state) => selectGroupInvites({ state: selectGroupSlice(state), groupId })); - const members = useAppSelector((state) => selectGroupMembers({ state: selectGroupSlice(state), groupId })); - const permissions = useAppSelector((state) => selectCurrentUserPermissions({ state: state, groupId })); - const invitesLoadingStatus = useAppSelector((state) => - selectGroupInviteStatus({ state: selectGroupSlice(state), groupId }) - ); + const group = useGroup(groupId); + const invites = useAppSelector((state) => selectGroupInvites(state, groupId)); + const members = useAppSelector((state) => selectGroupMembers(state, groupId)); + const permissions = useCurrentUserPermissions(groupId); + const invitesLoadingStatus = useAppSelector((state) => selectGroupInviteStatus(state, groupId)); - const isGuest = useAppSelector((state) => selectIsGuestUser({ state: selectAuthSlice(state) })); + const isGuest = useAppSelector(selectIsGuestUser); useTitle(t("groups.invites.tabTitle", "", { groupName: group?.name })); @@ -135,7 +133,7 @@ export const GroupInvites: React.FC = ({ groupId }) => { } /> - {permissions.canWrite && ( + {permissions.can_write && ( = ({ groupId }) => { )} )} - {permissions.canWrite && !isGuest && ( + {permissions.can_write && !isGuest && ( <> setShowModal(true)}> diff --git a/frontend/apps/web/src/pages/groups/GroupList.tsx b/frontend/apps/web/src/pages/groups/GroupList.tsx index a6246796..7aae0262 100644 --- a/frontend/apps/web/src/pages/groups/GroupList.tsx +++ b/frontend/apps/web/src/pages/groups/GroupList.tsx @@ -14,7 +14,7 @@ import { import { Add, Delete } from "@mui/icons-material"; import { MobilePaper, ListItemLink } from "@/components/style"; import { selectIsGuestUser, selectGroups } from "@abrechnung/redux"; -import { useAppSelector, selectGroupSlice, selectAuthSlice } from "@/store"; +import { useAppSelector } from "@/store"; import { useTitle } from "@/core/utils"; import { useTranslation } from "react-i18next"; import { Group } from "@abrechnung/api"; @@ -24,8 +24,8 @@ export const GroupList: React.FC = () => { useTitle(t("groups.list.tabTitle")); const [showGroupCreationModal, setShowGroupCreationModal] = useState(false); const [groupToDelete, setGroupToDelete] = useState(null); - const groups = useAppSelector((state) => selectGroups({ state: selectGroupSlice(state) })); - const isGuest = useAppSelector((state) => selectIsGuestUser({ state: selectAuthSlice(state) })); + const groups = useAppSelector((state) => selectGroups(state)); + const isGuest = useAppSelector(selectIsGuestUser); const openGroupDeletionModal = (groupID: number) => { const g = groups.find((group) => group.id === groupID); diff --git a/frontend/apps/web/src/pages/groups/GroupLog.tsx b/frontend/apps/web/src/pages/groups/GroupLog.tsx index ac9cab58..925ba558 100644 --- a/frontend/apps/web/src/pages/groups/GroupLog.tsx +++ b/frontend/apps/web/src/pages/groups/GroupLog.tsx @@ -1,11 +1,11 @@ import { fetchGroupLog, - selectGroupById, selectGroupLogStatus, selectGroupLogs, selectGroupMembers, subscribe, unsubscribe, + useGroup, } from "@abrechnung/redux"; import { Button, @@ -25,7 +25,7 @@ import { Loading } from "@/components/style/Loading"; import { MobilePaper } from "@/components/style"; import { api, ws } from "@/core/api"; import { useTitle } from "@/core/utils"; -import { selectGroupSlice, useAppDispatch, useAppSelector } from "@/store"; +import { useAppDispatch, useAppSelector } from "@/store"; import { useTranslation } from "react-i18next"; interface Props { @@ -35,12 +35,10 @@ interface Props { export const GroupLog: React.FC = ({ groupId }) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const group = useAppSelector((state) => selectGroupById({ state: selectGroupSlice(state), groupId })); - const members = useAppSelector((state) => selectGroupMembers({ state: selectGroupSlice(state), groupId })); - const logEntries = useAppSelector((state) => selectGroupLogs({ state: selectGroupSlice(state), groupId })); - const logLoadingStatus = useAppSelector((state) => - selectGroupLogStatus({ state: selectGroupSlice(state), groupId }) - ); + const group = useGroup(groupId); + const members = useAppSelector((state) => selectGroupMembers(state, groupId)); + const logEntries = useAppSelector((state) => selectGroupLogs(state, groupId)); + const logLoadingStatus = useAppSelector((state) => selectGroupLogStatus(state, groupId)); const [showAllLogs, setShowAllLogs] = useState(false); const [message, setMessage] = useState(""); diff --git a/frontend/apps/web/src/pages/groups/GroupMemberList.tsx b/frontend/apps/web/src/pages/groups/GroupMemberList.tsx index 224917dd..8c69fe91 100644 --- a/frontend/apps/web/src/pages/groups/GroupMemberList.tsx +++ b/frontend/apps/web/src/pages/groups/GroupMemberList.tsx @@ -1,10 +1,10 @@ import { GroupMember } from "@abrechnung/api"; import { selectCurrentUserId, - selectCurrentUserPermissions, - selectGroupById, selectGroupMembers, updateGroupMemberPrivileges, + useCurrentUserPermissions, + useGroup, } from "@abrechnung/redux"; import { Edit } from "@mui/icons-material"; import { @@ -30,7 +30,7 @@ import { toast } from "react-toastify"; import { MobilePaper } from "@/components/style"; import { api } from "@/core/api"; import { useTitle } from "@/core/utils"; -import { selectAuthSlice, selectGroupSlice, useAppDispatch, useAppSelector } from "@/store"; +import { useAppDispatch, useAppSelector } from "@/store"; import { useTranslation } from "react-i18next"; import { Navigate } from "react-router-dom"; @@ -47,10 +47,10 @@ type FormValues = { export const GroupMemberList: React.FC = ({ groupId }) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const currentUserId = useAppSelector((state) => selectCurrentUserId({ state: selectAuthSlice(state) })); - const members = useAppSelector((state) => selectGroupMembers({ state: selectGroupSlice(state), groupId })); - const group = useAppSelector((state) => selectGroupById({ state: selectGroupSlice(state), groupId })); - const permissions = useAppSelector((state) => selectCurrentUserPermissions({ state: state, groupId })); + const currentUserId = useAppSelector(selectCurrentUserId); + const members = useAppSelector((state) => selectGroupMembers(state, groupId)); + const group = useGroup(groupId); + const permissions = useCurrentUserPermissions(groupId); const [memberToEdit, setMemberToEdit] = useState(undefined); @@ -162,7 +162,7 @@ export const GroupMemberList: React.FC = ({ groupId }) => { } /> - {(permissions.isOwner || permissions.canWrite) && ( + {(permissions.is_owner || permissions.can_write) && ( openEditMemberModal(member.user_id)}> diff --git a/frontend/apps/web/src/pages/groups/GroupSettings.tsx b/frontend/apps/web/src/pages/groups/GroupSettings.tsx index 13475d3b..ef867ac9 100644 --- a/frontend/apps/web/src/pages/groups/GroupSettings.tsx +++ b/frontend/apps/web/src/pages/groups/GroupSettings.tsx @@ -1,4 +1,4 @@ -import { leaveGroup, selectCurrentUserPermissions, selectGroupById, updateGroup } from "@abrechnung/redux"; +import { leaveGroup, updateGroup, useCurrentUserPermissions, useGroup } from "@abrechnung/redux"; import { Cancel, Edit, Save } from "@mui/icons-material"; import { Alert, @@ -22,7 +22,7 @@ import { DisabledFormControlLabel, DisabledTextField } from "@/components/style/ import { MobilePaper } from "@/components/style"; import { api } from "@/core/api"; import { useTitle } from "@/core/utils"; -import { selectGroupSlice, useAppDispatch, useAppSelector } from "@/store"; +import { useAppDispatch } from "@/store"; import { toFormikValidationSchema } from "@abrechnung/utils"; import { useTranslation } from "react-i18next"; @@ -46,8 +46,8 @@ export const GroupSettings: React.FC = ({ groupId }) => { const navigate = useNavigate(); const dispatch = useAppDispatch(); - const group = useAppSelector((state) => selectGroupById({ state: selectGroupSlice(state), groupId })); - const permissions = useAppSelector((state) => selectCurrentUserPermissions({ state: state, groupId })); + const group = useGroup(groupId); + const permissions = useCurrentUserPermissions(groupId); const [isEditing, setIsEditing] = useState(false); @@ -106,9 +106,9 @@ export const GroupSettings: React.FC = ({ groupId }) => { return ( - {permissions.isOwner ? ( + {permissions.is_owner ? ( {t("groups.settings.ownerDisclaimer")} - ) : !permissions.canWrite ? ( + ) : !permissions.can_write ? ( {t("groups.settings.readAccessDisclaimer")} ) : null} @@ -134,7 +134,7 @@ export const GroupSettings: React.FC = ({ groupId }) => { type="text" label={t("common.name")} name="name" - disabled={!permissions.canWrite || !isEditing} + disabled={!permissions.can_write || !isEditing} onBlur={handleBlur} onChange={handleChange} value={values.name} @@ -147,7 +147,7 @@ export const GroupSettings: React.FC = ({ groupId }) => { type="text" name="description" label={t("common.description")} - disabled={!permissions.canWrite || !isEditing} + disabled={!permissions.can_write || !isEditing} onBlur={handleBlur} onChange={handleChange} value={values.description} @@ -160,7 +160,7 @@ export const GroupSettings: React.FC = ({ groupId }) => { type="text" name="currency_symbol" label={t("common.currency")} - disabled={!permissions.canWrite || !isEditing} + disabled={!permissions.can_write || !isEditing} onBlur={handleBlur} onChange={handleChange} value={values.currency_symbol} @@ -173,7 +173,7 @@ export const GroupSettings: React.FC = ({ groupId }) => { type="text" name="terms" label={t("groups.settings.terms")} - disabled={!permissions.canWrite || !isEditing} + disabled={!permissions.can_write || !isEditing} onBlur={handleBlur} onChange={handleChange} value={values.terms} @@ -183,7 +183,7 @@ export const GroupSettings: React.FC = ({ groupId }) => { control={ = ({ groupId }) => { {isSubmitting && }
- {permissions.canWrite && isEditing && ( + {permissions.can_write && isEditing && ( <> )} - {permissions.canWrite && !isEditing && ( + {permissions.can_write && !isEditing && (