Skip to content

Commit

Permalink
Add Infinite Scroll loader component to Table (#756)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
adhityamamallan authored Dec 10, 2024
1 parent fe91339 commit 71feb5a
Show file tree
Hide file tree
Showing 13 changed files with 76 additions and 101 deletions.
5 changes: 4 additions & 1 deletion src/components/table/__tests__/table.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,10 @@ function setup({
data={SAMPLE_ROWS}
columns={SAMPLE_COLUMNS}
shouldShowResults={shouldShowResults}
endMessage={<div>Sample end message</div>}
endMessageProps={{
kind: 'simple',
content: <div>Sample end message</div>,
}}
{...(!omitOnSort && { onSort: mockOnSort })}
sortColumn={SAMPLE_COLUMNS[SAMPLE_DATA_NUM_COLUMNS - 1].id}
sortOrder="DESC"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 });

Expand All @@ -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,
Expand All @@ -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();
});
Expand All @@ -62,14 +62,14 @@ describe(DomainWorkflowsTableEndMessage.name, () => {
function setup(overrides: Partial<Props>) {
const mockFetchNextPage = jest.fn();
const defaultProps: Props = {
hasWorkflows: true,
hasData: true,
error: null,
fetchNextPage: mockFetchNextPage,
hasNextPage: true,
isFetchingNextPage: false,
};

render(<DomainWorkflowsTableEndMessage {...defaultProps} {...overrides} />);
render(<TableInfiniteScrollLoader {...defaultProps} {...overrides} />);

return { mockFetchNextPage };
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Spinner data-testid="loading-spinner" />;
}
Expand Down Expand Up @@ -42,7 +42,7 @@ export default function DomainWorkflowsTableEndMessage(props: Props) {
);
}

if (props.hasWorkflows) {
if (props.hasData) {
return (
<styled.EndMessageContainer>End of results</styled.EndMessageContainer>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export type Props = {
hasWorkflows: boolean;
hasData: boolean;
error: Error | null;
fetchNextPage: () => void;
hasNextPage: boolean;
Expand Down
11 changes: 9 additions & 2 deletions src/components/table/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -16,7 +17,7 @@ export default function Table<T extends object, C extends TableConfig<T>>({
data,
columns,
shouldShowResults,
endMessage,
endMessageProps,
onSort,
sortColumn,
sortOrder,
Expand Down Expand Up @@ -69,7 +70,13 @@ export default function Table<T extends object, C extends TableConfig<T>>({
))}
<tr>
<td colSpan={columns.length}>
<styled.TableMessage>{endMessage}</styled.TableMessage>
<styled.TableMessage>
{endMessageProps.kind === 'infinite-scroll' ? (
<TableInfiniteScrollLoader {...endMessageProps} />
) : (
<>{endMessageProps.content}</>
)}
</styled.TableMessage>
</td>
</tr>
</StyledTableBody>
Expand Down
15 changes: 14 additions & 1 deletion src/components/table/table.types.ts
Original file line number Diff line number Diff line change
@@ -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<T> = {
name: string;
id: string;
Expand All @@ -16,6 +20,15 @@ type AreAnyColumnsSortable<T, C extends TableConfig<T>> = true extends {
? true
: false;

type EndMessageProps =
| {
kind: 'simple';
content: React.ReactNode;
}
| ({
kind: 'infinite-scroll';
} & InfiniteScrollLoaderProps);

type OnSortFunctionOptional<T, C extends TableConfig<T>> =
AreAnyColumnsSortable<T, C> extends true
? { onSort: (column: string) => void }
Expand All @@ -25,7 +38,7 @@ export type Props<T, C extends TableConfig<T>> = {
data: Array<T>;
columns: C;
shouldShowResults: boolean;
endMessage: React.ReactNode;
endMessageProps: EndMessageProps;
sortColumn?: string;
sortOrder?: SortOrder;
} & OnSortFunctionOptional<T, C>;
Original file line number Diff line number Diff line change
Expand Up @@ -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', () =>
Expand Down Expand Up @@ -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) => (
<button data-testid="mock-end-message" onClick={props.fetchNextPage}>
jest.fn((props: LoaderProps) => (
<button data-testid="mock-loader" onClick={props.fetchNextPage}>
Mock end message: {props.error ? 'Error' : 'OK'}
</button>
))
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -63,15 +62,14 @@ export default function DomainWorkflowsTable({ domain, cluster }: Props) {
<Table
data={workflows}
shouldShowResults={!isLoading && workflows.length > 0}
endMessage={
<DomainWorkflowsTableEndMessage
hasWorkflows={workflows.length > 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,
Expand Down

This file was deleted.

This file was deleted.

34 changes: 11 additions & 23 deletions src/views/domains-page/domains-table/domains-table.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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 (
<PageSection>
<section className={cls.tableContainer}>
Expand All @@ -95,16 +82,17 @@ function DomainsTable({
}
sortColumn={queryParams.sortColumn}
sortOrder={queryParams.sortOrder as SortOrder}
endMessage={
<DomainsTableEndMessage
key={visibleListItems}
canLoadMoreResults={
paginatedDomains.length < sortedDomains.length
}
hasSearchResults={sortedDomains.length > 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,
}}
/>
</section>
</PageSection>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,15 @@ export default function TaskListWorkersTable({ taskList }: Props) {
}),
})
}
endMessage={
filteredAndSortedWorkers.length === 0 ? (
<styled.EndMessageContainer>No workers</styled.EndMessageContainer>
) : null
}
endMessageProps={{
kind: 'simple',
content:
filteredAndSortedWorkers.length === 0 ? (
<styled.EndMessageContainer>
No workers
</styled.EndMessageContainer>
) : null,
}}
/>
</styled.TableContainer>
);
Expand Down

0 comments on commit 71feb5a

Please sign in to comment.