mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-29 19:10:19 +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 { useInView } from 'react-intersection-observer';
|
||||||
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
|
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 { isFetchingRecordTableDataState } from '@/ui/object/record-table/states/isFetchingRecordTableDataState';
|
||||||
import { tableRowIdsState } from '@/ui/object/record-table/states/tableRowIdsState';
|
import { tableRowIdsState } from '@/ui/object/record-table/states/tableRowIdsState';
|
||||||
import { getRecordTableScopedStates } from '@/ui/object/record-table/utils/getRecordTableScopedStates';
|
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 = () => {
|
export const RecordTableBody = () => {
|
||||||
const { scopeId } = useRecordTable();
|
const { scopeId } = useRecordTable();
|
||||||
@ -41,36 +44,49 @@ export const RecordTableBody = () => {
|
|||||||
const isFetchingRecordTableData = useRecoilValue(
|
const isFetchingRecordTableData = useRecoilValue(
|
||||||
isFetchingRecordTableDataState,
|
isFetchingRecordTableDataState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const lastRowId = tableRowIds[tableRowIds.length - 1];
|
const lastRowId = tableRowIds[tableRowIds.length - 1];
|
||||||
|
|
||||||
|
const scrollWrapperRef = useContext(ScrollWrapperContext);
|
||||||
|
|
||||||
if (isFetchingRecordTableData) {
|
if (isFetchingRecordTableData) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tbody>
|
<>
|
||||||
{tableRowIds.map((rowId, rowIndex) => (
|
{tableRowIds.map((rowId, rowIndex) => (
|
||||||
<RowIdContext.Provider value={rowId} key={rowId}>
|
<RowIdContext.Provider value={rowId} key={rowId}>
|
||||||
<RowIndexContext.Provider value={rowIndex}>
|
<RowIndexContext.Provider value={rowIndex}>
|
||||||
<RecordTableRow
|
<RenderIfVisible
|
||||||
key={rowId}
|
rootElement="tbody"
|
||||||
ref={
|
placeholderElement="tr"
|
||||||
rowId === lastRowId && rowIndex > 30
|
defaultHeight={32}
|
||||||
? lastTableRowRef
|
initialVisible={rowIndex < 30}
|
||||||
: undefined
|
root={scrollWrapperRef.current}
|
||||||
}
|
>
|
||||||
rowId={rowId}
|
<RecordTableRow
|
||||||
/>
|
key={rowId}
|
||||||
|
ref={
|
||||||
|
rowId === lastRowId && rowIndex > 30
|
||||||
|
? lastTableRowRef
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
rowId={rowId}
|
||||||
|
/>
|
||||||
|
</RenderIfVisible>
|
||||||
</RowIndexContext.Provider>
|
</RowIndexContext.Provider>
|
||||||
</RowIdContext.Provider>
|
</RowIdContext.Provider>
|
||||||
))}
|
))}
|
||||||
{isFetchingMoreObjects && (
|
<tbody>
|
||||||
<StyledRow selected={false}>
|
{isFetchingMoreObjects && (
|
||||||
<td style={{ height: 50 }} colSpan={1000}>
|
<StyledRow selected={false}>
|
||||||
Loading more...
|
<td style={{ height: 50 }} colSpan={1000}>
|
||||||
</td>
|
Loading more...
|
||||||
</StyledRow>
|
</td>
|
||||||
)}
|
</StyledRow>
|
||||||
</tbody>
|
)}
|
||||||
|
</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