From e8630d3ecc2f30c0e19a0646dd975a6b2101a6ec Mon Sep 17 00:00:00 2001 From: Sasha Date: Thu, 16 Jan 2025 12:11:04 +0100 Subject: [PATCH] feat(monitoring): preserves scroll position on a data table (BAL-3248) (#2962) --- .../hooks/useNavbarLogic/useNavbarLogic.tsx | 15 ++++--- .../organisms/UrlDataTable/UrlDataTable.tsx | 11 +++-- .../usePersistentScroll.tsx | 30 ++++++++++++++ .../src/pages/MerchantMonitoring/constants.ts | 1 + .../useMerchantMonitoringLogic.tsx | 40 +++++++------------ .../src/pages/MerchantMonitoring/schemas.ts | 5 ++- .../MerchantMonitoringBusinessReport.page.tsx | 2 +- apps/backoffice-v2/tailwind.config.cjs | 8 +++- .../atoms/ScrollArea/ScrollArea.tsx | 10 ++--- .../organisms/DataTable/DataTable.tsx | 36 +++++++++++------ 10 files changed, 102 insertions(+), 56 deletions(-) create mode 100644 apps/backoffice-v2/src/common/hooks/usePersistentScroll/usePersistentScroll.tsx create mode 100644 apps/backoffice-v2/src/pages/MerchantMonitoring/constants.ts diff --git a/apps/backoffice-v2/src/common/components/organisms/Header/hooks/useNavbarLogic/useNavbarLogic.tsx b/apps/backoffice-v2/src/common/components/organisms/Header/hooks/useNavbarLogic/useNavbarLogic.tsx index 5b2183c42b..be770e20cb 100644 --- a/apps/backoffice-v2/src/common/components/organisms/Header/hooks/useNavbarLogic/useNavbarLogic.tsx +++ b/apps/backoffice-v2/src/common/components/organisms/Header/hooks/useNavbarLogic/useNavbarLogic.tsx @@ -1,11 +1,13 @@ -import { useFiltersQuery } from '@/domains/filters/hooks/queries/useFiltersQuery/useFiltersQuery'; -import { useFilterId } from '@/common/hooks/useFilterId/useFilterId'; -import { useCallback, useMemo } from 'react'; import { Building, Goal, Home, MonitorDot, Users } from 'lucide-react'; -import { TRoutes, TRouteWithChildren } from '@/Router/types'; +import { useCallback, useMemo } from 'react'; import { useLocation } from 'react-router-dom'; -import { useCustomerQuery } from '@/domains/customer/hooks/queries/useCustomerQuery/useCustomerQuery'; + +import { useFilterId } from '@/common/hooks/useFilterId/useFilterId'; import { useLocale } from '@/common/hooks/useLocale/useLocale'; +import { useCustomerQuery } from '@/domains/customer/hooks/queries/useCustomerQuery/useCustomerQuery'; +import { useFiltersQuery } from '@/domains/filters/hooks/queries/useFiltersQuery/useFiltersQuery'; +import { MERCHANT_MONITORING_QUERY_PARAMS_KEY } from '@/pages/MerchantMonitoring/constants'; +import { TRoutes, TRouteWithChildren } from '@/Router/types'; export const useNavbarLogic = () => { const { data: filters } = useFiltersQuery(); @@ -20,6 +22,7 @@ export const useNavbarLogic = () => { [filters], ); const { data: customer } = useCustomerQuery(); + const merchantMonitoringParams = sessionStorage.getItem(MERCHANT_MONITORING_QUERY_PARAMS_KEY); const navItems = [ { @@ -33,7 +36,7 @@ export const useNavbarLogic = () => { { text: 'Merchant Monitoring', icon: , - href: `/en/merchant-monitoring`, + href: `/en/merchant-monitoring${merchantMonitoringParams ?? ''}`, key: 'nav-item-merchant-monitoring', }, ] diff --git a/apps/backoffice-v2/src/common/components/organisms/UrlDataTable/UrlDataTable.tsx b/apps/backoffice-v2/src/common/components/organisms/UrlDataTable/UrlDataTable.tsx index 3fe3c35843..b983b5c227 100644 --- a/apps/backoffice-v2/src/common/components/organisms/UrlDataTable/UrlDataTable.tsx +++ b/apps/backoffice-v2/src/common/components/organisms/UrlDataTable/UrlDataTable.tsx @@ -1,19 +1,24 @@ -import { ComponentProps, FunctionComponent } from 'react'; -import { useSort } from '@/common/hooks/useSort/useSort'; -import { useSelect } from '@/common/hooks/useSelect/useSelect'; import { DataTable } from '@ballerine/ui'; +import { ComponentProps, FunctionComponent } from 'react'; import { PartialDeep } from 'type-fest'; +import { usePersistentScroll } from '@/common/hooks/usePersistentScroll/usePersistentScroll'; +import { useSelect } from '@/common/hooks/useSelect/useSelect'; +import { useSort } from '@/common/hooks/useSort/useSort'; + export const UrlDataTable: FunctionComponent< Omit, 'sort' | 'select'> & PartialDeep, 'sort' | 'select'>> > = props => { const { sortDir, sortBy, onSort } = useSort(); const { selected, onSelect } = useSelect(); + const { ref, handleScroll } = usePersistentScroll(); return ( { + const scrollAreaRef = useRef(null); + + const resetScrollPosition = () => { + sessionStorage.removeItem('scrollPosition'); + }; + + const restoreScrollPosition = () => { + const savedPosition = sessionStorage.getItem('scrollPosition'); + + if (savedPosition && scrollAreaRef.current) { + scrollAreaRef.current.scroll(0, parseInt(savedPosition, 10)); + } + }; + + useEffect(() => { + if (scrollAreaRef.current?.scrollTop === 0) { + return restoreScrollPosition(); + } + }, []); + + const handleScroll = () => { + const scrollTop = scrollAreaRef.current?.scrollTop ?? 0; + sessionStorage.setItem('scrollPosition', scrollTop.toString()); + }; + + return { ref: scrollAreaRef, handleScroll }; +}; diff --git a/apps/backoffice-v2/src/pages/MerchantMonitoring/constants.ts b/apps/backoffice-v2/src/pages/MerchantMonitoring/constants.ts new file mode 100644 index 0000000000..d515538662 --- /dev/null +++ b/apps/backoffice-v2/src/pages/MerchantMonitoring/constants.ts @@ -0,0 +1 @@ +export const MERCHANT_MONITORING_QUERY_PARAMS_KEY = 'merchantMonitoringParams'; diff --git a/apps/backoffice-v2/src/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic.tsx b/apps/backoffice-v2/src/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic.tsx index 29c0730842..268fa5c3a0 100644 --- a/apps/backoffice-v2/src/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic.tsx +++ b/apps/backoffice-v2/src/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic.tsx @@ -1,40 +1,27 @@ import dayjs from 'dayjs'; import { SlidersHorizontal } from 'lucide-react'; -import { useCallback, ComponentProps, useMemo, useEffect } from 'react'; +import { ComponentProps, useCallback, useEffect, useMemo } from 'react'; +import { DateRangePicker } from '@/common/components/molecules/DateRangePicker/DateRangePicker'; import { useLocale } from '@/common/hooks/useLocale/useLocale'; -import { useSearch } from '@/common/hooks/useSearch/useSearch'; import { usePagination } from '@/common/hooks/usePagination/usePagination'; -import { useFindings } from '@/pages/MerchantMonitoring/hooks/useFindings/useFindings'; +import { useSearch } from '@/common/hooks/useSearch/useSearch'; import { useZodSearchParams } from '@/common/hooks/useZodSearchParams/useZodSearchParams'; -import { DateRangePicker } from '@/common/components/molecules/DateRangePicker/DateRangePicker'; -import { useCustomerQuery } from '@/domains/customer/hooks/queries/useCustomerQuery/useCustomerQuery'; import { useBusinessReportsQuery } from '@/domains/business-reports/hooks/queries/useBusinessReportsQuery/useBusinessReportsQuery'; +import { useCustomerQuery } from '@/domains/customer/hooks/queries/useCustomerQuery/useCustomerQuery'; +import { useFindings } from '@/pages/MerchantMonitoring/hooks/useFindings/useFindings'; import { + DISPLAY_TEXT_TO_IS_ALERT, DISPLAY_TEXT_TO_MERCHANT_REPORT_TYPE, + IS_ALERT_TO_DISPLAY_TEXT, MerchantMonitoringSearchSchema, + REPORT_STATUS_LABEL_TO_VALUE_MAP, REPORT_TYPE_TO_DISPLAY_TEXT, RISK_LEVEL_FILTER, STATUS_LEVEL_FILTER, - REPORT_STATUS_LABEL_TO_VALUE_MAP, - IS_ALERT_TO_DISPLAY_TEXT, - DISPLAY_TEXT_TO_IS_ALERT, } from '@/pages/MerchantMonitoring/schemas'; - -const useDefaultDateRange = () => { - const [{ from, to }, setSearchParams] = useZodSearchParams(MerchantMonitoringSearchSchema); - - useEffect(() => { - if (from || to) { - return; - } - - setSearchParams({ - from: dayjs().subtract(30, 'day').format('YYYY-MM-DD'), - to: dayjs().format('YYYY-MM-DD'), - }); - }, []); -}; +import { useLocation } from 'react-router-dom'; +import { MERCHANT_MONITORING_QUERY_PARAMS_KEY } from '@/pages/MerchantMonitoring/constants'; export const useMerchantMonitoringLogic = () => { const locale = useLocale(); @@ -59,6 +46,11 @@ export const useMerchantMonitoringLogic = () => { setSearchParams, ] = useZodSearchParams(MerchantMonitoringSearchSchema, { replace: true }); + const { search: searchString } = useLocation(); + useEffect(() => { + sessionStorage.setItem(MERCHANT_MONITORING_QUERY_PARAMS_KEY, searchString); + }, [searchString]); + const { findings: findingsOptions, isLoading: isLoadingFindings } = useFindings(); const { data, isLoading: isLoadingBusinessReports } = useBusinessReportsQuery({ @@ -172,8 +164,6 @@ export const useMerchantMonitoringLogic = () => { [findingsOptions], ); - useDefaultDateRange(); - return { totalPages: data?.totalPages || 0, totalItems: Intl.NumberFormat(locale).format(data?.totalItems || 0), diff --git a/apps/backoffice-v2/src/pages/MerchantMonitoring/schemas.ts b/apps/backoffice-v2/src/pages/MerchantMonitoring/schemas.ts index c9bdc492de..1f96385351 100644 --- a/apps/backoffice-v2/src/pages/MerchantMonitoring/schemas.ts +++ b/apps/backoffice-v2/src/pages/MerchantMonitoring/schemas.ts @@ -2,6 +2,7 @@ import { z } from 'zod'; import { BaseSearchSchema } from '@/common/hooks/useSearchParamsByEntity/validation-schemas'; import { TBusinessReport } from '@/domains/business-reports/fetchers'; import { BooleanishRecordSchema } from '@ballerine/ui'; +import dayjs from 'dayjs'; export const REPORT_TYPE_TO_DISPLAY_TEXT = { All: 'All', @@ -125,6 +126,6 @@ export const MerchantMonitoringSearchSchema = BaseSearchSchema.extend({ ], ) .catch('All'), - from: z.string().date().optional(), - to: z.string().date().optional(), + from: z.string().date().catch(dayjs().subtract(30, 'day').format('YYYY-MM-DD')), + to: z.string().date().catch(dayjs().format('YYYY-MM-DD')), }); diff --git a/apps/backoffice-v2/src/pages/MerchantMonitoringBusinessReport/MerchantMonitoringBusinessReport.page.tsx b/apps/backoffice-v2/src/pages/MerchantMonitoringBusinessReport/MerchantMonitoringBusinessReport.page.tsx index 5fba52d0a8..e98c6d52cb 100644 --- a/apps/backoffice-v2/src/pages/MerchantMonitoringBusinessReport/MerchantMonitoringBusinessReport.page.tsx +++ b/apps/backoffice-v2/src/pages/MerchantMonitoringBusinessReport/MerchantMonitoringBusinessReport.page.tsx @@ -306,7 +306,7 @@ export const MerchantMonitoringBusinessReport: FunctionComponent = () => { ))} - + {isFetchingBusinessReport ? ( <> diff --git a/apps/backoffice-v2/tailwind.config.cjs b/apps/backoffice-v2/tailwind.config.cjs index 413e8783e0..bde9bd292f 100644 --- a/apps/backoffice-v2/tailwind.config.cjs +++ b/apps/backoffice-v2/tailwind.config.cjs @@ -4,7 +4,13 @@ const { fontFamily } = require('tailwindcss/defaultTheme'); /** @type {import('tailwindcss').Config} */ module.exports = { darkMode: ['class'], - content: ['./*.html', './src/**/*.css', './src/**/*.ts', './src/**/*.tsx'], + content: [ + './*.html', + './src/**/*.css', + './src/**/*.ts', + './src/**/*.tsx', + './node_modules/@ballerine/ui/src/**/*.js', + ], theme: { container: { center: true, diff --git a/packages/ui/src/components/atoms/ScrollArea/ScrollArea.tsx b/packages/ui/src/components/atoms/ScrollArea/ScrollArea.tsx index 7ad2ca4cea..5e36eadd4f 100644 --- a/packages/ui/src/components/atoms/ScrollArea/ScrollArea.tsx +++ b/packages/ui/src/components/atoms/ScrollArea/ScrollArea.tsx @@ -3,13 +3,11 @@ import { ScrollBar } from '@/components/atoms'; import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; import * as React from 'react'; -interface Props extends ScrollAreaPrimitive.ScrollAreaProps { - orientation: 'vertical' | 'horizontal' | 'both'; -} - export const ScrollArea = React.forwardRef< - React.ElementRef>, - React.ComponentPropsWithoutRef> + React.ElementRef, + React.ComponentPropsWithoutRef & { + orientation: 'vertical' | 'horizontal' | 'both'; + } >(({ className, children, orientation, ...props }, ref) => ( diff --git a/packages/ui/src/components/organisms/DataTable/DataTable.tsx b/packages/ui/src/components/organisms/DataTable/DataTable.tsx index f9dc0914f4..cd8b7722df 100644 --- a/packages/ui/src/components/organisms/DataTable/DataTable.tsx +++ b/packages/ui/src/components/organisms/DataTable/DataTable.tsx @@ -73,19 +73,26 @@ export interface IDataTableProps { onSelect: (ids: Record) => void; selected: Record; }; + + scrollRef?: React.RefObject; + handleScroll?: (event: React.UIEvent) => void; } -export const DataTable = ({ - data, - props, - caption, - columns, - CellContentWrapper, - options = {}, - CollapsibleContent, - sort, - select, -}: IDataTableProps) => { +const DataTableBase = ( + { + data, + props, + caption, + columns, + CellContentWrapper, + options = {}, + CollapsibleContent, + sort, + select, + handleScroll, + }: IDataTableProps, + ref: React.ForwardedRef, +) => { const [expanded, setExpanded] = useState({}); const { enableSorting = false } = options; @@ -204,7 +211,7 @@ export const DataTable = ({ return (
- + {caption && ( ({ ); }; + +const forward = React.forwardRef as >( + render: (props: P, ref: React.Ref) => React.ReactNode, +) => (props: P & React.RefAttributes) => React.ReactNode; +export const DataTable = forward(DataTableBase);