diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/__tests__/LocationDetailView.spec.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/__tests__/LocationDetailView.spec.tsx index de4dd9b0962..dcf2794eb90 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/__tests__/LocationDetailView.spec.tsx +++ b/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/__tests__/LocationDetailView.spec.tsx @@ -293,6 +293,7 @@ describe('LocationDetailView', () => { handlePaginateNext, handlePaginatePrevious, handleReset: jest.fn(), + range: [0, testResult.length], }); mockListItemsAction({ result: testResult }); @@ -329,6 +330,7 @@ describe('LocationDetailView', () => { handlePaginateNext, handlePaginatePrevious, handleReset: jest.fn(), + range: [0, testResult.length], }); mockListItemsAction({ result: testResult }); diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationsView/Controls/DataTable.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/Controls/DataTable.tsx index 3dce52972cd..e96bd35b618 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/LocationsView/Controls/DataTable.tsx +++ b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/Controls/DataTable.tsx @@ -8,8 +8,6 @@ import { TABLE_HEADER_BUTTON_CLASS_NAME, TABLE_HEADER_CLASS_NAME, } from '../../../components/DataTable'; -import { useLocationsData } from '../../../context/actions'; -import { useControl } from '../../../context/control'; import { LocationAccess } from '../../../context/types'; import { ButtonElement, IconElement } from '../../../context/elements'; @@ -121,33 +119,26 @@ const getLocationsData = ({ return { columns, rows }; }; -export function DataTableControl({ - range, -}: { - range: [start: number, end: number]; -}): React.JSX.Element | null { - const [{ data, hasError }] = useLocationsData(); - - const [, handleUpdateState] = useControl('NAVIGATE'); +interface DataTableControlProps { + items: LocationAccess[]; + handleLocationClick: (location: LocationAccess) => void; +} +export function DataTableControl({ + items, + handleLocationClick, +}: DataTableControlProps): React.JSX.Element | null { const [sortState, setSortState] = React.useState({ selection: 'scope', direction: 'ascending', }); - const [start, end] = range; - const locationsData = React.useMemo( () => getLocationsData({ - data: data.result.slice(start, end), + data: items, sortState, - onLocationClick: (location) => { - handleUpdateState({ - type: 'ACCESS_LOCATION', - location, - }); - }, + onLocationClick: handleLocationClick, onTableHeaderClick: (location: string) => { setSortState((prevState) => ({ selection: location, @@ -156,8 +147,8 @@ export function DataTableControl({ })); }, }), - [data.result, handleUpdateState, sortState, start, end] + [items, handleLocationClick, sortState] ); - return hasError ? null : ; + return ; } diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationsView/Controls/__tests__/DataTable.spec.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/Controls/__tests__/DataTable.spec.tsx index bce47343f76..f5c9b7352ee 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/LocationsView/Controls/__tests__/DataTable.spec.tsx +++ b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/Controls/__tests__/DataTable.spec.tsx @@ -8,8 +8,6 @@ import { LocationAccess } from '../../../../context/types'; import { DataTableControl } from '../DataTable'; -const TEST_RANGE: [number, number] = [0, 100]; - const useControlModuleSpy = jest.spyOn(UseControlModule, 'useControl'); const useLocationsDataSpy = jest.spyOn( UseLocationsDataModule, @@ -36,7 +34,9 @@ describe('LocationsViewTableControl', () => { }); it('renders the table with data', () => { - const { getByText } = render(); + const { getByText } = render( + + ); expect(getByText('Name')).toBeInTheDocument(); expect(getByText('Type')).toBeInTheDocument(); @@ -46,7 +46,9 @@ describe('LocationsViewTableControl', () => { }); it('renders the correct icon based on sort state', () => { - const { getByText } = render(); + const { getByText } = render( + + ); const nameTh = screen.getByRole('columnheader', { name: 'Name' }); @@ -58,17 +60,17 @@ describe('LocationsViewTableControl', () => { }); it('triggers location click handler when a row is clicked', () => { - const mockHandleUpdateState = jest.fn(); - useControlModuleSpy.mockReturnValue([{}, mockHandleUpdateState]); - - render(); + const mockHandleLocationClick = jest.fn(); + render( + + ); const firstRowButton = screen.getByRole('button', { name: 'Location A' }); fireEvent.click(firstRowButton); - expect(mockHandleUpdateState).toHaveBeenCalledWith({ - type: 'ACCESS_LOCATION', - location: mockData[0], - }); + expect(mockHandleLocationClick).toHaveBeenCalledWith(mockData[0]); }); }); diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationsView/LocationsView.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/LocationsView.tsx index 1953bd5a6e3..d7c597162c9 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/LocationsView/LocationsView.tsx +++ b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/LocationsView.tsx @@ -1,23 +1,16 @@ import React from 'react'; - import { CLASS_BASE } from '../constants'; import { Controls } from '../Controls'; import { useLocationsData } from '../../context/actions'; - -import { usePaginate } from '../hooks/usePaginate'; -import { listViewHelpers, resolveClassName } from '../utils'; - +import { resolveClassName } from '../utils'; import { DataTableControl } from './Controls/DataTable'; +import { LocationAccess } from '../../context/types'; +import { useLocationsView } from './useLocationsView'; export interface LocationsViewProps { className?: (defaultClassName: string) => string; } -const DEFAULT_PAGE_SIZE = 100; -export const DEFAULT_LIST_OPTIONS = { - exclude: 'WRITE' as const, - pageSize: DEFAULT_PAGE_SIZE, -}; export const DEFAULT_ERROR_MESSAGE = 'There was an error loading locations.'; const { @@ -64,40 +57,15 @@ const LocationsEmptyMessage = () => { export function LocationsView({ className, }: LocationsViewProps): React.JSX.Element { - const [{ data, isLoading, hasError }, handleList] = useLocationsData(); + const [{ data, isLoading, hasError }, handleAction] = useLocationsView(); + const { getProcessedItems, hasMoreData, page } = data; - const { result, nextToken } = data; - const resultCount = result.length; - const hasNextToken = !!nextToken; + const handleLocationClick = (location: LocationAccess) => { + handleAction({ type: 'SELECT_LOCATION', location }); + }; - // initial load - React.useEffect(() => { - handleList({ - options: { ...DEFAULT_LIST_OPTIONS, refresh: true }, - }); - }, [handleList]); - - const onPaginateNext = () => - handleList({ - options: { ...DEFAULT_LIST_OPTIONS, nextToken }, - }); - - const { - currentPage, - handlePaginateNext, - handlePaginatePrevious, - handleReset, - } = usePaginate({ onPaginateNext, pageSize: DEFAULT_PAGE_SIZE }); - - const { disableNext, disablePrevious, disableRefresh, range } = - listViewHelpers({ - currentPage, - hasNextToken, - isLoading, - pageSize: DEFAULT_PAGE_SIZE, - resultCount, - hasError, - }); + const disableNext = !hasMoreData || isLoading || hasError; + const disablePrevious = page <= 1 || isLoading || hasError; return (
Home { - handleReset(); - handleList({ - options: { ...DEFAULT_LIST_OPTIONS, refresh: true }, - }); + handleAction({ type: 'REFRESH' }); }} /> { - handlePaginateNext({ resultCount, hasNextToken }); + handleAction({ type: 'PAGINATE_NEXT' }); + }} + handlePrevious={() => { + handleAction({ type: 'PAGINATE_PREVIOUS' }); }} - handlePrevious={handlePaginatePrevious} /> - + {hasError ? null : ( + + )}
); diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationsView/__tests__/LocationsView.spec.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/__tests__/LocationsView.spec.tsx index dc9793e3a3f..9c3d34d0de3 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/LocationsView/__tests__/LocationsView.spec.tsx +++ b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/__tests__/LocationsView.spec.tsx @@ -1,16 +1,19 @@ import React from 'react'; -import { render, screen, waitFor } from '@testing-library/react'; +import { act, render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import createProvider from '../../../createProvider'; import * as ActionsModule from '../../../context/actions'; import * as ControlsModule from '../../../context/control'; - import { LocationsView } from '..'; -import { DEFAULT_LIST_OPTIONS, DEFAULT_ERROR_MESSAGE } from '../LocationsView'; +import { DEFAULT_ERROR_MESSAGE } from '../LocationsView'; +import { DEFAULT_LIST_OPTIONS } from '../useLocationsView'; +import { LocationAccess } from '../../../context/types'; +const navigateSpy = jest.fn(); const INITIAL_NAVIGATE_STATE = [ { location: undefined, history: [], path: '' }, - jest.fn(), + navigateSpy, ]; const INITIAL_ACTION_STATE = [ { selected: { type: undefined, items: undefined }, actions: {} }, @@ -54,16 +57,36 @@ const loadingState: ActionsModule.LocationsDataState = [ handleListLocations, ]; +const EXPECTED_PAGE_SIZE = 100; +const result: LocationAccess = { + permission: 'READWRITE', + scope: 'test-bucket', + type: 'BUCKET', +}; +const results: LocationAccess[] = Array(EXPECTED_PAGE_SIZE) + .fill(null) + .map((_, idx) => ({ ...result, scope: `${result.scope}-${idx}` })); const resolvedState: ActionsModule.LocationsDataState = [ { data: { - result: [ - { - permission: 'READWRITE', - scope: 's3://test-bucket/*', - type: 'BUCKET', - }, - ], + result: results, + nextToken: 'some-token', + }, + hasError: false, + isLoading: false, + message: undefined, + }, + handleListLocations, +]; + +const nextPageResults = results.map((item, idx) => ({ + ...item, + scope: `next-page-${idx}`, +})); +const nextPageState: ActionsModule.LocationsDataState = [ + { + data: { + result: [...results, ...nextPageResults], nextToken: undefined, }, hasError: false, @@ -74,7 +97,7 @@ const resolvedState: ActionsModule.LocationsDataState = [ ]; describe('LocationsListView', () => { - beforeEach(() => { + beforeAll(() => { useControlSpy.mockImplementation( (type) => ({ @@ -82,9 +105,10 @@ describe('LocationsListView', () => { NAVIGATE: INITIAL_NAVIGATE_STATE, })[type] ); + }); - handleListLocations.mockClear(); - useLocationsDataSpy.mockClear(); + afterEach(() => { + jest.clearAllMocks(); }); it('renders a `LocationsListView`', async () => { @@ -195,7 +219,7 @@ describe('LocationsListView', () => { const table = screen.getByRole('table'); - expect(table).toBeDefined(); + expect(table).toBeInTheDocument(); }); it.todo('handles failure from locations loading as expected'); @@ -271,7 +295,11 @@ describe('LocationsListView', () => { expect(handleListLocations).toHaveBeenCalledTimes(1); expect(handleListLocations).toHaveBeenCalledWith({ - options: { exclude: 'WRITE', pageSize: 100, refresh: true }, + options: { + exclude: 'WRITE', + pageSize: EXPECTED_PAGE_SIZE, + refresh: true, + }, }); expect(updatedHandleListLocations).not.toHaveBeenCalled(); @@ -290,7 +318,80 @@ describe('LocationsListView', () => { expect(handleListLocations).toHaveBeenCalledTimes(1); expect(updatedHandleListLocations).toHaveBeenCalledTimes(1); expect(updatedHandleListLocations).toHaveBeenCalledWith({ - options: { exclude: 'WRITE', pageSize: 100, refresh: true }, + options: { + exclude: 'WRITE', + pageSize: EXPECTED_PAGE_SIZE, + refresh: true, + }, + }); + }); + + it('can paginate forward and back', async () => { + useLocationsDataSpy.mockReturnValue(resolvedState); + render( + + + + ); + + // table renders + const table = screen.queryByRole('table'); + expect(table).toBeInTheDocument(); + + // pagination enabled + const nextPage = await screen.findByLabelText('Go to next page'); + expect(nextPage).not.toBeDisabled(); + + // first page data matches input + expect(screen.queryByLabelText('Page 1')).toBeInTheDocument(); + expect(screen.queryByText('test-bucket-0')).toBeInTheDocument(); + expect(screen.queryByText('next-page-0')).not.toBeInTheDocument(); + + useLocationsDataSpy.mockReturnValue(nextPageState); + + // go forward + await act(async () => { + await userEvent.click(nextPage); + }); + + // second page data matches input + expect(screen.queryByLabelText('Page 2')).toBeInTheDocument(); + expect(screen.queryByText('test-bucket-0')).not.toBeInTheDocument(); + expect(screen.queryByText('next-page-0')).toBeInTheDocument(); + + // pagination enabled + const previousPage = await screen.findByLabelText('Go to previous page'); + expect(previousPage).not.toBeDisabled(); + + // go back + await act(async () => { + await userEvent.click(previousPage); + }); + + // first page data matches input + expect(screen.queryByLabelText('Page 1')).toBeInTheDocument(); + expect(screen.queryByText('test-bucket-0')).toBeInTheDocument(); + expect(screen.queryByText('next-page-0')).not.toBeInTheDocument(); + }); + + it('should navigate to detail page when folder is clicked', async () => { + useLocationsDataSpy.mockReturnValue(resolvedState); + render( + + + + ); + + const scopeButton = await screen.findByText('test-bucket-0'); + await userEvent.click(scopeButton); + + expect(navigateSpy).toHaveBeenCalledWith({ + location: { + permission: 'READWRITE', + scope: 'test-bucket-0', + type: 'BUCKET', + }, + type: 'ACCESS_LOCATION', }); }); }); diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationsView/__tests__/useLocationsView.spec.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/__tests__/useLocationsView.spec.ts new file mode 100644 index 00000000000..cd938e0f546 --- /dev/null +++ b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/__tests__/useLocationsView.spec.ts @@ -0,0 +1,175 @@ +import { renderHook, act } from '@testing-library/react'; +import { useLocationsView, DEFAULT_LIST_OPTIONS } from '../useLocationsView'; +import { useLocationsData } from '../../../context/actions'; +import { useControl } from '../../../context/control'; +import { usePaginate } from '../../hooks/usePaginate'; +import { LocationAccess } from '../../../context/types'; +import { DataState } from '@aws-amplify/ui-react-core'; + +jest.mock('../../../context/actions'); +jest.mock('../../../context/control'); +jest.mock('../../hooks/usePaginate'); + +const mockData: LocationAccess[] = [ + { scope: 'Location A', type: 'BUCKET', permission: 'READ' }, + { scope: 'Location B', type: 'PREFIX', permission: 'WRITE' }, + { scope: 'Location C', type: 'BUCKET', permission: 'READ' }, + { scope: 'Location D', type: 'PREFIX', permission: 'WRITE' }, + { scope: 'Location E', type: 'BUCKET', permission: 'READ' }, +]; +const PAGE_SIZE = 3; + +function mockUseLocationsData( + returnValue: DataState<{ result: LocationAccess[] }> +) { + const handleList = jest.fn(); + (useLocationsData as jest.Mock).mockReturnValue([returnValue, handleList]); + return handleList; +} + +describe('useLocationsView', () => { + beforeEach(() => { + (useControl as jest.Mock).mockReturnValue([{}, jest.fn()]); + (usePaginate as jest.Mock).mockReturnValue({ + currentPage: 1, + handlePaginateNext: jest.fn(), + handlePaginatePrevious: jest.fn(), + handleReset: jest.fn(), + range: [0, PAGE_SIZE], + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should fetch and set location data on mount', () => { + const mockDataState = { + data: { result: mockData, nextToken: null }, + message: '', + hasError: false, + isLoading: false, + }; + const handleList = mockUseLocationsData(mockDataState); + + const { result } = renderHook(() => useLocationsView()); + + expect(handleList).toHaveBeenCalledWith({ + options: { ...DEFAULT_LIST_OPTIONS, refresh: true }, + }); + + const [state] = result.current; + expect(state.isLoading).toBe(false); + expect(state.hasError).toBe(false); + expect(state.data.items).toEqual(mockData); + }); + + it('should handle pagination actions', () => { + const handlePaginateNext = jest.fn(); + const handlePaginatePrevious = jest.fn(); + + const mockDataState = { + data: { result: mockData, nextToken: 'token123' }, + message: '', + hasError: false, + isLoading: false, + }; + mockUseLocationsData(mockDataState); + + (usePaginate as jest.Mock).mockReturnValue({ + currentPage: 1, + handlePaginateNext, + handlePaginatePrevious, + handleReset: jest.fn(), + range: [0, PAGE_SIZE], + }); + + const { result } = renderHook(() => useLocationsView()); + + act(() => { + const [, handleAction] = result.current; + handleAction({ type: 'PAGINATE_NEXT' }); + }); + + expect(handlePaginateNext).toHaveBeenCalledWith({ + resultCount: mockData.length, + hasNextToken: true, + }); + + act(() => { + const [, handleAction] = result.current; + handleAction({ type: 'PAGINATE_PREVIOUS' }); + }); + + expect(handlePaginatePrevious).toHaveBeenCalledWith(); + }); + + it('should handle refreshing location data', () => { + const handleReset = jest.fn(); + const mockDataState = { + data: { result: [], nextToken: null }, + message: '', + hasError: false, + isLoading: false, + }; + const handleList = mockUseLocationsData(mockDataState); + + (usePaginate as jest.Mock).mockReturnValue({ + currentPage: 1, + handlePaginateNext: jest.fn(), + handlePaginatePrevious: jest.fn(), + handleReset, + range: [0, PAGE_SIZE], + }); + + const { result } = renderHook(() => useLocationsView()); + + act(() => { + const [, handleAction] = result.current; + handleAction({ type: 'REFRESH' }); + }); + + expect(handleReset).toHaveBeenCalled(); + expect(handleList).toHaveBeenCalledWith({ + options: { ...DEFAULT_LIST_OPTIONS, refresh: true }, + }); + }); + + it('should handle selecting a location', () => { + const handleUpdateState = jest.fn(); + (useControl as jest.Mock).mockReturnValue([{}, handleUpdateState]); + + const { result } = renderHook(() => useLocationsView()); + + const location: LocationAccess = { + type: 'BUCKET', + scope: 'Location A', + permission: 'READ', + }; // Example location object + + act(() => { + const [, handleAction] = result.current; + handleAction({ type: 'SELECT_LOCATION', location }); + }); + + expect(handleUpdateState).toHaveBeenCalledWith({ + type: 'ACCESS_LOCATION', + location, + }); + }); + + it('should return paginated items based on current page and page size', () => { + const mockDataState = { + data: { result: mockData, nextToken: null }, + message: '', + hasError: false, + isLoading: false, + }; + mockUseLocationsData(mockDataState); + const { result } = renderHook(() => useLocationsView()); + const [state] = result.current; + expect(state.data.getProcessedItems()).toEqual( + mockData.slice(0, PAGE_SIZE) + ); + }); +}); diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationsView/useLocationsView.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/useLocationsView.ts new file mode 100644 index 00000000000..bd91eef604b --- /dev/null +++ b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/useLocationsView.ts @@ -0,0 +1,120 @@ +import { LocationAccess } from '../../context/types'; +import { ActionState } from '../../context/actions/createActionStateContext'; +import { useLocationsData } from '../../context/actions'; +import { useControl } from '../../context/control'; +import React from 'react'; +import { usePaginate } from '../hooks/usePaginate'; + +interface LocationsViewState { + // all locations in memory + items: LocationAccess[]; + + // current page + page: number; + + // more items available to display + hasMoreData: boolean; + + // windowed subset of locations + getProcessedItems: () => LocationAccess[]; +} + +export type LocationsViewActionType = + // refresh data + | { type: 'REFRESH' } + // reset view to initial state + | { type: 'RESET' } + // paginate + | { type: 'PAGINATE_NEXT' } + | { type: 'PAGINATE_PREVIOUS' } + // set location to be provided to LocationDetailView + | { type: 'SELECT_LOCATION'; location: LocationAccess } + // query locations + | { type: 'SEARCH'; query: string; includeSubfolders?: boolean }; + +const DEFAULT_PAGE_SIZE = 100; +export const DEFAULT_LIST_OPTIONS = { + exclude: 'WRITE' as const, + pageSize: DEFAULT_PAGE_SIZE, +}; + +export function useLocationsView(): ActionState< + LocationsViewState, + LocationsViewActionType +> { + const [state, handleList] = useLocationsData(); + const [, handleUpdateState] = useControl('NAVIGATE'); + + const { data, message, hasError, isLoading } = state; + const { result, nextToken } = data; + const resultCount = result.length; + const hasNextToken = !!nextToken; + const pageSize = DEFAULT_PAGE_SIZE; + + // initial load + React.useEffect(() => { + handleList({ + options: { ...DEFAULT_LIST_OPTIONS, refresh: true }, + }); + }, [handleList]); + + // set up pagination + const onPaginateNext = () => + handleList({ + options: { ...DEFAULT_LIST_OPTIONS, nextToken }, + }); + + const { + currentPage, + handlePaginateNext, + handlePaginatePrevious, + handleReset, + range, + } = usePaginate({ onPaginateNext, pageSize }); + + const handleAction = (action: LocationsViewActionType) => { + const { type } = action; + switch (type) { + case 'PAGINATE_NEXT': + handlePaginateNext({ resultCount, hasNextToken }); + break; + case 'PAGINATE_PREVIOUS': + handlePaginatePrevious(); + break; + case 'REFRESH': + handleReset(); + handleList({ + options: { ...DEFAULT_LIST_OPTIONS, refresh: true }, + }); + break; + case 'SEARCH': + // TODO + break; + case 'SELECT_LOCATION': + handleUpdateState({ + type: 'ACCESS_LOCATION', + location: action.location, + }); + break; + } + }; + const processedItems = React.useMemo(() => { + const [start, end] = range; + return result.slice(start, end); + }, [range, result]); + + const hooksState = { + isLoading, + hasError, + message, + data: { + page: currentPage, + hasMoreData: hasNextToken, + items: result, + getProcessedItems() { + return processedItems; + }, + }, + }; + return [hooksState, handleAction]; +} diff --git a/packages/react-storage/src/components/StorageBrowser/views/hooks/usePaginate.ts b/packages/react-storage/src/components/StorageBrowser/views/hooks/usePaginate.ts index 7626711c3c1..90bf7127edc 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/hooks/usePaginate.ts +++ b/packages/react-storage/src/components/StorageBrowser/views/hooks/usePaginate.ts @@ -8,6 +8,7 @@ interface UsePaginate { }) => void; handlePaginatePrevious: (input?: {}) => void; handleReset: () => void; + range: [start: number, end: number]; } export const usePaginate = ({ @@ -25,8 +26,11 @@ export const usePaginate = ({ setCurrentPage(1); }).current; - return React.useMemo( - () => ({ + return React.useMemo(() => { + const isInitialPage = currentPage === 1; + const start = isInitialPage ? 0 : (currentPage - 1) * pageSize; + const end = isInitialPage ? pageSize : currentPage * pageSize; + return { currentPage, handlePaginateNext: (input) => { const { hasNextToken, resultCount } = input; @@ -45,7 +49,7 @@ export const usePaginate = ({ setCurrentPage((prev) => prev - 1); }, handleReset, - }), - [currentPage, handleReset, onPaginateNext, onPaginatePrevious, pageSize] - ); + range: [start, end], + }; + }, [currentPage, handleReset, onPaginateNext, onPaginatePrevious, pageSize]); };