From c1e24b7a899db88dbb8af9a016fd293d1a141127 Mon Sep 17 00:00:00 2001 From: Kilerd Chan Date: Thu, 11 Jul 2024 14:51:29 +0800 Subject: [PATCH 1/7] feat: #357 introduce jotai and refactor error fetcher and pagination --- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 21 +++++++++ frontend/src/App.tsx | 15 ++++--- frontend/src/components/ErrorBox.tsx | 23 +++++----- frontend/src/index.tsx | 2 +- frontend/src/pages/Home.tsx | 4 +- frontend/src/states/account.ts | 2 +- frontend/src/states/basic.ts | 2 +- frontend/src/states/commodity.ts | 2 +- frontend/src/states/errors.ts | 64 +++++++++------------------- frontend/src/states/index.ts | 2 - frontend/src/states/journals.ts | 2 +- 12 files changed, 68 insertions(+), 72 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index d78fdec2..da0ef330 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,6 +27,7 @@ "framer-motion": "^11.1.9", "i18next": "^23.11.4", "i18next-http-backend": "^2.5.1", + "jotai": "^2.9.0", "lodash-es": "^4.17.21", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index ede50b1a..f9c854df 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -77,6 +77,9 @@ dependencies: i18next-http-backend: specifier: ^2.5.1 version: 2.5.1 + jotai: + specifier: ^2.9.0 + version: 2.9.0(@types/react@18.3.2)(react@18.3.1) lodash-es: specifier: ^4.17.21 version: 4.17.21 @@ -2257,6 +2260,7 @@ packages: /@humanwhocodes/config-array@0.11.14: resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead dependencies: '@humanwhocodes/object-schema': 2.0.3 debug: 4.3.4 @@ -2270,6 +2274,7 @@ packages: /@humanwhocodes/object-schema@2.0.3: resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead /@isaacs/cliui@8.0.2: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} @@ -8177,6 +8182,22 @@ packages: hasBin: true dev: false + /jotai@2.9.0(@types/react@18.3.2)(react@18.3.1): + resolution: {integrity: sha512-MioTpMvR78IGfJ+W8EwQj3kwTkb+u0reGnTyg3oJZMWK9rK9v8NBSC9Rhrg9jrrFYA6bGZtzJa96zsuAYF6W3w==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=17.0.0' + react: '>=17.0.0' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + dependencies: + '@types/react': 18.3.2 + react: 18.3.1 + dev: false + /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f829f5f3..b0cf3985 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -29,11 +29,12 @@ import { useAppDispatch, useAppSelector } from './states'; import { accountsSlice, fetchAccounts } from './states/account'; import { basicInfoSlice, fetchBasicInfo, reloadLedger } from './states/basic'; import { fetchCommodities } from './states/commodity'; -import { fetchError } from './states/errors'; import { journalsSlice } from './states/journals'; import { useSWRConfig } from 'swr'; import { createStyles } from '@mantine/emotion'; import { Router } from './router'; +import { useAtom, useAtomValue, useSetAtom } from 'jotai'; +import { errorAtom, errorCountAtom, errorsFetcher } from './states/errors'; const useStyles = createStyles((theme, _, u) => ({ onlineIcon: { @@ -207,6 +208,9 @@ export default function App() { const [opened] = useDisclosure(); const isMobile = useMediaQuery('(max-width: 768px)'); + const errorsCount = useAtomValue(errorCountAtom); + const refreshErrors = useSetAtom(errorsFetcher); + useEffect(() => { if (i18n.language !== lang) { i18n.changeLanguage(lang); @@ -214,7 +218,6 @@ export default function App() { }, [i18n, lang]); useEffect(() => { - dispatch(fetchError(1)); dispatch(fetchCommodities()); dispatch(fetchBasicInfo()); dispatch(fetchAccounts()); @@ -235,8 +238,8 @@ export default function App() { autoClose: 3000, }); mutate('/api/for-new-transaction'); + refreshErrors(); dispatch(fetchBasicInfo()); - dispatch(fetchError(1)); dispatch(fetchCommodities()); dispatch(accountsSlice.actions.clear()); dispatch(journalsSlice.actions.clear()); @@ -279,8 +282,6 @@ export default function App() { dispatch(reloadLedger()); }; - const { total_number } = useAppSelector((state) => state.errors); - const mainLinks = links.map((link) => ( {t('NAV_HOME')} - {(total_number ?? 0) > 0 && ( + {errorsCount > 0 && ( - {total_number ?? 0} + {errorsCount} )} diff --git a/frontend/src/components/ErrorBox.tsx b/frontend/src/components/ErrorBox.tsx index 0a8d67d9..7d7ee899 100644 --- a/frontend/src/components/ErrorBox.tsx +++ b/frontend/src/components/ErrorBox.tsx @@ -1,29 +1,26 @@ import { Anchor, Button, Group, Modal, Pagination, Stack, Text, Textarea } from '@mantine/core'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { LoadingState } from '../rest-model'; -import { useAppDispatch, useAppSelector } from '../states'; -import { fetchError, LedgerError } from '../states/errors'; +import { errorAtom, errorPageAtom, LedgerError } from '../states/errors'; import { ErrorsSkeleton } from './skeletons/errorsSkeleton'; +import { useAtomValue, useSetAtom } from 'jotai'; export default function ErrorBox() { const { t } = useTranslation(); const [isOpen, setIsOpen] = useState(false); - const dispatch = useAppDispatch(); - const { items, total_page, status } = useAppSelector((state) => state.errors); - - const [page, setPage] = useState(1); - const [selectError, setSelectError] = useState(null); const [selectErrorContent, setSelectErrorContent] = useState(''); - if (status === LoadingState.Loading || status === LoadingState.NotReady) { + const errors = useAtomValue(errorAtom); + const setErrorPage = useSetAtom(errorPageAtom); + + console.log('jotai error', errors); + if (errors.state === 'loading' || errors.state === 'hasError') { return ; } const handlePageChange = (newPage: number) => { - setPage(newPage); - dispatch(fetchError(newPage)); + setErrorPage(newPage); }; const toggleError = (error: LedgerError) => { @@ -79,14 +76,14 @@ export default function ErrorBox() { - {items.map((error, idx) => ( + {errors.data.records.map((error, idx) => ( toggleError(error)}> {t(`ERROR.${error.error_type}`)} ))} - + diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index b6ff477f..1db6199d 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -21,7 +21,7 @@ import { TransactionEditModal } from './components/modals/TransactionEditModal'; import { MantineEmotionProvider } from '@mantine/emotion'; // @ts-ignore -export const fetcher = (...args) => axiosInstance.get(...args).then((res) => res.data.data); +export const fetcher = (...args) => axiosInstance.get(...args).then((res) => res.data.data as T); const development: boolean = !process.env.NODE_ENV || process.env.NODE_ENV === 'development'; const backendUri: string = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000'; diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 1f653639..497a333d 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -10,9 +10,11 @@ import { useAppSelector } from '../states'; import ReportGraph from '../components/ReportGraph'; import { Heading } from '../components/basic/Heading'; import { useDocumentTitle } from '@mantine/hooks'; +import { useAtomValue } from 'jotai'; +import { errorCountAtom } from '../states/errors'; function Home() { - const error_total_number = useAppSelector((state) => state.errors.total_number); + const error_total_number = useAtomValue(errorCountAtom); const ledgerTitle = useAppSelector((state) => state.basic.title ?? 'Zhang Accounting'); useDocumentTitle(`Dashboard - ${ledgerTitle}`); diff --git a/frontend/src/states/account.ts b/frontend/src/states/account.ts index d101c98b..7b0eb4dc 100644 --- a/frontend/src/states/account.ts +++ b/frontend/src/states/account.ts @@ -6,7 +6,7 @@ import AccountTrie from '../utils/AccountTrie'; import { groupBy } from 'lodash-es'; export const fetchAccounts = createAsyncThunk('accounts/fetch', async (thunkApi) => { - const ret = await fetcher(`/api/accounts`); + const ret = await fetcher(`/api/accounts`); return ret; }); diff --git a/frontend/src/states/basic.ts b/frontend/src/states/basic.ts index a0ca7ac9..57cebcc6 100644 --- a/frontend/src/states/basic.ts +++ b/frontend/src/states/basic.ts @@ -3,7 +3,7 @@ import { axiosInstance, fetcher } from '..'; import { LoadingState } from '../rest-model'; export const fetchBasicInfo = createAsyncThunk('basic/fetch', async (thunkApi) => { - const ret = await fetcher(`/api/info`); + const ret = await fetcher(`/api/info`); return ret; }); diff --git a/frontend/src/states/commodity.ts b/frontend/src/states/commodity.ts index a2832c9a..6eddbd2f 100644 --- a/frontend/src/states/commodity.ts +++ b/frontend/src/states/commodity.ts @@ -4,7 +4,7 @@ import { fetcher } from '..'; import { CommodityListItem, LoadingState } from '../rest-model'; export const fetchCommodities = createAsyncThunk('commodities/fetch', async (thunkApi) => { - const ret = await fetcher(`/api/commodities`); + const ret = await fetcher(`/api/commodities`); return ret; }); diff --git a/frontend/src/states/errors.ts b/frontend/src/states/errors.ts index 63433b16..23e671d9 100644 --- a/frontend/src/states/errors.ts +++ b/frontend/src/states/errors.ts @@ -1,55 +1,31 @@ -import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import { fetcher } from '..'; -import { LoadingState, SpanInfo } from '../rest-model'; - -export enum LedgerErrorType { - AccountBalanceCheckError = 'AccountBalanceCheckError', - AccountDoesNotExist = 'AccountDoesNotExist', - AccountClosed = 'AccountClosed', - CommodityDoesNotDefine = 'CommodityDoesNotDefine', - TransactionHasMultipleImplicitPosting = 'TransactionHasMultipleImplicitPosting', -} +import { Pageable, SpanInfo } from '../rest-model'; +import { atomWithRefresh, loadable } from 'jotai/utils'; +import { atom } from 'jotai'; export interface LedgerError { id: string; span: SpanInfo; - error_type: LedgerErrorType; + error_type: string; metas: { [key: string]: string }; } -export const fetchError = createAsyncThunk('errors/fetch', async (page: number, thunkApi) => { - const ret = await fetcher(`/api/errors?page=${page}`); - return ret; -}); +/** + * the page to current error box + */ +export const errorPageAtom = atom(1); -interface ErrorState { - total_number: number; - total_page: number; - items: LedgerError[]; - status: LoadingState; -} - -const initialState: ErrorState = { - total_number: 0, - total_page: 1, - items: [], - status: LoadingState.NotReady, -}; - -export const errorsSlice = createSlice({ - name: 'errors', - initialState, - reducers: {}, - extraReducers: (builder) => { - builder.addCase(fetchError.pending, (state, action) => { - state.status = LoadingState.Loading; - }); +export const errorsFetcher = atomWithRefresh(async (get) => { + const page = get(errorPageAtom); + return await fetcher>(`/api/errors?page=${page}&size=10`); +}); +export const errorAtom = loadable(errorsFetcher); - builder.addCase(fetchError.fulfilled, (state, action) => { - state.status = LoadingState.Success; - state.total_number = action.payload.total_count; - state.total_page = action.payload.total_page; - state.items = action.payload.records; - }); - }, +export const errorCountAtom = atom((get) => { + const errors = get(errorAtom); + if (errors.state === 'hasError' || errors.state === 'loading') { + return 0; + } else { + return errors.data.total_count; + } }); diff --git a/frontend/src/states/index.ts b/frontend/src/states/index.ts index 58293dbf..4fae6b04 100644 --- a/frontend/src/states/index.ts +++ b/frontend/src/states/index.ts @@ -3,13 +3,11 @@ import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; import { accountsSlice } from './account'; import { basicInfoSlice } from './basic'; import { commoditiesSlice } from './commodity'; -import { errorsSlice } from './errors'; import { journalsSlice } from './journals'; export const store = configureStore({ reducer: { basic: basicInfoSlice.reducer, - errors: errorsSlice.reducer, commodities: commoditiesSlice.reducer, accounts: accountsSlice.reducer, journals: journalsSlice.reducer, diff --git a/frontend/src/states/journals.ts b/frontend/src/states/journals.ts index 116efce8..69b3922e 100644 --- a/frontend/src/states/journals.ts +++ b/frontend/src/states/journals.ts @@ -6,7 +6,7 @@ import { LoadingState } from '../rest-model'; export const fetchJournals = createAsyncThunk('journals/fetch', async (keyword: string, { getState }) => { const current_page = (getState() as RootState).journals.current_page; const url = keyword.trim() === '' ? `/api/journals?page=${current_page}` : `/api/journals?page=${current_page}&keyword=${keyword.trim()}`; - const ret = await fetcher(url); + const ret = await fetcher(url); return ret; }); From cccdcd68f66a111639d45507d5b84d42ca6b0e8c Mon Sep 17 00:00:00 2001 From: Kilerd Chan Date: Thu, 11 Jul 2024 14:59:43 +0800 Subject: [PATCH 2/7] perf: remove useless console log --- frontend/src/App.tsx | 4 ++-- frontend/src/components/AccountBalanceCheckLine.tsx | 1 - frontend/src/components/ErrorBox.tsx | 1 - frontend/src/components/basic/LoadingComponent.tsx | 1 - frontend/src/components/modals/TransactionEditModal.tsx | 2 +- frontend/src/pages/SingleAccount.tsx | 2 -- 6 files changed, 3 insertions(+), 8 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b0cf3985..7fe25253 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -33,8 +33,8 @@ import { journalsSlice } from './states/journals'; import { useSWRConfig } from 'swr'; import { createStyles } from '@mantine/emotion'; import { Router } from './router'; -import { useAtom, useAtomValue, useSetAtom } from 'jotai'; -import { errorAtom, errorCountAtom, errorsFetcher } from './states/errors'; +import { useAtomValue, useSetAtom } from 'jotai'; +import { errorCountAtom, errorsFetcher } from './states/errors'; const useStyles = createStyles((theme, _, u) => ({ onlineIcon: { diff --git a/frontend/src/components/AccountBalanceCheckLine.tsx b/frontend/src/components/AccountBalanceCheckLine.tsx index 91180472..9e69ef7c 100644 --- a/frontend/src/components/AccountBalanceCheckLine.tsx +++ b/frontend/src/components/AccountBalanceCheckLine.tsx @@ -18,7 +18,6 @@ export default function AccountBalanceCheckLine({ currentAmount, commodity, acco const dispatch = useAppDispatch(); const accountItems = [...useAppSelector(getAccountSelectItems())]; - console.log('accountItems', accountItems); const onSave = async () => { try { diff --git a/frontend/src/components/ErrorBox.tsx b/frontend/src/components/ErrorBox.tsx index 7d7ee899..ebdc6da6 100644 --- a/frontend/src/components/ErrorBox.tsx +++ b/frontend/src/components/ErrorBox.tsx @@ -15,7 +15,6 @@ export default function ErrorBox() { const errors = useAtomValue(errorAtom); const setErrorPage = useSetAtom(errorPageAtom); - console.log('jotai error', errors); if (errors.state === 'loading' || errors.state === 'hasError') { return ; } diff --git a/frontend/src/components/basic/LoadingComponent.tsx b/frontend/src/components/basic/LoadingComponent.tsx index 216547aa..d177fa81 100644 --- a/frontend/src/components/basic/LoadingComponent.tsx +++ b/frontend/src/components/basic/LoadingComponent.tsx @@ -11,7 +11,6 @@ interface Props { export default function LoadingComponent(props: Props) { const { data, error } = useSWR(props.url, fetcher); - console.log('loading', props.url, data); if (error) return
failed to load
; if (!data) return <>{props.skeleton}; return <>{props.render(data ?? [])}; diff --git a/frontend/src/components/modals/TransactionEditModal.tsx b/frontend/src/components/modals/TransactionEditModal.tsx index 94e18000..9cf4281c 100644 --- a/frontend/src/components/modals/TransactionEditModal.tsx +++ b/frontend/src/components/modals/TransactionEditModal.tsx @@ -32,7 +32,7 @@ export const TransactionEditModal = ({ message: error?.response?.data ?? '', autoClose: false, }); - console.log(error); + console.error(error); }); }; return ( diff --git a/frontend/src/pages/SingleAccount.tsx b/frontend/src/pages/SingleAccount.tsx index e5da847d..28cbed27 100644 --- a/frontend/src/pages/SingleAccount.tsx +++ b/frontend/src/pages/SingleAccount.tsx @@ -37,8 +37,6 @@ function SingleAccount() { const { data: account, error } = useSWR(`/api/accounts/${accountName}`, fetcher); const { data: account_balance_data, error: account_balance_error } = useSWR(`/api/accounts/${accountName}/balances`, fetcher); - console.log('account data', account); - console.log('account balance data', account_balance_data); const ledgerTitle = useAppSelector((state) => state.basic.title ?? 'Zhang Accounting'); useDocumentTitle(`${accountName} | Accounts - ${ledgerTitle}`); From eaef368a98dde08709c8f8759d5125dc8d321a53 Mon Sep 17 00:00:00 2001 From: Kilerd Chan Date: Thu, 11 Jul 2024 15:15:42 +0800 Subject: [PATCH 3/7] feat: add help text for healthy ledger --- frontend/public/locales/en/translation.json | 3 ++- frontend/public/locales/zh/translation.json | 3 ++- frontend/src/assets/joyride.svg | 1 + frontend/src/assets/loading.svg | 1 + frontend/src/components/ErrorBox.tsx | 30 ++++++++++++++------- 5 files changed, 26 insertions(+), 12 deletions(-) create mode 100644 frontend/src/assets/joyride.svg create mode 100644 frontend/src/assets/loading.svg diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index 1028f4d9..eef129d0 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -33,5 +33,6 @@ }, "ACCOUNT_FILTER_PLACEHOLDER": "filter by keyword...", "ACCOUNT_FILTER_CLOSE_BUTTON_ARIA": "clean account filter keyword", - "ERROR_BOX_WHY": "Why is it a problem?" + "ERROR_BOX_WHY": "Why is it a problem?", + "LEDGER_IS_HEALTHY": "Ledger is healthy" } \ No newline at end of file diff --git a/frontend/public/locales/zh/translation.json b/frontend/public/locales/zh/translation.json index 35757382..de39cb1b 100644 --- a/frontend/public/locales/zh/translation.json +++ b/frontend/public/locales/zh/translation.json @@ -33,5 +33,6 @@ "DefineDuplicatedBudget": "尝试创建一个重复的预算", "UnbalancedTransaction": "交易不平衡" }, - "ERROR_BOX_WHY": "为什么出错?" + "ERROR_BOX_WHY": "为什么出错?", + "LEDGER_IS_HEALTHY": "不可思议,账本一点错都没有" } \ No newline at end of file diff --git a/frontend/src/assets/joyride.svg b/frontend/src/assets/joyride.svg new file mode 100644 index 00000000..f50112e5 --- /dev/null +++ b/frontend/src/assets/joyride.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/loading.svg b/frontend/src/assets/loading.svg new file mode 100644 index 00000000..684641f0 --- /dev/null +++ b/frontend/src/assets/loading.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/ErrorBox.tsx b/frontend/src/components/ErrorBox.tsx index ebdc6da6..46ad894a 100644 --- a/frontend/src/components/ErrorBox.tsx +++ b/frontend/src/components/ErrorBox.tsx @@ -1,9 +1,10 @@ -import { Anchor, Button, Group, Modal, Pagination, Stack, Text, Textarea } from '@mantine/core'; +import { Anchor, Button, Group, Modal, Pagination, Stack, Text, Textarea, Image } from '@mantine/core'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { errorAtom, errorPageAtom, LedgerError } from '../states/errors'; import { ErrorsSkeleton } from './skeletons/errorsSkeleton'; import { useAtomValue, useSetAtom } from 'jotai'; +import Joyride from '../assets/joyride.svg'; export default function ErrorBox() { const { t } = useTranslation(); @@ -74,16 +75,25 @@ export default function ErrorBox() { - - {errors.data.records.map((error, idx) => ( - toggleError(error)}> - {t(`ERROR.${error.error_type}`)} - - ))} + + {errors.data.total_count === 0 ? ( + + + {t('LEDGER_IS_HEALTHY')} + + ) : ( + <> + {errors.data.records.map((error, idx) => ( + toggleError(error)}> + {t(`ERROR.${error.error_type}`)} + + ))} - - - + + + + + )} ); From 6fe9b3b3c7af8273fcbd289ce03663a57ca928b6 Mon Sep 17 00:00:00 2001 From: Kilerd Chan Date: Thu, 11 Jul 2024 17:17:29 +0800 Subject: [PATCH 4/7] refact: #357 use jotai for all reduce data --- frontend/src/App.tsx | 55 +++++++----- .../components/AccountBalanceCheckLine.tsx | 12 +-- frontend/src/components/Amount.tsx | 9 +- frontend/src/components/ErrorBox.tsx | 4 +- .../src/components/TransactionEditForm.tsx | 7 +- .../tableView/TableViewTransactionLine.tsx | 8 +- .../modals/TransactionPreviewModal.tsx | 16 +++- frontend/src/pages/Accounts.tsx | 67 +++++++++----- frontend/src/pages/Budgets.tsx | 6 +- frontend/src/pages/Commodities.tsx | 17 ++-- frontend/src/pages/Documents.tsx | 6 +- frontend/src/pages/Home.tsx | 5 +- frontend/src/pages/Journals.tsx | 56 ++++++------ frontend/src/pages/RawEdit.tsx | 6 +- frontend/src/pages/Report.tsx | 6 +- frontend/src/pages/Settings.tsx | 12 +-- frontend/src/pages/SingleAccount.tsx | 6 +- frontend/src/pages/SingleBudget.tsx | 6 +- frontend/src/pages/SingleCommodity.tsx | 7 +- frontend/src/pages/tools/BatchBalance.tsx | 22 ++--- frontend/src/states/account.ts | 87 +++++-------------- frontend/src/states/basic.ts | 65 +++----------- frontend/src/states/commodity.ts | 44 +++------- frontend/src/states/errors.ts | 8 +- frontend/src/states/index.ts | 20 ++--- frontend/src/states/journals.ts | 64 ++++---------- 26 files changed, 261 insertions(+), 360 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7fe25253..9556f4c3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -24,17 +24,17 @@ import NewTransactionButton from './components/NewTransactionButton'; import { notifications } from '@mantine/notifications'; import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { serverBaseUrl } from './index'; -import { useAppDispatch, useAppSelector } from './states'; -import { accountsSlice, fetchAccounts } from './states/account'; -import { basicInfoSlice, fetchBasicInfo, reloadLedger } from './states/basic'; -import { fetchCommodities } from './states/commodity'; -import { journalsSlice } from './states/journals'; +import { axiosInstance, serverBaseUrl } from './index'; +import { useAppDispatch } from './states'; +import { basicInfoFetcher, onlineAtom, titleAtom, updatableVersionAtom } from './states/basic'; import { useSWRConfig } from 'swr'; import { createStyles } from '@mantine/emotion'; import { Router } from './router'; -import { useAtomValue, useSetAtom } from 'jotai'; +import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import { errorCountAtom, errorsFetcher } from './states/errors'; +import { accountFetcher } from './states/account'; +import { commoditiesFetcher } from './states/commodity'; +import { journalFetcher } from './states/journals'; const useStyles = createStyles((theme, _, u) => ({ onlineIcon: { @@ -202,14 +202,24 @@ export default function App() { const { classes } = useStyles(); const { t, i18n } = useTranslation(); const dispatch = useAppDispatch(); - const basicInfo = useAppSelector((state) => state.basic); const location = useLocation(); const [lang] = useLocalStorage({ key: 'lang', defaultValue: 'en' }); const [opened] = useDisclosure(); const isMobile = useMediaQuery('(max-width: 768px)'); const errorsCount = useAtomValue(errorCountAtom); + const ledgerTitle = useAtomValue(titleAtom); + const [ledgerOnline, setLedgerOnline] = useAtom(onlineAtom); + const [updatableVersion, setUpdatableVersion] = useAtom(updatableVersionAtom); + const refreshErrors = useSetAtom(errorsFetcher); + const refreshAccounts = useSetAtom(accountFetcher); + const refreshBasicInfo = useSetAtom(basicInfoFetcher); + const refreshCommodities = useSetAtom(commoditiesFetcher); + const refreshJournal = useSetAtom(journalFetcher); + const refreshLedger = async () => { + await axiosInstance.post('/api/reload'); + }; useEffect(() => { if (i18n.language !== lang) { @@ -218,10 +228,6 @@ export default function App() { }, [i18n, lang]); useEffect(() => { - dispatch(fetchCommodities()); - dispatch(fetchBasicInfo()); - dispatch(fetchAccounts()); - let events = new EventSource(serverBaseUrl + '/api/sse'); events.onmessage = (event) => { console.log(event); @@ -239,10 +245,10 @@ export default function App() { }); mutate('/api/for-new-transaction'); refreshErrors(); - dispatch(fetchBasicInfo()); - dispatch(fetchCommodities()); - dispatch(accountsSlice.actions.clear()); - dispatch(journalsSlice.actions.clear()); + refreshAccounts(); + refreshBasicInfo(); + refreshCommodities(); + refreshJournal(); break; case 'Connected': notifications.show({ @@ -250,17 +256,18 @@ export default function App() { icon: , message: '', }); - dispatch(fetchBasicInfo()); + setLedgerOnline(true); + refreshBasicInfo(); break; case 'NewVersionFound': - dispatch(basicInfoSlice.actions.setUpdatableVersion({ newVersion: data.version })); + setUpdatableVersion(data.version); break; default: break; } }; events.onerror = () => { - dispatch(basicInfoSlice.actions.offline()); + setLedgerOnline(false); notifications.show({ id: 'offline', title: 'Server Offline', @@ -269,7 +276,7 @@ export default function App() { message: 'Client can not connect to server', }); }; - }, [dispatch, mutate]); + }, [dispatch, mutate]); // eslint-disable-line const sendReloadEvent = () => { notifications.show({ @@ -279,7 +286,7 @@ export default function App() { loading: true, autoClose: false, }); - dispatch(reloadLedger()); + refreshLedger(); }; const mainLinks = links.map((link) => ( @@ -329,8 +336,8 @@ export default function App() { - - {basicInfo.title ?? 'Zhang Accounting'} + + {ledgerTitle} @@ -364,7 +371,7 @@ export default function App() { - {basicInfo.updatableVersion && ( + {updatableVersion && ( diff --git a/frontend/src/components/AccountBalanceCheckLine.tsx b/frontend/src/components/AccountBalanceCheckLine.tsx index 9e69ef7c..c720f8ee 100644 --- a/frontend/src/components/AccountBalanceCheckLine.tsx +++ b/frontend/src/components/AccountBalanceCheckLine.tsx @@ -2,9 +2,10 @@ import { Autocomplete, Button, Group, Table, TextInput } from '@mantine/core'; import { useState } from 'react'; import { axiosInstance } from '../index'; import { showNotification } from '@mantine/notifications'; -import { useAppDispatch, useAppSelector } from '../states'; -import { accountsSlice, getAccountSelectItems } from '../states/account'; +import { accountFetcher, accountSelectItemsAtom } from '../states/account'; import Amount from './Amount'; +import { useAtomValue } from 'jotai'; +import { useSetAtom } from 'jotai/index'; interface Props { currentAmount: string; @@ -15,9 +16,8 @@ interface Props { export default function AccountBalanceCheckLine({ currentAmount, commodity, accountName }: Props) { const [amount, setAmount] = useState(''); const [padAccount, setPadAccount] = useState(''); - const dispatch = useAppDispatch(); - - const accountItems = [...useAppSelector(getAccountSelectItems())]; + const refreshAccounts = useSetAtom(accountFetcher); + const accountItems = useAtomValue(accountSelectItemsAtom); const onSave = async () => { try { @@ -34,7 +34,7 @@ export default function AccountBalanceCheckLine({ currentAmount, commodity, acco title: 'Balance account successfully', message: '', }); - dispatch(accountsSlice.actions.clear()); + refreshAccounts(); } catch (e: any) { showNotification({ title: 'Fail to Balance Account', diff --git a/frontend/src/components/Amount.tsx b/frontend/src/components/Amount.tsx index 9dea306d..63bd5294 100644 --- a/frontend/src/components/Amount.tsx +++ b/frontend/src/components/Amount.tsx @@ -1,7 +1,10 @@ import BigNumber from 'bignumber.js'; -import { useAppSelector } from '../states'; -import { getCommodityByName } from '../states/commodity'; +import { loadable_unwrap } from '../states'; +import { commoditiesAtom } from '../states/commodity'; import { createStyles } from '@mantine/emotion'; +import { selectAtom } from 'jotai/utils'; +import { useAtomValue } from 'jotai'; +import { useMemo } from 'react'; const useStyles = createStyles((theme, _, u) => ({ wrapper: { @@ -26,7 +29,7 @@ interface Props { export default function Amount({ amount, currency, negative, mask, className }: Props) { const { classes } = useStyles(); - const commodity = useAppSelector(getCommodityByName(currency)); + const commodity = useAtomValue(useMemo(() => selectAtom(commoditiesAtom, (val) => loadable_unwrap(val, undefined, (val) => val[currency])), [currency])); const flag = negative || false ? -1 : 1; const shouldMask = mask || false; diff --git a/frontend/src/components/ErrorBox.tsx b/frontend/src/components/ErrorBox.tsx index 46ad894a..156492c3 100644 --- a/frontend/src/components/ErrorBox.tsx +++ b/frontend/src/components/ErrorBox.tsx @@ -1,4 +1,4 @@ -import { Anchor, Button, Group, Modal, Pagination, Stack, Text, Textarea, Image } from '@mantine/core'; +import { Anchor, Button, Group, Image, Modal, Pagination, Stack, Text, Textarea } from '@mantine/core'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { errorAtom, errorPageAtom, LedgerError } from '../states/errors'; @@ -75,7 +75,7 @@ export default function ErrorBox() { - + {errors.data.total_count === 0 ? ( diff --git a/frontend/src/components/TransactionEditForm.tsx b/frontend/src/components/TransactionEditForm.tsx index 67518b99..887a5308 100644 --- a/frontend/src/components/TransactionEditForm.tsx +++ b/frontend/src/components/TransactionEditForm.tsx @@ -9,8 +9,8 @@ import { InfoForNewTransaction, JournalTransactionItem } from '../rest-model'; import { fetcher } from '../index'; import { useTranslation } from 'react-i18next'; import { format } from 'date-fns'; -import { useAppSelector } from '../states'; -import { getAccountSelectItems } from '../states/account'; +import { accountSelectItemsAtom } from '../states/account'; +import { useAtomValue } from 'jotai/index'; interface Posting { account: string | null; @@ -51,8 +51,7 @@ export default function TransactionEditForm(props: Props) { const [metas, metaHandler] = useListState<{ key: string; value: string }>(props.data?.metas ?? []); const [payeeSelectItems, setPayeeSelectItems] = useState([]); - const accountItems = [...useAppSelector(getAccountSelectItems())]; - + const accountItems = useAtomValue(accountSelectItemsAtom); useEffect(() => { const newPayeeSelectItems: SelectItem[] = (data?.payee ?? []).map((item) => { return { diff --git a/frontend/src/components/journalLines/tableView/TableViewTransactionLine.tsx b/frontend/src/components/journalLines/tableView/TableViewTransactionLine.tsx index 3468a681..2754e013 100644 --- a/frontend/src/components/journalLines/tableView/TableViewTransactionLine.tsx +++ b/frontend/src/components/journalLines/tableView/TableViewTransactionLine.tsx @@ -92,7 +92,13 @@ export default function TableViewTransactionLine({ data }: Props) { {Array.from(summary.values()).map((each) => ( - + ))} diff --git a/frontend/src/components/modals/TransactionPreviewModal.tsx b/frontend/src/components/modals/TransactionPreviewModal.tsx index 18c69c12..812c1624 100644 --- a/frontend/src/components/modals/TransactionPreviewModal.tsx +++ b/frontend/src/components/modals/TransactionPreviewModal.tsx @@ -1,9 +1,21 @@ import { ContextModalProps } from '@mantine/modals'; import JournalPreview from '../journalPreview/JournalPreview'; -import { useAppSelector } from '../../states'; +import { loadable_unwrap } from '../../states'; +import { useAtomValue } from 'jotai'; +import { useMemo } from 'react'; +import { selectAtom } from 'jotai/utils'; +import { journalAtom } from '../../states/journals'; export const TransactionPreviewModal = ({ context, id, innerProps }: ContextModalProps<{ journalId: string }>) => { - const targetJournal = useAppSelector((state) => (state.journals.items ?? []).find((journalItem) => journalItem.id === innerProps.journalId)); + const targetJournal = useAtomValue( + useMemo( + () => + selectAtom(journalAtom, (val) => + loadable_unwrap(val, undefined, (data) => data.records.find((journalItem) => journalItem.id === innerProps.journalId)), + ), + [innerProps], + ), + ); return ( <> diff --git a/frontend/src/pages/Accounts.tsx b/frontend/src/pages/Accounts.tsx index f736b2cd..c98aec7e 100644 --- a/frontend/src/pages/Accounts.tsx +++ b/frontend/src/pages/Accounts.tsx @@ -1,31 +1,54 @@ import { Button, Checkbox, CloseButton, Container, Group, Input, Table } from '@mantine/core'; import { useDocumentTitle, useInputState, useLocalStorage } from '@mantine/hooks'; -import { useEffect } from 'react'; import AccountLine from '../components/AccountLine'; -import { LoadingState } from '../rest-model'; -import { useAppDispatch, useAppSelector } from '../states'; -import { fetchAccounts, getAccountsTrie } from '../states/account'; +import { AccountStatus } from '../rest-model'; +import { loadable_unwrap } from '../states'; +import { accountAtom, accountFetcher } from '../states/account'; import { Heading } from '../components/basic/Heading'; import { useTranslation } from 'react-i18next'; import { IconFilter } from '@tabler/icons-react'; -import { AccountListSkeleton } from '../components/skeletons/accountListSkeleton'; +import { useAtom, useAtomValue, useSetAtom } from 'jotai'; +import { selectAtom } from 'jotai/utils'; +import AccountTrie from '../utils/AccountTrie'; +import { titleAtom } from '../states/basic'; +import { useMemo } from 'react'; export default function Accounts() { const { t } = useTranslation(); const [filterKeyword, setFilterKeyword] = useInputState(''); const [hideClosedAccount, setHideClosedAccount] = useLocalStorage({ key: 'hideClosedAccount', defaultValue: false }); - const dispatch = useAppDispatch(); - const accountStatus = useAppSelector((state) => state.accounts.status); - const accountTrie = useAppSelector(getAccountsTrie(hideClosedAccount, filterKeyword)); - const ledgerTitle = useAppSelector((state) => state.basic.title ?? 'Zhang Accounting'); - useDocumentTitle(`Accounts - ${ledgerTitle}`); + const [accountTrie] = useAtom( + useMemo( + () => + selectAtom(accountAtom, (val) => { + return loadable_unwrap(val, new AccountTrie(), (data) => { + let trie = new AccountTrie(); + for (let account of data.filter((it) => (hideClosedAccount ? it.status === AccountStatus.Open : true))) { + let trimmedKeyword = filterKeyword.trim(); + if (trimmedKeyword !== '') { + if ( + account.name.toLowerCase().includes(trimmedKeyword.toLowerCase()) || + (account.alias?.toLowerCase() ?? '').includes(trimmedKeyword.toLowerCase()) + ) { + trie.insert(account); + } + } else { + trie.insert(account); + } + } + return trie; + }); + }), + [filterKeyword, hideClosedAccount], + ), + ); + + const refreshAccounts = useSetAtom(accountFetcher); - useEffect(() => { - if (accountStatus === LoadingState.NotReady) { - dispatch(fetchAccounts()); - } - }, [dispatch, accountStatus]); + const ledgerTitle = useAtomValue(titleAtom); + + useDocumentTitle(`Accounts - ${ledgerTitle}`); return ( @@ -40,7 +63,7 @@ export default function Accounts() { /> - setHideClosedAccount(!hideClosedAccount)} label={'Hide closed accounts'} /> @@ -54,13 +77,11 @@ export default function Accounts() { - {accountStatus !== LoadingState.Success ? ( - - ) : ( - Object.keys(accountTrie.children) - .sort() - .map((item) => ) - )} + {Object.keys(accountTrie.children) + .sort() + .map((item) => ( + + ))} diff --git a/frontend/src/pages/Budgets.tsx b/frontend/src/pages/Budgets.tsx index d43fd156..7fc76e63 100644 --- a/frontend/src/pages/Budgets.tsx +++ b/frontend/src/pages/Budgets.tsx @@ -10,7 +10,8 @@ import BudgetCategory from '../components/budget/BudgetCategory'; import { format } from 'date-fns'; import { MonthPicker } from '@mantine/dates'; import { IconChevronLeft, IconChevronRight } from '@tabler/icons-react'; -import { useAppSelector } from '../states'; +import { useAtomValue } from 'jotai/index'; +import { titleAtom } from '../states/basic'; export default function Budgets() { const { t } = useTranslation(); @@ -19,8 +20,7 @@ export default function Budgets() { defaultValue: false, }); const [date, setDate] = useState(new Date()); - const ledgerTitle = useAppSelector((state) => state.basic.title ?? 'Zhang Accounting'); - + const ledgerTitle = useAtomValue(titleAtom); useDocumentTitle(`Budgets - ${ledgerTitle}`); const { data: budgets, error } = useSWR(`/api/budgets?year=${date.getFullYear()}&month=${date.getMonth() + 1}`, fetcher); diff --git a/frontend/src/pages/Commodities.tsx b/frontend/src/pages/Commodities.tsx index 4b0f9085..9e68a21f 100644 --- a/frontend/src/pages/Commodities.tsx +++ b/frontend/src/pages/Commodities.tsx @@ -1,21 +1,16 @@ import { Box, Container, SimpleGrid, Title } from '@mantine/core'; -import { LoadingState } from '../rest-model'; -import { useAppSelector } from '../states'; import { Heading } from '../components/basic/Heading'; -import { groupBy } from 'lodash-es'; import CommodityBox from '../components/CommodityBox'; import { useDocumentTitle } from '@mantine/hooks'; +import { useAtomValue } from 'jotai/index'; +import { titleAtom } from '../states/basic'; +import { FRONTEND_DEFAULT_GROUP, groupedCommoditiesAtom } from '../states/commodity'; -const FRONTEND_DEFAULT_GROUP = '__ZHANG__FRONTEND_DEFAULT__GROUP__'; export default function Commodities() { - const { value: commodities, status } = useAppSelector((state) => state.commodities); - const ledgerTitle = useAppSelector((state) => state.basic.title ?? 'Zhang Accounting'); - + const ledgerTitle = useAtomValue(titleAtom); useDocumentTitle(`Commodities - ${ledgerTitle}`); - if (status === LoadingState.Loading || status === LoadingState.NotReady) return <>loading; - - const groupedCommodities = groupBy(commodities, (it) => it.group ?? FRONTEND_DEFAULT_GROUP); + const groupedCommodities = useAtomValue(groupedCommoditiesAtom); return ( @@ -25,7 +20,7 @@ export default function Commodities() { {groupedCommodities[FRONTEND_DEFAULT_GROUP].map((commodity) => ( - + ))} diff --git a/frontend/src/pages/Documents.tsx b/frontend/src/pages/Documents.tsx index bbea6390..8b72c174 100644 --- a/frontend/src/pages/Documents.tsx +++ b/frontend/src/pages/Documents.tsx @@ -9,11 +9,12 @@ import { Heading } from '../components/basic/Heading'; import { groupBy, reverse, sortBy } from 'lodash-es'; import { TextBadge } from '../components/basic/TextBadge'; import { useNavigate } from 'react-router'; -import { useAppSelector } from '../states'; import { useState } from 'react'; import 'yet-another-react-lightbox/styles.css'; import { ImageLightBox } from '../components/ImageLightBox'; import { isDocumentAnImage } from '../utils/documents'; +import { useAtomValue } from 'jotai/index'; +import { titleAtom } from '../states/basic'; export default function Documents() { let navigate = useNavigate(); @@ -21,8 +22,7 @@ export default function Documents() { const { data: documents, error } = useSWR('/api/documents', fetcher); const [lightboxSrc, setLightboxSrc] = useState(undefined); - const ledgerTitle = useAppSelector((state) => state.basic.title ?? 'Zhang Accounting'); - + const ledgerTitle = useAtomValue(titleAtom); useDocumentTitle(`Documents - ${ledgerTitle}`); if (error) return
failed to load
; diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 497a333d..6b5a87b2 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -6,17 +6,16 @@ import Section from '../components/Section'; import StatisticBar from '../components/StatisticBar'; import { fetcher } from '../index'; import { StatisticGraphResponse } from '../rest-model'; -import { useAppSelector } from '../states'; import ReportGraph from '../components/ReportGraph'; import { Heading } from '../components/basic/Heading'; import { useDocumentTitle } from '@mantine/hooks'; import { useAtomValue } from 'jotai'; import { errorCountAtom } from '../states/errors'; +import { titleAtom } from '../states/basic'; function Home() { const error_total_number = useAtomValue(errorCountAtom); - const ledgerTitle = useAppSelector((state) => state.basic.title ?? 'Zhang Accounting'); - + const ledgerTitle = useAtomValue(titleAtom); useDocumentTitle(`Dashboard - ${ledgerTitle}`); const now = new Date(); diff --git a/frontend/src/pages/Journals.tsx b/frontend/src/pages/Journals.tsx index 2f91e427..bf83048e 100644 --- a/frontend/src/pages/Journals.tsx +++ b/frontend/src/pages/Journals.tsx @@ -1,49 +1,47 @@ import { Button, CloseButton, Group, Input, Pagination, Table, Text } from '@mantine/core'; -import { format } from 'date-fns'; -import { groupBy } from 'lodash-es'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import TableViewJournalLine from '../components/journalLines/tableView/TableViewJournalLine'; -import { LoadingState } from '../rest-model'; -import { useAppDispatch, useAppSelector } from '../states'; -import { fetchJournals, journalsSlice } from '../states/journals'; import { Heading } from '../components/basic/Heading'; import { useTranslation } from 'react-i18next'; import { useDebouncedValue, useDocumentTitle } from '@mantine/hooks'; import { IconFilter } from '@tabler/icons-react'; import { JournalListSkeleton } from '../components/skeletons/journalListSkeleton'; +import { useAtomValue } from 'jotai/index'; +import { titleAtom } from '../states/basic'; +import { groupedJournalsAtom, journalAtom, journalFetcher, journalKeywordAtom, journalPageAtom } from '../states/journals'; +import { useAtom, useSetAtom } from 'jotai'; +import { loadable_unwrap } from '../states'; +import { selectAtom } from 'jotai/utils'; function Journals() { const { t } = useTranslation(); const [filter, setFilter] = useState(''); const [debouncedFilter] = useDebouncedValue(filter, 200); - const { current_page, status: journalStatus, items, total_number, total_page } = useAppSelector((state) => state.journals); - const dispatch = useAppDispatch(); - const ledgerTitle = useAppSelector((state) => state.basic.title ?? 'Zhang Accounting'); + const ledgerTitle = useAtomValue(titleAtom); useDocumentTitle(`Journals - ${ledgerTitle}`); - useEffect(() => { - if (journalStatus === LoadingState.NotReady) { - dispatch(fetchJournals(debouncedFilter)); - } - }, [dispatch, journalStatus, debouncedFilter]); + const [journalPage, setJournalPage] = useAtom(journalPageAtom); + const setKeyword = useSetAtom(journalKeywordAtom); + const refreshJournals = useSetAtom(journalFetcher); + const groupedRecords = useAtomValue(groupedJournalsAtom); + const journalItems = useAtomValue(journalAtom); + const total_count = useAtomValue(useMemo(() => selectAtom(journalAtom, (val) => loadable_unwrap(val, 0, (val) => val.total_count)), [])); + const total_page = useAtomValue(useMemo(() => selectAtom(journalAtom, (val) => loadable_unwrap(val, 0, (val) => val.total_page)), [])); useEffect(() => { - dispatch(fetchJournals(debouncedFilter)); - }, [dispatch, debouncedFilter]); + setKeyword(debouncedFilter); + }, [setKeyword, debouncedFilter]); const onPage = (page: number) => { - dispatch(journalsSlice.actions.setPage({ current_page: page })); - dispatch(fetchJournals(debouncedFilter)); + setJournalPage(page); }; - const groupedRecords = groupBy(items, (record) => format(new Date(record.datetime), 'yyyy-MM-dd')); - return ( <> - + - - {(journalStatus === LoadingState.Loading || journalStatus === LoadingState.NotReady) && } - {journalStatus === LoadingState.Success && - Object.entries(groupedRecords).map((entry) => { + {(journalItems.state === 'loading' || journalItems.state === 'hasError') && } + {journalItems.state === 'hasData' && + Object.keys(groupedRecords).map((date) => { return ( <> - + - {entry[0]} + {date} - {entry[1].map((journal) => ( + {groupedRecords[date].map((journal) => ( ))} @@ -87,7 +85,7 @@ function Journals() { - + ); diff --git a/frontend/src/pages/RawEdit.tsx b/frontend/src/pages/RawEdit.tsx index 96008878..7479df55 100644 --- a/frontend/src/pages/RawEdit.tsx +++ b/frontend/src/pages/RawEdit.tsx @@ -4,14 +4,14 @@ import { fetcher } from '..'; import SingleFileEdit from '../components/SingleFileEdit'; import { TableOfContentsFloating, Tier, ZHANG_VALUE } from '../components/basic/TableOfContentsFloating'; import { useState } from 'react'; -import { useAppSelector } from '../states'; import { useDocumentTitle } from '@mantine/hooks'; +import { useAtomValue } from 'jotai/index'; +import { titleAtom } from '../states/basic'; function RawEdit() { const { data, error } = useSWR('/api/files', fetcher); const [selectedFile, setSelectedFile] = useState(null); - const ledgerTitle = useAppSelector((state) => state.basic.title ?? 'Zhang Accounting'); - + const ledgerTitle = useAtomValue(titleAtom); useDocumentTitle(selectedFile ? `${selectedFile} | Raw Editing - ${ledgerTitle}` : `Raw Editing - ${ledgerTitle}`); if (error) return
failed to load
; diff --git a/frontend/src/pages/Report.tsx b/frontend/src/pages/Report.tsx index e34ac181..d40e5f0f 100644 --- a/frontend/src/pages/Report.tsx +++ b/frontend/src/pages/Report.tsx @@ -12,8 +12,9 @@ import { StatisticGraphResponse, StatisticResponse, StatisticTypeResponse } from import PayeeNarration from '../components/basic/PayeeNarration'; import BigNumber from 'bignumber.js'; import { Heading } from '../components/basic/Heading'; -import { useAppSelector } from '../states'; import { useDocumentTitle } from '@mantine/hooks'; +import { useAtomValue } from 'jotai/index'; +import { titleAtom } from '../states/basic'; const color_set = ['pink', 'grape', 'violet']; @@ -27,8 +28,7 @@ export default function Report() { new Date(new Date().getFullYear(), new Date().getMonth(), 1, 0, 0, 1), new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0, 23, 59, 59), ]); - const ledgerTitle = useAppSelector((state) => state.basic.title ?? 'Zhang Accounting'); - + const ledgerTitle = useAtomValue(titleAtom); useDocumentTitle(`Report - ${ledgerTitle}`); useEffect(() => { diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index d6e169ad..6e1e7c43 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -4,12 +4,13 @@ import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Setting } from '../components/basic/Setting'; import Section from '../components/Section'; -import { useAppSelector } from '../states'; import useSWR from 'swr'; import { fetcher } from '..'; import { Option, PluginResponse } from '../rest-model'; import { Heading } from '../components/basic/Heading'; import PluginBox from '../components/PluginBox'; +import { titleAtom, versionAtom } from '../states/basic'; +import { useAtomValue } from 'jotai/index'; export default function Settings() { const { i18n } = useTranslation(); @@ -17,7 +18,8 @@ export default function Settings() { const { data } = useSWR('/api/options', fetcher); const { data: plugins } = useSWR('/api/plugins', fetcher); - const ledgerTitle = useAppSelector((state) => state.basic.title ?? 'Zhang Accounting'); + const ledgerTitle = useAtomValue(titleAtom); + const ledgerVersion = useAtomValue(versionAtom); useDocumentTitle(`Settings - ${ledgerTitle}`); @@ -29,15 +31,13 @@ export default function Settings() { i18n.changeLanguage(lang); }, [lang, i18n]); - const basicInfo = useAppSelector((state) => state.basic); - return (
- - + + ({ calculatedAmount: { @@ -37,8 +38,7 @@ function SingleAccount() { const { data: account, error } = useSWR(`/api/accounts/${accountName}`, fetcher); const { data: account_balance_data, error: account_balance_error } = useSWR(`/api/accounts/${accountName}/balances`, fetcher); - const ledgerTitle = useAppSelector((state) => state.basic.title ?? 'Zhang Accounting'); - + const ledgerTitle = useAtomValue(titleAtom); useDocumentTitle(`${accountName} | Accounts - ${ledgerTitle}`); if (error) return
failed to load
; diff --git a/frontend/src/pages/SingleBudget.tsx b/frontend/src/pages/SingleBudget.tsx index 248766d2..57c75822 100644 --- a/frontend/src/pages/SingleBudget.tsx +++ b/frontend/src/pages/SingleBudget.tsx @@ -9,14 +9,14 @@ import PayeeNarration from '../components/basic/PayeeNarration'; import { BudgetInfoResponse, BudgetIntervalEventResponse } from '../rest-model'; import { MonthPicker } from '@mantine/dates'; import { useState } from 'react'; -import { useAppSelector } from '../states'; import { useDocumentTitle } from '@mantine/hooks'; +import { useAtomValue } from 'jotai/index'; +import { titleAtom } from '../states/basic'; function SingleBudget() { let { budgetName } = useParams(); const [date, setDate] = useState(new Date()); - const ledgerTitle = useAppSelector((state) => state.basic.title ?? 'Zhang Accounting'); - + const ledgerTitle = useAtomValue(titleAtom); useDocumentTitle(`${budgetName} | Budgets - ${ledgerTitle}`); const goToMonth = (gap: number) => { diff --git a/frontend/src/pages/SingleCommodity.tsx b/frontend/src/pages/SingleCommodity.tsx index 3b35c651..da234cd3 100644 --- a/frontend/src/pages/SingleCommodity.tsx +++ b/frontend/src/pages/SingleCommodity.tsx @@ -6,15 +6,14 @@ import { fetcher } from '..'; import Amount from '../components/Amount'; import { CommodityDetail } from '../rest-model'; import { Heading } from '../components/basic/Heading'; -import { useAppSelector } from '../states'; import { useDocumentTitle } from '@mantine/hooks'; +import { useAtomValue } from 'jotai/index'; +import { titleAtom } from '../states/basic'; export default function SingleCommodity() { let { commodityName } = useParams(); const { data, error } = useSWR(`/api/commodities/${commodityName}`, fetcher); - - const ledgerTitle = useAppSelector((state) => state.basic.title ?? 'Zhang Accounting'); - + const ledgerTitle = useAtomValue(titleAtom); useDocumentTitle(`${commodityName} | Commodities - ${ledgerTitle}`); if (error) return
failed to load
; diff --git a/frontend/src/pages/tools/BatchBalance.tsx b/frontend/src/pages/tools/BatchBalance.tsx index e97a9920..64e89e10 100644 --- a/frontend/src/pages/tools/BatchBalance.tsx +++ b/frontend/src/pages/tools/BatchBalance.tsx @@ -4,12 +4,13 @@ import { createSelector } from '@reduxjs/toolkit'; import { cloneDeep, sortBy } from 'lodash-es'; import { useEffect } from 'react'; import Amount from '../../components/Amount'; -import { Account, LoadingState } from '../../rest-model'; -import { useAppDispatch, useAppSelector } from '../../states'; -import { accountsSlice, fetchAccounts, getAccountSelectItems } from '../../states/account'; +import { Account } from '../../rest-model'; +import { useAppSelector } from '../../states'; +import { accountFetcher, accountSelectItemsAtom } from '../../states/account'; import BigNumber from 'bignumber.js'; import { showNotification } from '@mantine/notifications'; import { axiosInstance } from '../..'; +import { useAtomValue, useSetAtom } from 'jotai/index'; interface BalanceLineItem { commodity: string; @@ -36,12 +37,10 @@ const getFilteredItems = createSelector([(states) => states.accounts], (accounts }); export default function BatchBalance() { - const dispatch = useAppDispatch(); - const accountStatus = useAppSelector((state) => state.accounts.status); const stateItems = useAppSelector(getFilteredItems); - const accountSelectItems = [...useAppSelector(getAccountSelectItems())]; const [accounts, accountsHandler] = useListState(stateItems); - + const accountItems = useAtomValue(accountSelectItemsAtom); + const refreshAccounts = useSetAtom(accountFetcher); const [maskCurrentAmount, setMaskCurrentAmount] = useLocalStorage({ key: 'tool/maskCurrentAmount', defaultValue: false, @@ -51,11 +50,6 @@ export default function BatchBalance() { defaultValue: true, }); - useEffect(() => { - if (accountStatus === LoadingState.NotReady) { - dispatch(fetchAccounts()); - } - }, [accountStatus, dispatch]); useEffect(() => { accountsHandler.setState(stateItems); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -100,7 +94,7 @@ export default function BatchBalance() { title: 'Balance account successfully', message: 'waiting page to refetch latest data', }); - dispatch(accountsSlice.actions.clear()); + refreshAccounts(); } catch (e: any) { showNotification({ title: 'Fail to Balance Account', @@ -144,7 +138,7 @@ export default function BatchBalance() { searchable clearable placeholder="Pad to" - data={accountSelectItems} + data={accountItems} value={account.pad} onChange={(e) => { updateBalanceLineItem(idx, e ?? undefined, account.balanceAmount); diff --git a/frontend/src/states/account.ts b/frontend/src/states/account.ts index 7b0eb4dc..ed104706 100644 --- a/frontend/src/states/account.ts +++ b/frontend/src/states/account.ts @@ -1,74 +1,27 @@ -import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; -import { RootState } from '.'; +import { loadable_unwrap } from '.'; import { fetcher } from '..'; -import { Account, AccountStatus, LoadingState } from '../rest-model'; -import AccountTrie from '../utils/AccountTrie'; +import { Account } from '../rest-model'; import { groupBy } from 'lodash-es'; +import { atomWithRefresh, loadable } from 'jotai/utils'; +import { atom } from 'jotai'; -export const fetchAccounts = createAsyncThunk('accounts/fetch', async (thunkApi) => { - const ret = await fetcher(`/api/accounts`); - return ret; +export const accountFetcher = atomWithRefresh(async (get) => { + return await fetcher(`/api/accounts`); }); -interface AccountsState { - dataMap: { [accountName: string]: Account }; - data: Account[]; - status: LoadingState; -} +export const accountAtom = loadable(accountFetcher); -const initialState: AccountsState = { - dataMap: {}, - data: [], - status: LoadingState.NotReady, -}; - -export const accountsSlice = createSlice({ - name: 'accounts', - initialState, - reducers: { - clear: (state) => { - state.status = LoadingState.NotReady; - }, - }, - extraReducers: (builder) => { - builder.addCase(fetchAccounts.pending, (state, action) => { - state.status = LoadingState.Loading; - }); - - builder.addCase(fetchAccounts.fulfilled, (state, action) => { - state.status = LoadingState.Success; - state.dataMap = Object.fromEntries(action.payload.map((item: Account) => [item.name, item])); - state.data = action.payload; - }); - }, +export const accountSelectItemsAtom = atom((get) => { + return loadable_unwrap(get(accountAtom), [], (data) => { + const groupedAccount = groupBy( + data.map((account) => account.name), + (it) => it.split(':')[0], + ); + return Object.keys(groupedAccount) + .sort() + .map((groupName) => ({ + group: groupName, + items: groupedAccount[groupName], + })); + }); }); - -export const getAccountByName = (name: string) => (state: RootState) => state.accounts.dataMap[name]; -export const getAccountsTrie = (hideClosedAccount: boolean, filterKeyword: string) => (state: RootState) => { - const data = state.accounts.data; - let trie = new AccountTrie(); - for (let account of data.filter((it) => (hideClosedAccount ? it.status === AccountStatus.Open : true))) { - let trimmedKeyword = filterKeyword.trim(); - if (trimmedKeyword !== '') { - if (account.name.toLowerCase().includes(trimmedKeyword.toLowerCase()) || (account.alias?.toLowerCase() ?? '').includes(trimmedKeyword.toLowerCase())) { - trie.insert(account); - } - } else { - trie.insert(account); - } - } - return trie; -}; - -export const getAccountSelectItems = () => (state: RootState) => { - const groupedAccount = groupBy( - state.accounts.data.map((account) => account.name), - (it) => it.split(':')[0], - ); - return Object.keys(groupedAccount) - .sort() - .map((groupName) => ({ - group: groupName, - items: groupedAccount[groupName], - })); -}; diff --git a/frontend/src/states/basic.ts b/frontend/src/states/basic.ts index 57cebcc6..793555c3 100644 --- a/frontend/src/states/basic.ts +++ b/frontend/src/states/basic.ts @@ -1,57 +1,20 @@ -import { PayloadAction, createAsyncThunk, createSlice } from '@reduxjs/toolkit'; -import { axiosInstance, fetcher } from '..'; -import { LoadingState } from '../rest-model'; +import { fetcher } from '..'; +import { atom } from 'jotai'; +import { atomWithRefresh, loadable } from 'jotai/utils'; +import { loadable_unwrap } from './index'; -export const fetchBasicInfo = createAsyncThunk('basic/fetch', async (thunkApi) => { - const ret = await fetcher(`/api/info`); - return ret; -}); +export const onlineAtom = atom(false); +export const updatableVersionAtom = atom(undefined); -export const reloadLedger = createAsyncThunk('basic/fetch', async (thunkApi) => { - const ret = await axiosInstance.post('/api/reload'); - return ret; +export const basicInfoFetcher = atomWithRefresh(async (get) => { + return await fetcher<{ title: string; version: string }>(`/api/info`); }); -interface BasicInfoState { - title?: string; - version?: string; - isOnline: boolean; - status: LoadingState; - updatableVersion?: string; -} - -const initialState: BasicInfoState = { - title: undefined, - version: undefined, - isOnline: false, - status: LoadingState.NotReady, - updatableVersion: undefined, -}; +export const basicInfoAtom = loadable(basicInfoFetcher); -export const basicInfoSlice = createSlice({ - name: 'basic', - initialState, - reducers: { - online: (state) => { - state.isOnline = true; - }, - offline: (state) => { - state.isOnline = false; - }, - setUpdatableVersion: (state, action: PayloadAction<{ newVersion: string }>) => { - state.updatableVersion = action.payload.newVersion; - }, - }, - extraReducers: (builder) => { - builder.addCase(fetchBasicInfo.pending, (state, action) => { - state.status = LoadingState.Loading; - }); - - builder.addCase(fetchBasicInfo.fulfilled, (state, action) => { - state.status = LoadingState.Success; - state.isOnline = true; - state.title = action.payload.title; - state.version = action.payload.version; - }); - }, +export const titleAtom = atom((get) => { + return loadable_unwrap(get(basicInfoAtom), 'Zhang Accounting', (data) => data.title); +}); +export const versionAtom = atom((get) => { + return loadable_unwrap(get(basicInfoAtom), undefined, (data) => data.version); }); diff --git a/frontend/src/states/commodity.ts b/frontend/src/states/commodity.ts index 6eddbd2f..36b0417c 100644 --- a/frontend/src/states/commodity.ts +++ b/frontend/src/states/commodity.ts @@ -1,37 +1,19 @@ -import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; -import { RootState } from '.'; +import { loadable_unwrap } from '.'; import { fetcher } from '..'; -import { CommodityListItem, LoadingState } from '../rest-model'; +import { CommodityListItem } from '../rest-model'; +import { atomWithRefresh, loadable } from 'jotai/utils'; +import { groupBy } from 'lodash-es'; +import { atom } from 'jotai'; -export const fetchCommodities = createAsyncThunk('commodities/fetch', async (thunkApi) => { - const ret = await fetcher(`/api/commodities`); - return ret; -}); - -interface CommoditiesState { - value: { [key: string]: CommodityListItem }; - status: LoadingState; -} +export const FRONTEND_DEFAULT_GROUP = '__ZHANG__FRONTEND_DEFAULT__GROUP__'; -const initialState: CommoditiesState = { - value: {}, - status: LoadingState.NotReady, -}; +export const commoditiesFetcher = atomWithRefresh(async (get) => { + const ret = await fetcher(`/api/commodities`); + return Object.fromEntries(ret.map((item: CommodityListItem) => [item.name, item])); +}); -export const commoditiesSlice = createSlice({ - name: 'commodities', - initialState, - reducers: {}, - extraReducers: (builder) => { - builder.addCase(fetchCommodities.pending, (state, action) => { - state.status = LoadingState.Loading; - }); +export const commoditiesAtom = loadable(commoditiesFetcher); - builder.addCase(fetchCommodities.fulfilled, (state, action) => { - state.status = LoadingState.Success; - state.value = Object.fromEntries(action.payload.map((item: CommodityListItem) => [item.name, item])); - }); - }, +export const groupedCommoditiesAtom = atom((get) => { + return loadable_unwrap(get(commoditiesAtom), {}, (data) => groupBy(data, (it) => it.group ?? FRONTEND_DEFAULT_GROUP)); }); - -export const getCommodityByName = (name: string) => (state: RootState) => state.commodities.value[name]; diff --git a/frontend/src/states/errors.ts b/frontend/src/states/errors.ts index 23e671d9..10206710 100644 --- a/frontend/src/states/errors.ts +++ b/frontend/src/states/errors.ts @@ -2,6 +2,7 @@ import { fetcher } from '..'; import { Pageable, SpanInfo } from '../rest-model'; import { atomWithRefresh, loadable } from 'jotai/utils'; import { atom } from 'jotai'; +import { loadable_unwrap } from './index'; export interface LedgerError { id: string; @@ -22,10 +23,5 @@ export const errorsFetcher = atomWithRefresh(async (get) => { export const errorAtom = loadable(errorsFetcher); export const errorCountAtom = atom((get) => { - const errors = get(errorAtom); - if (errors.state === 'hasError' || errors.state === 'loading') { - return 0; - } else { - return errors.data.total_count; - } + return loadable_unwrap(get(errorAtom), 0, (data) => data.total_count); }); diff --git a/frontend/src/states/index.ts b/frontend/src/states/index.ts index 4fae6b04..a039a583 100644 --- a/frontend/src/states/index.ts +++ b/frontend/src/states/index.ts @@ -1,17 +1,9 @@ import { configureStore } from '@reduxjs/toolkit'; import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; -import { accountsSlice } from './account'; -import { basicInfoSlice } from './basic'; -import { commoditiesSlice } from './commodity'; -import { journalsSlice } from './journals'; +import { Loadable } from 'jotai/vanilla/utils/loadable'; export const store = configureStore({ - reducer: { - basic: basicInfoSlice.reducer, - commodities: commoditiesSlice.reducer, - accounts: accountsSlice.reducer, - journals: journalsSlice.reducer, - }, + reducer: {}, }); export type RootState = ReturnType; @@ -19,3 +11,11 @@ export type AppDispatch = typeof store.dispatch; export const useAppDispatch: () => AppDispatch = useDispatch; export const useAppSelector: TypedUseSelectorHook = useSelector; + +export function loadable_unwrap(val: Loadable>, init_value: F, mapper: (data: T) => F): F { + if (val.state === 'hasError' || val.state === 'loading') { + return init_value; + } else { + return mapper(val.data as T); + } +} diff --git a/frontend/src/states/journals.ts b/frontend/src/states/journals.ts index 69b3922e..71600b9c 100644 --- a/frontend/src/states/journals.ts +++ b/frontend/src/states/journals.ts @@ -1,52 +1,26 @@ -import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { RootState } from '.'; import { fetcher } from '..'; -import { LoadingState } from '../rest-model'; +import { JournalItem, Pageable } from '../rest-model'; +import { atom } from 'jotai'; +import { atomWithRefresh, loadable } from 'jotai/utils'; +import { loadable_unwrap } from './index'; +import { groupBy } from 'lodash-es'; +import { format } from 'date-fns'; -export const fetchJournals = createAsyncThunk('journals/fetch', async (keyword: string, { getState }) => { - const current_page = (getState() as RootState).journals.current_page; - const url = keyword.trim() === '' ? `/api/journals?page=${current_page}` : `/api/journals?page=${current_page}&keyword=${keyword.trim()}`; - const ret = await fetcher(url); +export const journalKeywordAtom = atom(''); +export const journalPageAtom = atom(1); + +export const journalFetcher = atomWithRefresh(async (get) => { + const page = get(journalPageAtom); + const keyword = get(journalKeywordAtom); + const url = keyword.trim() === '' ? `/api/journals?page=${page}` : `/api/journals?page=${page}&keyword=${keyword.trim()}`; + const ret = await fetcher>(url); return ret; }); -interface JournalState { - total_number: number; - total_page: number; - current_page: number; - items: any[]; - status: LoadingState; -} - -const initialState: JournalState = { - total_number: 0, - total_page: 1, - current_page: 1, - items: [], - status: LoadingState.NotReady, -}; - -export const journalsSlice = createSlice({ - name: 'journals', - initialState, - reducers: { - setPage: (state, action: PayloadAction<{ current_page: number }>) => { - state.current_page = action.payload.current_page; - }, - clear: (state) => { - state.status = LoadingState.NotReady; - }, - }, - extraReducers: (builder) => { - builder.addCase(fetchJournals.pending, (state, action) => { - state.status = LoadingState.Loading; - }); +export const journalAtom = loadable(journalFetcher); - builder.addCase(fetchJournals.fulfilled, (state, action) => { - state.status = LoadingState.Success; - state.total_number = action.payload.total_count; - state.total_page = action.payload.total_page; - state.items = action.payload.records; - }); - }, +export const groupedJournalsAtom = atom((get) => { + return loadable_unwrap(get(journalAtom), {}, (data) => { + return groupBy(data.records, (record) => format(new Date(record.datetime), 'yyyy-MM-dd')); + }); }); From 4bc3a92bf9000d74f3aabf38e285c072ddac337e Mon Sep 17 00:00:00 2001 From: Kilerd Chan Date: Thu, 11 Jul 2024 17:19:09 +0800 Subject: [PATCH 5/7] perf: remove redux dependencies --- frontend/package.json | 2 -- frontend/pnpm-lock.yaml | 68 ----------------------------------------- 2 files changed, 70 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index da0ef330..7a9de136 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,7 +16,6 @@ "@mantine/hooks": "^7.9.1", "@mantine/modals": "^7.9.1", "@mantine/notifications": "^7.9.1", - "@reduxjs/toolkit": "^2.2.4", "@tabler/icons-react": "^3.3.0", "@uiw/react-codemirror": "^4.22.0", "axios": "^1.6.8", @@ -34,7 +33,6 @@ "react-dropzone": "^14.2.3", "react-i18next": "^14.1.1", "react-icons": "^5.2.1", - "react-redux": "^9.1.2", "react-router": "^6.23.1", "react-router-dom": "^6.23.1", "react-scripts": "5.0.1", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index f9c854df..be95e1e1 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -44,9 +44,6 @@ dependencies: '@mantine/notifications': specifier: ^7.9.1 version: 7.9.1(@mantine/core@7.9.1)(@mantine/hooks@7.9.1)(react-dom@18.3.1)(react@18.3.1) - '@reduxjs/toolkit': - specifier: ^2.2.4 - version: 2.2.4(react-redux@9.1.2)(react@18.3.1) '@tabler/icons-react': specifier: ^3.3.0 version: 3.3.0(react@18.3.1) @@ -98,9 +95,6 @@ dependencies: react-icons: specifier: ^5.2.1 version: 5.2.1(react@18.3.1) - react-redux: - specifier: ^9.1.2 - version: 9.1.2(@types/react@18.3.2)(react@18.3.1)(redux@5.0.1) react-router: specifier: ^6.23.1 version: 6.23.1(react@18.3.1) @@ -2809,25 +2803,6 @@ packages: webpack-dev-server: 4.15.2(webpack@5.91.0) dev: false - /@reduxjs/toolkit@2.2.4(react-redux@9.1.2)(react@18.3.1): - resolution: {integrity: sha512-EoIC9iC2V/DLRBVMXRHrO/oM3QBT7RuJNeBRx8Cpnz/NHINeZBEqgI8YOxAYUjLp+KYxGgc4Wd6KoAKsaUBGhg==} - peerDependencies: - react: ^16.9.0 || ^17.0.0 || ^18 - react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 - peerDependenciesMeta: - react: - optional: true - react-redux: - optional: true - dependencies: - immer: 10.1.1 - react: 18.3.1 - react-redux: 9.1.2(@types/react@18.3.2)(react@18.3.1)(redux@5.0.1) - redux: 5.0.1 - redux-thunk: 3.1.0(redux@5.0.1) - reselect: 5.1.0 - dev: false - /@remix-run/router@1.16.1: resolution: {integrity: sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==} engines: {node: '>=14.0.0'} @@ -3429,10 +3404,6 @@ packages: resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} dev: false - /@types/use-sync-external-store@0.0.3: - resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==} - dev: false - /@types/ws@8.5.10: resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} dependencies: @@ -7189,10 +7160,6 @@ packages: resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} engines: {node: '>= 4'} - /immer@10.1.1: - resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} - dev: false - /immer@9.0.21: resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==} dev: false @@ -10259,25 +10226,6 @@ packages: resolution: {integrity: sha512-+PbtI3VuDV0l6CleQMsx2gtK0JZbZKbpdu5ynr+lbsuvtmgbNcS3VM0tuY2QjFNOcWxvXeHjDpy42RO+4U2rug==} dev: false - /react-redux@9.1.2(@types/react@18.3.2)(react@18.3.1)(redux@5.0.1): - resolution: {integrity: sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==} - peerDependencies: - '@types/react': ^18.2.25 - react: ^18.0 - redux: ^5.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - redux: - optional: true - dependencies: - '@types/react': 18.3.2 - '@types/use-sync-external-store': 0.0.3 - react: 18.3.1 - redux: 5.0.1 - use-sync-external-store: 1.2.2(react@18.3.1) - dev: false - /react-refresh@0.11.0: resolution: {integrity: sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==} engines: {node: '>=0.10.0'} @@ -10596,18 +10544,6 @@ packages: strip-indent: 3.0.0 dev: true - /redux-thunk@3.1.0(redux@5.0.1): - resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} - peerDependencies: - redux: ^5.0.0 - dependencies: - redux: 5.0.1 - dev: false - - /redux@5.0.1: - resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} - dev: false - /reflect.getprototypeof@1.0.6: resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} engines: {node: '>= 0.4'} @@ -10714,10 +10650,6 @@ packages: /requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - /reselect@5.1.0: - resolution: {integrity: sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg==} - dev: false - /resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} From 5f49421e695bf4dfc47a2227c215aee488c57389 Mon Sep 17 00:00:00 2001 From: Kilerd Chan Date: Thu, 11 Jul 2024 17:27:28 +0800 Subject: [PATCH 6/7] fix the batch balance functionality --- frontend/src/App.tsx | 4 +-- frontend/src/index.tsx | 34 ++++++++---------- frontend/src/pages/tools/BatchBalance.tsx | 44 ++++++++++++----------- frontend/src/states/index.ts | 12 ------- 4 files changed, 40 insertions(+), 54 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9556f4c3..a5ab76a9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -25,7 +25,6 @@ import { notifications } from '@mantine/notifications'; import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { axiosInstance, serverBaseUrl } from './index'; -import { useAppDispatch } from './states'; import { basicInfoFetcher, onlineAtom, titleAtom, updatableVersionAtom } from './states/basic'; import { useSWRConfig } from 'swr'; import { createStyles } from '@mantine/emotion'; @@ -201,7 +200,6 @@ export default function App() { const { mutate } = useSWRConfig(); const { classes } = useStyles(); const { t, i18n } = useTranslation(); - const dispatch = useAppDispatch(); const location = useLocation(); const [lang] = useLocalStorage({ key: 'lang', defaultValue: 'en' }); const [opened] = useDisclosure(); @@ -276,7 +274,7 @@ export default function App() { message: 'Client can not connect to server', }); }; - }, [dispatch, mutate]); // eslint-disable-line + }, [mutate]); // eslint-disable-line const sendReloadEvent = () => { notifications.show({ diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 1db6199d..1f5fc151 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -4,12 +4,10 @@ import { Notifications } from '@mantine/notifications'; import axios from 'axios'; import React from 'react'; import { createRoot } from 'react-dom/client'; -import { Provider } from 'react-redux'; import { BrowserRouter } from 'react-router-dom'; import App from './App'; import { TransactionPreviewModal } from './components/modals/TransactionPreviewModal'; import './i18n'; -import { store } from './states'; import { themeConfig } from './theme'; import './global.css'; import '@mantine/core/styles.css'; @@ -41,22 +39,20 @@ const container = document.getElementById('root'); const root = createRoot(container!); root.render( - - - - - - - - - - - - + + + + + + + + + + , ); diff --git a/frontend/src/pages/tools/BatchBalance.tsx b/frontend/src/pages/tools/BatchBalance.tsx index 64e89e10..3eace804 100644 --- a/frontend/src/pages/tools/BatchBalance.tsx +++ b/frontend/src/pages/tools/BatchBalance.tsx @@ -1,16 +1,14 @@ import { Button, Chip, Container, Group, Select, Table, TextInput, Title } from '@mantine/core'; import { useListState, useLocalStorage } from '@mantine/hooks'; -import { createSelector } from '@reduxjs/toolkit'; -import { cloneDeep, sortBy } from 'lodash-es'; -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; import Amount from '../../components/Amount'; -import { Account } from '../../rest-model'; -import { useAppSelector } from '../../states'; -import { accountFetcher, accountSelectItemsAtom } from '../../states/account'; +import { loadable_unwrap } from '../../states'; +import { accountAtom, accountFetcher, accountSelectItemsAtom } from '../../states/account'; import BigNumber from 'bignumber.js'; import { showNotification } from '@mantine/notifications'; import { axiosInstance } from '../..'; import { useAtomValue, useSetAtom } from 'jotai/index'; +import { selectAtom } from 'jotai/utils'; interface BalanceLineItem { commodity: string; @@ -22,22 +20,28 @@ interface BalanceLineItem { error: boolean; } -const getFilteredItems = createSelector([(states) => states.accounts], (accounts) => { - const items = accounts.data.flatMap((account: Account) => - Object.entries(account.amount.detail).map(([commodity, value]) => ({ - commodity: commodity, - currentAmount: value, - accountName: account.name, - balanceAmount: '', - pad: undefined, - error: false, - })), +export default function BatchBalance() { + const stateItems = useAtomValue( + useMemo( + () => + selectAtom(accountAtom, (val) => + loadable_unwrap(val, [], (data) => { + return data.flatMap((account) => + Object.entries(account.amount.detail).map(([commodity, value]) => ({ + commodity: commodity, + currentAmount: value, + accountName: account.name, + balanceAmount: '', + pad: undefined, + error: false, + })), + ); + }), + ), + [], + ), ); - return sortBy(cloneDeep(items), (item) => item.accountName); -}); -export default function BatchBalance() { - const stateItems = useAppSelector(getFilteredItems); const [accounts, accountsHandler] = useListState(stateItems); const accountItems = useAtomValue(accountSelectItemsAtom); const refreshAccounts = useSetAtom(accountFetcher); diff --git a/frontend/src/states/index.ts b/frontend/src/states/index.ts index a039a583..b1f9b1ce 100644 --- a/frontend/src/states/index.ts +++ b/frontend/src/states/index.ts @@ -1,17 +1,5 @@ -import { configureStore } from '@reduxjs/toolkit'; -import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; import { Loadable } from 'jotai/vanilla/utils/loadable'; -export const store = configureStore({ - reducer: {}, -}); - -export type RootState = ReturnType; -export type AppDispatch = typeof store.dispatch; - -export const useAppDispatch: () => AppDispatch = useDispatch; -export const useAppSelector: TypedUseSelectorHook = useSelector; - export function loadable_unwrap(val: Loadable>, init_value: F, mapper: (data: T) => F): F { if (val.state === 'hasError' || val.state === 'loading') { return init_value; From ae580029bdb7071b2a862c82552eb791005a1b42 Mon Sep 17 00:00:00 2001 From: Kilerd Chan Date: Thu, 11 Jul 2024 17:36:47 +0800 Subject: [PATCH 7/7] fix(server): journal api should return correct total count given filtered keyword Signed-off-by: Kilerd Chan --- .../validations.json | 8 ++++++++ zhang-server/src/routes/transaction.rs | 8 ++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/integration-tests/should_txn_support_filter_by_payee_narration/validations.json b/integration-tests/should_txn_support_filter_by_payee_narration/validations.json index 38824a6b..496a82d9 100644 --- a/integration-tests/should_txn_support_filter_by_payee_narration/validations.json +++ b/integration-tests/should_txn_support_filter_by_payee_narration/validations.json @@ -74,6 +74,10 @@ [ "$.data.records[0].postings.length()", 2 + ], + [ + "$.data.total_count", + 1 ] ] }, @@ -83,6 +87,10 @@ [ "$.data.records.length()", 0 + ], + [ + "$.data.total_count", + 0 ] ] } diff --git a/zhang-server/src/routes/transaction.rs b/zhang-server/src/routes/transaction.rs index 01065bdd..6a8ffb9d 100644 --- a/zhang-server/src/routes/transaction.rs +++ b/zhang-server/src/routes/transaction.rs @@ -43,10 +43,14 @@ pub async fn get_journals(ledger: State>>, params: Query = store .transactions .values()