Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/client/src/ce/constants/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2631,4 +2631,6 @@ export const DATASOURCE_SECURE_TEXT = () =>

export const TABLE_LOADING_RECORDS = () => "loading records";

export const TABLE_LOAD_MORE = () => "Load More";

export const UPCOMING_SAAS_INTEGRATIONS = () => "Upcoming SaaS Integrations";
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Colors } from "constants/Colors";
import React from "react";
import { Flex, Text } from "@appsmith/ads";
import { createMessage, TABLE_LOAD_MORE } from "ee/constants/messages";
import { TEXT_SIZES } from "constants/WidgetConstants";
const LoadMoreButton = ({
loadMore,
style,
}: {
loadMore?: () => void;
style: React.CSSProperties;
}) => {
return (
<Flex
alignItems="center"
aria-label="Load more records"
cursor="pointer"
justifyContent="flex-start"
onClick={loadMore}
role="button"
style={{ ...style }}
tabIndex={0}
zIndex={1000}
>
<Text
className="underline pl-[10px]"
style={{
fontWeight: "var(--ads-v2-font-weight-normal)",
fontSize: TEXT_SIZES.PARAGRAPH,
color: Colors.GRAY,
position: "sticky",
left: 10,
}}
>
{createMessage(TABLE_LOAD_MORE)}
</Text>
</Flex>
);
};

export default LoadMoreButton;
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { VariableInfiniteVirtualList } from "./VirtualList";
import type { Row as ReactTableRowType } from "react-table";
import "@testing-library/jest-dom";

jest.mock("react-window", () => {
return {
VariableSizeList: React.forwardRef<
unknown,
{
children: (props: {
index: number;
style: React.CSSProperties;
data: Record<string, unknown>[];
}) => React.ReactNode;
itemCount: number;
itemData: Record<string, unknown>[];
onItemsRendered?: (props: {
overscanStartIndex: number;
overscanStopIndex: number;
visibleStartIndex: number;
visibleStopIndex: number;
}) => void;
height?: number;
width?: number;
}
>(({ children, itemCount, itemData, onItemsRendered }, ref) => {
const items = [];

for (let i = 0; i < itemCount; i++) {
items.push(
children({
index: i,
style: {},
data: itemData,
}),
);
}

// Call onItemsRendered if provided
React.useEffect(() => {
if (onItemsRendered) {
onItemsRendered({
overscanStartIndex: 0,
overscanStopIndex: itemCount - 1,
visibleStartIndex: 0,
visibleStopIndex: itemCount - 1,
});
}
}, [itemCount, onItemsRendered]);

return (
<div
data-testid="virtual-list"
ref={ref as React.RefObject<HTMLDivElement>}
>
{items.map((item, idx) => (
<React.Fragment key={idx}>{item}</React.Fragment>
))}
</div>
);
}),
areEqual: jest.fn((prevProps, nextProps) => prevProps === nextProps),
};
});

jest.mock("./Row", () => ({
Row: jest.fn(({ index, style }) => (
<div data-index={index} role="row" style={style} />
)),
}));

