From 3a215d0b832a37e5e2f1e7c1eefea055a77508aa Mon Sep 17 00:00:00 2001 From: Iveta Date: Wed, 11 Oct 2023 14:27:20 -0400 Subject: [PATCH] SDP-782: State refactor: payments (#22) * State refactor: payments * Cleanup --- src/apiQueries/usePayments.ts | 24 ++++++ src/components/Pagination/index.tsx | 41 +++++---- src/components/PaymentsTable.tsx | 10 +-- src/components/Table/index.tsx | 12 ++- src/components/Table/styles.scss | 23 +++++- src/pages/DisbursementDetails.tsx | 35 ++------ src/pages/Disbursements.tsx | 37 ++------- src/pages/Payments.tsx | 108 ++++++------------------ src/pages/ReceiverDetails.tsx | 50 +++-------- src/pages/Receivers.tsx | 35 ++------ src/store/ducks/payments.ts | 124 ---------------------------- src/store/index.ts | 2 - src/types/index.ts | 9 -- 13 files changed, 141 insertions(+), 369 deletions(-) create mode 100644 src/apiQueries/usePayments.ts delete mode 100644 src/store/ducks/payments.ts diff --git a/src/apiQueries/usePayments.ts b/src/apiQueries/usePayments.ts new file mode 100644 index 0000000..aa91f43 --- /dev/null +++ b/src/apiQueries/usePayments.ts @@ -0,0 +1,24 @@ +import { useQuery } from "@tanstack/react-query"; +import { handleSearchParams } from "api/handleSearchParams"; +import { API_URL } from "constants/settings"; +import { fetchApi } from "helpers/fetchApi"; +import { ApiPayments, AppError, PaymentsSearchParams } from "types"; + +export const usePayments = (searchParams?: PaymentsSearchParams) => { + // ALL status is for UI only + if (searchParams?.status === "ALL") { + delete searchParams.status; + } + + const params = handleSearchParams(searchParams); + + const query = useQuery({ + queryKey: ["payments", { ...searchParams }], + queryFn: async () => { + return await fetchApi(`${API_URL}/payments/${params}`); + }, + keepPreviousData: true, + }); + + return query; +}; diff --git a/src/components/Pagination/index.tsx b/src/components/Pagination/index.tsx index 10744c7..abfcdd1 100644 --- a/src/components/Pagination/index.tsx +++ b/src/components/Pagination/index.tsx @@ -1,26 +1,27 @@ +import { useState } from "react"; import { Button, Icon, Input } from "@stellar/design-system"; import "./styles.scss"; interface PaginationProps { currentPage: number; maxPages: number; - onChange: (event: React.ChangeEvent) => void; - onBlur: (currentPage: number) => void; - onPrevious: (event: React.MouseEvent) => void; - onNext: (event: React.MouseEvent) => void; + onSetPage: (page: number) => void; isLoading: boolean; } export const Pagination = ({ currentPage, maxPages, - onChange, - onBlur, - onPrevious, - onNext, + onSetPage, isLoading, }: PaginationProps) => { - const isError = currentPage > maxPages; + const [page, setPage] = useState(); + const isError = (page || 0) > maxPages; + + const handleChange = (event: React.ChangeEvent) => { + event.preventDefault(); + setPage(Number(event.target.value)); + }; const handleBlur = ( event: @@ -29,11 +30,21 @@ export const Pagination = ({ ) => { event.preventDefault(); - if (!isError) { - onBlur(currentPage); + if (!isError && page) { + onSetPage(page); + setPage(undefined); } }; + const handlePageChange = ( + event: React.MouseEvent, + direction: "prev" | "next", + ) => { + event.preventDefault(); + const newPage = direction === "prev" ? currentPage - 1 : currentPage + 1; + onSetPage(newPage); + }; + return (
Page @@ -41,8 +52,8 @@ export const Pagination = ({ } - onClick={onPrevious} + onClick={(event) => handlePageChange(event, "prev")} disabled={isError || isLoading || currentPage === 1} />
diff --git a/src/components/PaymentsTable.tsx b/src/components/PaymentsTable.tsx index 321fd38..444e0e2 100644 --- a/src/components/PaymentsTable.tsx +++ b/src/components/PaymentsTable.tsx @@ -6,20 +6,20 @@ import { AssetAmount } from "components/AssetAmount"; import { PaymentStatus } from "components/PaymentStatus"; import { Table } from "components/Table"; import { formatDateTime } from "helpers/formatIntlDateTime"; -import { ApiPayment, ActionStatus } from "types"; +import { ApiPayment } from "types"; interface PaymentsTableProps { paymentItems: ApiPayment[]; apiError: string | boolean | undefined; isFiltersSelected: boolean | undefined; - status: ActionStatus | undefined; + isLoading: boolean; } export const PaymentsTable = ({ paymentItems, apiError, isFiltersSelected, - status, + isLoading, }: PaymentsTableProps) => { const navigate = useNavigate(); @@ -48,7 +48,7 @@ export const PaymentsTable = ({ } if (paymentItems?.length === 0) { - if (status === "PENDING") { + if (isLoading) { return
Loading…
; } @@ -66,7 +66,7 @@ export const PaymentsTable = ({ return (
- +
{/* TODO: put back once ready */} {/* diff --git a/src/components/Table/index.tsx b/src/components/Table/index.tsx index b744f57..016b317 100644 --- a/src/components/Table/index.tsx +++ b/src/components/Table/index.tsx @@ -1,4 +1,4 @@ -import { Icon } from "@stellar/design-system"; +import { Icon, Loader } from "@stellar/design-system"; import { SortDirection } from "types"; import "./styles.scss"; @@ -140,16 +140,24 @@ interface TableComponent { interface TableProps extends React.HtmlHTMLAttributes { children: JSX.Element[]; + isLoading?: boolean; } export const Table: React.FC & TableComponent = ({ children, + isLoading, }: TableProps) => { return ( -
+
{children}
+ {isLoading ? : null} ); }; diff --git a/src/components/Table/styles.scss b/src/components/Table/styles.scss index 5490939..699e174 100644 --- a/src/components/Table/styles.scss +++ b/src/components/Table/styles.scss @@ -3,11 +3,32 @@ .Table-v2__container { width: 100%; position: relative; + + &--loading { + overflow-y: hidden; + pointer-events: none; + + .Table-v2__wrapper { + opacity: var(--opacity-disabled-button); + } + + .Loader { + --Loader-color: var(--color-gray-60); + --Loader-size: 2rem; + + position: absolute; + top: 2rem; + left: 50%; + transform: translate(-50%, -50%); + } + } } .Table-v2__wrapper { overflow-x: auto; overflow-y: hidden; + opacity: 1; + transition: opacity var(--anim-transition-default); } table.Table-v2 { @@ -17,7 +38,7 @@ table.Table-v2 { thead tr, tr:not(:last-child) { border-bottom: 1px solid var(--color-gray-30); - transition: background-color linear var(--anim-transition-default); + transition: background-color var(--anim-transition-default); &.Table-v2__row--highlighted { background-color: var(--color-gray-10); diff --git a/src/pages/DisbursementDetails.tsx b/src/pages/DisbursementDetails.tsx index 660cfef..69d4c37 100644 --- a/src/pages/DisbursementDetails.tsx +++ b/src/pages/DisbursementDetails.tsx @@ -128,30 +128,6 @@ export const DisbursementDetails = () => { ); }; - const handlePageChange = (currentPage: number) => { - dispatch(getDisbursementReceiversAction({ page: currentPage.toString() })); - }; - - const handleNextPage = ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - const newPage = currentPage + 1; - - setCurrentPage(newPage); - handlePageChange(newPage); - }; - - const handlePrevPage = ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - const newPage = currentPage - 1; - - setCurrentPage(newPage); - handlePageChange(newPage); - }; - const goToReceiver = ( event: React.MouseEvent, receiverId: string, @@ -511,13 +487,12 @@ export const DisbursementDetails = () => { { - event.preventDefault(); - setCurrentPage(Number(event.target.value)); + onSetPage={(page) => { + setCurrentPage(page); + dispatch( + getDisbursementReceiversAction({ page: page.toString() }), + ); }} - onBlur={handlePageChange} - onNext={handleNextPage} - onPrevious={handlePrevPage} isLoading={disbursementDetails.status === "PENDING"} /> diff --git a/src/pages/Disbursements.tsx b/src/pages/Disbursements.tsx index 7b85b04..8079e2a 100644 --- a/src/pages/Disbursements.tsx +++ b/src/pages/Disbursements.tsx @@ -164,32 +164,6 @@ export const Disbursements = () => { ); }; - const handlePageChange = (currentPage: number) => { - dispatch( - getDisbursementsWithParamsAction({ page: currentPage.toString() }), - ); - }; - - const handleNextPage = ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - const newPage = currentPage + 1; - - setCurrentPage(newPage); - handlePageChange(newPage); - }; - - const handlePrevPage = ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - const newPage = currentPage - 1; - - setCurrentPage(newPage); - handlePageChange(newPage); - }; - return ( <> @@ -304,13 +278,12 @@ export const Disbursements = () => { { - event.preventDefault(); - setCurrentPage(Number(event.target.value)); + onSetPage={(page) => { + setCurrentPage(page); + dispatch( + getDisbursementsWithParamsAction({ page: page.toString() }), + ); }} - onBlur={handlePageChange} - onNext={handleNextPage} - onPrevious={handlePrevPage} isLoading={disbursements.status === "PENDING"} /> diff --git a/src/pages/Payments.tsx b/src/pages/Payments.tsx index 7986a35..c7bda8a 100644 --- a/src/pages/Payments.tsx +++ b/src/pages/Payments.tsx @@ -1,6 +1,5 @@ -import { useEffect, useState } from "react"; +import { useState } from "react"; import { Button, Heading, Icon, Input, Select } from "@stellar/design-system"; -import { useDispatch } from "react-redux"; import { FilterMenu } from "components/FilterMenu"; import { Pagination } from "components/Pagination"; @@ -8,20 +7,12 @@ import { PaymentsTable } from "components/PaymentsTable"; import { SearchInput } from "components/SearchInput"; import { SectionHeader } from "components/SectionHeader"; +import { usePayments } from "apiQueries/usePayments"; import { PAGE_LIMIT_OPTIONS } from "constants/settings"; import { number } from "helpers/formatIntlNumber"; -import { useRedux } from "hooks/useRedux"; -import { AppDispatch } from "store"; -import { - getPaymentsWithParamsAction, - getPaymentsAction, -} from "store/ducks/payments"; import { CommonFilters } from "types"; export const Payments = () => { - const { payments } = useRedux("payments"); - - // TODO: handle search in progress const [isSearchInProgress] = useState(false); const [currentPage, setCurrentPage] = useState(1); const [pageLimit, setPageLimit] = useState(20); @@ -33,18 +24,25 @@ export const Payments = () => { }; const [filters, setFilters] = useState(initFilters); + // Using extra param to trigger API call when we want, not on every filter + // state change + const [queryFilters, setQueryFilters] = useState({}); + + const { + data: payments, + error, + isLoading, + isFetching, + } = usePayments({ + page: currentPage.toString(), + page_limit: pageLimit.toString(), + ...queryFilters, + }); const isFiltersSelected = Object.values(filters).filter((v) => Boolean(v)).length > 0; - const dispatch: AppDispatch = useDispatch(); - - useEffect(() => { - dispatch(getPaymentsAction()); - }, [dispatch]); - - const apiError = payments.status === "ERROR" && payments.errorString; - const maxPages = payments.pagination?.pages || 1; + const maxPages = payments?.pagination?.pages || 1; const handleSearchSubmit = () => { alert("TODO: search submit"); @@ -64,26 +62,14 @@ export const Payments = () => { }; const handleFilterSubmit = () => { - dispatch( - getPaymentsWithParamsAction({ - page: "1", - ...filters, - }), - ); - setCurrentPage(1); + setQueryFilters(filters); }; const handleFilterReset = () => { - dispatch( - getPaymentsWithParamsAction({ - page: "1", - ...initFilters, - }), - ); - - setFilters(initFilters); setCurrentPage(1); + setFilters(initFilters); + setQueryFilters(initFilters); }; const handleExport = ( @@ -97,42 +83,8 @@ export const Payments = () => { event: React.ChangeEvent, ) => { event.preventDefault(); - - const pageLimit = Number(event.target.value); - setPageLimit(pageLimit); setCurrentPage(1); - - // Need to make sure we'll be loading page 1 - dispatch( - getPaymentsWithParamsAction({ - page_limit: pageLimit.toString(), - page: "1", - }), - ); - }; - - const handlePageChange = (currentPage: number) => { - dispatch(getPaymentsWithParamsAction({ page: currentPage.toString() })); - }; - - const handleNextPage = ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - const newPage = currentPage + 1; - - setCurrentPage(newPage); - handlePageChange(newPage); - }; - - const handlePrevPage = ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - const newPage = currentPage - 1; - - setCurrentPage(newPage); - handlePageChange(newPage); + setPageLimit(Number(event.target.value)); }; return ( @@ -141,7 +93,7 @@ export const Payments = () => { - {payments.pagination?.total && payments.pagination.total > 0 + {payments?.pagination?.total && payments.pagination.total > 0 ? `${number.format(payments.pagination.total)} ` : ""} Payments @@ -238,24 +190,18 @@ export const Payments = () => { { - event.preventDefault(); - setCurrentPage(Number(event.target.value)); - }} - onBlur={handlePageChange} - onNext={handleNextPage} - onPrevious={handlePrevPage} - isLoading={payments.status === "PENDING"} + onSetPage={(page) => setCurrentPage(page)} + isLoading={isLoading || isFetching} /> ); diff --git a/src/pages/ReceiverDetails.tsx b/src/pages/ReceiverDetails.tsx index fa8fd88..b545cf9 100644 --- a/src/pages/ReceiverDetails.tsx +++ b/src/pages/ReceiverDetails.tsx @@ -99,37 +99,6 @@ export const ReceiverDetails = () => { } }; - const handlePageChange = (currentPage: number) => { - if (receiverId) { - dispatch( - getReceiverPaymentsWithParamsAction({ - receiver_id: receiverId, - page: currentPage.toString(), - }), - ); - } - }; - - const handleNextPage = ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - const newPage = currentPage + 1; - - setCurrentPage(newPage); - handlePageChange(newPage); - }; - - const handlePrevPage = ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - const newPage = currentPage - 1; - - setCurrentPage(newPage); - handlePageChange(newPage); - }; - const handleRetryInvitation = (receiverWalletId: string) => { dispatch(retryInvitationSMSAction({ receiverWalletId })); }; @@ -570,13 +539,18 @@ export const ReceiverDetails = () => { { - event.preventDefault(); - setCurrentPage(Number(event.target.value)); + onSetPage={(page) => { + setCurrentPage(page); + + if (receiverId) { + dispatch( + getReceiverPaymentsWithParamsAction({ + receiver_id: receiverId, + page: page.toString(), + }), + ); + } }} - onBlur={handlePageChange} - onNext={handleNextPage} - onPrevious={handlePrevPage} isLoading={receiverPayments.status === "PENDING"} /> @@ -587,7 +561,7 @@ export const ReceiverDetails = () => { paymentItems={receiverPayments.items} apiError={receiverPayments.errorString} isFiltersSelected={undefined} - status={receiverPayments.status} + isLoading={receiverPayments.status === "PENDING"} /> diff --git a/src/pages/Receivers.tsx b/src/pages/Receivers.tsx index b6244f4..d343299 100644 --- a/src/pages/Receivers.tsx +++ b/src/pages/Receivers.tsx @@ -140,30 +140,6 @@ export const Receivers = () => { ); }; - const handlePageChange = (currentPage: number) => { - dispatch(getReceiversWithParamsAction({ page: currentPage.toString() })); - }; - - const handleNextPage = ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - const newPage = currentPage + 1; - - setCurrentPage(newPage); - handlePageChange(newPage); - }; - - const handlePrevPage = ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - const newPage = currentPage - 1; - - setCurrentPage(newPage); - handlePageChange(newPage); - }; - const handleReceiverClicked = ( event: React.MouseEvent, receiverId: string, @@ -273,13 +249,12 @@ export const Receivers = () => { { - event.preventDefault(); - setCurrentPage(Number(event.target.value)); + onSetPage={(page) => { + setCurrentPage(page); + dispatch( + getReceiversWithParamsAction({ page: page.toString() }), + ); }} - onBlur={handlePageChange} - onNext={handleNextPage} - onPrevious={handlePrevPage} isLoading={receivers.status === "PENDING"} /> diff --git a/src/store/ducks/payments.ts b/src/store/ducks/payments.ts deleted file mode 100644 index eb14605..0000000 --- a/src/store/ducks/payments.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; -import { RootState } from "store"; -import { getPayments } from "api/getPayments"; -import { handleApiErrorString } from "api/handleApiErrorString"; -import { endSessionIfTokenInvalid } from "helpers/endSessionIfTokenInvalid"; -import { refreshSessionToken } from "helpers/refreshSessionToken"; -import { - ApiError, - ApiPayments, - PaymentsInitialState, - PaymentsSearchParams, - RejectMessage, -} from "types"; - -export const getPaymentsAction = createAsyncThunk< - ApiPayments, - undefined, - { rejectValue: RejectMessage; state: RootState } ->( - "payments/getPaymentsAction", - async (_, { rejectWithValue, getState, dispatch }) => { - const { token } = getState().userAccount; - - try { - const payments = await getPayments(token); - refreshSessionToken(dispatch); - - return payments; - } catch (error: unknown) { - const errorString = handleApiErrorString(error as ApiError); - endSessionIfTokenInvalid(errorString, dispatch); - - return rejectWithValue({ - errorString: `Error fetching payments: ${errorString}`, - }); - } - }, -); - -export const getPaymentsWithParamsAction = createAsyncThunk< - { - payments: ApiPayments; - searchParams: PaymentsSearchParams; - }, - PaymentsSearchParams, - { rejectValue: RejectMessage; state: RootState } ->( - "payments/getPaymentsWithParamsAction", - async (params, { rejectWithValue, getState, dispatch }) => { - const { token } = getState().userAccount; - const { searchParams } = getState().payments; - - const newParams = { ...searchParams, ...params }; - - try { - const payments = await getPayments(token, newParams); - refreshSessionToken(dispatch); - - return { - payments: payments, - searchParams: newParams, - }; - } catch (error: unknown) { - const errorString = handleApiErrorString(error as ApiError); - endSessionIfTokenInvalid(errorString, dispatch); - - return rejectWithValue({ - errorString: `Error fetching paginated payments: ${errorString}`, - }); - } - }, -); - -const initialState: PaymentsInitialState = { - items: [], - status: undefined, - pagination: undefined, - errorString: undefined, - searchParams: undefined, -}; - -const paymentsSlice = createSlice({ - name: "payments", - initialState, - reducers: {}, - extraReducers: (builder) => { - // Get payments - builder.addCase(getPaymentsAction.pending, (state = initialState) => { - state.status = "PENDING"; - }); - builder.addCase(getPaymentsAction.fulfilled, (state, action) => { - state.items = action.payload.data; - state.pagination = action.payload.pagination; - state.status = "SUCCESS"; - state.errorString = undefined; - state.searchParams = undefined; - }); - builder.addCase(getPaymentsAction.rejected, (state, action) => { - state.status = "ERROR"; - state.errorString = action.payload?.errorString; - }); - // Payments with search params - builder.addCase( - getPaymentsWithParamsAction.pending, - (state = initialState) => { - state.status = "PENDING"; - }, - ); - builder.addCase(getPaymentsWithParamsAction.fulfilled, (state, action) => { - state.items = action.payload.payments.data; - state.pagination = action.payload.payments.pagination; - state.status = "SUCCESS"; - state.errorString = undefined; - state.searchParams = action.payload.searchParams; - }); - builder.addCase(getPaymentsWithParamsAction.rejected, (state, action) => { - state.status = "ERROR"; - state.errorString = action.payload?.errorString; - }); - }, -}); - -export const paymentsSelector = (state: RootState) => state.payments; -export const { reducer } = paymentsSlice; diff --git a/src/store/index.ts b/src/store/index.ts index 049e2e9..1016f78 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -16,7 +16,6 @@ import { reducer as disbursementDrafts } from "store/ducks/disbursementDrafts"; import { reducer as disbursements } from "store/ducks/disbursements"; import { reducer as forgotPassword } from "store/ducks/forgotPassword"; import { reducer as organization } from "store/ducks/organization"; -import { reducer as payments } from "store/ducks/payments"; import { reducer as profile } from "store/ducks/profile"; import { reducer as receiverDetails } from "store/ducks/receiverDetails"; import { reducer as receiverPayments } from "store/ducks/receiverPayments"; @@ -48,7 +47,6 @@ const reducers = combineReducers({ disbursements, forgotPassword, organization, - payments, profile, receiverDetails, receiverPayments, diff --git a/src/types/index.ts b/src/types/index.ts index 57aac75..5594460 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -114,14 +114,6 @@ export type ForgotPasswordInitialState = { errorExtras?: AnyObject; }; -export type PaymentsInitialState = { - items: ApiPayment[]; - status: ActionStatus | undefined; - pagination?: Pagination; - errorString?: string; - searchParams?: PaymentsSearchParams; -}; - export type StatisticsInitialState = { stats: HomeStatistics | undefined; status: ActionStatus | undefined; @@ -220,7 +212,6 @@ export interface Store { disbursements: DisbursementsInitialState; forgotPassword: ForgotPasswordInitialState; organization: OrganizationInitialState; - payments: PaymentsInitialState; profile: ProfileInitialState; receiverDetails: ReceiverDetailsInitialState; receiverPayments: ReceiverPaymentsInitialState;