mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-26 04:17:15 +03:00
Implement table record virtualizer back (#2839)
Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com> Co-authored-by: v1b3m <vibenjamin6@gmail.com> Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
This commit is contained in:
parent
9df83c9a5a
commit
b09100e3f3
@ -1,3 +1,4 @@
|
||||
import { useContext } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
|
||||
|
||||
@ -12,6 +13,8 @@ import { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable';
|
||||
import { isFetchingRecordTableDataState } from '@/ui/object/record-table/states/isFetchingRecordTableDataState';
|
||||
import { tableRowIdsState } from '@/ui/object/record-table/states/tableRowIdsState';
|
||||
import { getRecordTableScopedStates } from '@/ui/object/record-table/utils/getRecordTableScopedStates';
|
||||
import { ScrollWrapperContext } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
import RenderIfVisible from '@/ui/utilities/virtualizer/RenderIfVisible';
|
||||
|
||||
export const RecordTableBody = () => {
|
||||
const { scopeId } = useRecordTable();
|
||||
@ -41,17 +44,27 @@ export const RecordTableBody = () => {
|
||||
const isFetchingRecordTableData = useRecoilValue(
|
||||
isFetchingRecordTableDataState,
|
||||
);
|
||||
|
||||
const lastRowId = tableRowIds[tableRowIds.length - 1];
|
||||
|
||||
const scrollWrapperRef = useContext(ScrollWrapperContext);
|
||||
|
||||
if (isFetchingRecordTableData) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<tbody>
|
||||
<>
|
||||
{tableRowIds.map((rowId, rowIndex) => (
|
||||
<RowIdContext.Provider value={rowId} key={rowId}>
|
||||
<RowIndexContext.Provider value={rowIndex}>
|
||||
<RenderIfVisible
|
||||
rootElement="tbody"
|
||||
placeholderElement="tr"
|
||||
defaultHeight={32}
|
||||
initialVisible={rowIndex < 30}
|
||||
root={scrollWrapperRef.current}
|
||||
>
|
||||
<RecordTableRow
|
||||
key={rowId}
|
||||
ref={
|
||||
@ -61,9 +74,11 @@ export const RecordTableBody = () => {
|
||||
}
|
||||
rowId={rowId}
|
||||
/>
|
||||
</RenderIfVisible>
|
||||
</RowIndexContext.Provider>
|
||||
</RowIdContext.Provider>
|
||||
))}
|
||||
<tbody>
|
||||
{isFetchingMoreObjects && (
|
||||
<StyledRow selected={false}>
|
||||
<td style={{ height: 50 }} colSpan={1000}>
|
||||
@ -72,5 +87,6 @@ export const RecordTableBody = () => {
|
||||
</StyledRow>
|
||||
)}
|
||||
</tbody>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
112
front/src/modules/ui/utilities/virtualizer/RenderIfVisible.tsx
Normal file
112
front/src/modules/ui/utilities/virtualizer/RenderIfVisible.tsx
Normal file
@ -0,0 +1,112 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
type RenderIfVisibleProps = {
|
||||
/**
|
||||
* Whether the element should be visible initially or not.
|
||||
* Useful e.g. for always setting the first N items to visible.
|
||||
* Default: false
|
||||
*/
|
||||
initialVisible?: boolean;
|
||||
/** An estimate of the element's height */
|
||||
defaultHeight?: number;
|
||||
/** How far outside the viewport in pixels should elements be considered visible? */
|
||||
visibleOffset?: number;
|
||||
/** Should the element stay rendered after it becomes visible? */
|
||||
stayRendered?: boolean;
|
||||
root?: HTMLElement | null;
|
||||
/** E.g. 'span', 'tbody'. Default = 'div' */
|
||||
rootElement?: string;
|
||||
rootElementClass?: string;
|
||||
/** E.g. 'span', 'tr'. Default = 'div' */
|
||||
placeholderElement?: string;
|
||||
placeholderElementClass?: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const RenderIfVisible = ({
|
||||
initialVisible = false,
|
||||
defaultHeight = 300,
|
||||
visibleOffset = 1000,
|
||||
stayRendered = false,
|
||||
root = null,
|
||||
rootElement = 'div',
|
||||
rootElementClass = '',
|
||||
placeholderElement = 'div',
|
||||
placeholderElementClass = '',
|
||||
children,
|
||||
}: RenderIfVisibleProps) => {
|
||||
const [isVisible, setIsVisible] = useState<boolean>(initialVisible);
|
||||
|
||||
// eslint-disable-next-line twenty/no-state-useref
|
||||
const wasVisible = useRef<boolean>(initialVisible);
|
||||
// eslint-disable-next-line twenty/no-state-useref
|
||||
const placeholderHeight = useRef<number>(defaultHeight);
|
||||
const intersectionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Set visibility with intersection observer
|
||||
useEffect(() => {
|
||||
if (intersectionRef.current) {
|
||||
const localRef = intersectionRef.current;
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
// Before switching off `isVisible`, set the height of the placeholder
|
||||
if (!entries[0].isIntersecting) {
|
||||
placeholderHeight.current = localRef!.offsetHeight;
|
||||
}
|
||||
if (typeof window !== undefined && window.requestIdleCallback) {
|
||||
window.requestIdleCallback(
|
||||
() => setIsVisible(entries[0].isIntersecting),
|
||||
{
|
||||
timeout: 600,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
setIsVisible(entries[0].isIntersecting);
|
||||
}
|
||||
},
|
||||
{ root, rootMargin: `${visibleOffset}px 0px ${visibleOffset}px 0px` },
|
||||
);
|
||||
|
||||
observer.observe(localRef);
|
||||
return () => {
|
||||
if (localRef) {
|
||||
observer.unobserve(localRef);
|
||||
}
|
||||
};
|
||||
}
|
||||
return () => {};
|
||||
}, [root, visibleOffset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
wasVisible.current = true;
|
||||
}
|
||||
}, [isVisible]);
|
||||
|
||||
const placeholderStyle = { height: placeholderHeight.current };
|
||||
const rootClasses = useMemo(
|
||||
() => `renderIfVisible ${rootElementClass}`,
|
||||
[rootElementClass],
|
||||
);
|
||||
const placeholderClasses = useMemo(
|
||||
() => `renderIfVisible-placeholder ${placeholderElementClass}`,
|
||||
[placeholderElementClass],
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react/no-children-prop
|
||||
return React.createElement(rootElement, {
|
||||
children:
|
||||
isVisible || (stayRendered && wasVisible.current) ? (
|
||||
<>{children}</>
|
||||
) : (
|
||||
React.createElement(placeholderElement, {
|
||||
className: placeholderClasses,
|
||||
style: placeholderStyle,
|
||||
})
|
||||
),
|
||||
ref: intersectionRef,
|
||||
className: rootClasses,
|
||||
});
|
||||
};
|
||||
|
||||
export default RenderIfVisible;
|
Loading…
Reference in New Issue
Block a user