feat: Implement empty meta widget generation for handling empty data scenarios (#40886)

## Description
<ins>Problem</ins>

Errors appeared in the console when the List widget in edit mode
received an empty array as data.

<ins>Root cause</ins>

In edit mode, the List widget uses template widgets to create meta
widgets. When the list becomes empty, meta widgets are not generated,
but template widgets with `currentItem` bindings remain. Since
`currentItem` requires meta widgets for evaluation, this results in
errors.

<ins>Solution</ins>

This PR handles the generation of empty meta widgets when the List
widget receives empty data, ensuring `currentItem` bindings don’t break.
It introduces a `generateEmptyMetaWidgets` method in
`MetaWidgetGenerator`, enhances the List widget to track and respond to
empty data states, and resets the meta widget cache when transitioning
between empty and non-empty states.

Fixes #`Issue Number`  
_or_  
Fixes https://github.com/appsmithorg/appsmith/issues/31925
> [!WARNING]  
> _If no issue exists, please create an issue first, and check with the
maintainers if the issue is valid._

## Automation

/ok-to-test tags="@tag.List"

### 🔍 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/15756809644>
> Commit: 58818e09dd83faaa92ee0dad53aab93dcf5f55a5
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=15756809644&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.List`
> Spec:
> <hr>Thu, 19 Jun 2025 12:11:58 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

- **Bug Fixes**
- Improved handling of empty list data on the first page, ensuring
widgets display correctly when no data is present.
- Enhanced consistency in detecting and rendering empty lists across the
List Widget.

- **New Features**
- Added a utility function to accurately identify fully empty lists,
enabling more predictable widget behavior.
- Introduced an option to handle empty list cases by injecting
placeholder data for consistent widget generation.

- **Tests**
- Added a test to verify the display of "No data to display" message
when the list is empty and confirm no errors occur.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Rahul Barwal 2025-06-20 09:39:51 +05:30 committed by GitHub
parent f2b76c5577
commit a801a19581
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 70 additions and 15 deletions

View File

@ -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);
});
},
);

View File

@ -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();

View File

@ -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();

View File

@ -129,3 +129,17 @@ export const isTargetElementClickable = (e: React.MouseEvent<HTMLElement>) => {
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;
};

View File

@ -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 (
<>