Skip to content

Commit

Permalink
feat(ui): define InfiniteListBox component and related logic
Browse files Browse the repository at this point in the history
  • Loading branch information
ascariandrea committed Jan 20, 2024
1 parent f21daaf commit db87024
Show file tree
Hide file tree
Showing 10 changed files with 674 additions and 10 deletions.
13 changes: 12 additions & 1 deletion packages/@liexp/shared/src/providers/api-rest.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ const liftClientRequest = <T>(
);
};

export const paramsToPagination = (
start: number,
end: number,
): RA.GetListParams["pagination"] => {
return {
page: start < 20 ? 1 : Math.ceil(start / 20) + 1,
perPage: 20,
};
};

const formatParams = <P extends RA.GetListParams | RA.GetManyReferenceParams>(
params: P,
): RA.GetListParams => {
Expand Down Expand Up @@ -104,9 +114,10 @@ export const APIRESTClient = ({
client.get(`${resource}/${params.id}`, { params }),
)(),
getList: (resource, params) => {
const formattedParams = formatParams(params);
return liftClientRequest<RA.GetListResult<any>>(() =>
client.get(resource, {
params: formatParams(params),
params: formattedParams,
}),
)();
},
Expand Down
2 changes: 0 additions & 2 deletions packages/@liexp/ui/src/components/Common/Editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,6 @@ const Editor: React.FC<Omit<EditorProps, "cellPlugins">> = ({
}),
);

// console.log({ lastChar });

if (lastChar === "/") {
if (!open) {
setOpen(true);
Expand Down
3 changes: 3 additions & 0 deletions packages/@liexp/ui/src/components/lists/ActorList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface ActorListItemProps extends ListItemProps<ActorItem> {
avatarSize?: AvatarSize;
displayFullName?: boolean;
style?: React.CSSProperties;
onLoad?: () => void;
}

export const ActorListItem: React.FC<ActorListItemProps> = ({
Expand All @@ -23,6 +24,7 @@ export const ActorListItem: React.FC<ActorListItemProps> = ({
displayFullName = false,
onClick,
style,
onLoad
}) => {
return (
<Box
Expand All @@ -47,6 +49,7 @@ export const ActorListItem: React.FC<ActorListItemProps> = ({
src={src}
size={avatarSize}
style={{ margin: 5 }}
onLoad={onLoad}
/>
)),
O.toNullable,
Expand Down
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);
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>
);
};
Loading

0 comments on commit db87024

Please sign in to comment.