Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 0 additions & 24 deletions src/__tests__/utilsFunction/paginationUtils.test.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,4 @@
import { AppDispatch } from 'state'
import { cleanSearchParams, getCleanGroupId } from 'utils/paginationUtils'
import { validatePageNumber } from 'utils/url'
import { vi } from 'vitest'

describe('validatePageNumber', () => {
it('should not return a dispatch with error ', () => {
const totalPages = Math.ceil(0 / 20)
const currentPage = 1
const setPage = vi.fn()
const dispatch = vi.fn()

validatePageNumber(totalPages, currentPage, setPage, dispatch as unknown as AppDispatch)
expect(dispatch).not.toHaveBeenCalled()
})
it('should return dispatch with error', () => {
const totalPages = Math.ceil(300 / 20)
const currentPage = 155
const setPage = vi.fn()
const dispatch = vi.fn()

validatePageNumber(currentPage, totalPages, setPage, dispatch as unknown as AppDispatch)
expect(dispatch).toHaveBeenCalled()
})
})

describe('test of getCleanGroupId function', () => {
it('should return no groupId if perimeter is empty or null', () => {
Expand Down
7 changes: 2 additions & 5 deletions src/components/ExplorationBoard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,19 @@ import { Alert, Grid, Snackbar } from '@mui/material'
import SearchSection from './SearchSection'
import CriteriasSection from './CriteriasSection'
import { useExplorationBoard } from './useExplorationBoard'
import { useData } from './useData'
import DataSection from './DataSection'
import { FetchStatus } from 'types'
import CustomAlert from 'components/ui/Alert'
import { GAP, ExplorationConfig } from 'types/exploration'
import { useSearchParams } from 'react-router-dom'

type ExplorationBoardProps<T> = {
config: ExplorationConfig<T>
}

const ExplorationBoard = <T,>({ config }: ExplorationBoardProps<T>) => {
const [searchParams] = useSearchParams()
const pageFromUrl = parseInt(searchParams.get('page') ?? '1', 10)
const {
page: { pagination, onChangePage },
data: { data, count, dataLoading },
fetchStatus,
additionalInfo,
criterias,
Expand All @@ -30,7 +28,6 @@ const ExplorationBoard = <T,>({ config }: ExplorationBoardProps<T>) => {
onSaveFilter,
resetFetchStatus
} = useExplorationBoard(config)
const { count, pagination, data, dataLoading, onChangePage } = useData(config, searchCriterias, pageFromUrl)

return (
<Grid size={12} container sx={{ gap: GAP }} margin={'16px 0'}>
Expand Down
175 changes: 56 additions & 119 deletions src/components/ExplorationBoard/useData.ts
Original file line number Diff line number Diff line change
@@ -1,149 +1,86 @@
import { CanceledError } from 'axios'
import { useEffect, useRef, useState } from 'react'
import { useCallback, useState } from 'react'
import { LoadingStatus } from 'types'
import { CountDisplay, Data, Diagram, ExplorationConfig, Timeline } from 'types/exploration'
import { Card } from 'types/card'
import { Filters, SearchCriterias } from 'types/searchCriterias'
import { Table } from 'types/table'
import { cancelPendingRequest } from 'utils/abortController'
import { useLocation, useNavigate } from 'react-router-dom'
import { useAppDispatch } from 'state'
import { validatePageNumber } from 'utils/url'

const RESULTS_PER_PAGE = 20

export const useData = <T>(
config: ExplorationConfig<T>,
searchCriterias: SearchCriterias<Filters>,
initialPage = 1
) => {
const navigate = useNavigate()
const location = useLocation()
const dispatch = useAppDispatch()
export const RESULTS_PER_PAGE = 20

export const useData = <T>(config: ExplorationConfig<T>) => {
const [loadingStatus, setLoadingStatus] = useState(LoadingStatus.IDDLE)
const currentType = useRef(config.type)
const abortController = useRef(new AbortController())
const [data, setData] = useState<Data | null>(null)
const [tableData, setTableData] = useState<Table>({ rows: [], columns: [] })
const [diagrams, setDiagrams] = useState<Diagram[]>([])
const [timeline, setTimeline] = useState<Timeline | null>(null)
const [cards, setCards] = useState<Card[]>([])
const [pagination, setPagination] = useState({ currentPage: initialPage, total: 0 })
const [count, setCount] = useState<CountDisplay | null>(null)

const clearData = () => {
setData(null)
const clearData = <T>(config: ExplorationConfig<T>) => {
setTableData({ rows: [], columns: [] })
setDiagrams([])
setTimeline(null)
setCards([])
setCount(
config.getCount
? config.getCount([
{ results: 0, total: 0 },
{ results: 0, total: 0 }
])
: null
)
}

const updateUrlWithPage = (page: number) => {
const params = new URLSearchParams(location.search)
params.set('page', page.toString())
navigate(`${location.pathname}?${params.toString()}`, { replace: true })
}

const fetchData = async (page: number) => {
try {
setLoadingStatus(LoadingStatus.FETCHING)
const results = await config.fetchList(
{
size: RESULTS_PER_PAGE,
page,
includeFacets: true,
searchInput: searchCriterias.searchInput ?? '',
orderBy: searchCriterias.orderBy
},
{ filters: searchCriterias.filters as T, searchBy: searchCriterias.searchBy },
abortController.current.signal
)
if (currentType.current == config.type) {
setData(results)
const fetchData = useCallback(
async (page: number, searchCriterias: SearchCriterias<Filters>) => {
try {
setLoadingStatus(LoadingStatus.FETCHING)
const results = await config.fetchList(
{
size: RESULTS_PER_PAGE,
page,
includeFacets: true,
searchInput: searchCriterias.searchInput ?? '',
orderBy: searchCriterias.orderBy
},
{ filters: searchCriterias.filters as T, searchBy: searchCriterias.searchBy }
)
mapData(results, config, searchCriterias)
return results
} catch (error) {
if (error instanceof CanceledError) setLoadingStatus(LoadingStatus.FETCHING)
clearData(config)
} finally {
setLoadingStatus(LoadingStatus.SUCCESS)
}
return results
} catch (error) {
if (error instanceof CanceledError) setLoadingStatus(LoadingStatus.FETCHING)
setLoadingStatus(LoadingStatus.SUCCESS)
setData({ total: 0, totalAllResults: 0, totalPatients: 0, totalAllPatients: 0, list: [] })
setCount(
config.getCount
? config.getCount([
{ results: 0, total: 0 },
{ results: 0, total: 0 }
])
: null
)
}
}

useEffect(() => {
if (data) {
const hasSearch =
config.hasSearchDisplay && config.hasSearchDisplay(searchCriterias.searchInput, searchCriterias.searchBy)
if (config.mapToTimeline) config.mapToTimeline(data).then(setTimeline)
else setTimeline(null)
if (config.mapToDiagram && data.meta) setDiagrams(config.mapToDiagram(data.meta))
else setDiagrams([])
if (config.mapToTable) setTableData(config.mapToTable(data, hasSearch))
else setTableData({ rows: [], columns: [] })
if (config.mapToCards) setCards(config.mapToCards(data))
else setCards([])
if (config.getCount) {
const count = config.getCount([
{ results: data.total, total: data.totalAllResults },
{ results: data.totalPatients, total: data.totalAllPatients }
])
setPagination({
...pagination,
total: Math.ceil(count[0].count.results / RESULTS_PER_PAGE)
})
setCount(count)
} else setCount(null)
}
setLoadingStatus(LoadingStatus.SUCCESS)
}, [data])

useEffect(() => {
if (currentType.current !== config.type) {
clearData()
abortController.current = cancelPendingRequest(abortController.current)
}
currentType.current = config.type
const fetch = async () => {
const result = await fetchData(initialPage)
const totalItems = result?.total ?? 0
const totalPages = Math.ceil(totalItems / RESULTS_PER_PAGE)

setPagination({ currentPage: initialPage, total: totalPages })

validatePageNumber(
initialPage,
totalPages,
(page) => {
setPagination((prev) => ({ ...prev, currentPage: page }))
updateUrlWithPage(page)
fetchData(page)
},
dispatch
)
}
fetch()
}, [searchCriterias, config.type])
},
[config]
)

const handlePage = async (page: number) => {
await fetchData(page)
setPagination((prev) => ({ ...prev, currentPage: page }))
updateUrlWithPage(page)
const mapData = <T>(data: Data, config: ExplorationConfig<T>, searchCriterias: SearchCriterias<Filters>) => {
const hasSearch =
config.hasSearchDisplay && config.hasSearchDisplay(searchCriterias.searchInput, searchCriterias.searchBy)
if (config.mapToTimeline) config.mapToTimeline(data).then(setTimeline)
else setTimeline(null)
if (config.mapToDiagram && data.meta) setDiagrams(config.mapToDiagram(data.meta))
else setDiagrams([])
if (config.mapToTable) setTableData(config.mapToTable(data, hasSearch))
else setTableData({ rows: [], columns: [] })
if (config.mapToCards) setCards(config.mapToCards(data))
else setCards([])
if (config.getCount) {
const count = config.getCount([
{ results: data.total, total: data.totalAllResults },
{ results: data.totalPatients, total: data.totalAllPatients }
])
setCount(count)
} else setCount(null)
}

return {
fetchData,
count,
data: { table: tableData, cards, diagrams, timeline },
dataLoading: loadingStatus === LoadingStatus.FETCHING,
pagination,
onChangePage: handlePage
dataLoading: loadingStatus === LoadingStatus.FETCHING
}
}
59 changes: 50 additions & 9 deletions src/components/ExplorationBoard/useExplorationBoard.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import { usePagination } from 'components/ui/Pagination/usePagination'
import { useSavedFilters } from 'hooks/filters/useSavedFilters'
import { useEffect, useMemo, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import useSearchCriterias from 'reducers/searchCriteriasReducer'
import { AdditionalInfo, ExplorationConfig, SearchWithFilters } from 'types/exploration'
import { FilterKeys, FilterValue, Filters, SearchCriteriaKeys, SearchCriterias } from 'types/searchCriterias'
import { selectFiltersAsArray } from 'utils/filters'
import { useData } from './useData'

export const useExplorationBoard = <T>(config: ExplorationConfig<T>) => {
const [additionalInfo, setAdditionalInfo] = useState<AdditionalInfo>({ type: config.type, deidentified: false })
const prevTypeRef = useRef(config.type)

const [searchCriterias, { changeSearchBy, changeOrderBy, changeSearchInput, addFilters, removeFilter }] =
useSearchCriterias(config.initSearchCriterias(), config.type)
const { count, data, dataLoading, fetchData } = useData(config)
const { pagination, onChangePage, onChangeTotal } = usePagination()

const [additionalInfo, setAdditionalInfo] = useState<AdditionalInfo>({ type: config.type, deidentified: false })
const additionalInfoRef = useRef(additionalInfo)
additionalInfoRef.current = additionalInfo

const {
allSavedFilters,
Expand All @@ -19,7 +28,28 @@ export const useExplorationBoard = <T>(config: ExplorationConfig<T>) => {

const narrowSearch = useMemo(() => config.narrowSearchCriterias, [config])

const narrowedSearchCriterias = useMemo(() => narrowSearch(searchCriterias as SearchCriterias<T>), [searchCriterias])
const narrowedSearchCriterias = useMemo(
() => narrowSearch(searchCriterias as SearchCriterias<T>),
[searchCriterias, narrowSearch]
)

useEffect(() => {
let cancelled = false

const runFetch = async () => {
const results = await fetchData(pagination.currentPage, narrowedSearchCriterias)
if (!cancelled) onChangeTotal(results?.total ?? 0)
}

if (prevTypeRef.current !== config.type) {
onChangePage(1)
prevTypeRef.current = config.type
} else runFetch()

return () => {
cancelled = true
}
}, [pagination.currentPage, narrowedSearchCriterias, config.type])

const narrowedSelectedFilter = useMemo(
() =>
Expand All @@ -29,7 +59,7 @@ export const useExplorationBoard = <T>(config: ExplorationConfig<T>) => {
filterParams: narrowSearch(selectedSavedFilter.filterParams as SearchCriterias<T>)
}
: null,
[selectedSavedFilter]
[selectedSavedFilter, narrowSearch]
)

const criterias = useMemo(() => {
Expand All @@ -41,22 +71,24 @@ export const useExplorationBoard = <T>(config: ExplorationConfig<T>) => {
const onRemoveCriteria = (category: FilterKeys | SearchCriteriaKeys, value: FilterValue) => {
if (category === SearchCriteriaKeys.SEARCH_INPUT) changeSearchInput('')
else removeFilter(category as FilterKeys, value)
onChangePage(1)
}

const onSaveSearchCriterias = ({ searchBy, searchInput, filters, orderBy }: SearchWithFilters) => {
if (searchBy) changeSearchBy(searchBy)
if (searchInput !== undefined) changeSearchInput(searchInput)
if (filters) addFilters(filters)
if (orderBy) changeOrderBy(orderBy)
onChangePage(1)
}

useEffect(() => {
const fetchInfos = async () => {
const newInfo = await config.fetchAdditionalInfos(additionalInfo)
setAdditionalInfo({ ...additionalInfo, ...newInfo, type: config.type })
const fetch = async () => {
const newInfo = await config.fetchAdditionalInfos(additionalInfoRef.current)
setAdditionalInfo((prev) => ({ ...prev, ...newInfo, type: config.type }))
}
fetchInfos()
}, [config.type])
fetch()
}, [config])

return {
savedFiltersData: {
Expand All @@ -71,6 +103,15 @@ export const useExplorationBoard = <T>(config: ExplorationConfig<T>) => {
patchSavedFilter(name, searchCriterias, config.deidentified),
onSubmit: onSaveSearchCriterias
},
data: {
data,
dataLoading,
count
},
page: {
pagination,
onChangePage
},
fetchStatus,
additionalInfo,
searchCriterias: narrowedSearchCriterias,
Expand Down
Loading
Loading