All files / modules/72-templates-library/components/TemplateActivityLog InfiniteScroll.tsx

9.09% Statements 4/44
0% Branches 0/25
0% Functions 0/10
7.14% Lines 3/42

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159              24x   24x                                                                                       24x                                                                                                                                                                                                                  
/*
 * Copyright 2022 Harness Inc. All rights reserved.
 * Use of this source code is governed by the PolyForm Shield 1.0.0 license
 * that can be found in the licenses directory at the root of this repository, also available at
 * https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt.
 */
 
import { MutableRefObject, useCallback, useEffect, useRef, useState } from 'react'
 
const DEFAULT_PAGE_SIZE = 10
 
interface GetItemsWithOffset {
  offset?: number
  limit?: number
}
 
interface InfiniteScrollProps {
  // getItems is a function promise that will resolve to an array of items to be displayed
  getItems: (options: GetItemsWithOffset) => Promise<any>
 
  /*
  ref of the element which when visible in the DOM shall trigger the next call
  to bring the next set of data. Check the usage in TemplateActivityLog
  */
  loadMoreRef: MutableRefObject<Element | null>
 
  // limit prop for defining the size of pagination, default is DEFAULT_PAGE_SIZE
  limit?: number
}
 
interface InfiniteScrollReturnProps {
  fetching: boolean
  error: string
  items: any
  hasMore: MutableRefObject<boolean>
  offsetToFetch: MutableRefObject<number>
  loadItems: () => void
  attachRefToLastElement: (index: number) => boolean
}
 
/*
Hook for infinite scroll
 
It will only work with NG APIs that support pagination and resolve with this format
{ content: [], empty: false, pageIndex: 1, pageItemCount: 3, pageSize: 10, totalItems: 13, totalPages: 2 }
 
What will this hook manage?
 - Items that are visible on the view. Can be used with any list of items that are resulted from paginated API calls
 
loadItems
 - Fetch data based on a given offset and limit
 - Function that accepts a page number/offset variable. It will call the underlying API function with the offset variable
*/
export const useInfiniteScroll = (props: InfiniteScrollProps): InfiniteScrollReturnProps => {
  const { getItems, limit = DEFAULT_PAGE_SIZE } = props
  const [items, setItems] = useState<any>([])
  const [fetching, setFetching] = useState(false)
  const [error, setError] = useState('')
  const hasMore = useRef(false)
 
  const initialPageLoaded = useRef(false)
  const offsetToFetch = useRef(0)
 
  const loadItems = () => {
    setFetching(true)
 
    getItems({
      offset: offsetToFetch.current,
      limit
    })
      .then(response => {
        setFetching(false)
 
        // If the cuurent fetch count exceeds totalItems, set hasMore as false
        const canFetchMore =
          response.data.totalItems > response.data.pageIndex * response.data.pageSize + response.data.pageItemCount
        hasMore.current = canFetchMore
 
        const responseContent = response.data.content
        setItems((prevItems: any) => [...prevItems, ...responseContent])
        setError('')
      })
      .catch(err => {
        setFetching(false)
        setError(err)
        setItems([])
      })
  }
 
  /*
  returns true if index is the last element of the list
  this is done so that the last element can be noticed by the IntersectionObserver
  */
  const attachRefToLastElement = useCallback(
    (index: number) => {
      return index === items.length - 1 && hasMore.current && !fetching
    },
    [fetching, hasMore.current]
  )
 
  /*
  call loadItems() and fetch the next batch of data if the target is now visible in viewport
  hasMore is true
  fetching is false
 
  Example - the last element of the list. We identify the last element by applying ref in the above callback
  */
  const loadMoreCallback = useCallback(
    entries => {
      const target = entries[0]
      if (target.isIntersecting && hasMore.current && !fetching) {
        offsetToFetch.current += 1
        loadItems()
      }
    },
    [fetching, hasMore.current, loadItems]
  )
 
  // Set the intersection observer
  useEffect(() => {
    // Default options for the IO
    // 0.25 threshold means call the callback function when 25% of the target is visible in the DOM
    const options = {
      threshold: 0.25
    }
 
    const observer = new IntersectionObserver(loadMoreCallback, options)
 
    // loadMoreRef is the target element received from props
    if (props.loadMoreRef && props.loadMoreRef.current) {
      observer.observe(props.loadMoreRef.current)
    }
 
    return () => {
      props.loadMoreRef?.current && observer.unobserve(props.loadMoreRef?.current)
    }
  }, [props.loadMoreRef, loadMoreCallback])
 
  // Just to ensure we don't end up fetching multiple times in the initial load
  useEffect(() => {
    if (initialPageLoaded.current) {
      return
    }
 
    loadItems()
    initialPageLoaded.current = true
  }, [loadItems])
 
  return {
    fetching,
    error,
    items,
    hasMore,
    loadItems,
    attachRefToLastElement,
    offsetToFetch
  }
}