## 🐞 Problem We've identified several issues with the TableWidgetV2's infinite scroll functionality: - **Stale Data After Toggle** When users enable infinite scroll for the first time, the table's state and cached data aren't properly reset, potentially leading to incorrect data display and inconsistent behavior. - **Height Changes Breaking Existing Data** When a table's height is increased while infinite scroll is enabled, the existing cached data becomes invalid due to changing offsets, but the table wasn't resetting its state accordingly. - **Empty Initial View** When loading a table with infinite scroll enabled, rows were not visible during the initial load until data was fetched, creating a jarring user experience. - **Disabled Properties Still Rendering Controls** Property controls that should be disabled (based on section disabling conditions) were still being rendered and active, causing unexpected behavior. --- ## ✅ Solution ### 1. Implement Table Reset on Infinite Scroll Toggle Added a new method `resetTableForInfiniteScroll()` that properly resets the table's state when infinite scroll is enabled. This method: - Clears cached table data - Resets the "end of data" flag - Resets all meta properties to their default values - Sets the page number back to `1` and triggers a page load ```ts resetTableForInfiniteScroll = () => { const { infiniteScrollEnabled, pushBatchMetaUpdates } = this.props; if (infiniteScrollEnabled) { // reset the cachedRows pushBatchMetaUpdates("cachedTableData", {}); pushBatchMetaUpdates("endOfData", false); // reset the meta properties const metaProperties = Object.keys(TableWidgetV2.getMetaPropertiesMap()); metaProperties.forEach((prop) => { if (prop !== "pageNo") { const defaultValue = TableWidgetV2.getMetaPropertiesMap()[prop]; this.props.updateWidgetMetaProperty(prop, defaultValue); } }); // reset and reload page this.updatePageNumber(1, EventType.ON_NEXT_PAGE); } }; ``` --- ### 2. Reset on Height Changes Added a check in `componentDidUpdate` to detect height changes and reset the table when needed: ```ts // Reset widget state when height changes while infinite scroll is enabled if ( infiniteScrollEnabled && prevProps.componentHeight !== componentHeight ) { this.resetTableForInfiniteScroll(); } ``` --- ### 3. Improved Empty State Rendering Modified the `InfiniteScrollBodyComponent` to show placeholder rows during initial load by using the maximum of `rows.length` and `pageSize`: ```ts const itemCount = useMemo( () => Math.max(rows.length, pageSize), [rows.length, pageSize], ); ``` This ensures the table maintains its expected height and appearance even before data is loaded. --- ### 4. Fixed Property Control Rendering Fixed the `PropertyControl` component to respect the `isControlDisabled` flag by conditionally rendering the control: ```ts {!isControlDisabled && PropertyControlFactory.createControl( config, { onPropertyChange: onPropertyChange, // ...other props }, // ...other args )} ``` This prevents disabled controls from being rendered and potentially causing issues. --- These improvements significantly enhance the stability and user experience of **TableWidgetV2**'s infinite scroll functionality. Fixes #39377 ## Automation /ok-to-test tags="@tag.Table, @tag.Widget, @tag.Binding, @tag.Sanity, @tag.PropertyPane" ### 🔍 Cypress test results <!-- This is an auto-generated comment: Cypress test results --> > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: <https://github.com/appsmithorg/appsmith/actions/runs/14373134089> > Commit: 2b0715bbbe2e9a254cd287f831329be529a17c3c > <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=14373134089&attempt=1" target="_blank">Cypress dashboard</a>. > Tags: `@tag.Table, @tag.Widget, @tag.Binding, @tag.Sanity, @tag.PropertyPane` > Spec: > <hr>Thu, 10 Apr 2025 07:15:53 UTC <!-- end of auto-generated comment: Cypress test results --> ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Property panels now display controls only when enabled, enhancing clarity. - Table widgets offer smoother infinite scrolling with automatic resets on state or size changes. - Columns dynamically adjust for optimal display when infinite scrolling is active. - **Bug Fixes** - Improved handling of item counts and loading states in infinite scrolling. - **Refactor** - Improved performance through optimized item computations and streamlined scrolling logic. - Removed redundant loading button logic for a cleaner user experience. - **Tests** - Expanded test scenarios to verify improved content wrapping and rich HTML rendering in table cells, with a focus on internal logic and behavior. - Enhanced clarity and robustness of infinite scroll tests by verifying loading through scrolling actions. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Rahul Barwal <rahul.barwal@appsmith.com>
362 lines
11 KiB
TypeScript
362 lines
11 KiB
TypeScript
import React from "react";
|
|
import type { Row } from "react-table";
|
|
import {
|
|
CompactModeTypes,
|
|
type AddNewRowActions,
|
|
type CompactMode,
|
|
type ReactTableColumnProps,
|
|
type ReactTableFilter,
|
|
type StickyType,
|
|
} from "./Constants";
|
|
import Table from "./Table";
|
|
|
|
import type { EventType } from "constants/AppsmithActionConstants/ActionConstants";
|
|
import equal from "fast-deep-equal/es6";
|
|
import { useCallback } from "react";
|
|
import type { EditableCell, TableVariant } from "../constants";
|
|
import { ColumnTypes } from "../constants";
|
|
|
|
export interface ColumnMenuOptionProps {
|
|
content: string | JSX.Element;
|
|
closeOnClick?: boolean;
|
|
isSelected?: boolean;
|
|
editColumnName?: boolean;
|
|
columnAccessor?: string;
|
|
id?: string;
|
|
category?: boolean;
|
|
options?: ColumnMenuSubOptionProps[];
|
|
onClick?: (columnIndex: number, isSelected: boolean) => void;
|
|
}
|
|
|
|
export interface ColumnMenuSubOptionProps {
|
|
content: string | JSX.Element;
|
|
isSelected?: boolean;
|
|
closeOnClick?: boolean;
|
|
onClick?: (columnIndex: number) => void;
|
|
id?: string;
|
|
category?: boolean;
|
|
isHeader?: boolean;
|
|
}
|
|
|
|
interface ReactTableComponentProps {
|
|
widgetId: string;
|
|
widgetName: string;
|
|
searchKey: string;
|
|
isDisabled?: boolean;
|
|
isVisible?: boolean;
|
|
isLoading: boolean;
|
|
editMode: boolean;
|
|
editableCell: EditableCell;
|
|
width: number;
|
|
height: number;
|
|
pageSize: number;
|
|
totalRecordsCount?: number;
|
|
tableData: Array<Record<string, unknown>>;
|
|
disableDrag: (disable: boolean) => void;
|
|
onBulkEditDiscard: () => void;
|
|
onBulkEditSave: () => void;
|
|
onRowClick: (rowData: Record<string, unknown>, rowIndex: number) => void;
|
|
selectAllRow: (pageData: Row<Record<string, unknown>>[]) => void;
|
|
unSelectAllRow: (pageData: Row<Record<string, unknown>>[]) => void;
|
|
updatePageNo: (pageNo: number, event?: EventType) => void;
|
|
sortTableColumn: (column: string, asc: boolean) => void;
|
|
nextPageClick: () => void;
|
|
prevPageClick: () => void;
|
|
pageNo: number;
|
|
serverSidePaginationEnabled: boolean;
|
|
selectedRowIndex: number;
|
|
selectedRowIndices: number[];
|
|
multiRowSelection?: boolean;
|
|
hiddenColumns?: string[];
|
|
triggerRowSelection: boolean;
|
|
columnWidthMap?: { [key: string]: number };
|
|
handleResizeColumn: (columnWidthMap: { [key: string]: number }) => void;
|
|
handleReorderColumn: (columnOrder: string[]) => void;
|
|
// TODO: Fix this the next time the file is edited
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
searchTableData: (searchKey: any) => void;
|
|
filters?: ReactTableFilter[];
|
|
applyFilter: (filters: ReactTableFilter[]) => void;
|
|
columns: ReactTableColumnProps[];
|
|
compactMode?: CompactMode;
|
|
isVisibleSearch?: boolean;
|
|
isVisibleFilters?: boolean;
|
|
isVisibleDownload?: boolean;
|
|
isVisiblePagination?: boolean;
|
|
delimiter: string;
|
|
isSortable?: boolean;
|
|
accentColor: string;
|
|
borderRadius: string;
|
|
boxShadow: string;
|
|
borderColor?: string;
|
|
borderWidth?: number;
|
|
variant?: TableVariant;
|
|
isEditableCellsValid?: Record<string, boolean>;
|
|
primaryColumnId?: string;
|
|
isAddRowInProgress: boolean;
|
|
allowAddNewRow: boolean;
|
|
onAddNewRow: () => void;
|
|
onAddNewRowAction: (
|
|
type: AddNewRowActions,
|
|
onActionComplete: () => void,
|
|
) => void;
|
|
allowRowSelection: boolean;
|
|
allowSorting: boolean;
|
|
disabledAddNewRowSave: boolean;
|
|
handleColumnFreeze?: (columnName: string, sticky?: StickyType) => void;
|
|
canFreezeColumn?: boolean;
|
|
showConnectDataOverlay: boolean;
|
|
onConnectData: () => void;
|
|
isInfiniteScrollEnabled: boolean;
|
|
endOfData: boolean;
|
|
}
|
|
|
|
function ReactTableComponent(props: ReactTableComponentProps) {
|
|
const {
|
|
allowAddNewRow,
|
|
allowRowSelection,
|
|
allowSorting,
|
|
applyFilter,
|
|
borderColor,
|
|
borderWidth,
|
|
canFreezeColumn,
|
|
columnWidthMap,
|
|
compactMode,
|
|
delimiter,
|
|
disabledAddNewRowSave,
|
|
disableDrag,
|
|
editableCell,
|
|
editMode,
|
|
endOfData,
|
|
filters,
|
|
handleColumnFreeze,
|
|
handleReorderColumn,
|
|
handleResizeColumn,
|
|
height,
|
|
isAddRowInProgress,
|
|
isInfiniteScrollEnabled,
|
|
isLoading,
|
|
isSortable,
|
|
isVisibleDownload,
|
|
isVisibleFilters,
|
|
isVisiblePagination,
|
|
isVisibleSearch,
|
|
multiRowSelection,
|
|
nextPageClick,
|
|
onAddNewRow,
|
|
onAddNewRowAction,
|
|
onBulkEditDiscard,
|
|
onBulkEditSave,
|
|
onConnectData,
|
|
onRowClick,
|
|
pageNo,
|
|
pageSize,
|
|
prevPageClick,
|
|
primaryColumnId,
|
|
searchKey,
|
|
searchTableData,
|
|
selectAllRow,
|
|
selectedRowIndex,
|
|
selectedRowIndices,
|
|
serverSidePaginationEnabled,
|
|
showConnectDataOverlay,
|
|
sortTableColumn: _sortTableColumn,
|
|
tableData,
|
|
totalRecordsCount,
|
|
triggerRowSelection,
|
|
unSelectAllRow,
|
|
updatePageNo,
|
|
variant,
|
|
widgetId,
|
|
widgetName,
|
|
width,
|
|
} = props;
|
|
|
|
let columns = props.columns;
|
|
|
|
if (isInfiniteScrollEnabled) {
|
|
const regularColumns = columns.filter(
|
|
(col) => col.columnProperties?.columnType !== ColumnTypes.EDIT_ACTIONS,
|
|
);
|
|
|
|
columns = [...regularColumns];
|
|
}
|
|
|
|
const sortTableColumn = useCallback(
|
|
(columnIndex: number, asc: boolean) => {
|
|
if (allowSorting) {
|
|
if (columnIndex === -1) {
|
|
_sortTableColumn("", asc);
|
|
} else {
|
|
const column = columns[columnIndex];
|
|
const columnType = column.metaProperties?.type || ColumnTypes.TEXT;
|
|
|
|
if (
|
|
columnType !== ColumnTypes.IMAGE &&
|
|
columnType !== ColumnTypes.VIDEO
|
|
) {
|
|
_sortTableColumn(column.alias, asc);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
[_sortTableColumn, allowSorting, columns],
|
|
);
|
|
|
|
const selectTableRow = useCallback(
|
|
(row: { original: Record<string, unknown>; index: number }) => {
|
|
if (allowRowSelection) {
|
|
onRowClick(row.original, row.index);
|
|
}
|
|
},
|
|
[allowRowSelection, onRowClick],
|
|
);
|
|
|
|
const toggleAllRowSelect = useCallback(
|
|
(isSelect: boolean, pageData: Row<Record<string, unknown>>[]) => {
|
|
if (allowRowSelection) {
|
|
if (isSelect) {
|
|
selectAllRow(pageData);
|
|
} else {
|
|
unSelectAllRow(pageData);
|
|
}
|
|
}
|
|
},
|
|
[allowRowSelection, selectAllRow, unSelectAllRow],
|
|
);
|
|
|
|
const memoziedDisableDrag = useCallback(
|
|
() => disableDrag(true),
|
|
[disableDrag],
|
|
);
|
|
const memoziedEnableDrag = useCallback(
|
|
() => disableDrag(false),
|
|
[disableDrag],
|
|
);
|
|
|
|
return (
|
|
<Table
|
|
accentColor={props.accentColor}
|
|
allowAddNewRow={allowAddNewRow}
|
|
applyFilter={applyFilter}
|
|
borderColor={borderColor}
|
|
borderRadius={props.borderRadius}
|
|
borderWidth={borderWidth}
|
|
boxShadow={props.boxShadow}
|
|
canFreezeColumn={canFreezeColumn}
|
|
columnWidthMap={columnWidthMap}
|
|
columns={columns}
|
|
compactMode={compactMode || CompactModeTypes.DEFAULT}
|
|
data={tableData}
|
|
delimiter={delimiter}
|
|
disableDrag={memoziedDisableDrag}
|
|
disabledAddNewRowSave={disabledAddNewRowSave}
|
|
editMode={editMode}
|
|
editableCell={editableCell}
|
|
enableDrag={memoziedEnableDrag}
|
|
endOfData={endOfData}
|
|
filters={filters}
|
|
handleColumnFreeze={handleColumnFreeze}
|
|
handleReorderColumn={handleReorderColumn}
|
|
handleResizeColumn={handleResizeColumn}
|
|
height={height}
|
|
isAddRowInProgress={isAddRowInProgress}
|
|
isInfiniteScrollEnabled={isInfiniteScrollEnabled}
|
|
isLoading={isLoading}
|
|
isSortable={isSortable}
|
|
isVisibleDownload={isVisibleDownload}
|
|
isVisibleFilters={isVisibleFilters}
|
|
isVisiblePagination={isVisiblePagination}
|
|
isVisibleSearch={isVisibleSearch}
|
|
multiRowSelection={multiRowSelection}
|
|
nextPageClick={nextPageClick}
|
|
onAddNewRow={onAddNewRow}
|
|
onAddNewRowAction={onAddNewRowAction}
|
|
onBulkEditDiscard={onBulkEditDiscard}
|
|
onBulkEditSave={onBulkEditSave}
|
|
onConnectData={onConnectData}
|
|
pageNo={pageNo - 1}
|
|
pageSize={pageSize || 1}
|
|
prevPageClick={prevPageClick}
|
|
primaryColumnId={primaryColumnId}
|
|
searchKey={searchKey}
|
|
searchTableData={searchTableData}
|
|
selectTableRow={selectTableRow}
|
|
selectedRowIndex={selectedRowIndex}
|
|
selectedRowIndices={selectedRowIndices}
|
|
serverSidePaginationEnabled={serverSidePaginationEnabled}
|
|
showConnectDataOverlay={showConnectDataOverlay}
|
|
sortTableColumn={sortTableColumn}
|
|
toggleAllRowSelect={toggleAllRowSelect}
|
|
totalRecordsCount={totalRecordsCount}
|
|
triggerRowSelection={triggerRowSelection}
|
|
updatePageNo={updatePageNo}
|
|
variant={variant}
|
|
widgetId={widgetId}
|
|
widgetName={widgetName}
|
|
width={width}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export default React.memo(ReactTableComponent, (prev, next) => {
|
|
return (
|
|
prev.applyFilter === next.applyFilter &&
|
|
prev.compactMode === next.compactMode &&
|
|
prev.delimiter === next.delimiter &&
|
|
prev.disableDrag === next.disableDrag &&
|
|
prev.editMode === next.editMode &&
|
|
prev.isSortable === next.isSortable &&
|
|
prev.filters === next.filters &&
|
|
prev.handleReorderColumn === next.handleReorderColumn &&
|
|
prev.handleResizeColumn === next.handleResizeColumn &&
|
|
prev.height === next.height &&
|
|
prev.isLoading === next.isLoading &&
|
|
prev.isVisibleDownload === next.isVisibleDownload &&
|
|
prev.isVisibleFilters === next.isVisibleFilters &&
|
|
prev.isVisiblePagination === next.isVisiblePagination &&
|
|
prev.isVisibleSearch === next.isVisibleSearch &&
|
|
prev.nextPageClick === next.nextPageClick &&
|
|
prev.onRowClick === next.onRowClick &&
|
|
prev.pageNo === next.pageNo &&
|
|
prev.pageSize === next.pageSize &&
|
|
prev.prevPageClick === next.prevPageClick &&
|
|
prev.searchKey === next.searchKey &&
|
|
prev.searchTableData === next.searchTableData &&
|
|
prev.selectedRowIndex === next.selectedRowIndex &&
|
|
prev.selectedRowIndices === next.selectedRowIndices &&
|
|
prev.serverSidePaginationEnabled === next.serverSidePaginationEnabled &&
|
|
prev.sortTableColumn === next.sortTableColumn &&
|
|
prev.totalRecordsCount === next.totalRecordsCount &&
|
|
prev.triggerRowSelection === next.triggerRowSelection &&
|
|
prev.updatePageNo === next.updatePageNo &&
|
|
prev.widgetId === next.widgetId &&
|
|
prev.widgetName === next.widgetName &&
|
|
prev.width === next.width &&
|
|
prev.borderRadius === next.borderRadius &&
|
|
prev.boxShadow === next.boxShadow &&
|
|
prev.borderWidth === next.borderWidth &&
|
|
prev.borderColor === next.borderColor &&
|
|
prev.accentColor === next.accentColor &&
|
|
//shallow equal possible
|
|
equal(prev.columnWidthMap, next.columnWidthMap) &&
|
|
//static reference
|
|
prev.tableData === next.tableData &&
|
|
// Using JSON stringify becuase isEqual doesnt work with functions,
|
|
// and we are not changing the columns manually.
|
|
prev.columns === next.columns &&
|
|
equal(prev.editableCell, next.editableCell) &&
|
|
prev.variant === next.variant &&
|
|
prev.primaryColumnId === next.primaryColumnId &&
|
|
equal(prev.isEditableCellsValid, next.isEditableCellsValid) &&
|
|
prev.isAddRowInProgress === next.isAddRowInProgress &&
|
|
prev.allowAddNewRow === next.allowAddNewRow &&
|
|
prev.allowRowSelection === next.allowRowSelection &&
|
|
prev.allowSorting === next.allowSorting &&
|
|
prev.disabledAddNewRowSave === next.disabledAddNewRowSave &&
|
|
prev.canFreezeColumn === next.canFreezeColumn &&
|
|
prev.showConnectDataOverlay === next.showConnectDataOverlay &&
|
|
prev.isInfiniteScrollEnabled === next.isInfiniteScrollEnabled
|
|
);
|
|
});
|