diff --git a/app/client/cypress/e2e/Regression/ClientSide/Widgets/ListV2/Listv2_BasicClientSideData_spec.js b/app/client/cypress/e2e/Regression/ClientSide/Widgets/ListV2/Listv2_BasicClientSideData_spec.js index 0728fa37cc..2dc8613884 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/Widgets/ListV2/Listv2_BasicClientSideData_spec.js +++ b/app/client/cypress/e2e/Regression/ClientSide/Widgets/ListV2/Listv2_BasicClientSideData_spec.js @@ -162,5 +162,17 @@ describe( false, ); }); + + it("7. For the empty list, there should be no errors in appsmith console(as empty meta widgets are generated)", () => { + cy.openPropertyPane("listwidgetv2"); + + _.propPane.UpdatePropertyFieldValue("Items", "[]"); + + _.agHelper.AssertElementVisibility( + _.locators._visibleTextDiv("No data to display"), + ); + + _.debuggerHelper.AssertErrorCount(0); + }); }, ); diff --git a/app/client/src/widgets/ListWidgetV2/MetaWidgetGenerator.test.ts b/app/client/src/widgets/ListWidgetV2/MetaWidgetGenerator.test.ts index e05586955a..2e4053948e 100644 --- a/app/client/src/widgets/ListWidgetV2/MetaWidgetGenerator.test.ts +++ b/app/client/src/widgets/ListWidgetV2/MetaWidgetGenerator.test.ts @@ -167,6 +167,7 @@ const init = ({ const options = klona({ ...DEFAULT_OPTIONS, ...optionsProps, + isEmptyListWidgetCase: false, }); const cache = passedCache || new Cache(); @@ -850,6 +851,7 @@ describe("#generate", () => { levelData, pageSize: 3, widgetName: "List6", + isEmptyListWidgetCase: false, }) .generate(); @@ -897,6 +899,7 @@ describe("#generate", () => { levelData, pageSize: 3, widgetName: "List6", + isEmptyListWidgetCase: false, }) .generate(); @@ -957,6 +960,7 @@ describe("#generate", () => { levelData, pageSize: 3, widgetName: listWidgetName, + isEmptyListWidgetCase: false, }) .generate(); diff --git a/app/client/src/widgets/ListWidgetV2/MetaWidgetGenerator.ts b/app/client/src/widgets/ListWidgetV2/MetaWidgetGenerator.ts index 7d85d3dee2..0ec98567a8 100644 --- a/app/client/src/widgets/ListWidgetV2/MetaWidgetGenerator.ts +++ b/app/client/src/widgets/ListWidgetV2/MetaWidgetGenerator.ts @@ -92,6 +92,7 @@ export interface GeneratorOptions { serverSidePagination: boolean; templateHeight: number; widgetName: string; + isEmptyListWidgetCase: boolean; } export interface ConstructorProps { @@ -252,6 +253,7 @@ class MetaWidgetGenerator { private templateWidgetStatus: TemplateWidgetStatus; private virtualizer?: VirtualizerInstance; private widgetName: GeneratorOptions["widgetName"]; + private isEmptyListWidgetCase: boolean; constructor(props: ConstructorProps) { this.siblings = {}; @@ -296,6 +298,7 @@ class MetaWidgetGenerator { unchanged: new Set(), }; this.widgetName = ""; + this.isEmptyListWidgetCase = false; } withOptions = (options: GeneratorOptions) => { @@ -325,6 +328,7 @@ class MetaWidgetGenerator { this.level = options.level ?? 1; this.prevPrimaryKeys = this.primaryKeys; this.primaryKeys = this.generatePrimaryKeys(options); + this.isEmptyListWidgetCase = options.isEmptyListWidgetCase; this.updateModificationsQueue(this.prevOptions); @@ -1106,14 +1110,28 @@ class MetaWidgetGenerator { ) => { if (metaWidget.currentItem) return; - const shouldAddDataCacheToBinding = this.shouldAddDataCacheToBinding( - metaWidgetId, - key, - ); + /** + * Check if we're in the empty data case (when original listData was empty, we add [{}]) + * The page condition is included because there may be instances when a user uses navigation controls and lands on a specific page number, + * such as page 3, which returns an empty response. + * In this case, the list data will be empty, and we want to avoid generating empty meta widgets for that scenario. + */ - const dataBinding = shouldAddDataCacheToBinding - ? `{{${JSON.stringify(this.cachedKeyDataMap[key])}}}` - : `{{${this.widgetName}.listData[${metaWidgetName}.currentIndex]}}`; + let dataBinding: string; + + if (this.isEmptyListWidgetCase) { + // For empty data case, set currentItem to empty object + dataBinding = "{{{}}}"; + } else { + const shouldAddDataCacheToBinding = this.shouldAddDataCacheToBinding( + metaWidgetId, + key, + ); + + dataBinding = shouldAddDataCacheToBinding + ? `{{${JSON.stringify(this.cachedKeyDataMap[key])}}}` + : `{{${this.widgetName}.listData[${metaWidgetName}.currentIndex]}}`; + } metaWidget.currentItem = dataBinding; metaWidget.dynamicBindingPathList = [ @@ -1854,10 +1872,6 @@ class MetaWidgetGenerator { })?.metaWidgetName; }; - private resetCache = () => { - this.setWidgetCache({}); - }; - private initVirtualizer = () => { const options = this.virtualizerOptions(); diff --git a/app/client/src/widgets/ListWidgetV2/widget/helper.ts b/app/client/src/widgets/ListWidgetV2/widget/helper.ts index 2c297c77ca..33719b98b5 100644 --- a/app/client/src/widgets/ListWidgetV2/widget/helper.ts +++ b/app/client/src/widgets/ListWidgetV2/widget/helper.ts @@ -129,3 +129,17 @@ export const isTargetElementClickable = (e: React.MouseEvent) => { return isInput || hasControl || parentHasControl || hasLink || hasOnClick; }; + +export const isListFullyEmpty = ( + listData: unknown[] | undefined, + pageNo: number, +) => { + if (!listData) return false; + + /** + * This empty check is included because there may be instances when a user uses navigation controls and lands on a specific page number, + * such as page 3, which returns an empty response. + * In this case, the list data will be empty, and we want to avoid generating empty meta widgets for that scenario. + */ + return listData.length === 0 && pageNo === 1; +}; diff --git a/app/client/src/widgets/ListWidgetV2/widget/index.tsx b/app/client/src/widgets/ListWidgetV2/widget/index.tsx index 05cea0fd3e..f14fb87f16 100644 --- a/app/client/src/widgets/ListWidgetV2/widget/index.tsx +++ b/app/client/src/widgets/ListWidgetV2/widget/index.tsx @@ -43,7 +43,11 @@ import type { TabContainerWidgetProps, TabsWidgetProps, } from "widgets/TabsWidget/constants"; -import { getMetaFlexLayers, isTargetElementClickable } from "./helper"; +import { + getMetaFlexLayers, + isListFullyEmpty, + isTargetElementClickable, +} from "./helper"; import { DefaultAutocompleteDefinitions } from "widgets/WidgetUtils"; import type { ExtraDef } from "utils/autocomplete/defCreatorUtils"; import { LayoutSystemTypes } from "layoutSystems/types"; @@ -558,12 +562,16 @@ class ListWidget extends BaseWidget< serverSidePagination = false, } = this.props; const pageSize = this.pageSize; + const data = this.props.listData; + + const isEmptyListWidgetCase = + (data && isListFullyEmpty(data, pageNo)) ?? false; return { containerParentId: mainCanvasId, containerWidgetId: mainContainerId, currTemplateWidgets: this.currFlattenedChildCanvasWidgets, - data: listData, + data: isEmptyListWidgetCase ? [{}] : listData, itemSpacing: this.props.itemSpacing || 0, infiniteScroll: this.props.infiniteScroll ?? false, level: this.props.level ?? 1, @@ -580,6 +588,7 @@ class ListWidget extends BaseWidget< hooks: { afterMetaWidgetGenerate: this.afterMetaWidgetGenerate, }, + isEmptyListWidgetCase, }; }; @@ -602,6 +611,9 @@ class ListWidget extends BaseWidget< }; generateMetaWidgets = () => { + // The metaWidgetGeneratorOptions method already handles empty data + // by providing [{}] when listData is empty, so we can use normal generation flow + const generatorOptions = this.metaWidgetGeneratorOptions(); const { metaWidgets, propertyUpdates, removedMetaWidgetIds } = @@ -1458,8 +1470,7 @@ class ListWidget extends BaseWidget< if ( Array.isArray(this.props.listData) && - this.props.listData.filter((item) => !isEmpty(item)).length === 0 && - this.props.renderMode === RenderModes.PAGE + this.props.listData.filter((item) => !isEmpty(item)).length === 0 ) { return ( <>