mirror of
https://github.com/supabase/supabase.git
synced 2026-06-18 13:43:53 +08:00
## Problem While trying to update `react` to version `19`, I noticed type related errors that can be fixed in version `18`, mostly usage of `JSX.Element` instead of `ReactNode`. ## Solution - Use `ReactNode` instead of `JSX.Element` - Fix some invalid usage of `rechart` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **Refactor** * Standardized React component type annotations across the codebase for improved type consistency and flexibility. * Updated component prop types to accept a broader range of renderable content. * **Bug Fixes** * Adjusted chart layout positioning to improve visual alignment. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
382 lines
11 KiB
TypeScript
382 lines
11 KiB
TypeScript
import { useVirtualizer, Virtualizer } from '@tanstack/react-virtual'
|
|
import {
|
|
ComponentPropsWithRef,
|
|
ComponentType,
|
|
createContext,
|
|
createElement,
|
|
CSSProperties,
|
|
ElementType,
|
|
memo,
|
|
ReactNode,
|
|
Ref,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
type ComponentProps,
|
|
type PropsWithChildren,
|
|
} from 'react'
|
|
import { cn, Skeleton } from 'ui'
|
|
|
|
// Regular memo erases generics, so this helper adds them back
|
|
// any here is intentional to allow for generic components and does not affect
|
|
// type safety of the wrapped component
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const typedMemo = <Component extends (props: any) => ReactNode>(
|
|
component: Component,
|
|
propsAreEqual?: (
|
|
prevProps: Readonly<Parameters<Component>[0]>,
|
|
nextProps: Readonly<Parameters<Component>[0]>
|
|
) => boolean
|
|
) => memo(component, propsAreEqual) as unknown as Component & { displayName?: string }
|
|
|
|
const createStyleObject = ({ size, start }: { size: number; start: number }): CSSProperties => ({
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
width: '100%',
|
|
height: `${size}px`,
|
|
transform: `translateY(${start}px)`,
|
|
})
|
|
|
|
type VirtualizerInstance = Virtualizer<Element, Element>
|
|
type VirtualItems = ReturnType<VirtualizerInstance['getVirtualItems']>
|
|
|
|
type VirtualizerContextValue = {
|
|
virtualizer: VirtualizerInstance
|
|
virtualItems: VirtualItems
|
|
}
|
|
|
|
const VirtualizerContext = createContext<VirtualizerContextValue | null>(null)
|
|
|
|
export const VirtualizerProvider = ({
|
|
children,
|
|
value,
|
|
}: PropsWithChildren<{ value: VirtualizerContextValue }>) => {
|
|
return <VirtualizerContext.Provider value={value}>{children}</VirtualizerContext.Provider>
|
|
}
|
|
|
|
export const useVirtualizerContext = () => {
|
|
const context = useContext(VirtualizerContext)
|
|
if (!context) {
|
|
throw new Error('useVirtualizerContext must be used within a VirtualizerProvider')
|
|
}
|
|
return context
|
|
}
|
|
|
|
type ExtractRefType<Elem extends ElementType> =
|
|
ComponentPropsWithRef<Elem> extends { ref?: Ref<infer RefType> } ? RefType : never
|
|
|
|
type ExtractScrollElementFromRefComponent<RefComponent extends ElementType> = Extract<
|
|
ExtractRefType<RefComponent>,
|
|
Element
|
|
>
|
|
|
|
type ScrollWrapperComponentConstraints<Component extends ElementType> =
|
|
ComponentPropsWithRef<Component> extends { className?: string }
|
|
? ComponentPropsWithRef<Component> extends { children?: ReactNode | ReactNode[] }
|
|
? ExtractRefType<Component> extends never
|
|
? { ERROR_WRAPPER_COMPONENT_REQUIRES_REF_SUPPORT: never }
|
|
: ExtractRefType<Component> extends Element
|
|
? {}
|
|
: { ERROR_WRAPPER_COMPONENT_REF_MUST_EXTEND_ELEMENT: never }
|
|
: { ERROR_WRAPPER_COMPONENT_REQUIRES_CHILDREN: never }
|
|
: { ERROR_WRAPPER_COMPONENT_REQUIRES_CLASSNAME: never }
|
|
|
|
type InfiniteListWrapperProps<Item, Component extends ElementType = 'div'> = {
|
|
className?: string
|
|
items: Item[]
|
|
getItemKey?: (index: number) => string
|
|
getItemSize: (index: number) => number
|
|
gap?: number
|
|
hasNextPage?: boolean
|
|
isLoadingNextPage?: boolean
|
|
onLoadNextPage?: () => void
|
|
Component?: Component
|
|
} & ScrollWrapperComponentConstraints<Component>
|
|
|
|
export const InfiniteListScrollWrapper = <Item, Wrapper extends ElementType = 'div'>({
|
|
children,
|
|
items,
|
|
getItemKey,
|
|
getItemSize,
|
|
gap,
|
|
hasNextPage = false,
|
|
isLoadingNextPage = false,
|
|
onLoadNextPage = () => {},
|
|
className,
|
|
Component,
|
|
}: PropsWithChildren<InfiniteListWrapperProps<Item, Wrapper>>) => {
|
|
const scrollRef = useRef<ExtractScrollElementFromRefComponent<Wrapper> | null>(null)
|
|
|
|
const rowVirtualizer = useVirtualizer<ExtractScrollElementFromRefComponent<Wrapper>, Element>({
|
|
count: hasNextPage ? items.length + 1 : items.length,
|
|
getScrollElement: () => scrollRef.current,
|
|
getItemKey,
|
|
estimateSize: getItemSize,
|
|
overscan: 5,
|
|
gap,
|
|
})
|
|
|
|
const virtualItems = rowVirtualizer.getVirtualItems()
|
|
const virtualizerContextValue = useMemo(
|
|
() => ({
|
|
virtualizer: rowVirtualizer as unknown as Virtualizer<Element, Element>,
|
|
virtualItems,
|
|
}),
|
|
[rowVirtualizer, virtualItems]
|
|
)
|
|
|
|
useEffect(() => {
|
|
const lastItem = virtualItems[virtualItems.length - 1]
|
|
if (!lastItem) return
|
|
|
|
if (lastItem.index >= items.length - 1 && hasNextPage && !isLoadingNextPage) {
|
|
onLoadNextPage()
|
|
}
|
|
}, [virtualItems, items.length, hasNextPage, isLoadingNextPage, onLoadNextPage])
|
|
|
|
const WrapperToRender: Wrapper = Component ?? ('div' as Wrapper)
|
|
const wrapperProps = {
|
|
ref: (node: ExtractScrollElementFromRefComponent<Wrapper> | null) => {
|
|
scrollRef.current = node
|
|
},
|
|
className: cn('overflow-auto', className),
|
|
children,
|
|
} as ComponentPropsWithRef<Wrapper>
|
|
|
|
return (
|
|
<VirtualizerProvider value={virtualizerContextValue}>
|
|
<WrapperToRender {...wrapperProps} />
|
|
</VirtualizerProvider>
|
|
)
|
|
}
|
|
|
|
type ComponentWithStylePropConstraint<Component extends ElementType> =
|
|
ComponentProps<Component> extends { style?: CSSProperties }
|
|
? {}
|
|
: { ERROR_SIZER_COMPONENT_MUST_TAKE_STYLE_PROP: never }
|
|
|
|
type InfiniteListSizerProps = {
|
|
Component?: ElementType
|
|
} & ComponentWithStylePropConstraint<ElementType>
|
|
|
|
export const InfiniteListSizer = ({
|
|
children,
|
|
Component = 'div',
|
|
}: PropsWithChildren<InfiniteListSizerProps>) => {
|
|
const { virtualizer } = useVirtualizerContext()
|
|
|
|
return (
|
|
<Component
|
|
style={{
|
|
height: `${virtualizer.getTotalSize()}px`,
|
|
width: '100%',
|
|
position: 'relative',
|
|
}}
|
|
>
|
|
{children}
|
|
</Component>
|
|
)
|
|
}
|
|
|
|
export type RowComponentBaseProps<Item> = {
|
|
index: number
|
|
item: Item
|
|
style?: CSSProperties
|
|
}
|
|
|
|
type InfiniteListItemProps<
|
|
Item,
|
|
ExtraProps extends object = Record<string, never>,
|
|
RowComponent extends ComponentType<RowComponentBaseProps<Item> & ExtraProps> = ComponentType<
|
|
RowComponentBaseProps<Item> & ExtraProps
|
|
>,
|
|
> = {
|
|
index: number
|
|
start: number
|
|
size: number
|
|
item: Item
|
|
itemProps?: ExtraProps
|
|
ItemComponent: RowComponent
|
|
}
|
|
|
|
const MemoizedInfiniteListItem = typedMemo(
|
|
<
|
|
Item,
|
|
ExtraProps extends object = Record<string, never>,
|
|
RowComponent extends ComponentType<RowComponentBaseProps<Item> & ExtraProps> = ComponentType<
|
|
RowComponentBaseProps<Item> & ExtraProps
|
|
>,
|
|
>({
|
|
index,
|
|
start,
|
|
size,
|
|
item,
|
|
itemProps,
|
|
ItemComponent,
|
|
}: InfiniteListItemProps<Item, ExtraProps, RowComponent>) => {
|
|
const styleObject = useMemo<CSSProperties>(
|
|
() => createStyleObject({ size, start }),
|
|
[size, start]
|
|
)
|
|
|
|
const baseProps = useMemo<RowComponentBaseProps<Item>>(
|
|
() => ({
|
|
index,
|
|
item,
|
|
style: styleObject,
|
|
}),
|
|
[index, item, styleObject]
|
|
)
|
|
|
|
const combinedProps = useMemo(
|
|
() =>
|
|
({
|
|
...baseProps,
|
|
...(itemProps ?? ({} as ExtraProps)),
|
|
}) as RowComponentBaseProps<Item> & ExtraProps,
|
|
[baseProps, itemProps]
|
|
)
|
|
|
|
// Not JSX to avoid type error with generic function component
|
|
return createElement(ItemComponent, combinedProps)
|
|
}
|
|
)
|
|
MemoizedInfiniteListItem.displayName = 'MemoizedInfiniteListItem'
|
|
|
|
type InfiniteListItemsProps<
|
|
Item,
|
|
ExtraProps extends object = Record<string, never>,
|
|
RowComponent extends ComponentType<RowComponentBaseProps<Item> & ExtraProps> = ComponentType<
|
|
RowComponentBaseProps<Item> & ExtraProps
|
|
>,
|
|
> = {
|
|
items: Item[]
|
|
itemProps?: ExtraProps
|
|
ItemComponent: RowComponent
|
|
LoaderComponent: ComponentType<{ style?: CSSProperties }>
|
|
}
|
|
|
|
export const InfiniteListItems = <
|
|
Item,
|
|
ExtraProps extends object = Record<string, never>,
|
|
RowComponent extends ComponentType<RowComponentBaseProps<Item> & ExtraProps> = ComponentType<
|
|
RowComponentBaseProps<Item> & ExtraProps
|
|
>,
|
|
>({
|
|
items,
|
|
itemProps,
|
|
ItemComponent,
|
|
LoaderComponent,
|
|
}: InfiniteListItemsProps<Item, ExtraProps, RowComponent>) => {
|
|
const { virtualItems } = useVirtualizerContext()
|
|
|
|
return (
|
|
<>
|
|
{virtualItems.map((virtualRow) => {
|
|
const isLoaderRow = virtualRow.index > items.length - 1
|
|
const item = items[virtualRow.index]
|
|
|
|
return isLoaderRow ? (
|
|
<LoaderComponent
|
|
key={virtualRow.index}
|
|
style={createStyleObject({ size: virtualRow.size, start: virtualRow.start })}
|
|
/>
|
|
) : (
|
|
// Not JSX so we can pass type arguments to the generic function component
|
|
createElement(MemoizedInfiniteListItem<Item, ExtraProps>, {
|
|
key: virtualRow.index,
|
|
index: virtualRow.index,
|
|
start: virtualRow.start,
|
|
size: virtualRow.size,
|
|
item,
|
|
itemProps,
|
|
ItemComponent,
|
|
})
|
|
)
|
|
})}
|
|
</>
|
|
)
|
|
}
|
|
|
|
type InfiniteListDefaultProps<Item, ItemComponentProps extends object = Record<string, never>> = {
|
|
className?: string
|
|
items: Item[]
|
|
itemProps?: ItemComponentProps
|
|
getItemKey?: (index: number) => string
|
|
getItemSize: (index: number) => number
|
|
gap?: number
|
|
hasNextPage?: boolean
|
|
isLoadingNextPage?: boolean
|
|
onLoadNextPage?: () => void
|
|
ItemComponent: ComponentType<RowComponentBaseProps<Item> & ItemComponentProps>
|
|
LoaderComponent: ComponentType<{ style?: CSSProperties }>
|
|
}
|
|
|
|
export const InfiniteListDefault = <
|
|
Item,
|
|
ItemComponentProps extends object = Record<string, never>,
|
|
>({
|
|
className,
|
|
items,
|
|
itemProps,
|
|
getItemKey,
|
|
getItemSize,
|
|
gap,
|
|
hasNextPage = false,
|
|
isLoadingNextPage = false,
|
|
onLoadNextPage = () => {},
|
|
ItemComponent,
|
|
LoaderComponent,
|
|
}: InfiniteListDefaultProps<Item, ItemComponentProps>) => {
|
|
return (
|
|
<InfiniteListScrollWrapper
|
|
className={className}
|
|
items={items}
|
|
getItemKey={getItemKey}
|
|
getItemSize={getItemSize}
|
|
gap={gap}
|
|
hasNextPage={hasNextPage}
|
|
isLoadingNextPage={isLoadingNextPage}
|
|
onLoadNextPage={onLoadNextPage}
|
|
>
|
|
<InfiniteListSizer>
|
|
<InfiniteListItems
|
|
items={items}
|
|
itemProps={itemProps}
|
|
ItemComponent={ItemComponent}
|
|
LoaderComponent={LoaderComponent}
|
|
/>
|
|
</InfiniteListSizer>
|
|
</InfiniteListScrollWrapper>
|
|
)
|
|
}
|
|
|
|
export const LoaderForIconMenuItems = ({ style }: { style?: CSSProperties }) => (
|
|
<div className="space-y-1 my-1" style={style}>
|
|
<div className="flex flex-col gap-y-1">
|
|
<div className="flex flex-row h-6 px-4 items-center gap-2">
|
|
<Skeleton className="h-4 w-5" />
|
|
<Skeleton className="w-40 h-4" />
|
|
</div>
|
|
<div className="flex flex-row h-6 px-4 items-center gap-2">
|
|
<Skeleton className="h-4 w-5" />
|
|
<Skeleton className="w-32 h-4" />
|
|
</div>
|
|
<div className="flex flex-row h-6 px-4 items-center gap-2 opacity-75">
|
|
<Skeleton className="h-4 w-5" />
|
|
<Skeleton className="w-20 h-4" />
|
|
</div>
|
|
<div className="flex flex-row h-6 px-4 items-center gap-2 opacity-50">
|
|
<Skeleton className="h-4 w-5" />
|
|
<Skeleton className="w-40 h-4" />
|
|
</div>
|
|
<div className="flex flex-row h-6 px-4 items-center gap-2 opacity-25">
|
|
<Skeleton className="h-4 w-5" />
|
|
<Skeleton className="w-20 h-4" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|