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,
|
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({
|
const options = klona({
|
||||||
...DEFAULT_OPTIONS,
|
...DEFAULT_OPTIONS,
|
||||||
...optionsProps,
|
...optionsProps,
|
||||||
|
isEmptyListWidgetCase: false,
|
||||||
});
|
});
|
||||||
const cache = passedCache || new Cache();
|
const cache = passedCache || new Cache();
|
||||||
|
|
||||||
|
|
@ -850,6 +851,7 @@ describe("#generate", () => {
|
||||||
levelData,
|
levelData,
|
||||||
pageSize: 3,
|
pageSize: 3,
|
||||||
widgetName: "List6",
|
widgetName: "List6",
|
||||||
|
isEmptyListWidgetCase: false,
|
||||||
})
|
})
|
||||||
.generate();
|
.generate();
|
||||||
|
|
||||||
|
|
@ -897,6 +899,7 @@ describe("#generate", () => {
|
||||||
levelData,
|
levelData,
|
||||||
pageSize: 3,
|
pageSize: 3,
|
||||||
widgetName: "List6",
|
widgetName: "List6",
|
||||||
|
isEmptyListWidgetCase: false,
|
||||||
})
|
})
|
||||||
.generate();
|
.generate();
|
||||||
|
|
||||||
|
|
@ -957,6 +960,7 @@ describe("#generate", () => {
|
||||||
levelData,
|
levelData,
|
||||||
pageSize: 3,
|
pageSize: 3,
|
||||||
widgetName: listWidgetName,
|
widgetName: listWidgetName,
|
||||||
|
isEmptyListWidgetCase: false,
|
||||||
})
|
})
|
||||||
.generate();
|
.generate();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ export interface GeneratorOptions {
|
||||||
serverSidePagination: boolean;
|
serverSidePagination: boolean;
|
||||||
templateHeight: number;
|
templateHeight: number;
|
||||||
widgetName: string;
|
widgetName: string;
|
||||||
|
isEmptyListWidgetCase: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConstructorProps {
|
export interface ConstructorProps {
|
||||||
|
|
@ -252,6 +253,7 @@ class MetaWidgetGenerator {
|
||||||
private templateWidgetStatus: TemplateWidgetStatus;
|
private templateWidgetStatus: TemplateWidgetStatus;
|
||||||
private virtualizer?: VirtualizerInstance;
|
private virtualizer?: VirtualizerInstance;
|
||||||
private widgetName: GeneratorOptions["widgetName"];
|
private widgetName: GeneratorOptions["widgetName"];
|
||||||
|
private isEmptyListWidgetCase: boolean;
|
||||||
|
|
||||||
constructor(props: ConstructorProps) {
|
constructor(props: ConstructorProps) {
|
||||||
this.siblings = {};
|
this.siblings = {};
|
||||||
|
|
@ -296,6 +298,7 @@ class MetaWidgetGenerator {
|
||||||
unchanged: new Set(),
|
unchanged: new Set(),
|
||||||
};
|
};
|
||||||
this.widgetName = "";
|
this.widgetName = "";
|
||||||
|
this.isEmptyListWidgetCase = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
withOptions = (options: GeneratorOptions) => {
|
withOptions = (options: GeneratorOptions) => {
|
||||||
|
|
@ -325,6 +328,7 @@ class MetaWidgetGenerator {
|
||||||
this.level = options.level ?? 1;
|
this.level = options.level ?? 1;
|
||||||
this.prevPrimaryKeys = this.primaryKeys;
|
this.prevPrimaryKeys = this.primaryKeys;
|
||||||
this.primaryKeys = this.generatePrimaryKeys(options);
|
this.primaryKeys = this.generatePrimaryKeys(options);
|
||||||
|
this.isEmptyListWidgetCase = options.isEmptyListWidgetCase;
|
||||||
|
|
||||||
this.updateModificationsQueue(this.prevOptions);
|
this.updateModificationsQueue(this.prevOptions);
|
||||||
|
|
||||||
|
|
@ -1106,14 +1110,28 @@ class MetaWidgetGenerator {
|
||||||
) => {
|
) => {
|
||||||
if (metaWidget.currentItem) return;
|
if (metaWidget.currentItem) return;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
let dataBinding: string;
|
||||||
|
|
||||||
|
if (this.isEmptyListWidgetCase) {
|
||||||
|
// For empty data case, set currentItem to empty object
|
||||||
|
dataBinding = "{{{}}}";
|
||||||
|
} else {
|
||||||
const shouldAddDataCacheToBinding = this.shouldAddDataCacheToBinding(
|
const shouldAddDataCacheToBinding = this.shouldAddDataCacheToBinding(
|
||||||
metaWidgetId,
|
metaWidgetId,
|
||||||
key,
|
key,
|
||||||
);
|
);
|
||||||
|
|
||||||
const dataBinding = shouldAddDataCacheToBinding
|
dataBinding = shouldAddDataCacheToBinding
|
||||||
? `{{${JSON.stringify(this.cachedKeyDataMap[key])}}}`
|
? `{{${JSON.stringify(this.cachedKeyDataMap[key])}}}`
|
||||||
: `{{${this.widgetName}.listData[${metaWidgetName}.currentIndex]}}`;
|
: `{{${this.widgetName}.listData[${metaWidgetName}.currentIndex]}}`;
|
||||||
|
}
|
||||||
|
|
||||||
metaWidget.currentItem = dataBinding;
|
metaWidget.currentItem = dataBinding;
|
||||||
metaWidget.dynamicBindingPathList = [
|
metaWidget.dynamicBindingPathList = [
|
||||||
|
|
@ -1854,10 +1872,6 @@ class MetaWidgetGenerator {
|
||||||
})?.metaWidgetName;
|
})?.metaWidgetName;
|
||||||
};
|
};
|
||||||
|
|
||||||
private resetCache = () => {
|
|
||||||
this.setWidgetCache({});
|
|
||||||
};
|
|
||||||
|
|
||||||
private initVirtualizer = () => {
|
private initVirtualizer = () => {
|
||||||
const options = this.virtualizerOptions();
|
const options = this.virtualizerOptions();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -129,3 +129,17 @@ export const isTargetElementClickable = (e: React.MouseEvent<HTMLElement>) => {
|
||||||
|
|
||||||
return isInput || hasControl || parentHasControl || hasLink || hasOnClick;
|
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,
|
TabContainerWidgetProps,
|
||||||
TabsWidgetProps,
|
TabsWidgetProps,
|
||||||
} from "widgets/TabsWidget/constants";
|
} from "widgets/TabsWidget/constants";
|
||||||
import { getMetaFlexLayers, isTargetElementClickable } from "./helper";
|
import {
|
||||||
|
getMetaFlexLayers,
|
||||||
|
isListFullyEmpty,
|
||||||
|
isTargetElementClickable,
|
||||||
|
} from "./helper";
|
||||||
import { DefaultAutocompleteDefinitions } from "widgets/WidgetUtils";
|
import { DefaultAutocompleteDefinitions } from "widgets/WidgetUtils";
|
||||||
import type { ExtraDef } from "utils/autocomplete/defCreatorUtils";
|
import type { ExtraDef } from "utils/autocomplete/defCreatorUtils";
|
||||||
import { LayoutSystemTypes } from "layoutSystems/types";
|
import { LayoutSystemTypes } from "layoutSystems/types";
|
||||||
|
|
@ -558,12 +562,16 @@ class ListWidget extends BaseWidget<
|
||||||
serverSidePagination = false,
|
serverSidePagination = false,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const pageSize = this.pageSize;
|
const pageSize = this.pageSize;
|
||||||
|
const data = this.props.listData;
|
||||||
|
|
||||||
|
const isEmptyListWidgetCase =
|
||||||
|
(data && isListFullyEmpty(data, pageNo)) ?? false;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
containerParentId: mainCanvasId,
|
containerParentId: mainCanvasId,
|
||||||
containerWidgetId: mainContainerId,
|
containerWidgetId: mainContainerId,
|
||||||
currTemplateWidgets: this.currFlattenedChildCanvasWidgets,
|
currTemplateWidgets: this.currFlattenedChildCanvasWidgets,
|
||||||
data: listData,
|
data: isEmptyListWidgetCase ? [{}] : listData,
|
||||||
itemSpacing: this.props.itemSpacing || 0,
|
itemSpacing: this.props.itemSpacing || 0,
|
||||||
infiniteScroll: this.props.infiniteScroll ?? false,
|
infiniteScroll: this.props.infiniteScroll ?? false,
|
||||||
level: this.props.level ?? 1,
|
level: this.props.level ?? 1,
|
||||||
|
|
@ -580,6 +588,7 @@ class ListWidget extends BaseWidget<
|
||||||
hooks: {
|
hooks: {
|
||||||
afterMetaWidgetGenerate: this.afterMetaWidgetGenerate,
|
afterMetaWidgetGenerate: this.afterMetaWidgetGenerate,
|
||||||
},
|
},
|
||||||
|
isEmptyListWidgetCase,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -602,6 +611,9 @@ class ListWidget extends BaseWidget<
|
||||||
};
|
};
|
||||||
|
|
||||||
generateMetaWidgets = () => {
|
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 generatorOptions = this.metaWidgetGeneratorOptions();
|
||||||
|
|
||||||
const { metaWidgets, propertyUpdates, removedMetaWidgetIds } =
|
const { metaWidgets, propertyUpdates, removedMetaWidgetIds } =
|
||||||
|
|
@ -1458,8 +1470,7 @@ class ListWidget extends BaseWidget<
|
||||||
|
|
||||||
if (
|
if (
|
||||||
Array.isArray(this.props.listData) &&
|
Array.isArray(this.props.listData) &&
|
||||||
this.props.listData.filter((item) => !isEmpty(item)).length === 0 &&
|
this.props.listData.filter((item) => !isEmpty(item)).length === 0
|
||||||
this.props.renderMode === RenderModes.PAGE
|
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user