-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ui): define InfiniteListBox component and related logic
- Loading branch information
1 parent
f21daaf
commit db87024
Showing
10 changed files
with
674 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
175 changes: 175 additions & 0 deletions
175
packages/@liexp/ui/src/containers/list/InfiniteListBox/InfiniteList.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
import React from "react"; | ||
import { | ||
CellMeasurer, | ||
CellMeasurerCache, | ||
List, | ||
type ListRowProps, | ||
} from "react-virtualized"; | ||
import { styled } from "../../../theme/index.js"; | ||
import { type InfiniteListBaseProps } from "./types.js"; | ||
|
||
export interface RowRendererProps extends Omit<ListRowProps, "parent"> { | ||
item: any; | ||
isLast: boolean; | ||
measure: () => void; | ||
onRowInvalidate?: () => void; | ||
} | ||
|
||
type RowRenderer = React.ForwardRefExoticComponent<RowRendererProps>; | ||
|
||
type InfiniteListRowProps<P> = ListRowProps & | ||
Omit<P, "onLoad" | "onRowInvalidate"> & | ||
Omit<RowRendererProps, "measure"> & { | ||
RowRenderer: RowRenderer; | ||
k: string; | ||
}; | ||
|
||
const Row: React.FC<InfiniteListRowProps<unknown>> = (props) => { | ||
const { | ||
isVisible, | ||
isLast, | ||
style, | ||
parent, | ||
index, | ||
k: key, | ||
RowRenderer, | ||
item, | ||
onRowInvalidate, | ||
...rest | ||
} = props; | ||
|
||
return ( | ||
<CellMeasurer | ||
key={key} | ||
cache={cellCache} | ||
columnIndex={0} | ||
rowIndex={index} | ||
parent={parent} | ||
> | ||
{({ registerChild, measure }) => { | ||
if (!item) { | ||
return ( | ||
<div | ||
ref={registerChild as React.Ref<any>} | ||
key={key} | ||
style={{ height: 300 }} | ||
/> | ||
); | ||
} | ||
|
||
if (!isVisible) { | ||
// console.log("no visible", index); | ||
return ( | ||
<div | ||
key={key} | ||
ref={registerChild as React.Ref<any>} | ||
style={{ | ||
display: "flex", | ||
alignItems: "center", | ||
justifyContent: "center", | ||
height: 300, | ||
width: "100%", | ||
...props.style, | ||
}} | ||
/> | ||
); | ||
} | ||
|
||
const rowRendererProps = { | ||
ref: registerChild as React.Ref<any>, | ||
isVisible, | ||
isLast, | ||
style, | ||
index, | ||
item, | ||
measure, | ||
onRowInvalidate: () => { | ||
cellCache.clear(index, 0); | ||
onRowInvalidate?.(); | ||
setTimeout(() => { | ||
measure(); | ||
}, 300); | ||
}, | ||
...rest, | ||
}; | ||
|
||
// console.log(rowRendererProps); | ||
|
||
return <RowRenderer {...rowRendererProps} />; | ||
}} | ||
</CellMeasurer> | ||
); | ||
}; | ||
|
||
const PREFIX = "InfiniteList"; | ||
const classes = { | ||
timeline: `${PREFIX}-timeline`, | ||
listSubheader: `${PREFIX}-listSubheader`, | ||
listItemUList: `${PREFIX}-listItemUList`, | ||
}; | ||
|
||
const StyledList = styled(List)(({ theme }) => ({ | ||
[`&.${classes.timeline}`]: { | ||
padding: 0, | ||
paddingTop: 20, | ||
width: "100%", | ||
}, | ||
|
||
[`& .${classes.listSubheader}`]: { | ||
backgroundColor: theme.palette.common.white, | ||
}, | ||
|
||
[`& .${classes.listItemUList}`]: { | ||
padding: 0, | ||
width: "100%", | ||
}, | ||
})); | ||
|
||
export interface InfiniteListProps extends InfiniteListBaseProps { | ||
onRowsRendered: (params: { startIndex: number; stopIndex: number }) => void; | ||
RowRenderer: RowRenderer; | ||
} | ||
|
||
const cellCache = new CellMeasurerCache({ | ||
fixedWidth: true, | ||
minWidth: 200, | ||
fixedHeight: true, | ||
defaultHeight: 300, | ||
}); | ||
|
||
const InfiniteListForwardRef: React.ForwardRefRenderFunction< | ||
unknown, | ||
InfiniteListProps | ||
> = ({ width, height, onRowsRendered, items, getItem, ...rest }, listRef) => { | ||
// console.log("items", items.length); | ||
return ( | ||
<StyledList | ||
ref={listRef} | ||
width={width} | ||
height={height} | ||
estimatedRowSize={300} | ||
overscanRowCount={10} | ||
onRowsRendered={onRowsRendered} | ||
rowRenderer={({ key, ...props }) => { | ||
const item = getItem(items, props.index); | ||
const isLast = items.length === props.index + 1; | ||
// console.log({ item: item.id, isLast, isVisible: props.isVisible }); | ||
return ( | ||
<Row | ||
{...rest} | ||
{...props} | ||
k={key} | ||
key={key} | ||
item={item} | ||
isLast={isLast} | ||
/> | ||
); | ||
}} | ||
rowCount={items.length} | ||
rowHeight={cellCache.rowHeight} | ||
deferredMeasurementCache={cellCache} | ||
/> | ||
); | ||
}; | ||
|
||
export const InfiniteList = React.forwardRef(InfiniteListForwardRef); |
161 changes: 161 additions & 0 deletions
161
packages/@liexp/ui/src/containers/list/InfiniteListBox/InfiniteListBox.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
import { type APIError } from "@liexp/shared/lib/io/http/Error/APIError.js"; | ||
import { type EndpointsQueryProvider } from "@liexp/shared/lib/providers/EndpointQueriesProvider/index.js"; | ||
import { type ResourceQuery } from "@liexp/shared/lib/providers/EndpointQueriesProvider/types.js"; | ||
import { paramsToPagination } from "@liexp/shared/lib/providers/api-rest.provider.js"; | ||
import { useInfiniteQuery } from "@tanstack/react-query"; | ||
import React from "react"; | ||
import { AutoSizer, InfiniteLoader, type Index } from "react-virtualized"; | ||
import { FullSizeLoader } from "../../../components/Common/FullSizeLoader.js"; | ||
import { useEndpointQueries } from "../../../hooks/useEndpointQueriesProvider.js"; | ||
import { InfiniteList, type InfiniteListProps } from "./InfiniteList.js"; | ||
import { | ||
InfiniteMasonry, | ||
type InfiniteMasonryProps, | ||
} from "./InfiniteMasonry.js"; | ||
|
||
export type ListType = "masonry" | "list"; | ||
|
||
type ListProps<T extends ListType> = T extends "masonry" | ||
? { type: "masonry" } & Omit< | ||
InfiniteMasonryProps, | ||
"width" | "height" | "items" | "cellMeasureCache" | ||
> | ||
: { type: "list" } & Omit< | ||
InfiniteListProps, | ||
"width" | "height" | "items" | "onRowsRendered" | ||
>; | ||
|
||
export interface InfiniteListBoxProps<T extends ListType> { | ||
listProps: ListProps<T>; | ||
useListQuery: <R extends ResourceQuery<any, any, any>>( | ||
queryProvider: EndpointsQueryProvider, | ||
) => R; | ||
} | ||
|
||
export const InfiniteListBox = <T extends ListType>({ | ||
useListQuery, | ||
...rest | ||
}: InfiniteListBoxProps<T>): JSX.Element => { | ||
const Q = useEndpointQueries(); | ||
|
||
const query = React.useMemo(() => { | ||
return useListQuery<any>(Q); | ||
}, [useListQuery]); | ||
|
||
const queryKey = query.getKey({} as any, undefined, false, "infinite-list"); | ||
|
||
const { | ||
data, | ||
hasNextPage, | ||
isFetching, | ||
isFetchingNextPage, | ||
fetchNextPage, | ||
isRefetching, | ||
// refetch, | ||
} = useInfiniteQuery< | ||
any, | ||
APIError, | ||
{ | ||
pages: Array<{ data: any[]; total: number }>; | ||
lastPage: { data: any[]; total: number }; | ||
}, | ||
any, | ||
{ _start: number; _end: number } | ||
>({ | ||
initialPageParam: { _start: 0, _end: 20 }, | ||
queryKey, | ||
queryFn: (opts) => { | ||
const pageParam: any = paramsToPagination( | ||
opts.pageParam._start, | ||
opts.pageParam._end, | ||
); | ||
|
||
return query.fetch( | ||
{ | ||
pagination: pageParam, | ||
filter: null, | ||
}, | ||
undefined, | ||
false, | ||
); | ||
}, | ||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => { | ||
// console.log("get next params", { | ||
// lastPage, | ||
// allPages, | ||
// lastPageParam, | ||
// allPageParams, | ||
// }); | ||
return { _start: lastPageParam._end, _end: lastPageParam._end + 20 }; | ||
}, | ||
}); | ||
|
||
const isRowLoaded = (params: Index): boolean => { | ||
const rowLoaded = items[params.index] !== undefined; | ||
return rowLoaded; | ||
}; | ||
|
||
const { items, total } = React.useMemo(() => { | ||
// console.log('data', data); | ||
const items = data?.pages.flatMap((p) => p.data) ?? []; | ||
const total = data?.pages[0]?.total ?? 0; | ||
return { items, total }; | ||
}, [data]); | ||
|
||
const handleLoadMoreRows = React.useCallback( | ||
async (props: any) => { | ||
if (hasNextPage && !(isFetchingNextPage || isRefetching)) { | ||
const pageParams = { | ||
_start: props.startIndex, | ||
_end: props.stopIndex + 1 - props.startIndex, | ||
} as any; | ||
// console.log("handleLoadMoreRows", pageParams); | ||
await fetchNextPage(pageParams); | ||
} | ||
}, | ||
[fetchNextPage, hasNextPage, isFetchingNextPage, isRefetching], | ||
); | ||
|
||
if (isFetching && items.length === 0) { | ||
return <FullSizeLoader />; | ||
} | ||
|
||
return ( | ||
<InfiniteLoader | ||
isRowLoaded={isRowLoaded} | ||
loadMoreRows={handleLoadMoreRows} | ||
rowCount={total} | ||
minimumBatchSize={20} | ||
> | ||
{({ onRowsRendered, registerChild }) => ( | ||
<AutoSizer style={{ height: "100%", width: "100%" }}> | ||
{({ width, height }) => { | ||
if (rest.listProps.type === "masonry") { | ||
return ( | ||
<InfiniteMasonry | ||
{...rest.listProps} | ||
width={width} | ||
height={height} | ||
total={total} | ||
items={items} | ||
ref={registerChild} | ||
onCellsRendered={onRowsRendered} | ||
/> | ||
); | ||
} | ||
return ( | ||
<InfiniteList | ||
ref={registerChild} | ||
width={width} | ||
height={height} | ||
onRowsRendered={onRowsRendered} | ||
items={items} | ||
{...rest.listProps} | ||
/> | ||
); | ||
}} | ||
</AutoSizer> | ||
)} | ||
</InfiniteLoader> | ||
); | ||
}; |
Oops, something went wrong.