Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement infra code for infinite scroll implementation. #39225

Open
wants to merge 15 commits into
base: release
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
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/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"@types/d3-geo": "^3.1.0",
"@types/google.maps": "^3.51.0",
"@types/react-page-visibility": "^6.4.1",
"@types/react-window-infinite-loader": "^1.0.9",
"@types/web": "^0.0.99",
"@uppy/core": "^1.16.0",
"@uppy/dashboard": "^1.16.0",
Expand Down Expand Up @@ -204,6 +205,7 @@
"react-virtuoso": "^4.5.0",
"react-webcam": "^7.0.1",
"react-window": "^1.8.6",
"react-window-infinite-loader": "^1.0.10",
"react-zoom-pan-pinch": "^1.6.1",
"redux": "^4.0.1",
"redux-form": "^8.2.6",
Expand Down
34 changes: 29 additions & 5 deletions app/client/src/sagas/ActionExecution/PluginActionSaga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,28 @@ function* readBlob(blobUrl: string): any {
});
}

// Add this utility function near the top of the file with other utility functions
const downloadBinaryFile = (binaryData: string, filename = "download.pdf") => {
// Convert binary string to array buffer
const bytes = new Uint8Array(binaryData.length);

for (let i = 0; i < binaryData.length; i++) {
bytes[i] = binaryData.charCodeAt(i);
}

// Create blob and download
const blob = new Blob([bytes], { type: "application/pdf" });
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");

link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
};