describe("VirtualList", () => {
const createMockRows = (
count: number,
): ReactTableRowType<Record<string, unknown>>[] => {
return Array.from({ length: count }, (_, i) => ({
id: `${i + 1}`,
original: { id: i + 1, name: `Test ${i + 1}` },
index: i,
cells: [],
values: {},
getRowProps: jest.fn(),
allCells: [],
subRows: [],
isExpanded: false,
canExpand: false,
depth: 0,
toggleRowExpanded: jest.fn(),
state: {},
toggleRowSelected: jest.fn(),
getToggleRowExpandedProps: jest.fn(),
isSelected: false,
isSomeSelected: false,
isGrouped: false,
groupByID: "",
groupByVal: "",
leafRows: [],
getToggleRowSelectedProps: jest.fn(),
setState: jest.fn(),
}));
};

const mockTableSizes = {
TABLE_HEADER_HEIGHT: 40,
ROW_HEIGHT: 40,
ROW_FONT_SIZE: 14,
VERTICAL_PADDING: 10,
EDIT_ICON_TOP: 10,
COLUMN_HEADER_HEIGHT: 40,
ROW_VIRTUAL_OFFSET: 0,
VERTICAL_EDITOR_PADDING: 4,
EDITABLE_CELL_HEIGHT: 32,
};

it("1. Should render rows correctly", () => {
const mockRows = createMockRows(3);

render(
<VariableInfiniteVirtualList
height={500}
itemCount={mockRows.length}
outerRef={{ current: null }}
pageSize={10}
rows={mockRows}
tableSizes={mockTableSizes}
/>,
);

expect(screen.getAllByRole("row")).toHaveLength(3);
});

it("2. Should render Load More button when hasMoreData is true", () => {
const mockRows = createMockRows(3);
const loadMoreMock = jest.fn();

render(
<VariableInfiniteVirtualList
hasMoreData
height={500}
itemCount={mockRows.length}
loadMore={loadMoreMock}
outerRef={{ current: null }}
pageSize={10}
rows={mockRows}
tableSizes={mockTableSizes}
/>,
);

const loadMoreButton = screen.getByRole("button", {
name: "Load more records",
});

expect(loadMoreButton).toBeInTheDocument();
expect(screen.getByText("Load More")).toBeInTheDocument();
});

it("3. Should not render Load More button when hasMoreData is false", () => {
const mockRows = createMockRows(3);
const loadMoreMock = jest.fn();

render(
<VariableInfiniteVirtualList
hasMoreData={false}
height={500}
itemCount={mockRows.length}
loadMore={loadMoreMock}
outerRef={{ current: null }}
pageSize={10}
rows={mockRows}
tableSizes={mockTableSizes}
/>,
);

expect(
screen.queryByRole("button", { name: "Load more records" }),
).not.toBeInTheDocument();
expect(screen.queryByText("Load More")).not.toBeInTheDocument();
});

it("4. Should call loadMore when Load More button is clicked", () => {
const mockRows = createMockRows(3);
const loadMoreMock = jest.fn();

render(
<VariableInfiniteVirtualList
hasMoreData
height={500}
itemCount={mockRows.length}
loadMore={loadMoreMock}
outerRef={{ current: null }}
pageSize={10}
rows={mockRows}
tableSizes={mockTableSizes}
/>,
);

const loadMoreButton = screen.getByRole("button", {
name: "Load more records",
});

fireEvent.click(loadMoreButton);

expect(loadMoreMock).toHaveBeenCalledTimes(1);
});

it("5. Should treat row data and load more data properly when both are provided", () => {
const mockRows = createMockRows(3);
const loadMoreMock = jest.fn();

render(
<VariableInfiniteVirtualList
hasMoreData
height={500}
itemCount={mockRows.length}
loadMore={loadMoreMock}
outerRef={{ current: null }}
pageSize={10}
rows={mockRows}
tableSizes={mockTableSizes}
/>,
);

// Should have regular rows
expect(screen.getAllByRole("row")).toHaveLength(3);

// And the Load More button
expect(screen.getByText("Load More")).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -1,22 +1,7 @@
import React, { type Ref } from "react";
import type { Row as ReactTableRowType } from "react-table";
import type { ListOnItemsRenderedProps, ReactElementType } from "react-window";
import type { VariableSizeList } from "react-window";
import type SimpleBar from "simplebar-react";
import type { TableSizes } from "../Constants";
import BaseVirtualList from "../VirtualTable/BaseVirtualList";

interface BaseVirtualListProps {
height: number;
tableSizes: TableSizes;
rows: ReactTableRowType<Record<string, unknown>>[];
innerElementType?: ReactElementType;
outerRef: Ref<SimpleBar>;
onItemsRendered?: (props: ListOnItemsRenderedProps) => void;
infiniteLoaderListRef?: React.Ref<VariableSizeList>;
itemCount: number;
pageSize: number;
}
import React from "react";
import BaseVirtualList, {
type BaseVirtualListProps,
} from "../VirtualTable/BaseVirtualList";

/**
* The difference between next two components is in the number of arguments they expect.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,29 @@ import type SimpleBar from "simplebar-react";
import type { TableSizes } from "../Constants";
import { Row } from "../TableBodyCoreComponents/Row";
import { EmptyRows } from "../cellComponents/EmptyCell";
import LoadMoreButton from "../LoadMoreButton";

type ExtendedListChildComponentProps = ListChildComponentProps & {
listRef: React.RefObject<VariableSizeList>;
rowHeights: React.RefObject<{ [key: number]: number }>;
rowNeedsMeasurement: React.RefObject<{ [key: number]: boolean }>;
loadMore?: () => void;
hasMoreData?: boolean;
};

// Create a memoized row component using areEqual from react-window
const MemoizedRow = React.memo(
export const MemoizedRow = React.memo(
(rowProps: ExtendedListChildComponentProps) => {
const { data, index, listRef, rowHeights, rowNeedsMeasurement, style } =
rowProps;
const {
data,
hasMoreData,
index,
listRef,
loadMore,
rowHeights,
rowNeedsMeasurement,
style,
} = rowProps;

if (index < data.length) {
const row = data[index];
Expand All @@ -39,6 +50,8 @@ const MemoizedRow = React.memo(
style={style}
/>
);
} else if (index === data.length && hasMoreData) {
return <LoadMoreButton loadMore={loadMore} style={style} />;
} else {
return <EmptyRows rows={1} style={style} />;
}
Expand Down Expand Up @@ -68,13 +81,19 @@ export interface BaseVirtualListProps {
infiniteLoaderListRef?: React.Ref<VariableSizeList>;
itemCount: number;
pageSize: number;
loadMore?: () => void;
hasMoreData?: boolean;
}

const LOAD_MORE_BUTTON_ROW = 1;

const BaseVirtualList = React.memo(function BaseVirtualList({
hasMoreData,
height,
infiniteLoaderListRef,
innerElementType,
itemCount,
loadMore,
onItemsRendered,
outerRef,
rows,
Expand All @@ -83,6 +102,7 @@ const BaseVirtualList = React.memo(function BaseVirtualList({
const listRef = useRef<VariableSizeList>(null);
const rowHeights = useRef<{ [key: number]: number }>({});
const rowNeedsMeasurement = useRef<{ [key: number]: boolean }>({});

const combinedRef = (list: VariableSizeList | null) => {
// Handle infiniteLoaderListRef
if (infiniteLoaderListRef) {
Expand Down Expand Up @@ -120,7 +140,9 @@ const BaseVirtualList = React.memo(function BaseVirtualList({
return (props: ListChildComponentProps) => (
<MemoizedRow
{...props}
hasMoreData={hasMoreData}
listRef={listRef}
loadMore={loadMore}
rowHeights={rowHeights}
rowNeedsMeasurement={rowNeedsMeasurement}
/>
Expand All @@ -137,7 +159,7 @@ const BaseVirtualList = React.memo(function BaseVirtualList({
2 * tableSizes.VERTICAL_PADDING
}
innerElementType={innerElementType}
itemCount={itemCount}
itemCount={hasMoreData ? itemCount + LOAD_MORE_BUTTON_ROW : itemCount}
itemData={rows}
itemSize={getItemSize}
onItemsRendered={onItemsRendered}
Expand Down
Loading
Loading