From 71feb5a408e03f39d971e70ee07f9656058bb351 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Tue, 10 Dec 2024 22:09:25 +0530 Subject: [PATCH] Add Infinite Scroll loader component to Table (#756) Tables with infinite scroll functionality (using inView to detect a spinner at the bottom of loaded items, triggering a fetch for more data) used to define their own end message components to handle infinite scrolling. This PR centralizes that logic, making it a part of the Table component itself. --- src/components/table/__tests__/table.test.tsx | 5 ++- .../table-infinite-scroll-loader.test.tsx} | 18 +++++----- .../table-infinite-scroll-loader.styles.ts} | 0 .../table-infinite-scroll-loader.tsx} | 8 ++--- .../table-infinite-scroll-loader.types.ts} | 2 +- src/components/table/table.tsx | 11 ++++-- src/components/table/table.types.ts | 15 +++++++- .../__tests__/domain-workflows-table.test.tsx | 14 ++++---- .../domain-workflows-table.tsx | 18 +++++----- .../domains-table-end-message.tsx | 31 ----------------- .../domains-table-end-message.type.ts | 7 ---- .../domains-table/domains-table.tsx | 34 ++++++------------- .../task-list-workers-table.tsx | 14 +++++--- 13 files changed, 76 insertions(+), 101 deletions(-) rename src/{views/domain-workflows/domain-workflows-table-end-message/__tests__/domain-workflows-table-end-message.test.tsx => components/table/table-infinite-scroll-loader/__tests__/table-infinite-scroll-loader.test.tsx} (76%) rename src/{views/domain-workflows/domain-workflows-table-end-message/domain-workflows-table-end-message.styles.ts => components/table/table-infinite-scroll-loader/table-infinite-scroll-loader.styles.ts} (100%) rename src/{views/domain-workflows/domain-workflows-table-end-message/domain-workflows-table-end-message.tsx => components/table/table-infinite-scroll-loader/table-infinite-scroll-loader.tsx} (81%) rename src/{views/domain-workflows/domain-workflows-table-end-message/domain-workflows-table-end-message.types.ts => components/table/table-infinite-scroll-loader/table-infinite-scroll-loader.types.ts} (84%) delete mode 100644 src/views/domains-page/domains-table-end-message/domains-table-end-message.tsx delete mode 100644 src/views/domains-page/domains-table-end-message/domains-table-end-message.type.ts diff --git a/src/components/table/__tests__/table.test.tsx b/src/components/table/__tests__/table.test.tsx index c21f0e643..37ce4a21a 100644 --- a/src/components/table/__tests__/table.test.tsx +++ b/src/components/table/__tests__/table.test.tsx @@ -102,7 +102,10 @@ function setup({ data={SAMPLE_ROWS} columns={SAMPLE_COLUMNS} shouldShowResults={shouldShowResults} - endMessage={
Sample end message
} + endMessageProps={{ + kind: 'simple', + content:
Sample end message
, + }} {...(!omitOnSort && { onSort: mockOnSort })} sortColumn={SAMPLE_COLUMNS[SAMPLE_DATA_NUM_COLUMNS - 1].id} sortOrder="DESC" diff --git a/src/views/domain-workflows/domain-workflows-table-end-message/__tests__/domain-workflows-table-end-message.test.tsx b/src/components/table/table-infinite-scroll-loader/__tests__/table-infinite-scroll-loader.test.tsx similarity index 76% rename from src/views/domain-workflows/domain-workflows-table-end-message/__tests__/domain-workflows-table-end-message.test.tsx rename to src/components/table/table-infinite-scroll-loader/__tests__/table-infinite-scroll-loader.test.tsx index 872602732..13dcbcadf 100644 --- a/src/views/domain-workflows/domain-workflows-table-end-message/__tests__/domain-workflows-table-end-message.test.tsx +++ b/src/components/table/table-infinite-scroll-loader/__tests__/table-infinite-scroll-loader.test.tsx @@ -5,10 +5,10 @@ import { import { render, screen, act, fireEvent } from '@/test-utils/rtl'; -import DomainWorkflowsTableEndMessage from '../domain-workflows-table-end-message'; -import { type Props } from '../domain-workflows-table-end-message.types'; +import TableInfiniteScrollLoader from '../table-infinite-scroll-loader'; +import { type Props } from '../table-infinite-scroll-loader.types'; -describe(DomainWorkflowsTableEndMessage.name, () => { +describe(TableInfiniteScrollLoader.name, () => { it('renders loading state while fetching next page', () => { setup({ isFetchingNextPage: true }); @@ -29,7 +29,7 @@ describe(DomainWorkflowsTableEndMessage.name, () => { expect(mockFetchNextPage).toHaveBeenCalled(); }); - it('renders loading state with the infinite scroll ref when more workflows can be loaded', () => { + it('renders loading state with the infinite scroll ref when more data can be loaded', () => { const { mockFetchNextPage } = setup({ hasNextPage: true, isFetchingNextPage: false, @@ -47,13 +47,13 @@ describe(DomainWorkflowsTableEndMessage.name, () => { }); it('renders end message when there are workflows', () => { - setup({ hasWorkflows: true, hasNextPage: false }); + setup({ hasData: true, hasNextPage: false }); expect(screen.getByText('End of results')).toBeInTheDocument(); }); - it('renders end message when there are no workflows', () => { - setup({ hasWorkflows: false, hasNextPage: false }); + it('renders end message when there is no data', () => { + setup({ hasData: false, hasNextPage: false }); expect(screen.getByText('No results')).toBeInTheDocument(); }); @@ -62,14 +62,14 @@ describe(DomainWorkflowsTableEndMessage.name, () => { function setup(overrides: Partial) { const mockFetchNextPage = jest.fn(); const defaultProps: Props = { - hasWorkflows: true, + hasData: true, error: null, fetchNextPage: mockFetchNextPage, hasNextPage: true, isFetchingNextPage: false, }; - render(); + render(); return { mockFetchNextPage }; } diff --git a/src/views/domain-workflows/domain-workflows-table-end-message/domain-workflows-table-end-message.styles.ts b/src/components/table/table-infinite-scroll-loader/table-infinite-scroll-loader.styles.ts similarity index 100% rename from src/views/domain-workflows/domain-workflows-table-end-message/domain-workflows-table-end-message.styles.ts rename to src/components/table/table-infinite-scroll-loader/table-infinite-scroll-loader.styles.ts diff --git a/src/views/domain-workflows/domain-workflows-table-end-message/domain-workflows-table-end-message.tsx b/src/components/table/table-infinite-scroll-loader/table-infinite-scroll-loader.tsx similarity index 81% rename from src/views/domain-workflows/domain-workflows-table-end-message/domain-workflows-table-end-message.tsx rename to src/components/table/table-infinite-scroll-loader/table-infinite-scroll-loader.tsx index f893e017f..5da3c8566 100644 --- a/src/views/domain-workflows/domain-workflows-table-end-message/domain-workflows-table-end-message.tsx +++ b/src/components/table/table-infinite-scroll-loader/table-infinite-scroll-loader.tsx @@ -3,10 +3,10 @@ import React from 'react'; import { Spinner } from 'baseui/spinner'; import { InView } from 'react-intersection-observer'; -import { styled } from './domain-workflows-table-end-message.styles'; -import { type Props } from './domain-workflows-table-end-message.types'; +import { styled } from './table-infinite-scroll-loader.styles'; +import { type Props } from './table-infinite-scroll-loader.types'; -export default function DomainWorkflowsTableEndMessage(props: Props) { +export default function TableInfiniteScrollLoader(props: Props) { if (props.isFetchingNextPage) { return ; } @@ -42,7 +42,7 @@ export default function DomainWorkflowsTableEndMessage(props: Props) { ); } - if (props.hasWorkflows) { + if (props.hasData) { return ( End of results ); diff --git a/src/views/domain-workflows/domain-workflows-table-end-message/domain-workflows-table-end-message.types.ts b/src/components/table/table-infinite-scroll-loader/table-infinite-scroll-loader.types.ts similarity index 84% rename from src/views/domain-workflows/domain-workflows-table-end-message/domain-workflows-table-end-message.types.ts rename to src/components/table/table-infinite-scroll-loader/table-infinite-scroll-loader.types.ts index 7aa65a7a3..3bd169d82 100644 --- a/src/views/domain-workflows/domain-workflows-table-end-message/domain-workflows-table-end-message.types.ts +++ b/src/components/table/table-infinite-scroll-loader/table-infinite-scroll-loader.types.ts @@ -1,5 +1,5 @@ export type Props = { - hasWorkflows: boolean; + hasData: boolean; error: Error | null; fetchNextPage: () => void; hasNextPage: boolean; diff --git a/src/components/table/table.tsx b/src/components/table/table.tsx index 7a8702b7c..603ccf865 100644 --- a/src/components/table/table.tsx +++ b/src/components/table/table.tsx @@ -8,6 +8,7 @@ import { StyledTableBodyRow, } from 'baseui/table-semantic'; +import TableInfiniteScrollLoader from './table-infinite-scroll-loader/table-infinite-scroll-loader'; import TableSortableHeadCell from './table-sortable-head-cell/table-sortable-head-cell'; import { styled } from './table.styles'; import type { Props, TableConfig } from './table.types'; @@ -16,7 +17,7 @@ export default function Table>({ data, columns, shouldShowResults, - endMessage, + endMessageProps, onSort, sortColumn, sortOrder, @@ -69,7 +70,13 @@ export default function Table>({ ))} - {endMessage} + + {endMessageProps.kind === 'infinite-scroll' ? ( + + ) : ( + <>{endMessageProps.content} + )} + diff --git a/src/components/table/table.types.ts b/src/components/table/table.types.ts index 2cf3e13a7..25b36b2e2 100644 --- a/src/components/table/table.types.ts +++ b/src/components/table/table.types.ts @@ -1,5 +1,9 @@ +import type React from 'react'; + import { type SortOrder } from '@/utils/sort-by'; +import { type Props as InfiniteScrollLoaderProps } from './table-infinite-scroll-loader/table-infinite-scroll-loader.types'; + export type TableColumn = { name: string; id: string; @@ -16,6 +20,15 @@ type AreAnyColumnsSortable> = true extends { ? true : false; +type EndMessageProps = + | { + kind: 'simple'; + content: React.ReactNode; + } + | ({ + kind: 'infinite-scroll'; + } & InfiniteScrollLoaderProps); + type OnSortFunctionOptional> = AreAnyColumnsSortable extends true ? { onSort: (column: string) => void } @@ -25,7 +38,7 @@ export type Props> = { data: Array; columns: C; shouldShowResults: boolean; - endMessage: React.ReactNode; + endMessageProps: EndMessageProps; sortColumn?: string; sortOrder?: SortOrder; } & OnSortFunctionOptional; diff --git a/src/views/domain-workflows/domain-workflows-table/__tests__/domain-workflows-table.test.tsx b/src/views/domain-workflows/domain-workflows-table/__tests__/domain-workflows-table.test.tsx index d61ada399..3dbdec3f7 100644 --- a/src/views/domain-workflows/domain-workflows-table/__tests__/domain-workflows-table.test.tsx +++ b/src/views/domain-workflows/domain-workflows-table/__tests__/domain-workflows-table.test.tsx @@ -2,13 +2,13 @@ import { HttpResponse } from 'msw'; import { render, screen, userEvent, waitFor } from '@/test-utils/rtl'; +import { type Props as LoaderProps } from '@/components/table/table-infinite-scroll-loader/table-infinite-scroll-loader.types'; import * as usePageQueryParamsModule from '@/hooks/use-page-query-params/use-page-query-params'; import { type ListWorkflowsResponse } from '@/route-handlers/list-workflows/list-workflows.types'; import type { Props as MSWMocksHandlersProps } from '../../../../test-utils/msw-mock-handlers/msw-mock-handlers.types'; import { mockDomainWorkflowsQueryParamsValues } from '../../__fixtures__/domain-workflows-query-params'; import { type DomainWorkflowsHeaderInputType } from '../../domain-workflows-header/domain-workflows-header.types'; -import { type Props as EndMessageProps } from '../../domain-workflows-table-end-message/domain-workflows-table-end-message.types'; import DomainWorkflowsTable from '../domain-workflows-table'; jest.mock('@/components/error-panel/error-panel', () => @@ -39,10 +39,10 @@ jest.mock('../helpers/get-workflows-error-panel-props', () => ); jest.mock( - '../../domain-workflows-table-end-message/domain-workflows-table-end-message', + '@/components/table/table-infinite-scroll-loader/table-infinite-scroll-loader', () => - jest.fn((props: EndMessageProps) => ( - )) @@ -74,7 +74,7 @@ describe(DomainWorkflowsTable.name, () => { ).toBeInTheDocument(); }); - await user.click(screen.getByTestId('mock-end-message')); + await user.click(screen.getByTestId('mock-loader')); expect(await screen.findByText('Mock end message: OK')).toBeInTheDocument(); Array(10).forEach((_, index) => { @@ -126,13 +126,13 @@ describe(DomainWorkflowsTable.name, () => { ).toBeInTheDocument(); }); - await user.click(screen.getByTestId('mock-end-message')); + await user.click(screen.getByTestId('mock-loader')); expect( await screen.findByText('Mock end message: Error') ).toBeInTheDocument(); - await user.click(screen.getByTestId('mock-end-message')); + await user.click(screen.getByTestId('mock-loader')); expect(await screen.findByText('Mock end message: OK')).toBeInTheDocument(); Array(10).forEach((_, index) => { diff --git a/src/views/domain-workflows/domain-workflows-table/domain-workflows-table.tsx b/src/views/domain-workflows/domain-workflows-table/domain-workflows-table.tsx index 797a68dfd..88b160994 100644 --- a/src/views/domain-workflows/domain-workflows-table/domain-workflows-table.tsx +++ b/src/views/domain-workflows/domain-workflows-table/domain-workflows-table.tsx @@ -10,7 +10,6 @@ import domainPageQueryParamsConfig from '@/views/domain-page/config/domain-page- import domainWorkflowsQueryTableConfig from '../config/domain-workflows-query-table.config'; import domainWorkflowsSearchTableConfig from '../config/domain-workflows-search-table.config'; import { type Props } from '../domain-workflows-table/domain-workflows-table.types'; -import DomainWorkflowsTableEndMessage from '../domain-workflows-table-end-message/domain-workflows-table-end-message'; import getNextSortOrder from '../helpers/get-next-sort-order'; import useListWorkflows from '../hooks/use-list-workflows'; @@ -63,15 +62,14 @@ export default function DomainWorkflowsTable({ domain, cluster }: Props) { 0} - endMessage={ - 0} - error={error} - fetchNextPage={fetchNextPage} - hasNextPage={hasNextPage} - isFetchingNextPage={isFetchingNextPage} - /> - } + endMessageProps={{ + kind: 'infinite-scroll', + hasData: workflows.length > 0, + error, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + }} {...(inputType === 'query' ? { columns: domainWorkflowsQueryTableConfig, diff --git a/src/views/domains-page/domains-table-end-message/domains-table-end-message.tsx b/src/views/domains-page/domains-table-end-message/domains-table-end-message.tsx deleted file mode 100644 index 7bdb11476..000000000 --- a/src/views/domains-page/domains-table-end-message/domains-table-end-message.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; - -import { styled } from 'baseui'; - -import { type Props } from './domains-table-end-message.type'; - -const EndMessageContainer = styled('div', ({ $theme }) => ({ - ...$theme.typography.LabelSmall, - color: $theme.colors.contentTertiary, -})); - -export default function DomainsTableEndMessage({ - canLoadMoreResults, - hasSearchResults, - infiniteScrollTargetRef, -}: Props) { - if (canLoadMoreResults) { - return ( -
- ); - } - - if (hasSearchResults) { - return End of results; - } - - return No results; -} diff --git a/src/views/domains-page/domains-table-end-message/domains-table-end-message.type.ts b/src/views/domains-page/domains-table-end-message/domains-table-end-message.type.ts deleted file mode 100644 index e05296c34..000000000 --- a/src/views/domains-page/domains-table-end-message/domains-table-end-message.type.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type Props = { - canLoadMoreResults: boolean; - hasSearchResults: boolean; - infiniteScrollTargetRef: - | React.MutableRefObject - | ((node?: Element | null | undefined) => void); -}; diff --git a/src/views/domains-page/domains-table/domains-table.tsx b/src/views/domains-page/domains-table/domains-table.tsx index b64df4d57..6f528d64e 100644 --- a/src/views/domains-page/domains-table/domains-table.tsx +++ b/src/views/domains-page/domains-table/domains-table.tsx @@ -1,8 +1,6 @@ 'use client'; import React, { useEffect, useMemo, useState } from 'react'; -import { useInView } from 'react-intersection-observer'; - import PageSection from '@/components/page-section/page-section'; import Table from '@/components/table/table'; import usePageQueryParams from '@/hooks/use-page-query-params/use-page-query-params'; @@ -12,7 +10,6 @@ import sortBy, { toggleSortOrder, type SortOrder, } from '@/utils/sort-by'; -import DomainsTableEndMessage from '@/views/domains-page/domains-table-end-message/domains-table-end-message'; import domainsPageFiltersConfig from '../config/domains-page-filters.config'; import domainsPageQueryParamsConfig from '../config/domains-page-query-params.config'; @@ -66,16 +63,6 @@ function DomainsTable({ [sortedDomains, visibleListItems] ); - const { ref: loadMoreRef } = useInView({ - onChange: (inView) => { - if (inView && visibleListItems < sortedDomains.length) { - setVisibleListItems((v) => - Math.min(v + DOMAINS_LIST_PAGE_SIZE, sortedDomains.length) - ); - } - }, - }); - return (
@@ -95,16 +82,17 @@ function DomainsTable({ } sortColumn={queryParams.sortColumn} sortOrder={queryParams.sortOrder as SortOrder} - endMessage={ - 0} - infiniteScrollTargetRef={loadMoreRef} - /> - } + endMessageProps={{ + kind: 'infinite-scroll', + hasData: sortedDomains.length > 0, + hasNextPage: paginatedDomains.length < sortedDomains.length, + fetchNextPage: () => + setVisibleListItems((v) => + Math.min(v + DOMAINS_LIST_PAGE_SIZE, sortedDomains.length) + ), + isFetchingNextPage: false, + error: null, + }} />
diff --git a/src/views/task-list-page/task-list-workers-table/task-list-workers-table.tsx b/src/views/task-list-page/task-list-workers-table/task-list-workers-table.tsx index 75493354e..6c98c9a73 100644 --- a/src/views/task-list-page/task-list-workers-table/task-list-workers-table.tsx +++ b/src/views/task-list-page/task-list-workers-table/task-list-workers-table.tsx @@ -50,11 +50,15 @@ export default function TaskListWorkersTable({ taskList }: Props) { }), }) } - endMessage={ - filteredAndSortedWorkers.length === 0 ? ( - No workers - ) : null - } + endMessageProps={{ + kind: 'simple', + content: + filteredAndSortedWorkers.length === 0 ? ( + + No workers + + ) : null, + }} /> );