/**
* This function resolves :
* - individual objects containing blob urls
Expand All @@ -299,10 +321,15 @@ function* resolvingBlobUrls(
isArray?: boolean,
arrDatatype?: string[],
) {
//Get datatypes of evaluated value.
// Check if the resolved value is a string and contains PDF-1.4
if (typeof value === "string") {
downloadBinaryFile(value, `debug_${Date.now()}.pdf`);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for binary data processing.

The binary data processing should include error handling for malformed data.

 if (typeof value === "string") {
+  try {
     downloadBinaryFile(value, `debug_${Date.now()}.pdf`);
+  } catch (error) {
+    log.error("Failed to process binary data:", error);
+    throw new Error("Failed to process binary data");
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Check if the resolved value is a string and contains PDF-1.4
if (typeof value === "string") {
downloadBinaryFile(value, `debug_${Date.now()}.pdf`);
}
// Check if the resolved value is a string and contains PDF-1.4
if (typeof value === "string") {
try {
downloadBinaryFile(value, `debug_${Date.now()}.pdf`);
} catch (error) {
log.error("Failed to process binary data:", error);
throw new Error("Failed to process binary data");
}
}

// Get datatypes of evaluated value
const dataType: string = findDatatype(value);

//If array elements then dont push datatypes to payload.
// If array elements then dont push datatypes to payload
isArray
? arrDatatype?.push(dataType)
: (executeActionRequest.paramProperties[`k${index}`] = {
Expand All @@ -324,9 +351,6 @@ function* resolvingBlobUrls(

set(value, blobUrlPath, resolvedBlobValue);

// We need to store the url path map to be able to update the blob data
// and send the info to server

// Here we fetch the blobUrlPathMap from the action payload and update it
const blobUrlPathMap = get(value, "blobUrlPaths", {}) as Record<
string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ type StaticTableProps = TableColumnHeaderProps & {
scrollContainerStyles: any;
useVirtual: boolean;
tableBodyRef?: React.MutableRefObject<HTMLDivElement | null>;
isLoading: boolean;
loadMoreFromEvaluations: () => void;
};

const StaticTable = (props: StaticTableProps, ref: React.Ref<SimpleBar>) => {
Expand Down Expand Up @@ -81,6 +83,9 @@ const StaticTable = (props: StaticTableProps, ref: React.Ref<SimpleBar>) => {
getTableBodyProps={props.getTableBodyProps}
height={props.height}
isAddRowInProgress={props.isAddRowInProgress}
isInfiniteScrollEnabled={false}
isLoading={props.isLoading}
loadMoreFromEvaluations={props.loadMoreFromEvaluations}
multiRowSelection={!!props.multiRowSelection}
pageSize={props.pageSize}
prepareRow={props.prepareRow}
Expand Down
4 changes: 4 additions & 0 deletions app/client/src/widgets/TableWidgetV2/component/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -473,8 +473,10 @@ export function Table(props: TableProps) {
headerGroups={headerGroups}
height={props.height}
isAddRowInProgress={props.isAddRowInProgress}
isLoading={props.isLoading}
isResizingColumn={isResizingColumn}
isSortable={props.isSortable}
loadMoreFromEvaluations={props.nextPageClick}
multiRowSelection={props?.multiRowSelection}
pageSize={props.pageSize}
prepareRow={prepareRow}
Expand Down Expand Up @@ -512,8 +514,10 @@ export function Table(props: TableProps) {
height={props.height}
isAddRowInProgress={props.isAddRowInProgress}
isInfiniteScrollEnabled={props.isInfiniteScrollEnabled}
isLoading={props.isLoading}
isResizingColumn={isResizingColumn}
isSortable={props.isSortable}
loadMoreFromEvaluations={props.nextPageClick}
multiRowSelection={props?.multiRowSelection}
pageSize={props.pageSize}
prepareRow={prepareRow}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React, { type Ref } from "react";
import type { Row as ReactTableRowType } from "react-table";
import { type ReactElementType } from "react-window";
import InfiniteLoader from "react-window-infinite-loader";
import type SimpleBar from "simplebar-react";
import type { TableSizes } from "../../Constants";
import { useInfiniteVirtualization } from "./useInfiniteVirtualization";
import { FixedInfiniteVirtualList } from "../VirtualList";

interface InfiniteScrollBodyProps {
rows: ReactTableRowType<Record<string, unknown>>[];
height: number;
tableSizes: TableSizes;
innerElementType?: ReactElementType;
isLoading: boolean;
totalRecordsCount?: number;
itemCount: number;
loadMoreFromEvaluations: () => void;
pageSize: number;
}

const InfiniteScrollBody = React.forwardRef(
(props: InfiniteScrollBodyProps, ref: Ref<SimpleBar>) => {
const { isLoading, loadMoreFromEvaluations, pageSize, rows } = props;
const { isItemLoaded, itemCount, loadMoreItems } =
useInfiniteVirtualization({
rows,
totalRecordsCount: rows.length,
rahulbarwal marked this conversation as resolved.
Show resolved Hide resolved
isLoading,
loadMore: loadMoreFromEvaluations,
pageSize,
});

return (
<div className="simplebar-content-wrapper">
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={itemCount + 5}
loadMoreItems={loadMoreItems}
>
{({ onItemsRendered, ref: infiniteLoaderRef }) => (
<FixedInfiniteVirtualList
height={props.height}
infiniteLoaderListRef={infiniteLoaderRef}
innerElementType={props.innerElementType}
onItemsRendered={onItemsRendered}
outerRef={ref}
pageSize={props.pageSize}
rows={props.rows}
tableSizes={props.tableSizes}
/>
)}
</InfiniteLoader>
</div>
);
},
);

export default InfiniteScrollBody;
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { renderHook } from "@testing-library/react-hooks";
import { useInfiniteVirtualization } from "./useInfiniteVirtualization";
import { act } from "@testing-library/react";
import type { Row as ReactTableRowType } from "react-table";

describe("useInfiniteVirtualization", () => {
const mockRows: ReactTableRowType<Record<string, unknown>>[] = [
{
id: "1",
original: { id: 1, name: "Test 1" },
index: 0,
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(),
},
{
id: "2",
original: { id: 2, name: "Test 2" },
index: 1,
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 defaultProps = {
rows: mockRows,
isLoading: false,
loadMore: jest.fn(),
pageSize: 10,
};

beforeEach(() => {
jest.clearAllMocks();
});

it("should return correct itemCount when totalRecordsCount is provided", () => {
const totalRecordsCount = 100;
const { result } = renderHook(() =>
useInfiniteVirtualization({
...defaultProps,
totalRecordsCount,
}),
);

expect(result.current.itemCount).toBe(totalRecordsCount);
});

it("should return rows length as itemCount when totalRecordsCount is not provided", () => {
const { result } = renderHook(() =>
useInfiniteVirtualization(defaultProps),
);

expect(result.current.itemCount).toBe(defaultProps.rows.length);
});

it("should call loadMore when loadMoreItems is called and not loading", async () => {
const { result } = renderHook(() =>
useInfiniteVirtualization(defaultProps),
);

await act(async () => {
await result.current.loadMoreItems(0, 10);
});

expect(defaultProps.loadMore).toHaveBeenCalledTimes(1);
});

it("should not call loadMore when loadMoreItems is called and is loading", async () => {
const { result } = renderHook(() =>
useInfiniteVirtualization({
...defaultProps,
isLoading: true,
}),
);

await act(async () => {
await result.current.loadMoreItems(0, 10);
});

expect(defaultProps.loadMore).not.toHaveBeenCalled();
});

it("should return correct isItemLoaded state for different scenarios", () => {
const { result } = renderHook(() =>
useInfiniteVirtualization(defaultProps),
);

// Index within rows length and not loading
expect(result.current.isItemLoaded(1)).toBe(true);

// Index beyond rows length and not loading
expect(result.current.isItemLoaded(5)).toBe(false);
});

it("should return false for isItemLoaded when loading", () => {
const { result } = renderHook(() =>
useInfiniteVirtualization({
...defaultProps,
isLoading: true,
}),
);

// Even for index within rows length, should return false when loading
expect(result.current.isItemLoaded(1)).toBe(false);
});

it("should return zero itemCount when there are no records", () => {
const { result } = renderHook(() =>
useInfiniteVirtualization({
...defaultProps,
rows: [],
}),
);

expect(result.current.itemCount).toBe(0);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useCallback } from "react";
import type { Row as ReactTableRowType } from "react-table";

interface InfiniteVirtualizationProps {
rows: ReactTableRowType<Record<string, unknown>>[];
totalRecordsCount?: number;
isLoading: boolean;
loadMore: () => void;
pageSize: number;
}

interface UseInfiniteVirtualizationReturn {
itemCount: number;
loadMoreItems: (startIndex: number, stopIndex: number) => void;
isItemLoaded: (index: number) => boolean;
}

export const useInfiniteVirtualization = ({
isLoading,
loadMore,
rows,
totalRecordsCount,
}: InfiniteVirtualizationProps): UseInfiniteVirtualizationReturn => {
const loadMoreItems = useCallback(async () => {
if (!isLoading) {
loadMore();
}

return Promise.resolve();
}, [isLoading, loadMore]);
rahulbarwal marked this conversation as resolved.
Show resolved Hide resolved

return {
itemCount: totalRecordsCount ?? rows.length,
loadMoreItems,
isItemLoaded: (index) => !isLoading && index < rows.length,
};
};
Loading
Loading