-
-
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
af40797
commit a10b2af
Showing
7 changed files
with
621 additions
and
1 deletion.
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
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 { 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); |
162 changes: 162 additions & 0 deletions
162
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,162 @@ | ||
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) => { | ||
console.log('handleLoadMoreRows', props) | ||
if (hasNextPage && !(isFetchingNextPage || isRefetching)) { | ||
// console.log("Need more data", { | ||
// hasNextPage, | ||
// isFetchingNextPage, | ||
// isRefetching, | ||
// }); | ||
await fetchNextPage({ | ||
_start: items.length, | ||
_end: 20, | ||
} as any); | ||
} | ||
}, [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.