mirror of
				https://github.com/lingble/twenty.git
				synced 2025-10-30 04:12:28 +00: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:
		| @@ -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,36 +44,49 @@ 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}> | ||||
|             <RecordTableRow | ||||
|               key={rowId} | ||||
|               ref={ | ||||
|                 rowId === lastRowId && rowIndex > 30 | ||||
|                   ? lastTableRowRef | ||||
|                   : undefined | ||||
|               } | ||||
|               rowId={rowId} | ||||
|             /> | ||||
|             <RenderIfVisible | ||||
|               rootElement="tbody" | ||||
|               placeholderElement="tr" | ||||
|               defaultHeight={32} | ||||
|               initialVisible={rowIndex < 30} | ||||
|               root={scrollWrapperRef.current} | ||||
|             > | ||||
|               <RecordTableRow | ||||
|                 key={rowId} | ||||
|                 ref={ | ||||
|                   rowId === lastRowId && rowIndex > 30 | ||||
|                     ? lastTableRowRef | ||||
|                     : undefined | ||||
|                 } | ||||
|                 rowId={rowId} | ||||
|               /> | ||||
|             </RenderIfVisible> | ||||
|           </RowIndexContext.Provider> | ||||
|         </RowIdContext.Provider> | ||||
|       ))} | ||||
|       {isFetchingMoreObjects && ( | ||||
|         <StyledRow selected={false}> | ||||
|           <td style={{ height: 50 }} colSpan={1000}> | ||||
|             Loading more... | ||||
|           </td> | ||||
|         </StyledRow> | ||||
|       )} | ||||
|     </tbody> | ||||
|       <tbody> | ||||
|         {isFetchingMoreObjects && ( | ||||
|           <StyledRow selected={false}> | ||||
|             <td style={{ height: 50 }} colSpan={1000}> | ||||
|               Loading more... | ||||
|             </td> | ||||
|           </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; | ||||
		Reference in New Issue
	
	Block a user
	 gitstart-twenty
					gitstart-twenty