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:
parent
f2b76c5577
commit
a801a19581
|
|
@ -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);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user